4756 lines
159 KiB
JavaScript
4756 lines
159 KiB
JavaScript
/*
|
|
Modded by tophf <github.com/tophf>
|
|
========== Original disclaimer:
|
|
|
|
Parser-Lib
|
|
Copyright (c) 2009-2016 Nicholas C. Zakas. All rights reserved.
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
*/
|
|
'use strict';
|
|
/* eslint-disable class-methods-use-this */
|
|
|
|
self.parserlib = (() => {
|
|
|
|
//#region Properties
|
|
|
|
// Global keywords that can be set for any property are conveniently listed in `all` prop:
|
|
// https://drafts.csswg.org/css-cascade/#all-shorthand
|
|
const GlobalKeywords = ['initial', 'inherit', 'revert', 'unset'];
|
|
const isGlobalKeyword = RegExp.prototype.test.bind(
|
|
new RegExp(`^(${GlobalKeywords.join('|')})$`, 'i'));
|
|
|
|
const Properties = {
|
|
'accent-color': 'auto | <color>',
|
|
'align-items': 'normal | stretch | <baseline-position> | [ <overflow-position>? <self-position> ]',
|
|
'align-content': '<align-content>',
|
|
'align-self': '<align-self>',
|
|
'all': GlobalKeywords.join(' | '),
|
|
'alignment-adjust': 'auto | baseline | before-edge | text-before-edge | middle | central | ' +
|
|
'after-edge | text-after-edge | ideographic | alphabetic | hanging | ' +
|
|
'mathematical | <len-pct>',
|
|
'alignment-baseline': 'auto | baseline | use-script | before-edge | text-before-edge | ' +
|
|
'after-edge | text-after-edge | central | middle | ideographic | alphabetic | ' +
|
|
'hanging | mathematical',
|
|
'animation': '[ <time> || <single-timing-function> || <time> || [ infinite | <number> ] || ' +
|
|
'<single-animation-direction> || <single-animation-fill-mode> || ' +
|
|
'[ running | paused ] || [ none | <ident> | <string> ] ]#',
|
|
'animation-delay': '<time>#',
|
|
'animation-direction': '<single-animation-direction>#',
|
|
'animation-duration': '<time>#',
|
|
'animation-fill-mode': '<single-animation-fill-mode>#',
|
|
'animation-iteration-count': '[ <number> | infinite ]#',
|
|
'animation-name': '[ none | <single-animation-name> ]#',
|
|
'animation-play-state': '[ running | paused ]#',
|
|
'animation-timing-function': '<single-timing-function>#',
|
|
'appearance': 'none | auto',
|
|
'-moz-appearance':
|
|
'none | button | button-arrow-down | button-arrow-next | button-arrow-previous | ' +
|
|
'button-arrow-up | button-bevel | button-focus | caret | checkbox | checkbox-container | ' +
|
|
'checkbox-label | checkmenuitem | dualbutton | groupbox | listbox | listitem | ' +
|
|
'menuarrow | menubar | menucheckbox | menuimage | menuitem | menuitemtext | menulist | ' +
|
|
'menulist-button | menulist-text | menulist-textfield | menupopup | menuradio | ' +
|
|
'menuseparator | meterbar | meterchunk | progressbar | progressbar-vertical | ' +
|
|
'progresschunk | progresschunk-vertical | radio | radio-container | radio-label | ' +
|
|
'radiomenuitem | range | range-thumb | resizer | resizerpanel | scale-horizontal | ' +
|
|
'scalethumbend | scalethumb-horizontal | scalethumbstart | scalethumbtick | ' +
|
|
'scalethumb-vertical | scale-vertical | scrollbarbutton-down | scrollbarbutton-left | ' +
|
|
'scrollbarbutton-right | scrollbarbutton-up | scrollbarthumb-horizontal | ' +
|
|
'scrollbarthumb-vertical | scrollbartrack-horizontal | scrollbartrack-vertical | ' +
|
|
'searchfield | separator | sheet | spinner | spinner-downbutton | spinner-textfield | ' +
|
|
'spinner-upbutton | splitter | statusbar | statusbarpanel | tab | tabpanel | tabpanels | ' +
|
|
'tab-scroll-arrow-back | tab-scroll-arrow-forward | textfield | textfield-multiline | ' +
|
|
'toolbar | toolbarbutton | toolbarbutton-dropdown | toolbargripper | toolbox | tooltip | ' +
|
|
'treeheader | treeheadercell | treeheadersortarrow | treeitem | treeline | treetwisty | ' +
|
|
'treetwistyopen | treeview | -moz-mac-unified-toolbar | -moz-win-borderless-glass | ' +
|
|
'-moz-win-browsertabbar-toolbox | -moz-win-communicationstext | ' +
|
|
'-moz-win-communications-toolbox | -moz-win-exclude-glass | -moz-win-glass | ' +
|
|
'-moz-win-mediatext | -moz-win-media-toolbox | -moz-window-button-box | ' +
|
|
'-moz-window-button-box-maximized | -moz-window-button-close | ' +
|
|
'-moz-window-button-maximize | -moz-window-button-minimize | -moz-window-button-restore | ' +
|
|
'-moz-window-frame-bottom | -moz-window-frame-left | -moz-window-frame-right | ' +
|
|
'-moz-window-titlebar | -moz-window-titlebar-maximized',
|
|
'-ms-appearance':
|
|
'none | icon | window | desktop | workspace | document | tooltip | dialog | button | ' +
|
|
'push-button | hyperlink | radio | radio-button | checkbox | menu-item | tab | menu | ' +
|
|
'menubar | pull-down-menu | pop-up-menu | list-menu | radio-group | checkbox-group | ' +
|
|
'outline-tree | range | field | combo-box | signature | password | normal',
|
|
'-webkit-appearance':
|
|
'auto | none | button | button-bevel | caps-lock-indicator | caret | checkbox | ' +
|
|
'default-button | listbox | listitem | media-fullscreen-button | media-mute-button | ' +
|
|
'media-play-button | media-seek-back-button | media-seek-forward-button | media-slider | ' +
|
|
'media-sliderthumb | menulist | menulist-button | menulist-text | menulist-textfield | ' +
|
|
'push-button | radio | searchfield | searchfield-cancel-button | searchfield-decoration | ' +
|
|
'searchfield-results-button | searchfield-results-decoration | slider-horizontal | ' +
|
|
'slider-vertical | sliderthumb-horizontal | sliderthumb-vertical | square-button | ' +
|
|
'textarea | textfield | scrollbarbutton-down | scrollbarbutton-left | ' +
|
|
'scrollbarbutton-right | scrollbarbutton-up | scrollbargripper-horizontal | ' +
|
|
'scrollbargripper-vertical | scrollbarthumb-horizontal | scrollbarthumb-vertical | ' +
|
|
'scrollbartrack-horizontal | scrollbartrack-vertical',
|
|
'-o-appearance':
|
|
'none | window | desktop | workspace | document | tooltip | dialog | button | ' +
|
|
'push-button | hyperlink | radio | radio-button | checkbox | menu-item | tab | menu | ' +
|
|
'menubar | pull-down-menu | pop-up-menu | list-menu | radio-group | checkbox-group | ' +
|
|
'outline-tree | range | field | combo-box | signature | password | normal',
|
|
'aspect-ratio': 'auto || [ <num0+> / <num0+> ]',
|
|
'azimuth': '<azimuth>',
|
|
|
|
'backdrop-filter': '<filter-function-list> | none',
|
|
'backface-visibility': 'visible | hidden',
|
|
'background': '[ <bg-layer> , ]* <final-bg-layer>',
|
|
'background-attachment': '<attachment>#',
|
|
'background-blend-mode': '<blend-mode>',
|
|
'background-clip': '[ <box> | text ]#',
|
|
'background-color': '<color>',
|
|
'background-image': '<bg-image>#',
|
|
'background-origin': '<box>#',
|
|
'background-position': '<bg-position>#',
|
|
'background-position-x': '[ center | [ left | right ]? <len-pct>? ]#',
|
|
'background-position-y': '[ center | [ top | bottom ]? <len-pct>? ]#',
|
|
'background-repeat': '<repeat-style>#',
|
|
'background-size': '<bg-size>#',
|
|
'baseline-shift': 'baseline | sub | super | <len-pct>',
|
|
'behavior': 1,
|
|
'binding': 1,
|
|
'bleed': '<length>',
|
|
'block-size': '<width>',
|
|
'bookmark-label': '<content-list>',
|
|
'bookmark-level': 'none | <integer>',
|
|
'bookmark-state': 'open | closed',
|
|
'bookmark-target': 'none | <uri>',
|
|
'border-boundary': 'none | parent | display',
|
|
'border-collapse': 'collapse | separate',
|
|
'border-image': '[ none | <image> ] || <border-image-slice> ' +
|
|
'[ / <border-image-width> | / <border-image-width>? / <border-image-outset> ]? || ' +
|
|
'<border-image-repeat>',
|
|
'border-image-outset': '<border-image-outset>',
|
|
'border-image-repeat': '<border-image-repeat>',
|
|
'border-image-slice': '<border-image-slice>',
|
|
'border-image-source': '<image> | none',
|
|
'border-image-width': '<border-image-width>',
|
|
'border-spacing': '<length>{1,2}',
|
|
|
|
'border-bottom-left-radius': '<len-pct>{1,2}',
|
|
'border-bottom-right-radius': '<len-pct>{1,2}',
|
|
'border-end-end-radius': '<len-pct>{1,2}',
|
|
'border-end-start-radius': '<len-pct>{1,2}',
|
|
'border-radius': '<border-radius>',
|
|
'border-start-end-radius': '<len-pct>{1,2}',
|
|
'border-start-start-radius': '<len-pct>{1,2}',
|
|
'border-top-left-radius': '<len-pct>{1,2}',
|
|
'border-top-right-radius': '<len-pct>{1,2}',
|
|
|
|
'bottom': '<width>',
|
|
'box-decoration-break': 'slice | clone',
|
|
'box-shadow': '<box-shadow>',
|
|
'box-sizing': 'content-box | border-box',
|
|
'break-after': 'auto | always | avoid | left | right | page | column | avoid-page | avoid-column',
|
|
'break-before': 'auto | always | avoid | left | right | page | column | avoid-page | avoid-column',
|
|
'break-inside': 'auto | avoid | avoid-page | avoid-column',
|
|
'-moz-box-align': 1,
|
|
'-moz-box-decoration-break': 1,
|
|
'-moz-box-direction': 1,
|
|
'-moz-box-flex': 1,
|
|
'-moz-box-flex-group': 1,
|
|
'-moz-box-lines': 1,
|
|
'-moz-box-ordinal-group': 1,
|
|
'-moz-box-orient': 1,
|
|
'-moz-box-pack': 1,
|
|
'-o-box-decoration-break': 1,
|
|
'-webkit-box-align': 1,
|
|
'-webkit-box-decoration-break': 1,
|
|
'-webkit-box-direction': 1,
|
|
'-webkit-box-flex': 1,
|
|
'-webkit-box-flex-group': 1,
|
|
'-webkit-box-lines': 1,
|
|
'-webkit-box-ordinal-group': 1,
|
|
'-webkit-box-orient': 1,
|
|
'-webkit-box-pack': 1,
|
|
|
|
'caret-color': 'auto | <color>',
|
|
'caption-side': 'top | bottom | inline-start | inline-end',
|
|
'clear': 'none | right | left | both | inline-start | inline-end',
|
|
'clip': '<rect> | auto',
|
|
'clip-path': '<clip-source> | <clip-path> | none',
|
|
'clip-rule': 'nonzero | evenodd',
|
|
'color': '<color>',
|
|
'color-adjust': 'economy | exact',
|
|
'color-interpolation': 'auto | sRGB | linearRGB',
|
|
'color-interpolation-filters': 'auto | sRGB | linearRGB',
|
|
'color-profile': 1,
|
|
'color-rendering': 'auto | optimizeSpeed | optimizeQuality',
|
|
'color-scheme': 'normal | [ light | dark | <ident> ]+ && only?',
|
|
'column-count': '<integer> | auto',
|
|
'column-fill': 'auto | balance',
|
|
'column-gap': '<column-gap>',
|
|
'column-rule': '<border-shorthand>',
|
|
'column-rule-color': '<color>',
|
|
'column-rule-style': '<border-style>',
|
|
'column-rule-width': '<border-width>',
|
|
'column-span': 'none | all',
|
|
'column-width': '<length> | auto',
|
|
'columns': 1,
|
|
'contain': 'none | strict | content | [ size || layout || style || paint ]',
|
|
'contain-intrinsic-size': 'none | <length>{1,2}',
|
|
'content': 'normal | none | <content-list> [ / <string> ]?',
|
|
'content-visibility': 'visible | auto | hidden',
|
|
'counter-increment': '<counter>',
|
|
'counter-reset': '<counter>',
|
|
'counter-set': '<counter>',
|
|
'cue': 'cue-after | cue-before',
|
|
'cue-after': 1,
|
|
'cue-before': 1,
|
|
'cursor': '[ <uri> [ <number> <number> ]? , ]* ' +
|
|
'[ auto | default | none | context-menu | help | pointer | progress | wait | ' +
|
|
'cell | crosshair | text | vertical-text | alias | copy | move | no-drop | ' +
|
|
'not-allowed | grab | grabbing | e-resize | n-resize | ne-resize | nw-resize | ' +
|
|
's-resize | se-resize | sw-resize | w-resize | ew-resize | ns-resize | ' +
|
|
'nesw-resize | nwse-resize | col-resize | row-resize | all-scroll | ' +
|
|
'zoom-in | zoom-out ]',
|
|
|
|
'direction': 'ltr | rtl',
|
|
'display': '[ <display-outside> || <display-inside> ] | ' +
|
|
'<display-listitem> | <display-internal> | <display-box> | <display-legacy> | ' +
|
|
// deprecated and nonstandard
|
|
'-webkit-box | -webkit-inline-box | -ms-flexbox',
|
|
'dominant-baseline': 'auto | use-script | no-change | reset-size | ideographic | alphabetic | ' +
|
|
'hanging | mathematical | central | middle | text-after-edge | text-before-edge',
|
|
'drop-initial-after-adjust': 'central | middle | after-edge | text-after-edge | ideographic | ' +
|
|
'alphabetic | mathematical | <len-pct>',
|
|
'drop-initial-after-align': 'baseline | use-script | before-edge | text-before-edge | ' +
|
|
'after-edge | text-after-edge | central | middle | ideographic | alphabetic | hanging | ' +
|
|
'mathematical',
|
|
'drop-initial-before-adjust': 'before-edge | text-before-edge | central | middle | ' +
|
|
'hanging | mathematical | <len-pct>',
|
|
'drop-initial-before-align': 'caps-height | baseline | use-script | before-edge | ' +
|
|
'text-before-edge | after-edge | text-after-edge | central | middle | ideographic | ' +
|
|
'alphabetic | hanging | mathematical',
|
|
'drop-initial-size': 'auto | line | <len-pct>',
|
|
'drop-initial-value': '<integer>',
|
|
|
|
'elevation': '<angle> | below | level | above | higher | lower',
|
|
'empty-cells': 'show | hide',
|
|
'enable-background': 1,
|
|
|
|
'fill': '<paint>',
|
|
'fill-opacity': '<opacity-value>',
|
|
'fill-rule': 'nonzero | evenodd',
|
|
'filter': '<filter-function-list> | <ie-function> | none',
|
|
'fit': 'fill | hidden | meet | slice',
|
|
'fit-position': 1,
|
|
'flex': '<flex-shorthand>',
|
|
'flex-basis': '<width>',
|
|
'flex-direction': 'row | row-reverse | column | column-reverse',
|
|
'flex-flow': '<flex-direction> || <flex-wrap>',
|
|
'flex-grow': '<number>',
|
|
'flex-shrink': '<number>',
|
|
'flex-wrap': 'nowrap | wrap | wrap-reverse',
|
|
'float': 'left | right | none | inline-start | inline-end',
|
|
'float-offset': 1,
|
|
'flood-color': 1,
|
|
'flood-opacity': '<opacity-value>',
|
|
// matching no-pct first because Matcher doesn't retry for a longer match in nested definitions
|
|
'font': '<font-short-tweak-no-pct>? <font-short-core> | ' +
|
|
'[ <font-short-tweak-no-pct> || <pct> ]? <font-short-core> | ' +
|
|
'caption | icon | menu | message-box | small-caption | status-bar',
|
|
'font-family': '<font-family>',
|
|
'font-feature-settings': '<feature-tag-value># | normal',
|
|
'font-kerning': 'auto | normal | none',
|
|
'font-language-override': 'normal | <string>',
|
|
'font-optical-sizing': 'auto | none',
|
|
'font-palette': 'none | normal | light | dark | <ident>',
|
|
'font-size': '<font-size>',
|
|
'font-size-adjust': '<number> | none',
|
|
'font-stretch': '<font-stretch>',
|
|
'font-style': '<font-style>',
|
|
'font-synthesis': 'none | [ weight || style ]',
|
|
'font-synthesis-style': 'auto | none',
|
|
'font-synthesis-weight': 'auto | none',
|
|
'font-synthesis-small-caps': 'auto | none',
|
|
'font-variant': '<font-variant>',
|
|
'font-variant-alternates': '<font-variant-alternates> | normal',
|
|
'font-variant-caps': '<font-variant-caps> | normal',
|
|
'font-variant-east-asian': '<font-variant-east-asian> | normal',
|
|
'font-variant-emoji': 'auto | text | emoji | unicode',
|
|
'font-variant-ligatures': '<font-variant-ligatures> | normal | none',
|
|
'font-variant-numeric': '<font-variant-numeric> | normal',
|
|
'font-variant-position': 'normal | sub | super',
|
|
'font-variation-settings': 'normal | [ <string> <number> ]#',
|
|
'font-weight': '<font-weight>',
|
|
'forced-color-adjust': 'auto | none',
|
|
'-ms-flex-align': 1,
|
|
'-ms-flex-order': 1,
|
|
'-ms-flex-pack': 1,
|
|
|
|
'gap': '<row-gap> <column-gap>?',
|
|
'glyph-orientation-horizontal': '<glyph-angle>',
|
|
'glyph-orientation-vertical': 'auto | <glyph-angle>',
|
|
|
|
'grid': '<grid-template> | <grid-template-rows> / [ auto-flow && dense? ] <grid-auto-columns>? | ' +
|
|
'[ auto-flow && dense? ] <grid-auto-rows>? / <grid-template-columns>',
|
|
'grid-area': '<grid-line> [ / <grid-line> ]{0,3}',
|
|
'grid-auto-columns': '<grid-auto-columns>',
|
|
'grid-auto-flow': '[ row | column ] || dense',
|
|
'grid-auto-rows': '<grid-auto-rows>',
|
|
'grid-column': '<grid-line> [ / <grid-line> ]?',
|
|
'grid-column-start': '<grid-line>',
|
|
'grid-column-end': '<grid-line>',
|
|
'grid-row': '<grid-line> [ / <grid-line> ]?',
|
|
'grid-row-start': '<grid-line>',
|
|
'grid-row-end': '<grid-line>',
|
|
'grid-template': 'none | [ <grid-template-rows> / <grid-template-columns> ] | ' +
|
|
'[ <line-names>? <string> <track-size>? <line-names>? ]+ [ / <explicit-track-list> ]?',
|
|
'grid-template-areas': 'none | <string>+',
|
|
'grid-template-columns': '<grid-template-columns>',
|
|
'grid-template-rows': '<grid-template-rows>',
|
|
'grid-row-gap': '<row-gap>',
|
|
'grid-column-gap': '<column-gap>',
|
|
'grid-gap': '<row-gap> <column-gap>?',
|
|
|
|
'hanging-punctuation': 'none | [ first || [ force-end | allow-end ] || last ]',
|
|
'height': 'auto | <width-height>',
|
|
'hyphenate-after': '<integer> | auto',
|
|
'hyphenate-before': '<integer> | auto',
|
|
'hyphenate-character': '<string> | auto',
|
|
'hyphenate-lines': 'no-limit | <integer>',
|
|
'hyphenate-resource': 1,
|
|
'hyphens': 'none | manual | auto',
|
|
|
|
'icon': 1,
|
|
'image-orientation': 'from-image | none | [ <angle> || flip ]',
|
|
'image-rendering': 'auto | smooth | high-quality | crisp-edges | pixelated | ' +
|
|
'optimizeSpeed | optimizeQuality | -webkit-optimize-contrast',
|
|
'image-resolution': 1,
|
|
'ime-mode': 'auto | normal | active | inactive | disabled',
|
|
'inline-box-align': 'last | <integer>',
|
|
'inline-size': '<width>',
|
|
'inset': '<width>{1,4}',
|
|
'inset-block': '<width>{1,2}',
|
|
'inset-block-end': '<width>',
|
|
'inset-block-start': '<width>',
|
|
'inset-inline': '<width>{1,2}',
|
|
'inset-inline-end': '<width>',
|
|
'inset-inline-start': '<width>',
|
|
'isolation': 'auto | isolate',
|
|
|
|
'justify-content': '<justify-content>',
|
|
'justify-items': 'normal | stretch | <baseline-position> | ' +
|
|
'[ <overflow-position>? <self-position> ] | ' +
|
|
'[ legacy || [ left | right | center ] ]',
|
|
'justify-self': '<justify-self>',
|
|
|
|
'kerning': 'auto | <length>',
|
|
|
|
'left': '<width>',
|
|
'letter-spacing': '<length> | normal',
|
|
'line-height': '<line-height>',
|
|
'line-break': 'auto | loose | normal | strict | anywhere',
|
|
'line-stacking': 1,
|
|
'line-stacking-ruby': 'exclude-ruby | include-ruby',
|
|
'line-stacking-shift': 'consider-shifts | disregard-shifts',
|
|
'line-stacking-strategy': 'inline-line-height | block-line-height | max-height | grid-height',
|
|
'list-style': 1,
|
|
'list-style-image': '<uri> | none',
|
|
'list-style-position': 'inside | outside',
|
|
'list-style-type': '<string> | disc | circle | square | decimal | decimal-leading-zero | ' +
|
|
'lower-roman | upper-roman | lower-greek | lower-latin | upper-latin | armenian | ' +
|
|
'georgian | lower-alpha | upper-alpha | none',
|
|
|
|
'margin': '<width>{1,4}',
|
|
'margin-bottom': '<width>',
|
|
'margin-left': '<width>',
|
|
'margin-right': '<width>',
|
|
'margin-top': '<width>',
|
|
'margin-block': '<width>{1,2}',
|
|
'margin-block-end': '<width>',
|
|
'margin-block-start': '<width>',
|
|
'margin-inline': '<width>{1,2}',
|
|
'margin-inline-end': '<width>',
|
|
'margin-inline-start': '<width>',
|
|
'mark': 1,
|
|
'mark-after': 1,
|
|
'mark-before': 1,
|
|
'marker': 1,
|
|
'marker-end': 1,
|
|
'marker-mid': 1,
|
|
'marker-start': 1,
|
|
'marks': 1,
|
|
'marquee-direction': 1,
|
|
'marquee-play-count': 1,
|
|
'marquee-speed': 1,
|
|
'marquee-style': 1,
|
|
'mask': 1,
|
|
'mask-image': '[ none | <image> | <uri> ]#',
|
|
'max-height': 'none | <width-height>',
|
|
'max-width': 'none | <width-height>',
|
|
'min-height': 'auto | <width-height>',
|
|
'min-width': 'auto | <width-height>',
|
|
'max-block-size': '<len-pct> | none',
|
|
'max-inline-size': '<len-pct> | none',
|
|
'min-block-size': '<len-pct>',
|
|
'min-inline-size': '<len-pct>',
|
|
'mix-blend-mode': '<blend-mode>',
|
|
'move-to': 1,
|
|
|
|
'nav-down': 1,
|
|
'nav-index': 1,
|
|
'nav-left': 1,
|
|
'nav-right': 1,
|
|
'nav-up': 1,
|
|
|
|
'object-fit': 'fill | contain | cover | none | scale-down',
|
|
'object-overflow': 'clip | visible',
|
|
'object-position': '<position>',
|
|
'object-view-box': 'none | <inset> | <rect> | <xywh>',
|
|
'opacity': '<opacity-value> | <pct>',
|
|
'order': '<integer>',
|
|
'orphans': '<integer>',
|
|
'outline': '[ <color> | invert ] || [ auto | <border-style> ] || <border-width>',
|
|
'outline-color': '<color> | invert',
|
|
'outline-offset': '<length>',
|
|
'outline-style': '<border-style> | auto',
|
|
'outline-width': '<border-width>',
|
|
'overflow': '<overflow>{1,2}',
|
|
'overflow-anchor': 'auto | none',
|
|
'overflow-block': '<overflow>',
|
|
'overflow-clip-margin': 'visual-box | <len0+>',
|
|
'overflow-inline': '<overflow>',
|
|
'overflow-style': 1,
|
|
'overflow-wrap': 'normal | break-word | anywhere',
|
|
'overflow-x': '<overflow>',
|
|
'overflow-y': '<overflow>',
|
|
'overscroll-behavior': '<overscroll>{1,2}',
|
|
'overscroll-behavior-block': '<overscroll>',
|
|
'overscroll-behavior-inline': '<overscroll>',
|
|
'overscroll-behavior-x': '<overscroll>',
|
|
'overscroll-behavior-y': '<overscroll>',
|
|
|
|
'padding': '<len-pct0+>{1,4}',
|
|
'padding-block': '<len-pct0+>{1,2}',
|
|
'padding-block-end': '<len-pct0+>',
|
|
'padding-block-start': '<len-pct0+>',
|
|
'padding-bottom': '<len-pct0+>',
|
|
'padding-inline': '<len-pct0+>{1,2}',
|
|
'padding-inline-end': '<len-pct0+>',
|
|
'padding-inline-start': '<len-pct0+>',
|
|
'padding-left': '<len-pct0+>',
|
|
'padding-right': '<len-pct0+>',
|
|
'padding-top': '<len-pct0+>',
|
|
'page': 1,
|
|
'page-break-after': 'auto | always | avoid | left | right | recto | verso',
|
|
'page-break-before': 'auto | always | avoid | left | right | recto | verso',
|
|
'page-break-inside': 'auto | avoid',
|
|
'page-policy': 1,
|
|
'pause': 1,
|
|
'pause-after': 1,
|
|
'pause-before': 1,
|
|
'perspective': 'none | <len0+>',
|
|
'perspective-origin': '<position>',
|
|
'phonemes': 1,
|
|
'pitch': 1,
|
|
'pitch-range': 1,
|
|
'place-content': '<align-content> <justify-content>?',
|
|
'place-items': '[ normal | stretch | <baseline-position> | <self-position> ] ' +
|
|
'[ normal | stretch | <baseline-position> | <self-position> ]?',
|
|
'place-self': '<align-self> <justify-self>?',
|
|
'play-during': 1,
|
|
'pointer-events': 'auto | none | visiblePainted | visibleFill | visibleStroke | visible | ' +
|
|
'painted | fill | stroke | all',
|
|
'position': 'static | relative | absolute | fixed | sticky | -webkit-sticky',
|
|
'presentation-level': 1,
|
|
'punctuation-trim': 1,
|
|
|
|
'quotes': 1,
|
|
|
|
'rendering-intent': 1,
|
|
'resize': 'none | both | horizontal | vertical | block | inline',
|
|
'rest': 1,
|
|
'rest-after': 1,
|
|
'rest-before': 1,
|
|
'richness': 1,
|
|
'right': '<width>',
|
|
'rotate': 'none | [ x | y | z | <number>{3} ]? && <angle>',
|
|
'rotation': 1,
|
|
'rotation-point': 1,
|
|
'row-gap': '<row-gap>',
|
|
'ruby-align': 1,
|
|
'ruby-overhang': 1,
|
|
'ruby-position': 1,
|
|
'ruby-span': 1,
|
|
|
|
'scale': 'none | <num-pct>{1,3}',
|
|
|
|
'scroll-behavior': 'auto | smooth',
|
|
'scroll-margin': '<length>{1,4}',
|
|
'scroll-margin-bottom': '<length>',
|
|
'scroll-margin-left': '<length>',
|
|
'scroll-margin-right': '<length>',
|
|
'scroll-margin-top': '<length>',
|
|
'scroll-margin-block': '<length>{1,2}',
|
|
'scroll-margin-block-end': '<length>',
|
|
'scroll-margin-block-start': '<length>',
|
|
'scroll-margin-inline': '<length>{1,2}',
|
|
'scroll-margin-inline-end': '<length>',
|
|
'scroll-margin-inline-start': '<length>',
|
|
'scroll-padding': '<width>{1,4}',
|
|
'scroll-padding-left': '<width>',
|
|
'scroll-padding-right': '<width>',
|
|
'scroll-padding-top': '<width>',
|
|
'scroll-padding-bottom': '<width>',
|
|
'scroll-padding-block': '<width>{1,2}',
|
|
'scroll-padding-block-end': '<width>',
|
|
'scroll-padding-block-start': '<width>',
|
|
'scroll-padding-inline': '<width>{1,2}',
|
|
'scroll-padding-inline-end': '<width>',
|
|
'scroll-padding-inline-start': '<width>',
|
|
'scroll-snap-align': '[ none | start | end | center ]{1,2}',
|
|
'scroll-snap-stop': 'normal | always',
|
|
'scroll-snap-type': 'none | [ x | y | block | inline | both ] [ mandatory | proximity ]?',
|
|
|
|
'scrollbar-color': 'auto | dark | light | <color>{2}',
|
|
'scrollbar-gutter': 'auto | stable && both-edges?',
|
|
'scrollbar-width': 'auto | thin | none',
|
|
'shape-inside': 'auto | outside-shape | [ <basic-shape> || shape-box ] | <image> | display',
|
|
'shape-rendering': 'auto | optimizeSpeed | crispEdges | geometricPrecision',
|
|
'size': 1,
|
|
'speak': 'normal | none | spell-out',
|
|
'speak-header': 'once | always',
|
|
'speak-numeral': 'digits | continuous',
|
|
'speak-punctuation': 'code | none',
|
|
'speech-rate': 1,
|
|
'stop-color': 1,
|
|
'stop-opacity': '<opacity-value>',
|
|
'stress': 1,
|
|
'string-set': 1,
|
|
'stroke': '<paint>',
|
|
'stroke-dasharray': 'none | <dasharray>',
|
|
'stroke-dashoffset': '<len-pct> | <number>',
|
|
'stroke-linecap': 'butt | round | square',
|
|
'stroke-linejoin': 'miter | miter-clip | round | bevel | arcs',
|
|
'stroke-miterlimit': '<num0+>',
|
|
'stroke-opacity': '<opacity-value>',
|
|
'stroke-width': '<len-pct> | <number>',
|
|
|
|
'table-layout': 'auto | fixed',
|
|
'tab-size': '<number> | <length>',
|
|
'target': 1,
|
|
'target-name': 1,
|
|
'target-new': 1,
|
|
'target-position': 1,
|
|
'text-align': '<text-align> | justify-all',
|
|
'text-align-all': '<text-align>',
|
|
'text-align-last': '<text-align> | auto',
|
|
'text-anchor': 'start | middle | end',
|
|
'text-decoration': '<text-decoration-line> || <text-decoration-style> || <color>',
|
|
'text-decoration-color': '<color>',
|
|
'text-decoration-line': '<text-decoration-line>',
|
|
'text-decoration-skip': 'none | ' +
|
|
'[ objects || [ spaces | [ leading-spaces || trailing-spaces ] ] || edges || box-decoration ]',
|
|
'text-decoration-style': '<text-decoration-style>',
|
|
'text-emphasis': '<text-emphasis-style> || <color>',
|
|
'text-emphasis-color': '<color>',
|
|
'text-emphasis-style': '<text-emphasis-style>',
|
|
'text-emphasis-position': '[ over | under ] && [ right | left ]?',
|
|
'text-height': 1,
|
|
'text-indent': '<len-pct> && hanging? && each-line?',
|
|
'text-justify': 'auto | none | inter-word | inter-character',
|
|
'text-outline': 1,
|
|
'text-overflow': 'clip | ellipsis',
|
|
'text-rendering': 'auto | optimizeSpeed | optimizeLegibility | geometricPrecision',
|
|
'text-shadow': 'none | [ <color>? && <length>{2,3} ]#',
|
|
'text-transform': 'none | [ capitalize | uppercase | lowercase ] || full-width || full-size-kana',
|
|
'text-underline-position': 'auto | [ under || [ left | right ] ]',
|
|
'text-wrap': 'normal | none | avoid',
|
|
'top': '<width>',
|
|
'touch-action': 'auto | none | ' +
|
|
'pan-x | pan-y | pan-left | pan-right | pan-up | pan-down | manipulation',
|
|
'transform': 'none | <transform-function>+',
|
|
'transform-box': 'border-box | fill-box | view-box',
|
|
'transform-origin': '<transform-origin>',
|
|
'transform-style': 'flat | preserve-3d',
|
|
'transition': '<transition>#',
|
|
'transition-delay': '<time>#',
|
|
'transition-duration': '<time>#',
|
|
'transition-property': 'none | [ all | <ident> ]#',
|
|
'transition-timing-function': '<single-timing-function>#',
|
|
'translate': 'none | <len-pct> [ <len-pct> <length>? ]?',
|
|
|
|
'unicode-range': '<unicode-range>#',
|
|
'unicode-bidi': 'normal | embed | isolate | bidi-override | isolate-override | plaintext',
|
|
'user-modify': 'read-only | read-write | write-only',
|
|
'user-select': 'auto | text | none | contain | all',
|
|
|
|
'vertical-align': 'auto | use-script | baseline | sub | super | top | text-top | ' +
|
|
'central | middle | bottom | text-bottom | <len-pct>',
|
|
'visibility': 'visible | hidden | collapse',
|
|
'voice-balance': 1,
|
|
'voice-duration': 1,
|
|
'voice-family': 1,
|
|
'voice-pitch': 1,
|
|
'voice-pitch-range': 1,
|
|
'voice-rate': 1,
|
|
'voice-stress': 1,
|
|
'voice-volume': 1,
|
|
'volume': 1,
|
|
|
|
'white-space': 'normal | pre | nowrap | pre-wrap | break-spaces | pre-line',
|
|
'white-space-collapse': 1,
|
|
'widows': '<integer>',
|
|
'width': 'auto | <width-height>',
|
|
'will-change': '<will-change>',
|
|
'word-break': 'normal | keep-all | break-all | break-word',
|
|
'word-spacing': '<length> | normal',
|
|
'word-wrap': 'normal | break-word | anywhere',
|
|
'writing-mode': 'horizontal-tb | vertical-rl | vertical-lr | ' +
|
|
'lr-tb | rl-tb | tb-rl | bt-rl | tb-lr | bt-lr | lr-bt | rl-bt | lr | rl | tb',
|
|
|
|
'z-index': '<integer> | auto',
|
|
'zoom': '<number> | <pct> | normal',
|
|
|
|
// nonstandard https://compat.spec.whatwg.org/
|
|
'-webkit-box-reflect': '[ above | below | right | left ]? <length>? <image>?',
|
|
'-webkit-text-fill-color': '<color>',
|
|
'-webkit-text-stroke': '<border-width> || <color>',
|
|
'-webkit-text-stroke-color': '<color>',
|
|
'-webkit-text-stroke-width': '<border-width>',
|
|
};
|
|
|
|
const ScopedProperties = {
|
|
'@font-face': Object.assign({
|
|
'ascent-override': '[ normal | <pct0+> ]{1,2}',
|
|
'descent-override': '[ normal | <pct0+> ]{1,2}',
|
|
'font-display': 'auto | block | swap | fallback | optional',
|
|
'font-stretch': 'auto | <font-stretch>{1,2}',
|
|
'font-style': 'auto | normal | italic | oblique <angle>{0,2}',
|
|
'font-weight': 'auto | [ normal | bold | <num1-1000> ]{1,2}',
|
|
'line-gap-override': '[ normal | <pct0+> ]{1,2}',
|
|
'size-adjust': '<pct0+>',
|
|
'src': '[ url() [ format( <string># ) ]? | local( <family-name> ) ]#',
|
|
}, ...[
|
|
'font-family',
|
|
'font-size',
|
|
'font-variant',
|
|
'font-variation-settings',
|
|
'unicode-range',
|
|
].map(p => ({[p]: Properties[p]}))),
|
|
|
|
'@font-palette-values': Object.assign({
|
|
'base-palette': 'light | dark | <int0+>',
|
|
'override-colors': '[ <int0+> <color> ]#',
|
|
}, ...[
|
|
'font-family',
|
|
].map(p => ({[p]: Properties[p]}))),
|
|
};
|
|
|
|
for (const [k, reps] of Object.entries({
|
|
'border': '{1,4}',
|
|
'border-bottom': '',
|
|
'border-left': '',
|
|
'border-right': '',
|
|
'border-top': '',
|
|
'border-block': '{1,2}',
|
|
'border-block-end': '',
|
|
'border-block-start': '',
|
|
'border-inline': '{1,2}',
|
|
'border-inline-end': '',
|
|
'border-inline-start': '',
|
|
})) {
|
|
Properties[k] = '<border-shorthand>';
|
|
Properties[`${k}-color`] = '<color>' + reps;
|
|
Properties[`${k}-style`] = '<border-style>' + reps;
|
|
Properties[`${k}-width`] = '<border-width>' + reps;
|
|
}
|
|
|
|
//#endregion
|
|
//#region Types
|
|
|
|
const TYPES = /** @namespace Parser */ {
|
|
DEFAULT_TYPE: 0,
|
|
COMBINATOR_TYPE: 1,
|
|
MEDIA_FEATURE_TYPE: 2,
|
|
MEDIA_QUERY_TYPE: 3,
|
|
PROPERTY_NAME_TYPE: 4,
|
|
PROPERTY_VALUE_TYPE: 5,
|
|
PROPERTY_VALUE_PART_TYPE: 6,
|
|
SELECTOR_TYPE: 7,
|
|
SELECTOR_PART_TYPE: 8,
|
|
SELECTOR_SUB_PART_TYPE: 9,
|
|
};
|
|
const UNITS = JSON.parse(`{${Object.entries({
|
|
angle: 'deg,grad,rad,turn',
|
|
frequency: 'hz,khz',
|
|
length: 'cap,ch,em,ex,ic,lh,rlh,rem,' +
|
|
'cm,mm,in,pc,pt,px,q,' +
|
|
'fr,' + // grids
|
|
'vb,vi,vh,vw,vmin,vmax'.replace(/\w+/g, '$&,d$&,l$&,s$&'),
|
|
resolution: 'dpcm,dpi,dppx,x',
|
|
time: 'ms,s',
|
|
}).map(([type, units]) => units.replace(/\w+/g, `"$&":"${type}"`)).join(',')}}`);
|
|
// Sticky `y` flag must be used in expressions used with peekTest and readMatch
|
|
const rxIdentStart = /[-\\_a-zA-Z\u00A0-\uFFFF]/u;
|
|
const rxNameChar = /[-\\_\da-zA-Z\u00A0-\uFFFF]/u;
|
|
const rxNameCharNoEsc = /[-_\da-zA-Z\u00A0-\uFFFF]+/yu; // must not match \\
|
|
const rxUnquotedUrlCharNoEsc = /[-!#$%&*-[\]-~\u00A0-\uFFFF]+/yu; // must not match \\
|
|
const rxVendorPrefix = /^-(webkit|moz|ms|o)-(.+)/i;
|
|
const rxCalc = /^(?:-(webkit|moz|ms|o)-)?(calc|min|max|clamp)\(/i;
|
|
const lowercaseCache = new Map();
|
|
|
|
//#endregion
|
|
//#region ValidationTypes - definitions
|
|
|
|
/** Allowed syntax: text, |, <syntax>, func() */
|
|
const VTSimple = {
|
|
'<absolute-size>': 'xx-small | x-small | small | medium | large | x-large | xx-large',
|
|
'<animateable-feature>': 'scroll-position | contents | <animateable-feature-name>',
|
|
'<animateable-feature-name>': p => vtIsIdent(p) && !isGlobalKeyword(p) &&
|
|
!/^(will-change|auto|scroll-position|contents)$/i.test(p),
|
|
'<angle>': p => p.type === 'angle' || p.isCalc,
|
|
'<angle-or-0>': p => p.text === '0' || p.type === 'angle' || p.isCalc,
|
|
'<attr>': vtIsAttr,
|
|
'<attachment>': 'scroll | fixed | local',
|
|
'<bg-image>': '<image> | none',
|
|
'<blend-mode>': 'normal | multiply | screen | overlay | darken | lighten | color-dodge | ' +
|
|
'color-burn | hard-light | soft-light | difference | exclusion | hue | ' +
|
|
'saturation | color | luminosity | plus-darker | plus-lighter',
|
|
'<border-style>': 'none | ' +
|
|
'hidden | dotted | dashed | solid | double | groove | ridge | inset | outset',
|
|
'<border-width>': '<length> | thin | medium | thick',
|
|
'<box>': 'padding-box | border-box | content-box',
|
|
'<clip-source>': '<uri>',
|
|
'<column-gap>': 'normal | <len-pct>',
|
|
'<content-distribution>': 'space-between | space-around | space-evenly | stretch',
|
|
'<content-position>': 'center | start | end | flex-start | flex-end',
|
|
'<display-box>': 'contents | none',
|
|
'<display-inside>': 'flow | flow-root | table | flex | grid | ruby',
|
|
'<display-internal>': 'table-row-group | table-header-group | table-footer-group | ' +
|
|
'table-row | table-cell | table-column-group | table-column | table-caption | ' +
|
|
'ruby-base | ruby-text | ruby-base-container | ruby-text-container',
|
|
'<display-legacy>': 'inline-block | inline-table | inline-flex | inline-grid',
|
|
'<display-outside>': 'block | inline | run-in',
|
|
'<feature-tag-value>': p => p.type === 'function' && /^[A-Z0-9]{4}$/i.test(p),
|
|
'<flex>': p => p.type === 'grid' && p.value >= 0 || p.isCalc,
|
|
'<flex-basis>': '<width>',
|
|
'<flex-direction>': 'row | row-reverse | column | column-reverse',
|
|
'<flex-grow>': '<number>',
|
|
'<flex-shrink>': '<number>',
|
|
'<flex-wrap>': 'nowrap | wrap | wrap-reverse',
|
|
'<font-size>': '<absolute-size> | <relative-size> | <len-pct0+>',
|
|
'<font-stretch>': '<font-stretch-named> | <pct>',
|
|
'<font-stretch-named>': 'normal | ultra-condensed | extra-condensed | condensed | ' +
|
|
'semi-condensed | semi-expanded | expanded | extra-expanded | ultra-expanded',
|
|
'<font-variant-caps>':
|
|
'small-caps | all-small-caps | petite-caps | all-petite-caps | unicase | titling-caps',
|
|
'<font-variant-css21>': 'normal | small-caps',
|
|
'<font-weight>': 'normal | bold | bolder | lighter | <num1-1000>',
|
|
'<generic-family>': 'serif | sans-serif | cursive | fantasy | monospace | system-ui | ' +
|
|
'emoji | math | fangsong | ui-serif | ui-sans-serif | ui-monospace | ui-rounded',
|
|
'<geometry-box>': '<shape-box> | fill-box | stroke-box | view-box',
|
|
'<glyph-angle>': p => p.type === 'angle' && p.units === 'deg',
|
|
'<gradient>': 'radial-gradient() | linear-gradient() | conic-gradient() | gradient() | ' +
|
|
'repeating-radial-gradient() | repeating-linear-gradient() | repeating-conic-gradient() | ' +
|
|
'repeating-gradient()',
|
|
'<hex-color>': p => p.tokenType === Tokens.HASH, //eslint-disable-line no-use-before-define
|
|
'<icccolor>': 'cielab() | cielch() | cielchab() | icc-color() | icc-named-color()',
|
|
'<ident>': vtIsIdent,
|
|
'<ident-for-grid>': p => vtIsIdent(p) && !isGlobalKeyword(p.value) &&
|
|
!/^(span|auto|default)$/i.test(p.value),
|
|
'<ident-not-generic-family>': p => vtIsIdent(p) && !VTSimple['<generic-family>'](p),
|
|
'<ident-not-none>': p => vtIsIdent(p) && !lowerCmp(p.value, 'none'),
|
|
'<ie-function>': p => p.tokenType === Tokens.IE_FUNCTION, //eslint-disable-line no-use-before-define
|
|
'<image>': '<uri> | <gradient> | cross-fade()',
|
|
'<inflexible-breadth>': '<len-pct> | min-content | max-content | auto',
|
|
'<inset>': 'inset( <len-pct>{1,4} <border-radius-round>? )',
|
|
'<integer>': p => p.isInt || p.isCalc,
|
|
'<int0+>': p => p.isInt && p.value >= 0 || p.isCalc,
|
|
'<int1+>': p => p.isInt && p.value > 0 || p.isCalc,
|
|
'<length>': vtIsLength,
|
|
'<len0+>': p =>
|
|
p.value >= 0 && vtIsLength(p) || p.isCalc,
|
|
'<len-pct>': p => vtIsLength(p) || vtIsPct(p),
|
|
'<len-pct0+>': p =>
|
|
p.value >= 0 && (p.type === 'percentage' || vtIsLength(p)) || p.isCalc,
|
|
'<len-pct-side>': '<len-pct> | closest-side | farthest-side',
|
|
'<line>': p => p.isInt,
|
|
'<line-height>': '<number> | <len-pct> | normal',
|
|
'<line-names>': p =>
|
|
p.tokenType === Tokens.LBRACKET && // eslint-disable-line no-use-before-define
|
|
p.text.endsWith(']') && (
|
|
!p.expr ||
|
|
!p.expr.parts.length ||
|
|
p.expr.parts.every(VTSimple['<ident-for-grid>'], VTSimple)
|
|
),
|
|
//eslint-disable-next-line no-use-before-define
|
|
'<named-color>': p => p.text in Colors || ColorsLC.has(lower(p.text)),
|
|
'<number>': p => p.type === 'number' || p.isCalc,
|
|
'<num0+>': p =>
|
|
p.value >= 0 && p.type === 'number' || p.isCalc,
|
|
'<num1-1000>': p => (p.type === 'number' && p.value >= 1 && p.value <= 1000) || p.isCalc,
|
|
'<num-pct>': p => p.type === 'number' || p.type === 'percentage' || p.isCalc,
|
|
'<num-pct0+>': p =>
|
|
p.value >= 0 && (p.type === 'number' || p.type === 'percentage') || p.isCalc,
|
|
'<opacity-value>': p => p.type === 'number' && p.value >= 0 && p.value <= 1 || p.isCalc,
|
|
'<overflow>': 'visible | hidden | clip | scroll | auto',
|
|
'<overflow-position>': 'unsafe | safe',
|
|
'<pct>': vtIsPct,
|
|
'<pct0+>': p =>
|
|
p.value >= 0 && p.type === 'percentage' || p.isCalc,
|
|
'<rect>': 'rect( [ <length> | auto ]#{4} <border-radius-round>? )',
|
|
'<relative-size>': 'smaller | larger',
|
|
'<row-gap>': '<column-gap>',
|
|
'<self-position>': 'center | start | end | self-start | self-end | flex-start | flex-end',
|
|
'<shape-box>': '<box> | margin-box',
|
|
'<single-animation-direction>': 'normal | reverse | alternate | alternate-reverse',
|
|
'<single-animation-fill-mode>': 'none | forwards | backwards | both',
|
|
'<single-animation-name>': p => vtIsIdent(p) && !isGlobalKeyword(p) &&
|
|
/^-?[a-z_][-a-z0-9_]+$/i.test(p),
|
|
'<string>': p => p.type === 'string',
|
|
'<text-align>': 'start | end | left | right | center | justify | match-parent',
|
|
'<text-decoration-style>': 'solid | double | dotted | dashed | wavy',
|
|
'<time>': p => p.type === 'time',
|
|
'<track-breadth>': '<len-pct> | <flex> | min-content | max-content | auto',
|
|
'<unicode-range>': p => /^U\+[0-9a-f?]{1,6}(-[0-9a-f?]{1,6})?\s*$/i.test(p),
|
|
'<unit>': p => p.text === '%' || p in UNITS || lower(p) in UNITS,
|
|
'<uri>': p => p.type === 'uri',
|
|
'<width>': p => vtIsLength(p) || vtIsPct(p) || lowerCmp(p.text, 'auto'),
|
|
'<xywh>': 'xywh( <len-pct>{2} <len-pct0+>{2} <border-radius-round>? )',
|
|
};
|
|
|
|
const VTComplex = {
|
|
'<align-content>': 'normal | <baseline-position> | <content-distribution> | ' +
|
|
'<overflow-position>? <content-position>',
|
|
'<align-self>':
|
|
'auto | normal | stretch | <baseline-position> | <overflow-position>? <self-position>',
|
|
'<auto-repeat>':
|
|
'repeat( [ auto-fill | auto-fit ] , [ <line-names>? <fixed-size> ]+ <line-names>? )',
|
|
'<auto-track-list>':
|
|
'[ <line-names>? [ <fixed-size> | <fixed-repeat> ] ]* <line-names>? <auto-repeat> ' +
|
|
'[ <line-names>? [ <fixed-size> | <fixed-repeat> ] ]* <line-names>?',
|
|
'<azimuth>':
|
|
'<angle> | [ [ left-side | far-left | left | center-left | center | center-right | ' +
|
|
'right | far-right | right-side ] || behind ] | leftwards | rightwards',
|
|
'<baseline-position>': '[ first | last ]? baseline',
|
|
'<basic-shape>':
|
|
'<inset> | ' +
|
|
'circle( <len-pct-side>? [ at <position> ]? ) | ' +
|
|
'ellipse( [ <len-pct-side>{2} ]? [ at <position> ]? ) | ' +
|
|
'path( [ [ nonzero | evenodd ] , ]? <string> ) | ' +
|
|
'polygon( [ [ nonzero | evenodd | inherit ] , ]? [ <len-pct> <len-pct> ]# )',
|
|
'<bg-layer>':
|
|
'<bg-image> || <bg-position> [ / <bg-size> ]? || <repeat-style> || <attachment> || <box>{1,2}',
|
|
'<bg-position>':
|
|
'[ center | [ left | right ] <len-pct>? ] && [ center | [ top | bottom ] <len-pct>? ] | ' +
|
|
'[ left | center | right | <len-pct> ] [ top | center | bottom | <len-pct> ] | ' +
|
|
'[ left | center | right | top | bottom | <len-pct> ]',
|
|
'<bg-size>': '[ <len-pct> | auto ]{1,2} | cover | contain',
|
|
'<border-image-outset>': '[ <length> | <number> ]{1,4}',
|
|
'<border-image-repeat>': '[ stretch | repeat | round | space ]{1,2}',
|
|
'<border-image-slice>': Matcher =>
|
|
// [<number> | <pct>]{1,4} && fill?
|
|
// but 'fill' can appear between any of the numbers
|
|
Matcher.many(
|
|
[true],
|
|
Matcher.parse('<num-pct0+>'),
|
|
Matcher.parse('<num-pct0+>'),
|
|
Matcher.parse('<num-pct0+>'),
|
|
Matcher.parse('<num-pct0+>'),
|
|
'fill'),
|
|
'<border-image-width>': '[ <len-pct> | <number> | auto ]{1,4}',
|
|
'<border-radius>': '<len-pct0+>{1,4} [ / <len-pct0+>{1,4} ]?',
|
|
'<border-radius-round>': 'round <border-radius>',
|
|
'<border-shorthand>': '<border-width> || <border-style> || <color>',
|
|
'<box-shadow>': 'none | <shadow>#',
|
|
'<clip-path>': '<basic-shape> || <geometry-box>',
|
|
'<color>': '<hex-color> | <named-color> | rgb( <rgb-color> ) | rgba( <rgb-color> ) | ' +
|
|
'hsl( <hsl-color> ) | hsla( <hsl-color> ) | hwb( <hwb-color> )',
|
|
'<content-list>':
|
|
'[ <string> | <image> | <attr> | ' +
|
|
'content( text | before | after | first-letter | marker ) | ' +
|
|
'counter() | counters() | leader() | ' +
|
|
'[ open-quote | close-quote | no-open-quote | no-close-quote ] | ' +
|
|
'[ target-counter() | target-counters() | target-text() ] ]+',
|
|
'<counter>': '[ <ident-not-none> <integer>? ]+ | none',
|
|
'<cubic-bezier-timing-function>': 'ease | ease-in | ease-out | ease-in-out | ' +
|
|
'cubic-bezier( <number>#{4} )',
|
|
'<dasharray>': Matcher =>
|
|
Matcher.parse('<len-pct0+> | <num0+>')
|
|
.braces(1, Infinity, '#', Matcher.parse(',').braces(0, 1, '?')),
|
|
'<display-listitem>': '<display-outside>? && [ flow | flow-root ]? && list-item',
|
|
'<explicit-track-list>': '[ <line-names>? <track-size> ]+ <line-names>?',
|
|
'<family-name>': '<string> | <ident-not-generic-family> <ident>*',
|
|
// https://drafts.fxtf.org/filter-effects/#supported-filter-functions
|
|
// Value may be omitted in which case the default is used
|
|
'<filter-function>':
|
|
'blur( <length>? ) | ' +
|
|
'brightness( <num-pct>? ) | ' +
|
|
'contrast( <num-pct>? ) | ' +
|
|
'drop-shadow( [ <length>{2,3} && <color>? ]? ) | ' +
|
|
'grayscale( <num-pct>? ) | ' +
|
|
'hue-rotate( <angle-or-0>? ) | ' +
|
|
'invert( <num-pct>? ) | ' +
|
|
'opacity( <num-pct>? ) | ' +
|
|
'saturate( <num-pct>? ) | ' +
|
|
'sepia( <num-pct>? )',
|
|
'<filter-function-list>': '[ <filter-function> | <uri> ]+',
|
|
'<final-bg-layer>': '<color> || <bg-image> || <bg-position> [ / <bg-size> ]? || ' +
|
|
'<repeat-style> || <attachment> || <box>{1,2}',
|
|
'<fixed-repeat>':
|
|
'repeat( [ <int1+> ] , [ <line-names>? <fixed-size> ]+ <line-names>? )',
|
|
'<fixed-size>': '<len-pct> | ' +
|
|
'minmax( <len-pct> , <track-breadth> ) | ' +
|
|
'minmax( <inflexible-breadth> , <len-pct> )',
|
|
'<flex-shorthand>': 'none | [ <flex-grow> <flex-shrink>? || <flex-basis> ]',
|
|
'<font-family>': '[ <generic-family> | <family-name> ]#',
|
|
'<font-style>': 'normal | italic | oblique <angle>?',
|
|
'<font-short-core>': '<font-size> [ / <line-height> ]? <font-family>',
|
|
'<font-short-tweak-no-pct>':
|
|
'<font-style> || <font-variant-css21> || <font-weight> || <font-stretch-named>',
|
|
'<font-variant-alternates>': 'stylistic() || historical-forms || styleset() || ' +
|
|
'character-variant() || swash() || ornaments() || annotation()',
|
|
'<font-variant-ligatures>': '[ common-ligatures | no-common-ligatures ] || ' +
|
|
'[ discretionary-ligatures | no-discretionary-ligatures ] || ' +
|
|
'[ historical-ligatures | no-historical-ligatures ] || ' +
|
|
'[ contextual | no-contextual ]',
|
|
'<font-variant-numeric>': '[ lining-nums | oldstyle-nums ] || ' +
|
|
'[ proportional-nums | tabular-nums ] || ' +
|
|
'[ diagonal-fractions | stacked-fractions ] || ' +
|
|
'ordinal || slashed-zero',
|
|
'<font-variant-east-asian>': '[ jis78 | jis83 | jis90 | jis04 | simplified | traditional ] || ' +
|
|
'[ full-width | proportional-width ] || ruby',
|
|
'<font-variant>': 'normal | none | [ ' +
|
|
'<font-variant-ligatures> || <font-variant-alternates> || ' +
|
|
'<font-variant-caps> || <font-variant-numeric> || <font-variant-east-asian> ]',
|
|
'<grid-auto-columns>': '<track-size>+',
|
|
'<grid-auto-rows>': '<track-size>+',
|
|
'<grid-line>': 'auto | [ <integer> && <ident-for-grid>? ] | <ident-for-grid> | ' +
|
|
'[ span && [ <integer> || <ident-for-grid> ] ]',
|
|
'<grid-template>': 'none | [ <grid-template-rows> / <grid-template-columns> ] | ' +
|
|
'[ <line-names>? <string> <track-size>? <line-names>? ]+ ' +
|
|
'[ / <explicit-track-list> ]?',
|
|
'<grid-template-columns>': 'none | <track-list> | <auto-track-list>',
|
|
'<grid-template-rows>': '<grid-template-columns>',
|
|
'<hsl-color>': '[ <number> | <angle> ] <pct>{2} [ / <num-pct0+> ]? | ' +
|
|
'[ <number> | <angle> ] , <pct>#{2} [ , <num-pct0+> ]?',
|
|
'<hwb-color>': '[ <number> | <angle> | none ] [ <pct> | none ] [ <pct> | none ] [ / [ <num-pct> | none ] ]?',
|
|
'<justify-content>': 'normal | <content-distribution> | ' +
|
|
'<overflow-position>? [ <content-position> | left | right ]',
|
|
'<justify-self>': 'auto | normal | stretch | <baseline-position> | <overflow-position>? ' +
|
|
'[ <self-position> | left | right ]',
|
|
'<overscroll>': 'contain | none | auto',
|
|
'<paint>': 'none | <color> | <uri> [ none | <color> ]? | context-fill | context-stroke',
|
|
// Because our `alt` combinator is ordered, we need to test these
|
|
// in order from longest possible match to shortest.
|
|
'<position>':
|
|
'[ [ left | right ] <len-pct> ] && [ [ top | bottom ] <len-pct> ] | ' +
|
|
'[ left | center | right | <len-pct> ] ' +
|
|
'[ top | center | bottom | <len-pct> ]? | ' +
|
|
'[ left | center | right ] || [ top | center | bottom ]',
|
|
'<repeat-style>': 'repeat-x | repeat-y | [ repeat | space | round | no-repeat ]{1,2}',
|
|
'<rgb-color>':
|
|
'[ <number>{3} | <pct>{3} ] [ / <num-pct0+> ]? | ' +
|
|
'[ <number>#{3} | <pct>#{3} ] [ , <num-pct0+> ]?',
|
|
'<shadow>': 'inset? && [ <length>{2,4} && <color>? ]',
|
|
'<single-timing-function>':
|
|
'linear | <cubic-bezier-timing-function> | <step-timing-function> | frames( <integer> )',
|
|
'<step-timing-function>': 'step-start | step-end | ' +
|
|
'steps( <integer> [ , [ jump-start | jump-end | jump-none | jump-both | start | end ] ]? )',
|
|
'<text-decoration-line>': 'none | [ underline || overline || line-through || blink ]',
|
|
'<text-emphasis-style>': 'none | ' +
|
|
'[ [ filled | open ] || [ dot | circle | double-circle | triangle | sesame ] ] | ' +
|
|
'<string>',
|
|
'<track-list>': '[ <line-names>? [ <track-size> | <track-repeat> ] ]+ <line-names>?',
|
|
'<track-repeat>': 'repeat( [ <int1+> ] , [ <line-names>? <track-size> ]+ <line-names>? )',
|
|
'<track-size>': '<track-breadth> | minmax( <inflexible-breadth> , <track-breadth> ) | ' +
|
|
'fit-content( <len-pct> )',
|
|
'<transform-function>':
|
|
'matrix( <number>#{6} ) | ' +
|
|
'matrix3d( <number>#{16} ) | ' +
|
|
'perspective( <len0+> | none ) | ' +
|
|
'rotate( <angle-or-0> | none ) | ' +
|
|
'rotate3d( <number>#{3} , <angle-or-0> ) | ' +
|
|
'rotateX( <angle-or-0> ) | ' +
|
|
'rotateY( <angle-or-0> ) | ' +
|
|
'rotateZ( <angle-or-0> ) | ' +
|
|
'scale( [ <num-pct> ]#{1,2} | none ) | ' +
|
|
'scale3d( <num-pct>#{3} ) | ' +
|
|
'scaleX( <num-pct> ) | ' +
|
|
'scaleY( <num-pct> ) | ' +
|
|
'scaleZ( <num-pct> ) | ' +
|
|
'skew( <angle-or-0> [ , <angle-or-0> ]? ) | ' +
|
|
'skewX( <angle-or-0> ) | ' +
|
|
'skewY( <angle-or-0> ) | ' +
|
|
'translate( <len-pct>#{1,2} | none ) | ' +
|
|
'translate3d( <len-pct>#{2} , <length> ) | ' +
|
|
'translateX( <len-pct> ) | ' +
|
|
'translateY( <len-pct> ) | ' +
|
|
'translateZ( <length> )',
|
|
'<transform-origin>': '[ left | center | right | <len-pct> ] ' +
|
|
'[ top | center | bottom | <len-pct> ] <length>? | ' +
|
|
'[ left | center | right | top | bottom | <len-pct> ] | ' +
|
|
'[ [ center | left | right ] && [ center | top | bottom ] ] <length>?',
|
|
'<transition>': '[ none | [ all | <ident> ]# ] || <time> || <single-timing-function> || <time>',
|
|
'<width-height>': '<len-pct> | min-content | max-content | ' +
|
|
'fit-content | fit-content( <len-pct> ) | -moz-available | -webkit-fill-available',
|
|
'<will-change>': 'auto | <animateable-feature>#',
|
|
};
|
|
|
|
//#endregion
|
|
//#region Colors
|
|
|
|
const Colors = Object.assign(Object.create(null), {
|
|
// 'currentColor' color keyword
|
|
// https://www.w3.org/TR/css3-color/#currentcolor
|
|
currentColor: '',
|
|
transparent: '#0000',
|
|
|
|
aliceblue: '#f0f8ff',
|
|
antiquewhite: '#faebd7',
|
|
aqua: '#00ffff',
|
|
aquamarine: '#7fffd4',
|
|
azure: '#f0ffff',
|
|
beige: '#f5f5dc',
|
|
bisque: '#ffe4c4',
|
|
black: '#000000',
|
|
blanchedalmond: '#ffebcd',
|
|
blue: '#0000ff',
|
|
blueviolet: '#8a2be2',
|
|
brown: '#a52a2a',
|
|
burlywood: '#deb887',
|
|
cadetblue: '#5f9ea0',
|
|
chartreuse: '#7fff00',
|
|
chocolate: '#d2691e',
|
|
coral: '#ff7f50',
|
|
cornflowerblue: '#6495ed',
|
|
cornsilk: '#fff8dc',
|
|
crimson: '#dc143c',
|
|
cyan: '#00ffff',
|
|
darkblue: '#00008b',
|
|
darkcyan: '#008b8b',
|
|
darkgoldenrod: '#b8860b',
|
|
darkgray: '#a9a9a9',
|
|
darkgrey: '#a9a9a9',
|
|
darkgreen: '#006400',
|
|
darkkhaki: '#bdb76b',
|
|
darkmagenta: '#8b008b',
|
|
darkolivegreen: '#556b2f',
|
|
darkorange: '#ff8c00',
|
|
darkorchid: '#9932cc',
|
|
darkred: '#8b0000',
|
|
darksalmon: '#e9967a',
|
|
darkseagreen: '#8fbc8f',
|
|
darkslateblue: '#483d8b',
|
|
darkslategray: '#2f4f4f',
|
|
darkslategrey: '#2f4f4f',
|
|
darkturquoise: '#00ced1',
|
|
darkviolet: '#9400d3',
|
|
deeppink: '#ff1493',
|
|
deepskyblue: '#00bfff',
|
|
dimgray: '#696969',
|
|
dimgrey: '#696969',
|
|
dodgerblue: '#1e90ff',
|
|
firebrick: '#b22222',
|
|
floralwhite: '#fffaf0',
|
|
forestgreen: '#228b22',
|
|
fuchsia: '#ff00ff',
|
|
gainsboro: '#dcdcdc',
|
|
ghostwhite: '#f8f8ff',
|
|
gold: '#ffd700',
|
|
goldenrod: '#daa520',
|
|
gray: '#808080',
|
|
grey: '#808080',
|
|
green: '#008000',
|
|
greenyellow: '#adff2f',
|
|
honeydew: '#f0fff0',
|
|
hotpink: '#ff69b4',
|
|
indianred: '#cd5c5c',
|
|
indigo: '#4b0082',
|
|
ivory: '#fffff0',
|
|
khaki: '#f0e68c',
|
|
lavender: '#e6e6fa',
|
|
lavenderblush: '#fff0f5',
|
|
lawngreen: '#7cfc00',
|
|
lemonchiffon: '#fffacd',
|
|
lightblue: '#add8e6',
|
|
lightcoral: '#f08080',
|
|
lightcyan: '#e0ffff',
|
|
lightgoldenrodyellow: '#fafad2',
|
|
lightgray: '#d3d3d3',
|
|
lightgrey: '#d3d3d3',
|
|
lightgreen: '#90ee90',
|
|
lightpink: '#ffb6c1',
|
|
lightsalmon: '#ffa07a',
|
|
lightseagreen: '#20b2aa',
|
|
lightskyblue: '#87cefa',
|
|
lightslategray: '#778899',
|
|
lightslategrey: '#778899',
|
|
lightsteelblue: '#b0c4de',
|
|
lightyellow: '#ffffe0',
|
|
lime: '#00ff00',
|
|
limegreen: '#32cd32',
|
|
linen: '#faf0e6',
|
|
magenta: '#ff00ff',
|
|
maroon: '#800000',
|
|
mediumaquamarine: '#66cdaa',
|
|
mediumblue: '#0000cd',
|
|
mediumorchid: '#ba55d3',
|
|
mediumpurple: '#9370db',
|
|
mediumseagreen: '#3cb371',
|
|
mediumslateblue: '#7b68ee',
|
|
mediumspringgreen: '#00fa9a',
|
|
mediumturquoise: '#48d1cc',
|
|
mediumvioletred: '#c71585',
|
|
midnightblue: '#191970',
|
|
mintcream: '#f5fffa',
|
|
mistyrose: '#ffe4e1',
|
|
moccasin: '#ffe4b5',
|
|
navajowhite: '#ffdead',
|
|
navy: '#000080',
|
|
oldlace: '#fdf5e6',
|
|
olive: '#808000',
|
|
olivedrab: '#6b8e23',
|
|
orange: '#ffa500',
|
|
orangered: '#ff4500',
|
|
orchid: '#da70d6',
|
|
palegoldenrod: '#eee8aa',
|
|
palegreen: '#98fb98',
|
|
paleturquoise: '#afeeee',
|
|
palevioletred: '#db7093',
|
|
papayawhip: '#ffefd5',
|
|
peachpuff: '#ffdab9',
|
|
peru: '#cd853f',
|
|
pink: '#ffc0cb',
|
|
plum: '#dda0dd',
|
|
powderblue: '#b0e0e6',
|
|
purple: '#800080',
|
|
rebeccapurple: '#663399',
|
|
red: '#ff0000',
|
|
rosybrown: '#bc8f8f',
|
|
royalblue: '#4169e1',
|
|
saddlebrown: '#8b4513',
|
|
salmon: '#fa8072',
|
|
sandybrown: '#f4a460',
|
|
seagreen: '#2e8b57',
|
|
seashell: '#fff5ee',
|
|
sienna: '#a0522d',
|
|
silver: '#c0c0c0',
|
|
skyblue: '#87ceeb',
|
|
slateblue: '#6a5acd',
|
|
slategray: '#708090',
|
|
slategrey: '#708090',
|
|
snow: '#fffafa',
|
|
springgreen: '#00ff7f',
|
|
steelblue: '#4682b4',
|
|
tan: '#d2b48c',
|
|
teal: '#008080',
|
|
thistle: '#d8bfd8',
|
|
tomato: '#ff6347',
|
|
turquoise: '#40e0d0',
|
|
violet: '#ee82ee',
|
|
wheat: '#f5deb3',
|
|
white: '#ffffff',
|
|
whitesmoke: '#f5f5f5',
|
|
yellow: '#ffff00',
|
|
yellowgreen: '#9acd32',
|
|
|
|
// old = CSS2 system colors: https://www.w3.org/TR/css3-color/#css2-system
|
|
// new = CSS4 system colors: https://drafts.csswg.org/css-color-4/#css-system-colors
|
|
ActiveBorder: '',
|
|
ActiveCaption: '',
|
|
ActiveText: '', // new
|
|
AppWorkspace: '',
|
|
Background: '',
|
|
ButtonBorder: '', // new
|
|
ButtonFace: '', // old+new
|
|
ButtonHighlight: '',
|
|
ButtonShadow: '',
|
|
ButtonText: '', // old+new
|
|
Canvas: '', // new
|
|
CanvasText: '', // new
|
|
CaptionText: '',
|
|
Field: '', // new
|
|
FieldText: '', // new
|
|
GrayText: '', // old+new
|
|
Highlight: '', // old+new
|
|
HighlightText: '', // old+new
|
|
InactiveBorder: '',
|
|
InactiveCaption: '',
|
|
InactiveCaptionText: '',
|
|
InfoBackground: '',
|
|
InfoText: '',
|
|
LinkText: '', // new
|
|
Mark: '', // new
|
|
MarkText: '', // new
|
|
Menu: '',
|
|
MenuText: '',
|
|
Scrollbar: '',
|
|
ThreeDDarkShadow: '',
|
|
ThreeDFace: '',
|
|
ThreeDHighlight: '',
|
|
ThreeDLightShadow: '',
|
|
ThreeDShadow: '',
|
|
VisitedText: '', // new
|
|
Window: '',
|
|
WindowFrame: '',
|
|
WindowText: '',
|
|
});
|
|
const ColorsLC = new Set(Object.keys(Colors).map(lower));
|
|
|
|
//#endregion
|
|
//#region Tokens
|
|
|
|
/* https://www.w3.org/TR/css3-syntax/#lexical */
|
|
/** @type {Object<string,number|Object>} */
|
|
const Tokens = Object.assign([], {
|
|
EOF: {}, // must be the first token
|
|
}, {
|
|
// HTML-style comments
|
|
CDC: {},
|
|
CDO: {},
|
|
// ignorables
|
|
COMMENT: {hide: true},
|
|
S: {},
|
|
// attribute equality
|
|
DASHMATCH: {text: '|='},
|
|
INCLUDES: {text: '~='},
|
|
PREFIXMATCH: {text: '^='},
|
|
SUBSTRINGMATCH: {text: '*='},
|
|
SUFFIXMATCH: {text: '$='},
|
|
// identifier types
|
|
HASH: {},
|
|
IDENT: {},
|
|
STRING: {},
|
|
// at-keywords
|
|
CHARSET_SYM: {text: '@charset'},
|
|
DOCUMENT_SYM: {text: ['@document', '@-moz-document']},
|
|
FONT_FACE_SYM: {text: '@font-face'},
|
|
FONT_PALETTE_VALUES_SYM: {text: '@font-palette-values'},
|
|
IMPORT_SYM: {text: '@import'},
|
|
KEYFRAMES_SYM: {text: ['@keyframes', '@-webkit-keyframes', '@-moz-keyframes', '@-o-keyframes']},
|
|
LAYER_SYM: {text: '@layer'},
|
|
MEDIA_SYM: {text: '@media'},
|
|
NAMESPACE_SYM: {text: '@namespace'},
|
|
PAGE_SYM: {text: '@page'},
|
|
SUPPORTS_SYM: {text: '@supports'},
|
|
UNKNOWN_SYM: {},
|
|
VIEWPORT_SYM: {text: ['@viewport', '@-ms-viewport', '@-o-viewport']},
|
|
// measurements
|
|
ANGLE: {},
|
|
DIMENSION: {},
|
|
FREQ: {},
|
|
LENGTH: {},
|
|
NUMBER: {},
|
|
PERCENTAGE: {},
|
|
TIME: {},
|
|
// functions
|
|
FUNCTION: {},
|
|
URI: {},
|
|
// Unicode ranges
|
|
UNICODE_RANGE: {},
|
|
// invalid string
|
|
INVALID: {},
|
|
// combinators
|
|
COLUMN: {text: '||'},
|
|
COMMA: {text: ','},
|
|
GREATER: {text: '>'},
|
|
PLUS: {text: '+'},
|
|
TILDE: {text: '~'},
|
|
// modifier
|
|
ANY: {text: ['any', '-webkit-any', '-moz-any']},
|
|
HAS: {},
|
|
IS: {},
|
|
NOT: {},
|
|
WHERE: {},
|
|
// CSS3 Paged Media
|
|
BOTTOMCENTER_SYM: {text: '@bottom-center'},
|
|
BOTTOMLEFTCORNER_SYM: {text: '@bottom-left-corner'},
|
|
BOTTOMLEFT_SYM: {text: '@bottom-left'},
|
|
BOTTOMRIGHTCORNER_SYM: {text: '@bottom-right-corner'},
|
|
BOTTOMRIGHT_SYM: {text: '@bottom-right'},
|
|
LEFTBOTTOM_SYM: {text: '@left-bottom'},
|
|
LEFTMIDDLE_SYM: {text: '@left-middle'},
|
|
LEFTTOP_SYM: {text: '@left-top'},
|
|
RIGHTBOTTOM_SYM: {text: '@right-bottom'},
|
|
RIGHTMIDDLE_SYM: {text: '@right-middle'},
|
|
RIGHTTOP_SYM: {text: '@right-top'},
|
|
TOPCENTER_SYM: {text: '@top-center'},
|
|
TOPLEFTCORNER_SYM: {text: '@top-left-corner'},
|
|
TOPLEFT_SYM: {text: '@top-left'},
|
|
TOPRIGHTCORNER_SYM: {text: '@top-right-corner'},
|
|
TOPRIGHT_SYM: {text: '@top-right'},
|
|
/* CSS3 Media Queries */
|
|
RESOLUTION: {state: 'media'},
|
|
/*
|
|
* The following token names are not defined in any CSS specification.
|
|
*/
|
|
CHAR: {},
|
|
COLON: {text: ':'},
|
|
DOT: {text: '.'},
|
|
EQUALS: {text: '='},
|
|
IE_FUNCTION: {},
|
|
IMPORTANT: {},
|
|
LBRACE: {text: '{', endChar: '}'},
|
|
LBRACKET: {text: '[', endChar: ']'},
|
|
LPAREN: {text: '(', endChar: ')'},
|
|
MINUS: {text: '-'},
|
|
PIPE: {text: '|'},
|
|
RBRACE: {text: '}'},
|
|
RBRACKET: {text: ']'},
|
|
RPAREN: {text: ')'},
|
|
SEMICOLON: {text: ';'},
|
|
SLASH: {text: '/'},
|
|
STAR: {text: '*'},
|
|
USO_VAR: {},
|
|
});
|
|
// make Tokens an array of tokens, store the index in original prop, add 'name' to each token
|
|
const typeMap = new Map();
|
|
for (const [k, val] of Object.entries(Tokens)) {
|
|
const index = Tokens[k] = Tokens.length;
|
|
val.name = k;
|
|
Tokens.push(val);
|
|
const {text} = val;
|
|
if (text) {
|
|
for (const item of Array.isArray(text) ? text : [text]) {
|
|
typeMap.set(item, index);
|
|
}
|
|
}
|
|
}
|
|
Tokens.UNKNOWN = -1;
|
|
Tokens.name = index => (Tokens[index] || {}).name;
|
|
Tokens.type = text => typeMap.get(text) || Tokens.UNKNOWN;
|
|
|
|
const TT = {
|
|
attrMatch: [
|
|
Tokens.PREFIXMATCH,
|
|
Tokens.SUFFIXMATCH,
|
|
Tokens.SUBSTRINGMATCH,
|
|
Tokens.EQUALS,
|
|
Tokens.INCLUDES,
|
|
Tokens.DASHMATCH,
|
|
],
|
|
combinator: [
|
|
Tokens.PLUS,
|
|
Tokens.GREATER,
|
|
Tokens.TILDE,
|
|
Tokens.COLUMN,
|
|
],
|
|
cruft: [
|
|
Tokens.S,
|
|
Tokens.CDO,
|
|
Tokens.CDC,
|
|
],
|
|
expression: [
|
|
Tokens.PLUS,
|
|
Tokens.MINUS,
|
|
Tokens.DIMENSION,
|
|
Tokens.NUMBER,
|
|
Tokens.STRING,
|
|
Tokens.IDENT,
|
|
Tokens.LENGTH,
|
|
Tokens.FREQ,
|
|
Tokens.ANGLE,
|
|
Tokens.TIME,
|
|
Tokens.RESOLUTION,
|
|
Tokens.SLASH,
|
|
],
|
|
identString: [
|
|
Tokens.IDENT,
|
|
Tokens.STRING,
|
|
Tokens.USO_VAR,
|
|
],
|
|
LParenBracket: [
|
|
Tokens.LPAREN,
|
|
Tokens.LBRACKET,
|
|
],
|
|
LParenBracketBrace: [
|
|
Tokens.LPAREN,
|
|
Tokens.LBRACKET,
|
|
Tokens.LBRACE,
|
|
],
|
|
margins: [
|
|
Tokens.TOPLEFTCORNER_SYM,
|
|
Tokens.TOPLEFT_SYM,
|
|
Tokens.TOPCENTER_SYM,
|
|
Tokens.TOPRIGHT_SYM,
|
|
Tokens.TOPRIGHTCORNER_SYM,
|
|
Tokens.BOTTOMLEFTCORNER_SYM,
|
|
Tokens.BOTTOMLEFT_SYM,
|
|
Tokens.BOTTOMCENTER_SYM,
|
|
Tokens.BOTTOMRIGHT_SYM,
|
|
Tokens.BOTTOMRIGHTCORNER_SYM,
|
|
Tokens.LEFTTOP_SYM,
|
|
Tokens.LEFTMIDDLE_SYM,
|
|
Tokens.LEFTBOTTOM_SYM,
|
|
Tokens.RIGHTTOP_SYM,
|
|
Tokens.RIGHTMIDDLE_SYM,
|
|
Tokens.RIGHTBOTTOM_SYM,
|
|
],
|
|
mediaValue: [
|
|
Tokens.IDENT,
|
|
Tokens.NUMBER,
|
|
Tokens.DIMENSION,
|
|
Tokens.LENGTH,
|
|
],
|
|
op: [
|
|
Tokens.SLASH,
|
|
Tokens.COMMA,
|
|
],
|
|
opInFunc: [
|
|
Tokens.SLASH,
|
|
Tokens.COMMA,
|
|
Tokens.PLUS,
|
|
Tokens.STAR,
|
|
Tokens.MINUS,
|
|
],
|
|
plusMinus: [
|
|
Tokens.MINUS,
|
|
Tokens.PLUS,
|
|
],
|
|
pseudo: [
|
|
Tokens.FUNCTION,
|
|
Tokens.IDENT,
|
|
],
|
|
semiS: [
|
|
Tokens.SEMICOLON,
|
|
Tokens.S,
|
|
],
|
|
stringUri: [
|
|
Tokens.STRING,
|
|
Tokens.URI,
|
|
Tokens.USO_VAR,
|
|
],
|
|
term: [
|
|
Tokens.NUMBER,
|
|
Tokens.PERCENTAGE,
|
|
Tokens.LENGTH,
|
|
Tokens.ANGLE,
|
|
Tokens.TIME,
|
|
Tokens.DIMENSION,
|
|
Tokens.FREQ,
|
|
Tokens.STRING,
|
|
Tokens.IDENT,
|
|
Tokens.URI,
|
|
Tokens.UNICODE_RANGE,
|
|
Tokens.USO_VAR,
|
|
],
|
|
usoS: [
|
|
Tokens.USO_VAR,
|
|
Tokens.S,
|
|
],
|
|
};
|
|
|
|
//#endregion
|
|
//#region StringReader
|
|
|
|
class StringReader {
|
|
|
|
constructor(text) {
|
|
this._input = text.replace(/\r\n?/g, '\n');
|
|
this._line = 1;
|
|
this._col = 1;
|
|
this._cursor = 0;
|
|
}
|
|
|
|
eof() {
|
|
return this._cursor >= this._input.length;
|
|
}
|
|
|
|
peek(count = 1) {
|
|
return this._input[this._cursor + count - 1] || null;
|
|
}
|
|
|
|
peekTest(stickyRx) {
|
|
stickyRx.lastIndex = this._cursor;
|
|
return stickyRx.test(this._input);
|
|
}
|
|
|
|
read() {
|
|
const c = this._input[this._cursor];
|
|
if (!c) return null;
|
|
if (c === '\n') {
|
|
this._line++;
|
|
this._col = 1;
|
|
} else {
|
|
this._col++;
|
|
}
|
|
this._cursor++;
|
|
return c;
|
|
}
|
|
|
|
mark() {
|
|
this._bookmark = {
|
|
cursor: this._cursor,
|
|
line: this._line,
|
|
col: this._col,
|
|
};
|
|
}
|
|
|
|
reset() {
|
|
if (this._bookmark) {
|
|
this._cursor = this._bookmark.cursor;
|
|
this._line = this._bookmark.line;
|
|
this._col = this._bookmark.col;
|
|
delete this._bookmark;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads up to and including the given string.
|
|
* @param {String} pattern The string to read.
|
|
* @return {String} The string when it is found.
|
|
* @throws Error when the string pattern is not found.
|
|
*/
|
|
readTo(pattern) {
|
|
const i = this._input.indexOf(pattern, this._cursor);
|
|
if (i < 0) throw new Error(`Expected '${pattern}'.`);
|
|
return this.readCount(i - this._cursor + pattern.length);
|
|
}
|
|
|
|
/**
|
|
* Reads characters that match either text or a regular expression and returns those characters.
|
|
* If a match is found, the row and column are adjusted.
|
|
* @param {String|RegExp} matcher
|
|
* @return {String} string or null if there was no match.
|
|
*/
|
|
readMatch(matcher) {
|
|
if (matcher.sticky) {
|
|
matcher.lastIndex = this._cursor;
|
|
return matcher.test(this._input) ?
|
|
this.readCount(RegExp.lastMatch.length) :
|
|
null;
|
|
}
|
|
if (typeof matcher === 'string') {
|
|
if (this._input[this._cursor] === matcher[0] &&
|
|
this._input.substr(this._cursor, matcher.length) === matcher) {
|
|
return this.readCount(matcher.length);
|
|
}
|
|
} else if (matcher instanceof RegExp) {
|
|
if (matcher.test(this._input.substr(this._cursor))) {
|
|
return this.readCount(RegExp.lastMatch.length);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Reads a given number of characters. If the end of the input is reached,
|
|
* it reads only the remaining characters and does not throw an error.
|
|
* @param {int} count The number of characters to read.
|
|
* @return {String} string or null if already at EOF
|
|
*/
|
|
readCount(count) {
|
|
const len = this._input.length;
|
|
if (this._cursor >= len) return null;
|
|
if (!count) return '';
|
|
const text = this._input.substr(this._cursor, count);
|
|
this._cursor = Math.min(this._cursor + count, len);
|
|
let prev = -1;
|
|
for (let i = 0; (i = text.indexOf('\n', i)) >= 0; prev = i, i++) this._line++;
|
|
this._col = prev < 0 ? this._col + count : count - prev;
|
|
return text;
|
|
}
|
|
}
|
|
|
|
//#endregion
|
|
//#region Matcher
|
|
|
|
/**
|
|
* Reuses a Matcher for a ValidationTypes definition string instead of reparsing it.
|
|
* @type {Map<string, Matcher>}
|
|
*/
|
|
const matcherCache = new Map();
|
|
|
|
/**
|
|
* This class implements a combinator library for matcher functions.
|
|
* https://developer.mozilla.org/docs/Web/CSS/Value_definition_syntax#Component_value_combinators
|
|
*/
|
|
class Matcher {
|
|
|
|
constructor(matchFunc, toString, options) {
|
|
this.matchFunc = matchFunc;
|
|
/** @type {function(?number):string} */
|
|
this.toString = typeof toString === 'function' ? toString : () => toString;
|
|
/** @type {?Matcher[]} */
|
|
this.options = options;
|
|
}
|
|
|
|
/**
|
|
* @param {PropertyValueIterator} e
|
|
* @return {?boolean}
|
|
*/
|
|
match(e) {
|
|
e._marks.push(e._i);
|
|
return e.popMark(this.matchFunc(e));
|
|
}
|
|
|
|
braces(min, max, marker, sep) {
|
|
return new Matcher(Matcher.funcBraces, Matcher.toStringBraces, {
|
|
min, max, marker,
|
|
sep: sep && Matcher.seq(sep, this),
|
|
embraced: this,
|
|
});
|
|
}
|
|
|
|
static parse(str) {
|
|
let m = matcherCache.get(str);
|
|
if (m) return m;
|
|
m = Matcher.doParse(str);
|
|
matcherCache.set(str, m);
|
|
return m;
|
|
}
|
|
|
|
/** Simple recursive-descent grammar to build matchers from strings. */
|
|
static doParse(str) {
|
|
const reader = new StringReader(str);
|
|
const result = Matcher.parseGrammar(reader);
|
|
if (!reader.eof()) {
|
|
throw new Error('Internal grammar error. ' +
|
|
`Expected end of string at ${reader._cursor}: ${reader._input}.`);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static cast(m) {
|
|
return m instanceof Matcher ? m : Matcher.parse(m);
|
|
}
|
|
|
|
// Matcher for a single type.
|
|
static fromType(type) {
|
|
let m = matcherCache.get(type);
|
|
if (m) return m;
|
|
m = new Matcher(Matcher.funcFromType, type, type);
|
|
matcherCache.set(type, m);
|
|
return m;
|
|
}
|
|
|
|
/**
|
|
* @param {string} name - functio name
|
|
* @param {Matcher} body - matcher for function body
|
|
* @returns {Matcher}
|
|
*/
|
|
static func(name, body) {
|
|
return new Matcher(Matcher.funcFunc, Matcher.toStringFunc, {name, body});
|
|
}
|
|
|
|
// Matcher for one or more juxtaposed words, which all must occur, in the given order.
|
|
static seq(...args) {
|
|
const ms = args.map(Matcher.cast);
|
|
if (ms.length === 1) return ms[0];
|
|
return new Matcher(Matcher.funcSeq, Matcher.toStringSeq, ms);
|
|
}
|
|
|
|
// Matcher for one or more alternatives, where exactly one must occur.
|
|
static alt(...args) {
|
|
const ms = args.map(Matcher.cast);
|
|
if (ms.length === 1) return ms[0];
|
|
return new Matcher(Matcher.funcAlt, Matcher.toStringAlt, ms);
|
|
}
|
|
|
|
/**
|
|
* Matcher for two or more options: double bar (||) and double ampersand (&&) operators,
|
|
* as well as variants of && where some of the alternatives are optional.
|
|
* This will backtrack through even successful matches to try to
|
|
* maximize the number of items matched.
|
|
*/
|
|
static many(required, ...args) {
|
|
const ms = args.map(Matcher.cast);
|
|
const m = new Matcher(Matcher.funcMany, Matcher.toStringMany, ms);
|
|
m.required = required === true ? Array(ms.length).fill(true) : required;
|
|
return m;
|
|
}
|
|
|
|
/**************************** matchFunc **********************/
|
|
|
|
/**
|
|
* @this {Matcher}
|
|
* @param {PropertyValueIterator} expr
|
|
*/
|
|
static funcAlt(expr) {
|
|
return this.options.some(Matcher.invoke, expr);
|
|
}
|
|
/**
|
|
* @this {Matcher}
|
|
* @param {PropertyValueIterator} expr
|
|
*/
|
|
static funcBraces(expr) {
|
|
const {min, max, sep, embraced} = this.options;
|
|
let i = 0;
|
|
while (i < max && (i && sep || embraced).match(expr)) {
|
|
i++;
|
|
}
|
|
return i >= min;
|
|
}
|
|
/**
|
|
* @this {Matcher}
|
|
* @param {PropertyValueIterator} expr
|
|
*/
|
|
static funcFromType(expr) {
|
|
const part = expr.peek();
|
|
if (!part) return;
|
|
const type = this.options;
|
|
let result, m;
|
|
if (part.isVar) {
|
|
result = true;
|
|
} else if (!type.startsWith('<')) {
|
|
result = vtIsLiteral(type, part);
|
|
} else if ((m = VTSimple[type])) {
|
|
result = m.call(VTSimple, part);
|
|
} else {
|
|
m = VTComplex[type];
|
|
return m instanceof Matcher ?
|
|
m.match(expr) :
|
|
m.call(VTComplex, expr);
|
|
}
|
|
if (!result && expr.tryAttr && part.isAttr) {
|
|
result = vtIsAttr(part);
|
|
}
|
|
if (result) expr.next();
|
|
return result;
|
|
}
|
|
/**
|
|
* @this {Matcher}
|
|
* @param {PropertyValueIterator} expr
|
|
*/
|
|
static funcFunc(expr) {
|
|
const p = expr.peek();
|
|
if (p && p.expr && p.tokenType === Tokens.FUNCTION && lowerCmp(p.name, this.options.name)) {
|
|
let res = hasVarParts(p.expr);
|
|
if (!res) {
|
|
const vi = new PropertyValueIterator(p.expr); // eslint-disable-line no-use-before-define
|
|
res = this.options.body.match(vi) && !vi.hasNext;
|
|
}
|
|
return res && expr.next();
|
|
}
|
|
}
|
|
/**
|
|
* @this {PropertyValueIterator}
|
|
* @param {Matcher} m
|
|
*/
|
|
static invoke(m) {
|
|
return m.match(this);
|
|
}
|
|
/**
|
|
* @this {Matcher}
|
|
* @param {PropertyValueIterator} expr
|
|
*/
|
|
static funcMany(expr) {
|
|
const seen = [];
|
|
const {/** @type {Matcher[]} */options: ms, required} = this;
|
|
let max = 0;
|
|
let pass = 0;
|
|
// If couldn't get a complete match, retrace our steps to make the
|
|
// match with the maximum # of required elements.
|
|
if (!tryMatch(0)) {
|
|
pass++;
|
|
tryMatch(0);
|
|
}
|
|
if (required === false) {
|
|
return max > 0;
|
|
}
|
|
// Use finer-grained specification of which matchers are required.
|
|
for (let i = 0; i < ms.length; i++) {
|
|
if (required[i] && !seen[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
function tryMatch(matchCount) {
|
|
for (let i = 0; i < ms.length; i++) {
|
|
if (seen[i]) continue;
|
|
expr.mark();
|
|
if (!ms[i].matchFunc(expr)) {
|
|
expr.popMark(true);
|
|
continue;
|
|
}
|
|
seen[i] = true;
|
|
// Increase matchCount if this was a required element
|
|
// (or if all the elements are optional)
|
|
if (tryMatch(matchCount + (required === false || required[i] ? 1 : 0))) {
|
|
expr.popMark(true);
|
|
return true;
|
|
}
|
|
// Backtrack: try *not* matching using this rule, and
|
|
// let's see if it leads to a better overall match.
|
|
expr.popMark();
|
|
seen[i] = false;
|
|
}
|
|
if (pass === 0) {
|
|
max = Math.max(matchCount, max);
|
|
return matchCount === ms.length;
|
|
} else {
|
|
return matchCount === max;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* @this {Matcher}
|
|
* @param {PropertyValueIterator} expr
|
|
*/
|
|
static funcSeq(expr) {
|
|
return this.options.every(Matcher.invoke, expr);
|
|
}
|
|
|
|
/**************************** toStringFunc **********************/
|
|
|
|
/** @this {Matcher} */
|
|
static toStringAlt(prec) {
|
|
const p = Matcher.prec.ALT;
|
|
const s = this.options.map(m => m.toString(p)).join(' | ');
|
|
return prec > p ? `[ ${s} ]` : s;
|
|
}
|
|
/** @this {Matcher} */
|
|
static toStringBraces() {
|
|
const {marker, min, max, embraced} = this.options;
|
|
return embraced.toString(Matcher.prec.MOD) + (
|
|
!marker || marker === '#'
|
|
? `${marker || ''}{${min}${min === max ? '' : ',' + max}}`
|
|
: marker);
|
|
}
|
|
/** @this {Matcher} */
|
|
static toStringFunc() {
|
|
const {name, body} = this.options;
|
|
return `${name}( ${body} )`;
|
|
}
|
|
/** @this {Matcher} */
|
|
static toStringMany(prec) {
|
|
const {options: ms, required} = this;
|
|
const p = required === false ? Matcher.prec.OROR : Matcher.prec.ANDAND;
|
|
const s = ms.map((m, i) => {
|
|
if (required !== false && !required[i]) {
|
|
const str = m.toString(Matcher.prec.MOD);
|
|
return str.endsWith('?') ? str : str + '?';
|
|
}
|
|
return m.toString(p);
|
|
}).join(required === false ? ' || ' : ' && ');
|
|
return prec > p ? `[ ${s} ]` : s;
|
|
}
|
|
/** @this {Matcher} */
|
|
static toStringSeq(prec) {
|
|
const p = Matcher.prec.SEQ;
|
|
const s = this.options.map(m => m.toString(p)).join(' ');
|
|
return prec > p ? `[ ${s} ]` : s;
|
|
}
|
|
}
|
|
|
|
// Precedence table of combinators.
|
|
Matcher.prec = {
|
|
MOD: 5,
|
|
SEQ: 4,
|
|
ANDAND: 3,
|
|
OROR: 2,
|
|
ALT: 1,
|
|
};
|
|
|
|
Matcher.parseGrammar = (() => {
|
|
/** @type {StringReader} */
|
|
let reader;
|
|
return newReader => {
|
|
reader = newReader;
|
|
return alt();
|
|
};
|
|
function alt() {
|
|
// alt = oror (" | " oror)*
|
|
const m = [oror()];
|
|
while (reader.readMatch(' | ')) {
|
|
m.push(oror());
|
|
}
|
|
return m.length === 1 ? m[0] : Matcher.alt(...m);
|
|
}
|
|
// Matcher for two or more options in any order, at least one must be present.
|
|
function oror() {
|
|
// oror = andand ( " || " andand)*
|
|
const m = [andand()];
|
|
while (reader.readMatch(' || ')) {
|
|
m.push(andand());
|
|
}
|
|
return m.length === 1 ? m[0] : Matcher.many(false, ...m);
|
|
}
|
|
// Matcher for two or more options in any order, all mandatory.
|
|
function andand() {
|
|
// andand = seq ( " && " seq)*
|
|
const m = [seq()];
|
|
let reqPrev = !isOptional(m[0]);
|
|
const required = [reqPrev];
|
|
while (reader.readMatch(' && ')) {
|
|
const item = seq();
|
|
const req = !isOptional(item);
|
|
// Matcher.many apparently can't handle optional items first
|
|
if (req && !reqPrev) {
|
|
m.unshift(item);
|
|
required.unshift(req);
|
|
} else {
|
|
m.push(item);
|
|
required.push(req);
|
|
reqPrev = req;
|
|
}
|
|
}
|
|
return m.length === 1 ? m[0] : Matcher.many(required, ...m);
|
|
}
|
|
function seq() {
|
|
// seq = mod ( " " mod)*
|
|
const ms = [mod()];
|
|
while (reader.readMatch(/\s(?![&|)\]])/y)) {
|
|
ms.push(mod());
|
|
}
|
|
return Matcher.seq(...ms);
|
|
}
|
|
function mod() {
|
|
// mod = term ( "?" | "*" | "+" | "#" | "{<num>,<num>}" )?
|
|
// term = <nt> | literal | "[ " expression " ]" | fn "( " alt " )"
|
|
let m, fn;
|
|
if (reader.readMatch('[ ')) {
|
|
m = alt();
|
|
eat(' ]');
|
|
} else if ((fn = reader.readMatch(/[-\w]+(?=\(\s)/y))) {
|
|
reader.readCount(2);
|
|
m = alt();
|
|
eat(' )');
|
|
return Matcher.func(fn, m);
|
|
} else {
|
|
m = Matcher.fromType(eat(/<[^>]+>|[^\s?*+#{]+/y));
|
|
}
|
|
reader.mark();
|
|
let hash;
|
|
switch (reader.read()) {
|
|
case '?': return m.braces(0, 1, '?');
|
|
case '*': return m.braces(0, Infinity, '*');
|
|
case '+': return m.braces(1, Infinity, '+');
|
|
case '#':
|
|
if (reader.peek() !== '{') return m.braces(1, Infinity, '#', ',');
|
|
reader.read();
|
|
hash = '#';
|
|
// fallthrough
|
|
case '{': {
|
|
const [min, max] = eat(/\s*\d+\s*(,\s*\d+\s*)?}/y).trim().split(/\s+|,|}/);
|
|
return m.braces(min | 0, max | min | 0, hash, hash && ',');
|
|
}
|
|
default:
|
|
reader.reset();
|
|
}
|
|
return m;
|
|
}
|
|
function eat(pattern) {
|
|
const s = reader.readMatch(pattern);
|
|
if (s != null) return s;
|
|
throw new Error('Internal grammar error. ' +
|
|
`Expected ${pattern} at ${reader._cursor} in ${reader._input}`);
|
|
}
|
|
function isOptional({options}) {
|
|
return options && options.marker === '?';
|
|
}
|
|
})();
|
|
|
|
//#endregion
|
|
//#region EventTarget
|
|
|
|
class EventTarget {
|
|
constructor() {
|
|
this._listeners = new Map();
|
|
}
|
|
addListener(type, fn) {
|
|
let list = this._listeners.get(type);
|
|
if (!list) this._listeners.set(type, (list = new Set()));
|
|
list.add(fn);
|
|
}
|
|
fire(event) {
|
|
if (typeof event === 'string') {
|
|
event = {type: event};
|
|
}
|
|
event.target = this;
|
|
const list = this._listeners.get(event.type);
|
|
if (list) {
|
|
for (const fn of list) {
|
|
fn.call(this, event);
|
|
}
|
|
}
|
|
}
|
|
removeListener(type, fn) {
|
|
const list = this._listeners.get(type);
|
|
if (list) list.delete(fn);
|
|
}
|
|
}
|
|
|
|
//#endregion
|
|
//#region Syntax units
|
|
|
|
/**
|
|
* @property {boolean|number} [_isAttr]
|
|
* @property {boolean|number} [_isCalc]
|
|
* @property {boolean|number} [_isVar]
|
|
*/
|
|
class SyntaxUnit {
|
|
constructor(text, pos, type, extras) {
|
|
this.col = pos.col;
|
|
this.line = pos.line;
|
|
this.offset = pos.offset;
|
|
this.text = text;
|
|
this.type = type;
|
|
if (extras) Object.assign(this, extras);
|
|
}
|
|
valueOf() {
|
|
return this.text;
|
|
}
|
|
toString() {
|
|
return this.text;
|
|
}
|
|
get isAttr() {
|
|
let res = this._isAttr;
|
|
if (res === 0) res = this._isAttr = lowerCmp(this.name, 'attr');
|
|
return res;
|
|
}
|
|
get isCalc() {
|
|
let res = this._isCalc;
|
|
if (res === 0) res = this._isCalc = rxCalc.test(this.text);
|
|
return res;
|
|
}
|
|
get isVar() {
|
|
let res = this._isVar;
|
|
if (res === 0) {
|
|
const pp = this.expr && this.expr.parts;
|
|
res = this._isVar = pp && pp.length > 0 && (
|
|
(pp.length === 1 || pp[1].text === ',') && (
|
|
pp[0].type === 'custom-property' && lowerCmp(this.name, 'var') ||
|
|
pp[0].type === 'identifier' && lowerCmp(this.name, 'env')));
|
|
}
|
|
return res;
|
|
}
|
|
static fromToken(token) {
|
|
return token && new SyntaxUnit(token.value, token);
|
|
}
|
|
|
|
/**
|
|
* @param {SyntaxUnit} unit
|
|
* @param {SyntaxUnit|parserlib.Token} token
|
|
* @returns {SyntaxUnit}
|
|
*/
|
|
static addFuncInfo(unit, {expr, name} = unit) {
|
|
const isColor = expr && expr.parts && /^((rgb|hsl)a?|hwb)$/i.test(name);
|
|
if (isColor) unit.type = 'color';
|
|
unit._isAttr =
|
|
unit._isCalc =
|
|
unit._isVar = isColor ? false : 0;
|
|
return unit;
|
|
}
|
|
}
|
|
|
|
class SyntaxError extends Error {
|
|
constructor(message, pos) {
|
|
super();
|
|
this.name = this.constructor.name;
|
|
this.col = pos.col;
|
|
this.line = pos.line;
|
|
this.offset = pos.offset;
|
|
this.message = message;
|
|
}
|
|
}
|
|
|
|
class ValidationError extends Error {
|
|
constructor(message, pos) {
|
|
super();
|
|
this.col = pos.col;
|
|
this.line = pos.line;
|
|
this.offset = pos.offset;
|
|
this.message = message;
|
|
}
|
|
}
|
|
|
|
// individual media query
|
|
class MediaQuery extends SyntaxUnit {
|
|
constructor(modifier, mediaType, features, pos) {
|
|
const text = (modifier ? modifier + ' ' : '') +
|
|
(mediaType ? mediaType : '') +
|
|
(mediaType && features.length > 0 ? ' and ' : '') +
|
|
features.join(' and ');
|
|
super(text, pos, TYPES.MEDIA_QUERY_TYPE);
|
|
this.modifier = modifier;
|
|
this.mediaType = mediaType;
|
|
this.features = features;
|
|
}
|
|
}
|
|
|
|
// e.g. max-width:500.
|
|
class MediaFeature extends SyntaxUnit {
|
|
constructor(name, value) {
|
|
const text = `(${name}${value != null ? ':' + value : ''})`;
|
|
super(text, name, TYPES.MEDIA_FEATURE_TYPE);
|
|
this.name = name;
|
|
this.value = value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An entire single selector, including all parts but not
|
|
* including multiple selectors (those separated by commas).
|
|
*/
|
|
class Selector extends SyntaxUnit {
|
|
constructor(parts, pos) {
|
|
super(parts.join(' '), pos, TYPES.SELECTOR_TYPE);
|
|
this.parts = parts;
|
|
// eslint-disable-next-line no-use-before-define
|
|
this.specificity = Specificity.calculate(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A single part of a selector string i.e. element name and modifiers.
|
|
* Does not include combinators such as spaces, +, >, etc.
|
|
*/
|
|
class SelectorPart extends SyntaxUnit {
|
|
constructor(elementName, modifiers, text, pos) {
|
|
super(text, pos, TYPES.SELECTOR_PART_TYPE);
|
|
this.elementName = elementName;
|
|
this.modifiers = modifiers;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Selector modifier string
|
|
*/
|
|
class SelectorSubPart extends SyntaxUnit {
|
|
constructor(text, type, pos) {
|
|
super(text, pos, TYPES.SELECTOR_SUB_PART_TYPE);
|
|
this.type = type;
|
|
// Some subparts have arguments
|
|
this.args = [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A selector combinator (whitespace, +, >).
|
|
*/
|
|
class Combinator extends SyntaxUnit {
|
|
constructor(token) {
|
|
const {value} = token;
|
|
super(value, token, TYPES.COMBINATOR_TYPE);
|
|
this.type =
|
|
value === '>' ? 'child' :
|
|
value === '+' ? 'adjacent-sibling' :
|
|
value === '~' ? 'sibling' :
|
|
value === '||' ? 'column' :
|
|
!value.trim() ? 'descendant' :
|
|
'unknown';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A selector specificity.
|
|
*/
|
|
class Specificity {
|
|
/**
|
|
* @param {int} a Should be 1 for inline styles, zero for stylesheet styles
|
|
* @param {int} b Number of ID selectors
|
|
* @param {int} c Number of classes and pseudo classes
|
|
* @param {int} d Number of element names and pseudo elements
|
|
*/
|
|
constructor(a, b, c, d) {
|
|
this.a = a;
|
|
this.b = b;
|
|
this.c = c;
|
|
this.d = d;
|
|
this.constructor = Specificity;
|
|
}
|
|
/**
|
|
* @param {Specificity} other The other specificity to compare to.
|
|
* @return {int} -1 if the other specificity is larger, 1 if smaller, 0 if equal.
|
|
*/
|
|
compare(other) {
|
|
const comps = ['a', 'b', 'c', 'd'];
|
|
for (let i = 0, len = comps.length; i < len; i++) {
|
|
if (this[comps[i]] < other[comps[i]]) {
|
|
return -1;
|
|
} else if (this[comps[i]] > other[comps[i]]) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
valueOf() {
|
|
return this.a * 1000 + this.b * 100 + this.c * 10 + this.d;
|
|
}
|
|
toString() {
|
|
return `${this.a},${this.b},${this.c},${this.d}`;
|
|
}
|
|
/**
|
|
* Calculates the specificity of the given selector.
|
|
* @param {Selector} selector The selector to calculate specificity for.
|
|
* @return {Specificity} The specificity of the selector.
|
|
*/
|
|
static calculate(selector) {
|
|
let b = 0;
|
|
let c = 0;
|
|
let d = 0;
|
|
selector.parts.forEach(updateValues);
|
|
return new Specificity(0, b, c, d);
|
|
function updateValues(part) {
|
|
if (!(part instanceof SelectorPart)) return;
|
|
const elementName = part.elementName ? part.elementName.text : '';
|
|
if (elementName && !elementName.endsWith('*')) {
|
|
d++;
|
|
}
|
|
for (const modifier of part.modifiers) {
|
|
switch (modifier.type) {
|
|
case 'class':
|
|
case 'attribute':
|
|
c++;
|
|
break;
|
|
case 'id':
|
|
b++;
|
|
break;
|
|
case 'pseudo':
|
|
if (isPseudoElement(modifier.text)) {
|
|
d++;
|
|
} else {
|
|
c++;
|
|
}
|
|
break;
|
|
case 'not':
|
|
modifier.args.forEach(updateValues);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class PropertyName extends SyntaxUnit {
|
|
constructor(text, hack, pos) {
|
|
super(text, pos, TYPES.PROPERTY_NAME_TYPE);
|
|
this.hack = hack;
|
|
}
|
|
toString() {
|
|
return (this.hack || '') + this.text;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A single value between ":" and ";", that is if there are multiple values
|
|
* separated by commas, this type represents just one of the values.
|
|
*/
|
|
class PropertyValue extends SyntaxUnit {
|
|
constructor(parts, pos) {
|
|
super(parts.join(' '), pos, TYPES.PROPERTY_VALUE_TYPE);
|
|
this.parts = parts;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A single part of a value
|
|
* e.g. '1px solid rgb(1, 2, 3)' has 3 parts
|
|
* @property {PropertyValue} expr
|
|
*/
|
|
class PropertyValuePart extends SyntaxUnit {
|
|
/** @param {parserlib.Token} token */
|
|
constructor(token) {
|
|
const {value, type} = token;
|
|
super(value, token, TYPES.PROPERTY_VALUE_PART_TYPE);
|
|
this.tokenType = type;
|
|
this.expr = token.expr || null;
|
|
switch (type) {
|
|
case Tokens.ANGLE:
|
|
case Tokens.DIMENSION:
|
|
case Tokens.FREQ:
|
|
case Tokens.LENGTH:
|
|
case Tokens.NUMBER:
|
|
case Tokens.PERCENTAGE:
|
|
case Tokens.TIME:
|
|
this.value = token.number;
|
|
this.units = token.units;
|
|
this.type = token.unitsType;
|
|
this.isInt = this.type === 'number' && !value.includes('.');
|
|
break;
|
|
case Tokens.HASH:
|
|
this.type = 'color';
|
|
this.value = value;
|
|
break;
|
|
case Tokens.IDENT:
|
|
if (value.startsWith('--')) {
|
|
this.type = 'custom-property';
|
|
this.value = value;
|
|
} else {
|
|
const namedColor = Colors[value] || Colors[lower(value)];
|
|
this.type = namedColor ? 'color' : 'identifier';
|
|
this.value = namedColor || value;
|
|
}
|
|
break;
|
|
case Tokens.FUNCTION: {
|
|
this.name = token.name;
|
|
SyntaxUnit.addFuncInfo(this, token);
|
|
break;
|
|
}
|
|
case Tokens.STRING:
|
|
this.type = 'string';
|
|
this.value = parseString(value);
|
|
break;
|
|
case Tokens.URI:
|
|
this.type = 'uri';
|
|
this.name = token.name;
|
|
this.uri = token.uri;
|
|
break;
|
|
case Tokens.USO_VAR:
|
|
this._isVar = true;
|
|
break;
|
|
default:
|
|
if (value === ',' || value === '/') {
|
|
this.type = 'operator';
|
|
this.value = value;
|
|
} else {
|
|
this.type = 'unknown';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class PropertyValueIterator {
|
|
/**
|
|
* @param {PropertyValue} value
|
|
*/
|
|
constructor(value) {
|
|
this._i = 0;
|
|
this._parts = value.parts;
|
|
this._marks = [];
|
|
this.value = value;
|
|
this.hasNext = this._parts.length > 0;
|
|
}
|
|
/** @returns {PropertyValuePart|null} */
|
|
peek(count) {
|
|
return this._parts[this._i + (count || 0)] || null;
|
|
}
|
|
/** @returns {?PropertyValuePart} */
|
|
next() {
|
|
if (this.hasNext) {
|
|
this.hasNext = this._i + 1 < this._parts.length;
|
|
return this._parts[this._i++];
|
|
}
|
|
}
|
|
/** @returns {PropertyValueIterator} */
|
|
mark() {
|
|
this._marks.push(this._i);
|
|
return this;
|
|
}
|
|
popMark(success) {
|
|
const i = this._marks.pop();
|
|
if (!success && i != null) {
|
|
this._i = i;
|
|
this.hasNext = i < this._parts.length;
|
|
}
|
|
return success;
|
|
}
|
|
resetTo(i) {
|
|
this._i = i;
|
|
this.hasNext = this._parts.length > i;
|
|
}
|
|
}
|
|
|
|
//#endregion
|
|
//#region ValidationTypes - implementation
|
|
|
|
for (const obj of [VTSimple, VTComplex]) {
|
|
const action = obj === VTSimple
|
|
? rule => vtIsLiteral.bind(obj, rule)
|
|
: Matcher.parse;
|
|
for (const [id, rule] of Object.entries(obj)) {
|
|
if (typeof rule === 'string') {
|
|
obj[id] = Object.defineProperty(action(rule), 'originalText', {value: rule});
|
|
} else if (/^Matcher\s/.test(rule)) {
|
|
obj[id] = rule(Matcher);
|
|
}
|
|
}
|
|
}
|
|
|
|
function vtDescribe(type) {
|
|
const complex = VTComplex[type];
|
|
const text = complex instanceof Matcher ? complex.toString(0) : type;
|
|
return vtExplode(text);
|
|
}
|
|
|
|
function vtExplode(text) {
|
|
if (!text.includes('<')) return text;
|
|
return text.replace(/(<.*?>)([{#?]?)/g, (s, rule, mod) => {
|
|
const ref = VTSimple[rule] || VTComplex[rule];
|
|
if (!ref || !ref.originalText) return s;
|
|
const full = vtExplode(ref.originalText);
|
|
const brace = mod || full.includes(' ');
|
|
return ((brace ? '[ ' : '') + full + (brace ? ' ]' : '')) + mod;
|
|
});
|
|
}
|
|
|
|
/** @param {PropertyValuePart} p */
|
|
function vtIsAttr(p) {
|
|
return p.isAttr && (p = p.expr) && (p = p.parts) && p.length && vtIsIdent(p[0]);
|
|
}
|
|
|
|
/** @param {PropertyValuePart} p */
|
|
function vtIsIdent(p) {
|
|
return p.tokenType === Tokens.IDENT;
|
|
}
|
|
|
|
/** @param {PropertyValuePart} p */
|
|
function vtIsLength(p) {
|
|
return p.text === '0' || p.type === 'length' || p.isCalc;
|
|
}
|
|
|
|
/**
|
|
* @param {string} literals
|
|
* @param {PropertyValuePart} part
|
|
* @return {?boolean}
|
|
*/
|
|
function vtIsLiteral(literals, part) {
|
|
let text;
|
|
for (const arg of literals.includes(' | ') ? literals.split(' | ') : [literals]) {
|
|
if (arg.startsWith('<')) {
|
|
const vt = VTSimple[arg];
|
|
if (vt && vt(part)) {
|
|
return true;
|
|
}
|
|
continue;
|
|
}
|
|
if (arg.endsWith('()') &&
|
|
part.name &&
|
|
part.name.length === arg.length - 2 &&
|
|
lowerCmp(part.name, arg.slice(0, -2))) {
|
|
return true;
|
|
}
|
|
if ((text || part.text) === arg ||
|
|
(text || part.text).length >= arg.length &&
|
|
lowerCmp(arg, text || (text = rxVendorPrefix.test(part.text) ? RegExp.$2 : part.text))) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @param {PropertyValuePart} p */
|
|
function vtIsPct(p) {
|
|
return p.text === '0' || p.type === 'percentage' || p.isCalc;
|
|
}
|
|
|
|
//#endregion
|
|
//#region Validation
|
|
|
|
const validationCache = new Map();
|
|
|
|
function validateProperty(name, property, value, Props = Properties) {
|
|
if (isGlobalKeyword(value.parts[0])) {
|
|
if (value.parts.length > 1) {
|
|
throwEndExpected(value.parts[1], true);
|
|
}
|
|
return;
|
|
}
|
|
const prop = lower(name);
|
|
const spec = Props[prop] || rxVendorPrefix.test(prop) && Props[RegExp.$2];
|
|
if (typeof spec === 'number' || !spec && prop.startsWith('-')) {
|
|
return;
|
|
}
|
|
if (!spec) {
|
|
const problem = Props === Properties || !Properties[prop] ? 'Unknown' : 'Misplaced';
|
|
throw new ValidationError(`${problem} property '${name}'.`, value);
|
|
}
|
|
if (hasVarParts(value)) {
|
|
return;
|
|
}
|
|
let known = validationCache.get(prop);
|
|
if (known && known.has(value.text)) {
|
|
return;
|
|
}
|
|
// Property-specific validation.
|
|
const expr = new PropertyValueIterator(value);
|
|
const m = Matcher.parse(spec);
|
|
let result = m.match(expr);
|
|
if (/\battr\(/i.test(value.text)) {
|
|
if (!result) {
|
|
expr.tryAttr = true;
|
|
expr.resetTo(0);
|
|
result = m.match(expr);
|
|
}
|
|
for (let p; (p = expr.peek()) && p.isAttr && vtIsAttr(p);) {
|
|
expr.next();
|
|
}
|
|
}
|
|
if (result) {
|
|
if (expr.hasNext) throwEndExpected(expr.next());
|
|
} else if (expr.hasNext && expr._i) {
|
|
throwEndExpected(expr.peek());
|
|
} else {
|
|
const {text} = expr.value;
|
|
throw new ValidationError(`Expected '${vtDescribe(spec)}' but found '${text}'.`,
|
|
expr.value);
|
|
}
|
|
if (!known) validationCache.set(prop, (known = new Set()));
|
|
known.add(value.text);
|
|
function throwEndExpected(unit, force) {
|
|
if (force || !unit.isVar) {
|
|
throw new ValidationError(`Expected end of value but found '${unit.text}'.`, unit);
|
|
}
|
|
}
|
|
}
|
|
|
|
//#endregion
|
|
//#region TokenStreamBase
|
|
|
|
/** lookup table size for TokenStreamBase */
|
|
const LT_SIZE = 5;
|
|
|
|
/**
|
|
* Generic TokenStream providing base functionality.
|
|
* @typedef TokenStream
|
|
*/
|
|
class TokenStreamBase {
|
|
|
|
constructor(input) {
|
|
this._reader = new StringReader(input ? input.toString() : '');
|
|
this.resetLT();
|
|
}
|
|
|
|
resetLT() {
|
|
/** @type {parserlib.Token} Last consumed token object */
|
|
this._token = null;
|
|
// Lookahead token buffer.
|
|
this._lt = Array(LT_SIZE).fill(null);
|
|
this._ltIndex = 0;
|
|
this._ltAhead = 0;
|
|
this._ltShift = 0;
|
|
}
|
|
|
|
/**
|
|
* Consumes the next token if that matches any of the given token type(s).
|
|
* @param {int|int[]} tokenTypes
|
|
* @param {string|string[]} [values]
|
|
* @return {parserlib.Token|boolean} token or `false`
|
|
*/
|
|
match(tokenTypes, values) {
|
|
const isArray = typeof tokenTypes === 'object';
|
|
for (let token, tt; (tt = (token = this.get(true)).type);) {
|
|
if ((isArray ? tokenTypes.includes(tt) : tt === tokenTypes) &&
|
|
(!values || values.some(lowerCmpThis, token.value))) {
|
|
return token;
|
|
}
|
|
if (tt !== Tokens.COMMENT) {
|
|
break;
|
|
}
|
|
}
|
|
this.unget();
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Consumes the next token if that matches the given token type(s).
|
|
* Otherwise an error is thrown.
|
|
* @param {int|int[]} tokenTypes
|
|
* @throws {SyntaxError}
|
|
*/
|
|
mustMatch(tokenTypes) {
|
|
return this.match(tokenTypes) ||
|
|
this.throwUnexpected(this.LT(1), tokenTypes);
|
|
}
|
|
|
|
/**
|
|
* Keeps reading until one of the specified token types is found or EOF.
|
|
* @param {int|int[]} tokenTypes
|
|
*/
|
|
advance(tokenTypes) {
|
|
while (this.LA(0) !== 0 && !this.match(tokenTypes)) {
|
|
this.get();
|
|
}
|
|
return this.LA(0);
|
|
}
|
|
|
|
/**
|
|
* Consumes the next token from the token stream.
|
|
* @param {boolean} [asToken]
|
|
* @return {int|parserlib.Token} The token type
|
|
*/
|
|
get(asToken) {
|
|
const i = this._ltIndex;
|
|
const next = i + 1;
|
|
const slot = (i + this._ltShift) % LT_SIZE;
|
|
if (i < this._ltAhead) {
|
|
this._ltIndex = next;
|
|
const token = this._token = this._lt[slot];
|
|
return asToken ? token : token.type;
|
|
}
|
|
const token = this._getToken();
|
|
const {type} = token;
|
|
const hide = type && (Tokens[type] || {}).hide;
|
|
if (type >= 0 && !hide) {
|
|
this._token = token;
|
|
this._lt[slot] = token;
|
|
if (this._ltAhead < LT_SIZE) {
|
|
this._ltIndex = next;
|
|
this._ltAhead++;
|
|
} else {
|
|
this._ltShift = (this._ltShift + 1) % LT_SIZE;
|
|
}
|
|
}
|
|
// Skip to the next token if the token type is marked as hidden.
|
|
return hide ? this.get(asToken) :
|
|
asToken ? token : type;
|
|
}
|
|
|
|
/**
|
|
* Looks ahead a certain number of tokens and returns the token type at that position.
|
|
* @param {int} index The index of the token type to retrieve.
|
|
* 0 for the current token, 1 for the next, -1 for the previous, etc.
|
|
* @return {int} The token type
|
|
* @throws if you lookahead past EOF, past the size of the lookahead buffer,
|
|
* or back past the first token in the lookahead buffer.
|
|
*/
|
|
LA(index) {
|
|
return (index ? this.LT(index) : this._token).type;
|
|
}
|
|
|
|
/**
|
|
* Looks ahead a certain number of tokens and returns the token at that position.
|
|
* @param {int} index The index of the token type to retrieve.
|
|
* 0 for the current token, 1 for the next, -1 for the previous, etc.
|
|
* @param {boolean} [forceCache] won't call get() so it's useful in fast tentative checks
|
|
* @return {Object} The token
|
|
* @throws if you lookahead past EOF, past the size of the lookahead buffer,
|
|
* or back past the first token in the lookahead buffer.
|
|
*/
|
|
LT(index, forceCache) {
|
|
if (!index) {
|
|
return this._token;
|
|
}
|
|
let i = index + this._ltIndex - (index > 0);
|
|
if (index < 0 ? i >= 0 : i < this._ltAhead) {
|
|
return this._lt[(i + this._ltShift) % LT_SIZE];
|
|
} else if (forceCache) {
|
|
return false;
|
|
}
|
|
if (index < 0) {
|
|
throw new Error('Too much lookbehind.');
|
|
}
|
|
if (index > LT_SIZE) {
|
|
throw new Error('Too much lookahead.');
|
|
}
|
|
i = index;
|
|
const oldToken = this._token;
|
|
while (i && i--) this.get();
|
|
const token = this._token;
|
|
this._ltIndex -= index;
|
|
this._token = oldToken;
|
|
return token;
|
|
}
|
|
|
|
/** Returns the token type for the next token in the stream without consuming it. */
|
|
peek() {
|
|
return this.LT(1).type;
|
|
}
|
|
|
|
/** Restores the last consumed token to the token stream. */
|
|
unget() {
|
|
if (this._ltIndex) {
|
|
this._ltIndex--;
|
|
this._token = this._lt[(this._ltIndex - 1 + this._ltShift + LT_SIZE) % LT_SIZE];
|
|
} else {
|
|
throw new Error('Too much lookahead.');
|
|
}
|
|
}
|
|
|
|
throwUnexpected(token = this._token, expected = []) {
|
|
expected = (Array.isArray(expected) ? expected : [expected])
|
|
.map(e => typeof e === 'string' ? e : Tokens.name(e))
|
|
.join(', ');
|
|
const msg = expected
|
|
? `Expected ${expected} but found '${token.value}'.`
|
|
: `Unexpected '${token.value}'.`;
|
|
throw new SyntaxError(msg, token);
|
|
}
|
|
}
|
|
|
|
//#endregion
|
|
//#region TokenStream
|
|
|
|
class TokenStream extends TokenStreamBase {
|
|
|
|
/**
|
|
* @param {Number|Number[]} tokenTypes
|
|
* @param {Boolean} [skipCruftBefore=true] - skip comments/whitespace before matching
|
|
* @returns {Object} token
|
|
*/
|
|
mustMatch(tokenTypes, skipCruftBefore = true) {
|
|
if (skipCruftBefore && tokenTypes !== Tokens.S) {
|
|
this.skipComment(true);
|
|
}
|
|
return super.mustMatch(tokenTypes);
|
|
}
|
|
|
|
/**
|
|
* @param {Boolean} [skipWS] - skip whitespace too
|
|
* @param {Boolean} [skipUsoVar] - skip USO_VAR too
|
|
*/
|
|
skipComment(skipWS, skipUsoVar) {
|
|
const tt = this.LT(1, true).type;
|
|
if (skipWS && tt === Tokens.S ||
|
|
skipUsoVar && tt === Tokens.USO_VAR ||
|
|
tt === Tokens.COMMENT ||
|
|
tt == null && this._ltIndex === this._ltAhead && (
|
|
skipWS && this._reader.readMatch(/\s+/y),
|
|
this._reader.peekTest(/\/\*/y))) {
|
|
while (this.match(skipUsoVar ? TT.usoS : Tokens.S)) { /*NOP*/ }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns {Object} token
|
|
*/
|
|
_getToken() {
|
|
const reader = this._reader;
|
|
/** @namespace parserlib.Token */
|
|
const tok = {
|
|
value: '',
|
|
type: Tokens.CHAR,
|
|
col: reader._col,
|
|
line: reader._line,
|
|
offset: reader._cursor,
|
|
};
|
|
let a = tok.value = reader.read();
|
|
let b = reader.peek();
|
|
if (a === '\\') {
|
|
if (b === '\n' || b === '\f') return tok;
|
|
a = this.readEscape();
|
|
b = reader.peek();
|
|
}
|
|
switch (a) {
|
|
case ' ':
|
|
case '\n':
|
|
case '\r':
|
|
case '\t':
|
|
case '\f':
|
|
tok.type = Tokens.S;
|
|
if (/\s/.test(b)) {
|
|
tok.value += reader.readMatch(/\s+/y) || '';
|
|
}
|
|
return tok;
|
|
case '{':
|
|
tok.type = Tokens.LBRACE;
|
|
tok.endChar = '}';
|
|
return tok;
|
|
case '(':
|
|
tok.type = Tokens.LPAREN;
|
|
tok.endChar = ')';
|
|
return tok;
|
|
case '[':
|
|
tok.type = Tokens.LBRACKET;
|
|
tok.endChar = ']';
|
|
return tok;
|
|
case '/':
|
|
if (b === '*') {
|
|
const str = tok.value = this.readComment(a);
|
|
tok.type = str.startsWith('/*[[') && str.endsWith(']]*/')
|
|
? Tokens.USO_VAR
|
|
: Tokens.COMMENT;
|
|
} else {
|
|
tok.type = Tokens.SLASH;
|
|
}
|
|
return tok;
|
|
case '|':
|
|
case '~':
|
|
case '^':
|
|
case '$':
|
|
case '*':
|
|
if (b === '=') {
|
|
tok.value = a + reader.read();
|
|
tok.type = typeMap.get(tok.value) || Tokens.CHAR;
|
|
} else if (a === '|' && b === '|') {
|
|
reader.read();
|
|
tok.value = '||';
|
|
tok.type = Tokens.COLUMN;
|
|
} else {
|
|
tok.type = typeMap.get(a) || Tokens.CHAR;
|
|
}
|
|
return tok;
|
|
case '"':
|
|
case "'":
|
|
return this.stringToken(a, tok);
|
|
case '#':
|
|
if (rxNameChar.test(b)) {
|
|
tok.type = Tokens.HASH;
|
|
tok.value = this.readName(a);
|
|
}
|
|
return tok;
|
|
case '.':
|
|
if (b >= '0' && b <= '9') {
|
|
this.numberToken(a, tok);
|
|
} else {
|
|
tok.type = Tokens.DOT;
|
|
}
|
|
return tok;
|
|
case '-':
|
|
// could be closing HTML-style comment or CSS variable
|
|
if (b === '-') {
|
|
if (reader.peekTest(/-\w/yu)) {
|
|
this.identOrFunctionToken(a, tok);
|
|
} else if (reader.readMatch('->')) {
|
|
tok.type = Tokens.CDC;
|
|
tok.value = '-->';
|
|
}
|
|
} else if (b >= '0' && b <= '9' || b === '.' && reader.peekTest(/\.\d/y)) {
|
|
this.numberToken(a, tok);
|
|
} else if (rxIdentStart.test(b)) {
|
|
this.identOrFunctionToken(a, tok);
|
|
} else {
|
|
tok.type = Tokens.MINUS;
|
|
}
|
|
return tok;
|
|
case '+':
|
|
if (b >= '0' && b <= '9' || b === '.' && reader.peekTest(/\.\d/y)) {
|
|
this.numberToken(a, tok);
|
|
} else {
|
|
tok.type = Tokens.PLUS;
|
|
}
|
|
return tok;
|
|
case '!':
|
|
return this.importantToken(a, tok);
|
|
case '@':
|
|
return this.atRuleToken(a, tok);
|
|
case ':': {
|
|
const func = /[-hniw]/i.test(b) &&
|
|
reader.readMatch(/(has|not|is|where|(-(moz|webkit)-)?any)\(/iy);
|
|
if (func) {
|
|
const first = b.toLowerCase();
|
|
tok.type =
|
|
first === 'h' ? Tokens.HAS :
|
|
first === 'n' ? Tokens.NOT :
|
|
first === 'i' ? Tokens.IS :
|
|
first === 'w' ? Tokens.WHERE : Tokens.ANY;
|
|
tok.value += func;
|
|
} else {
|
|
tok.type = Tokens.COLON;
|
|
}
|
|
return tok;
|
|
}
|
|
case '<':
|
|
if (b === '!' && reader.readMatch('!--')) {
|
|
tok.type = Tokens.CDO;
|
|
tok.value = '<!--';
|
|
}
|
|
return tok;
|
|
// EOF
|
|
case null:
|
|
tok.type = Tokens.EOF;
|
|
return tok;
|
|
case 'U':
|
|
case 'u':
|
|
return b === '+'
|
|
? this.unicodeRangeToken(a, tok)
|
|
: this.identOrFunctionToken(a, tok);
|
|
}
|
|
if (a >= '0' && a <= '9') {
|
|
this.numberToken(a, tok);
|
|
} else if (rxIdentStart.test(a)) {
|
|
this.identOrFunctionToken(a, tok);
|
|
} else {
|
|
tok.type = typeMap.get(a) || Tokens.CHAR;
|
|
}
|
|
return tok;
|
|
}
|
|
|
|
atRuleToken(first, token) {
|
|
this._reader.mark();
|
|
let rule = first + this.readName();
|
|
let tt = Tokens.type(lower(rule));
|
|
// if it's not valid, use the first character only and reset the reader
|
|
if (tt === Tokens.CHAR || tt === Tokens.UNKNOWN) {
|
|
if (rule.length > 1) {
|
|
tt = Tokens.UNKNOWN_SYM;
|
|
} else {
|
|
tt = Tokens.CHAR;
|
|
rule = first;
|
|
this._reader.reset();
|
|
}
|
|
}
|
|
token.type = tt;
|
|
token.value = rule;
|
|
return token;
|
|
}
|
|
|
|
identOrFunctionToken(first, token) {
|
|
const reader = this._reader;
|
|
const name = this.readChunksWithEscape(first, rxNameCharNoEsc);
|
|
const next = reader.peek();
|
|
token.value = name;
|
|
// might be a URI or function
|
|
if (next === '(') {
|
|
reader.read();
|
|
if (/^(url(-prefix)?|domain)$/i.test(name)) {
|
|
reader.mark();
|
|
const uri = this.readURI(name + '(');
|
|
if (uri) {
|
|
token.type = Tokens.URI;
|
|
token.value = uri.text;
|
|
token.name = name;
|
|
token.uri = uri.value;
|
|
return token;
|
|
}
|
|
reader.reset();
|
|
}
|
|
token.type = Tokens.FUNCTION;
|
|
token.value += '(';
|
|
} else if (next === ':' && lowerCmp(name, 'progid')) {
|
|
token.type = Tokens.IE_FUNCTION;
|
|
token.value += reader.readTo('(');
|
|
} else {
|
|
token.type = Tokens.IDENT;
|
|
}
|
|
return token;
|
|
}
|
|
|
|
importantToken(first, token) {
|
|
const reader = this._reader;
|
|
let text = first;
|
|
reader.mark();
|
|
for (let pass = 1; pass++ <= 2;) {
|
|
const important = reader.readMatch(/\s*important\b/iy);
|
|
if (important) {
|
|
token.type = Tokens.IMPORTANT;
|
|
token.value = text + important;
|
|
return token;
|
|
}
|
|
const comment = reader.readMatch('/*');
|
|
if (!comment) break;
|
|
text += this.readComment(comment);
|
|
}
|
|
reader.reset();
|
|
return token;
|
|
}
|
|
|
|
numberToken(first, token) {
|
|
const reader = this._reader;
|
|
const value = first + (
|
|
this._reader.readMatch(
|
|
first === '.' ?
|
|
/\d+(e[+-]?\d+)?/iy :
|
|
first >= '0' && first <= '9' ?
|
|
/\d*\.?\d*(e[+-]?\d+)?/iy :
|
|
/(\d*\.\d+|\d+\.?\d*)(e[+-]?\d+)?/iy
|
|
) || '');
|
|
let tt = Tokens.NUMBER;
|
|
let units, type;
|
|
const c = reader.peek();
|
|
if (rxIdentStart.test(c)) {
|
|
units = this.readName(reader.read());
|
|
type = UNITS[units] || UNITS[lower(units)];
|
|
tt = type && Tokens[type.toUpperCase()] ||
|
|
type === 'frequency' && Tokens.FREQ ||
|
|
Tokens.DIMENSION;
|
|
} else if (c === '%') {
|
|
units = reader.read();
|
|
type = 'percentage';
|
|
tt = Tokens.PERCENTAGE;
|
|
} else {
|
|
type = 'number';
|
|
}
|
|
token.type = tt;
|
|
token.value = units ? value + units : value;
|
|
token.number = parseFloat(value);
|
|
if (units) token.units = units;
|
|
if (type) token.unitsType = type;
|
|
return token;
|
|
}
|
|
|
|
stringToken(first, token) {
|
|
const delim = first;
|
|
const string = first ? [first] : [];
|
|
const reader = this._reader;
|
|
let tt = Tokens.STRING;
|
|
let c;
|
|
while (true) {
|
|
c = reader.readMatch(/[^\n\r\f\\'"]+|./y);
|
|
if (!c) break;
|
|
string.push(c);
|
|
if (c === '\\') {
|
|
c = reader.read();
|
|
if (c == null) {
|
|
break; // premature EOF after backslash
|
|
} else if (/[^\r\n\f0-9a-f]/i.test(c)) {
|
|
// single-character escape
|
|
string.push(c);
|
|
} else {
|
|
// read up to six hex digits + newline
|
|
string.push(c, reader.readMatch(/[0-9a-f]{1,6}\n?/yi));
|
|
}
|
|
} else if (c === delim) {
|
|
break; // delimiter found.
|
|
} else if (reader.peekTest(/[\n\r\f]/y)) {
|
|
// newline without an escapement: it's an invalid string
|
|
tt = Tokens.INVALID;
|
|
break;
|
|
}
|
|
}
|
|
token.type = c ? tt : Tokens.INVALID; // if the string wasn't closed
|
|
token.value = fastJoin(string);
|
|
return token;
|
|
}
|
|
|
|
unicodeRangeToken(first, token) {
|
|
const reader = this._reader;
|
|
reader.mark();
|
|
token.value += reader.read(); // +
|
|
let chunk = this.readUnicodeRangePart(true);
|
|
if (!chunk) {
|
|
reader.reset();
|
|
return token;
|
|
}
|
|
token.value += chunk;
|
|
// if there's a ? in the first part, there can't be a second part
|
|
if (!token.value.includes('?') && reader.peek() === '-') {
|
|
reader.mark();
|
|
reader.read();
|
|
chunk = this.readUnicodeRangePart(false);
|
|
if (!chunk) {
|
|
reader.reset();
|
|
} else {
|
|
token.value += '-' + chunk;
|
|
}
|
|
}
|
|
token.type = Tokens.UNICODE_RANGE;
|
|
return token;
|
|
}
|
|
|
|
readUnicodeRangePart(allowQuestionMark) {
|
|
const reader = this._reader;
|
|
let part = reader.readMatch(/[0-9a-f]{1,6}/iy);
|
|
while (allowQuestionMark && part.length < 6 && reader.peek() === '?') {
|
|
part += reader.read();
|
|
}
|
|
return part;
|
|
}
|
|
|
|
// returns null w/o resetting reader if string is invalid.
|
|
readString(first = this._reader.read()) {
|
|
const token = this.stringToken(first, {});
|
|
return token.type !== Tokens.INVALID ? token.value : null;
|
|
}
|
|
|
|
// returns null w/o resetting reader if URI is invalid.
|
|
readURI(first) {
|
|
const reader = this._reader;
|
|
const uri = first;
|
|
let value = '';
|
|
this._reader.readMatch(/\s+/y);
|
|
if (reader.peekTest(/['"]/y)) {
|
|
value = this.readString();
|
|
if (value == null) return null;
|
|
value = parseString(value);
|
|
} else {
|
|
value = this.readChunksWithEscape('', rxUnquotedUrlCharNoEsc);
|
|
}
|
|
this._reader.readMatch(/\s+/y);
|
|
// Ensure argument to URL is always double-quoted
|
|
// (This simplifies later processing in PropertyValuePart.)
|
|
return reader.peek() !== ')' ? null : {
|
|
value,
|
|
text: uri + serializeString(value) + reader.read(),
|
|
};
|
|
}
|
|
|
|
readName(first) {
|
|
return this.readChunksWithEscape(first, rxNameCharNoEsc);
|
|
}
|
|
|
|
readEscape() {
|
|
let res = this._reader.readMatch(/[0-9a-f]{1,6}\s?/iy);
|
|
if (res) {
|
|
res = parseInt(res, 16);
|
|
res = String.fromCodePoint(res && res <= 0x10FFFF ? res : 0xFFFD);
|
|
} else {
|
|
res = this._reader.read();
|
|
}
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* @param {?string} first
|
|
* @param {RegExp} rx - must not match \\
|
|
* @returns {string}
|
|
*/
|
|
readChunksWithEscape(first, rx) {
|
|
const reader = this._reader;
|
|
const url = first ? [first] : [];
|
|
while (true) {
|
|
const chunk = reader.readMatch(rx);
|
|
if (chunk) url.push(chunk);
|
|
if (reader.peekTest(/\\[^\r\n\f]/y)) {
|
|
reader.read();
|
|
url.push(this.readEscape());
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
return fastJoin(url);
|
|
}
|
|
|
|
readComment(first) {
|
|
return first +
|
|
this._reader.readCount(2 - first.length) +
|
|
this._reader.readMatch(/([^*]|\*(?!\/))*(\*\/|$)/y);
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} [omitComments]
|
|
* @param {string} [stopOn] - goes to the parent if used at the top nesting level of the value,
|
|
specifying an empty string will stop after consuming the first encountered top block.
|
|
* @returns {?string}
|
|
*/
|
|
readDeclValue({omitComments, stopOn = ';!})'} = {}) {
|
|
const reader = this._reader;
|
|
const value = [];
|
|
const endings = [];
|
|
let end = stopOn;
|
|
const rx = stopOn.includes(';')
|
|
? /([^;!'"{}()[\]/\\]|\/(?!\*))+/y
|
|
: /([^'"{}()[\]/\\]|\/(?!\*))+/y;
|
|
while (!reader.eof()) {
|
|
const chunk = reader.readMatch(rx);
|
|
if (chunk) {
|
|
value.push(chunk);
|
|
}
|
|
reader.mark();
|
|
const c = reader.read();
|
|
if (!endings.length && stopOn.includes(c)) {
|
|
reader.reset();
|
|
break;
|
|
}
|
|
value.push(c);
|
|
if (c === '\\') {
|
|
value[value.length - 1] = this.readEscape();
|
|
} else if (c === '/') {
|
|
value[value.length - 1] = this.readComment(c);
|
|
if (omitComments) value.pop();
|
|
} else if (c === '"' || c === "'") {
|
|
value[value.length - 1] = this.readString(c);
|
|
} else if (c === '{' || c === '(' || c === '[') {
|
|
endings.push(end);
|
|
end = c === '{' ? '}' : c === '(' ? ')' : ']';
|
|
} else if (c === '}' || c === ')' || c === ']') {
|
|
if (!end.includes(c)) {
|
|
reader.reset();
|
|
return null;
|
|
}
|
|
end = endings.pop();
|
|
if (!end && !stopOn) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return fastJoin(value);
|
|
}
|
|
|
|
readUnknownSym() {
|
|
const reader = this._reader;
|
|
let prelude = '';
|
|
let block;
|
|
while (true) {
|
|
let c = reader.peek();
|
|
if (!c) this.throwUnexpected();
|
|
if (c === '{') {
|
|
block = this.readDeclValue({stopOn: ''});
|
|
break;
|
|
} else if (c === ';') {
|
|
reader.read();
|
|
break;
|
|
} else {
|
|
c = this.readDeclValue({omitComments: true, stopOn: ';{}'});
|
|
if (!c) break;
|
|
prelude += c;
|
|
}
|
|
}
|
|
return {prelude: prelude.replace(/^\s+/, ''), block};
|
|
}
|
|
}
|
|
|
|
//#endregion
|
|
//#region parserCache
|
|
|
|
/**
|
|
* Caches the results and reuses them on subsequent parsing of the same code
|
|
*/
|
|
const parserCache = (() => {
|
|
const MAX_DURATION = 10 * 60e3;
|
|
const TRIM_DELAY = 10e3;
|
|
// all blocks since page load; key = text between block start and { inclusive
|
|
const data = new Map();
|
|
// nested block stack
|
|
const stack = [];
|
|
// performance.now() of the current parser
|
|
let generation = null;
|
|
// performance.now() of the first parser after reset or page load,
|
|
// used for weighted sorting in getBlock()
|
|
let generationBase = null;
|
|
// true on page load, first run is pure analysis
|
|
let firstRun = true;
|
|
let parser = null;
|
|
let stream = null;
|
|
|
|
return {
|
|
start(newParser) {
|
|
parser = newParser;
|
|
if (!parser) {
|
|
data.clear();
|
|
stack.length = 0;
|
|
generationBase = performance.now();
|
|
return;
|
|
}
|
|
if (firstRun) firstRun = false;
|
|
stream = parser._tokenStream;
|
|
generation = performance.now();
|
|
trim();
|
|
},
|
|
addEvent(event) {
|
|
if (!parser) return;
|
|
for (let i = stack.length; --i >= 0;) {
|
|
const {offset, endOffset, events} = stack[i];
|
|
if (event.offset >= offset && (!endOffset || event.offset <= endOffset)) {
|
|
events.push(event);
|
|
return;
|
|
}
|
|
}
|
|
},
|
|
findBlock(token = getToken()) {
|
|
if (!parser || firstRun || !token) return;
|
|
|
|
const reader = stream._reader;
|
|
const input = reader._input;
|
|
let start = token.offset;
|
|
const c = input[start];
|
|
if (c === ' ' || c === '\t' || c === '\n' || c === '\f' || c === '\r') {
|
|
const rx = /\s*/y;
|
|
rx.lastIndex = start;
|
|
rx.exec(input);
|
|
start = rx.lastIndex;
|
|
}
|
|
const key = input.slice(start, input.indexOf('{', start) + 1);
|
|
const blocks = data.get(key);
|
|
if (!blocks) return;
|
|
|
|
const block = getBlock(blocks, input, start, key);
|
|
if (!block) return;
|
|
|
|
reader.readCount(start - reader._cursor);
|
|
shiftBlock(reader, start, block);
|
|
shiftStream(reader, block);
|
|
parser._ws();
|
|
return true;
|
|
},
|
|
startBlock(start = getToken()) {
|
|
if (!parser) return;
|
|
stack.push({
|
|
text: '',
|
|
events: [],
|
|
generation: generation,
|
|
line: start.line,
|
|
col: start.col,
|
|
offset: start.offset,
|
|
endLine: undefined,
|
|
endCol: undefined,
|
|
endOffset: undefined,
|
|
});
|
|
},
|
|
adjustBlockStart(start = getToken()) {
|
|
if (!parser) return;
|
|
const block = stack[stack.length - 1];
|
|
block.line = start.line;
|
|
block.col = start.col;
|
|
block.offset = start.offset;
|
|
},
|
|
endBlock(end = getToken()) {
|
|
if (!parser) return;
|
|
const block = stack.pop();
|
|
block.endLine = end.line;
|
|
block.endCol = end.col + end.value.length;
|
|
block.endOffset = end.offset + end.value.length;
|
|
|
|
const input = stream._reader._input;
|
|
const key = input.slice(block.offset, input.indexOf('{', block.offset) + 1);
|
|
block.text = input.slice(block.offset, block.endOffset);
|
|
|
|
let blocks = data.get(key);
|
|
if (!blocks) data.set(key, (blocks = []));
|
|
blocks.push(block);
|
|
},
|
|
cancelBlock: () => stack.pop(),
|
|
feedback({messages}) {
|
|
messages = new Set(messages);
|
|
for (const blocks of data.values()) {
|
|
for (const block of blocks) {
|
|
if (!block.events.length) continue;
|
|
if (block.generation !== generation) continue;
|
|
const {
|
|
line: L1,
|
|
col: C1,
|
|
endLine: L2,
|
|
endCol: C2,
|
|
} = block;
|
|
let isClean = true;
|
|
for (const msg of messages) {
|
|
const {line, col} = msg;
|
|
if (L1 === L2 && line === L1 && C1 <= col && col <= C2 ||
|
|
line === L1 && col >= C1 ||
|
|
line === L2 && col <= C2 ||
|
|
line > L1 && line < L2) {
|
|
messages.delete(msg);
|
|
isClean = false;
|
|
}
|
|
}
|
|
if (isClean) block.events.length = 0;
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Removes old entries from the cache.
|
|
* 'Old' means older than MAX_DURATION or half the blocks from the previous generation(s).
|
|
* @param {Boolean} [immediately] - set internally when debounced by TRIM_DELAY
|
|
*/
|
|
function trim(immediately) {
|
|
if (!immediately) {
|
|
clearTimeout(trim.timer);
|
|
trim.timer = setTimeout(trim, TRIM_DELAY, true);
|
|
return;
|
|
}
|
|
const cutoff = performance.now() - MAX_DURATION;
|
|
for (const [key, blocks] of data.entries()) {
|
|
const halfLen = blocks.length >> 1;
|
|
const newBlocks = blocks
|
|
.sort((a, b) => a.time - b.time)
|
|
.filter((block, i) => block.generation > cutoff ||
|
|
block.generation !== generation && i < halfLen);
|
|
if (!newBlocks.length) {
|
|
data.delete(key);
|
|
} else if (newBlocks.length !== blocks.length) {
|
|
data.set(key, newBlocks);
|
|
}
|
|
}
|
|
}
|
|
|
|
// gets the matching block
|
|
function getBlock(blocks, input, start, key) {
|
|
// extracted to prevent V8 deopt
|
|
const keyLast = Math.max(key.length - 1);
|
|
const check1 = input[start];
|
|
const check2 = input[start + keyLast];
|
|
const generationSpan = performance.now() - generationBase;
|
|
blocks = blocks
|
|
.filter(({text, offset, endOffset}) =>
|
|
text[0] === check1 &&
|
|
text[keyLast] === check2 &&
|
|
text[text.length - 1] === input[start + text.length - 1] &&
|
|
text.startsWith(key) &&
|
|
text === input.substr(start, endOffset - offset))
|
|
.sort((a, b) =>
|
|
// newest and closest will be the first element
|
|
(a.generation - b.generation) / generationSpan +
|
|
(Math.abs(a.offset - start) - Math.abs(b.offset - start)) / input.length);
|
|
// identical blocks may produce different reports in CSSLint
|
|
// so we need to either hijack an older generation block or make a clone
|
|
const block = blocks.find(b => b.generation !== generation);
|
|
return block || deepCopy(blocks[0]);
|
|
}
|
|
|
|
// Shifts positions of the block and its events, also fires the events
|
|
function shiftBlock(reader, start, block) {
|
|
// extracted to prevent V8 deopt
|
|
const deltaLines = reader._line - block.line;
|
|
const deltaCols = block.col === 1 && reader._col === 1 ? 0 : reader._col - block.col;
|
|
const deltaOffs = reader._cursor - block.offset;
|
|
const hasDelta = deltaLines || deltaCols || deltaOffs;
|
|
const shifted = new Set();
|
|
for (const e of block.events) {
|
|
if (hasDelta) {
|
|
applyDelta(e, shifted, block.line, deltaLines, deltaCols, deltaOffs);
|
|
}
|
|
parser.fire(e, false);
|
|
}
|
|
block.generation = generation;
|
|
block.endCol += block.endLine === block.line ? deltaCols : 0;
|
|
block.endLine += deltaLines;
|
|
block.endOffset = reader._cursor + block.text.length;
|
|
block.line += deltaLines;
|
|
block.col += deltaCols;
|
|
block.offset = reader._cursor;
|
|
}
|
|
|
|
function shiftStream(reader, block) {
|
|
reader._line = block.endLine;
|
|
reader._col = block.endCol;
|
|
reader._cursor = block.endOffset;
|
|
|
|
stream.resetLT();
|
|
}
|
|
|
|
// Recursively applies the delta to the event and all its nested parts
|
|
function applyDelta(obj, seen, line, lines, cols, offs) {
|
|
if (seen.has(obj)) return;
|
|
seen.add(obj);
|
|
if (Array.isArray(obj)) {
|
|
for (const item of obj) {
|
|
if ((typeof item === 'object' || Array.isArray(item)) && item) {
|
|
applyDelta(item, seen, line, lines, cols, offs);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
// applyDelta may get surpisingly slow on complex objects so we're using an array
|
|
// because in js an array lookup is much faster than a property lookup
|
|
const keys = Object.keys(obj);
|
|
if (cols !== 0) {
|
|
if (keys.includes('col') && obj.line === line) obj.col += cols;
|
|
if (keys.includes('endCol') && obj.endLine === line) obj.endCol += cols;
|
|
}
|
|
if (lines !== 0) {
|
|
if (keys.includes('line')) obj.line += lines;
|
|
if (keys.includes('endLine')) obj.endLine += lines;
|
|
}
|
|
if (offs !== 0 && keys.includes('offset')) obj.offset += offs;
|
|
for (const k of keys) {
|
|
if (k !== 'col' && k !== 'endCol' &&
|
|
k !== 'line' && k !== 'endLine' &&
|
|
k !== 'offset') {
|
|
const v = obj[k];
|
|
if (v && typeof v === 'object') {
|
|
applyDelta(v, seen, line, lines, cols, offs);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// returns next token if it's already seen or the current token
|
|
function getToken() {
|
|
return parser && (stream.LT(1, true) || stream._token);
|
|
}
|
|
|
|
function deepCopy(obj) {
|
|
if (!obj || typeof obj !== 'object') {
|
|
return obj;
|
|
}
|
|
if (Array.isArray(obj)) {
|
|
return obj.map(deepCopy);
|
|
}
|
|
const copy = {};
|
|
for (const k in obj) {
|
|
copy[k] = deepCopy(obj[k]);
|
|
}
|
|
return copy;
|
|
}
|
|
})();
|
|
|
|
//#endregion
|
|
//#region Parser
|
|
|
|
const ParserRoute = {};
|
|
|
|
class Parser extends EventTarget {
|
|
/**
|
|
* @param {Object} [options]
|
|
* @param {boolean} [options.ieFilters] - accepts IE < 8 filters instead of throwing
|
|
* @param {boolean} [options.skipValidation] - skip syntax validation
|
|
* @param {boolean} [options.starHack] - allows IE6 star hack
|
|
* @param {boolean} [options.strict] - stop on errors instead of reporting them and continuing
|
|
* @param {boolean} [options.topDocOnly] - quickly extract all top-level @-moz-document,
|
|
their {}-block contents is retrieved as text using _simpleBlock()
|
|
* @param {boolean} [options.underscoreHack] - interprets leading _ as IE6-7 for known props
|
|
*/
|
|
constructor(options) {
|
|
super();
|
|
this.options = options || {};
|
|
/** @type {TokenStream|TokenStreamBase} */
|
|
this._tokenStream = null;
|
|
}
|
|
|
|
/**
|
|
* @param {string|Object} event
|
|
* @param {parserlib.Token|SyntaxUnit} [token=this._tokenStream._token] - sets the position
|
|
*/
|
|
fire(event, token = event.offset != null ? event : this._tokenStream._token) {
|
|
if (typeof event === 'string') {
|
|
event = {type: event};
|
|
}
|
|
if (event.offset === undefined && token) {
|
|
event.offset = token.offset;
|
|
if (event.line === undefined) event.line = token.line;
|
|
if (event.col === undefined) event.col = token.col;
|
|
}
|
|
if (token !== false) parserCache.addEvent(event);
|
|
super.fire(event);
|
|
}
|
|
|
|
/**
|
|
* @layer <layer-name>#;
|
|
* @layer <layer-name>? { <stylesheet> };
|
|
*/
|
|
_layer(start) {
|
|
const stream = this._tokenStream;
|
|
const ids = [];
|
|
let t, val;
|
|
do {
|
|
this._ws();
|
|
if ((t = stream.get(true)).type === Tokens.IDENT) {
|
|
ids.push(t.value);
|
|
this._ws();
|
|
t = stream.get(true);
|
|
}
|
|
if ((val = t.value) === '{') {
|
|
if (ids[1]) this.fire({type: 'error', message: '@layer block cannot have multiple ids'}, start);
|
|
this.fire({type: 'startlayer', id: ids[0] || null}, start);
|
|
this._rulesetBlock(start);
|
|
this.fire('endlayer');
|
|
this._ws();
|
|
return;
|
|
}
|
|
} while (val === ',');
|
|
if (val !== ';') stream.mustMatch(Tokens.SEMICOLON);
|
|
this.fire({type: 'layer', ids}, start);
|
|
this._ws();
|
|
}
|
|
|
|
_stylesheet() {
|
|
const stream = this._tokenStream;
|
|
this.fire('startstylesheet');
|
|
this._sheetGlobals();
|
|
const {topDocOnly} = this.options;
|
|
const allowedActions = topDocOnly ? ParserRoute.topDoc : ParserRoute.stylesheet;
|
|
for (let tt, token; (tt = (token = stream.get(true)).type); this._skipCruft()) {
|
|
try {
|
|
const action = allowedActions[tt];
|
|
if (action) {
|
|
action.call(this, token);
|
|
continue;
|
|
}
|
|
if (topDocOnly) {
|
|
stream.readDeclValue({stopOn: '{}'});
|
|
if (stream._reader.peek() === '{') {
|
|
stream.readDeclValue({stopOn: ''});
|
|
}
|
|
continue;
|
|
}
|
|
stream.unget();
|
|
if (!this._ruleset() && stream.peek() !== Tokens.EOF) {
|
|
stream.throwUnexpected(stream.get(true));
|
|
}
|
|
} catch (ex) {
|
|
if (ex instanceof SyntaxError && !this.options.strict) {
|
|
this.fire(Object.assign({}, ex, {type: 'error', error: ex}));
|
|
} else {
|
|
ex.message = ex.stack;
|
|
ex.line = token.line;
|
|
ex.col = token.col;
|
|
throw ex;
|
|
}
|
|
}
|
|
}
|
|
this.fire('endstylesheet');
|
|
}
|
|
|
|
_sheetGlobals() {
|
|
const stream = this._tokenStream;
|
|
this._skipCruft();
|
|
for (const [type, fn, max = Infinity] of [
|
|
[Tokens.CHARSET_SYM, this._charset, 1],
|
|
[Tokens.LAYER_SYM, this._layer],
|
|
[Tokens.IMPORT_SYM, this._import],
|
|
[Tokens.NAMESPACE_SYM, this._namespace],
|
|
]) {
|
|
for (let i = 0; i++ < max && stream.peek() === type;) {
|
|
fn.call(this, stream.get(true));
|
|
this._skipCruft();
|
|
}
|
|
}
|
|
}
|
|
|
|
_charset(start) {
|
|
const stream = this._tokenStream;
|
|
const charset = stream.mustMatch(Tokens.STRING).value;
|
|
stream.mustMatch(Tokens.SEMICOLON);
|
|
this.fire({type: 'charset', charset}, start);
|
|
}
|
|
|
|
_import(start) {
|
|
let t, layer;
|
|
const stream = this._tokenStream;
|
|
t = stream.mustMatch(TT.stringUri);
|
|
const uri = t.uri || t.value.replace(/^["']|["']$/g, '');
|
|
this._ws();
|
|
t = stream.get(true);
|
|
if (/^layer(\()?$/i.test(t.value)) {
|
|
layer = RegExp.$1 ? stream.mustMatch(Tokens.IDENT) : '';
|
|
if (layer) stream.mustMatch(Tokens.RPAREN);
|
|
this._ws();
|
|
t = stream.get(true);
|
|
}
|
|
if (lowerCmp('supports(', t.value)) {
|
|
this._ws();
|
|
if (!this._declaration()) this._supportsCondition();
|
|
stream.mustMatch(Tokens.RPAREN);
|
|
} else {
|
|
stream.unget();
|
|
}
|
|
const media = this._mediaQueryList();
|
|
stream.mustMatch(Tokens.SEMICOLON);
|
|
this.fire({type: 'import', layer, media, uri}, start);
|
|
this._ws();
|
|
}
|
|
|
|
_namespace(start) {
|
|
const stream = this._tokenStream;
|
|
this._ws();
|
|
const prefix = stream.match(Tokens.IDENT).value;
|
|
if (prefix) this._ws();
|
|
const token = stream.mustMatch(TT.stringUri);
|
|
const uri = token.uri || token.value.replace(/^["']|["']$/g, '');
|
|
stream.mustMatch(Tokens.SEMICOLON);
|
|
this.fire({type: 'namespace', prefix, uri}, start);
|
|
this._ws();
|
|
}
|
|
|
|
_supports(start) {
|
|
const stream = this._tokenStream;
|
|
this._ws();
|
|
this._supportsCondition();
|
|
stream.mustMatch(Tokens.LBRACE);
|
|
this.fire('startsupports', start);
|
|
this._rulesetBlock(start);
|
|
this.fire('endsupports');
|
|
this._ws();
|
|
}
|
|
|
|
_supportsCondition() {
|
|
const stream = this._tokenStream;
|
|
if (stream.match(Tokens.IDENT, ['not'])) {
|
|
stream.mustMatch(Tokens.S);
|
|
this._supportsConditionInParens();
|
|
} else {
|
|
this._supportsConditionInParens();
|
|
while (stream.match(Tokens.IDENT, ['and', 'or'])) {
|
|
this._ws();
|
|
this._supportsConditionInParens();
|
|
}
|
|
}
|
|
}
|
|
|
|
_supportsConditionInParens() {
|
|
const stream = this._tokenStream;
|
|
const next = stream.LT(1);
|
|
if (next.type === Tokens.LPAREN) {
|
|
stream.get();
|
|
this._ws();
|
|
const {type, value} = stream.LT(1);
|
|
if (type === Tokens.IDENT) {
|
|
if (lowerCmp(value, 'not')) {
|
|
this._supportsCondition();
|
|
stream.mustMatch(Tokens.RPAREN);
|
|
} else {
|
|
this._supportsDecl(false);
|
|
}
|
|
} else {
|
|
this._supportsCondition();
|
|
stream.mustMatch(Tokens.RPAREN);
|
|
}
|
|
} else if (stream.match(Tokens.FUNCTION, ['selector('])) {
|
|
this._ws();
|
|
const selector = this._selector();
|
|
this.fire({type: 'supportsSelector', selector}, selector);
|
|
stream.mustMatch(Tokens.RPAREN);
|
|
} else {
|
|
this._supportsDecl();
|
|
}
|
|
this._ws();
|
|
}
|
|
|
|
_supportsDecl(requireStartParen = true) {
|
|
const stream = this._tokenStream;
|
|
if (requireStartParen) {
|
|
stream.mustMatch(Tokens.LPAREN);
|
|
}
|
|
this._ws();
|
|
this._declaration();
|
|
stream.mustMatch(Tokens.RPAREN);
|
|
}
|
|
|
|
_media(start) {
|
|
const stream = this._tokenStream;
|
|
this._ws();
|
|
const mediaList = this._mediaQueryList();
|
|
stream.mustMatch(Tokens.LBRACE);
|
|
this.fire({
|
|
type: 'startmedia',
|
|
media: mediaList,
|
|
}, start);
|
|
this._rulesetBlock(start);
|
|
this.fire({
|
|
type: 'endmedia',
|
|
media: mediaList,
|
|
});
|
|
this._ws();
|
|
}
|
|
|
|
_mediaQueryList() {
|
|
const stream = this._tokenStream;
|
|
const mediaList = [];
|
|
this._ws();
|
|
if ([Tokens.IDENT, Tokens.LPAREN].includes(stream.peek())) {
|
|
mediaList.push(this._mediaQuery());
|
|
}
|
|
while (stream.match(Tokens.COMMA)) {
|
|
this._ws();
|
|
mediaList.push(this._mediaQuery());
|
|
}
|
|
return mediaList;
|
|
}
|
|
|
|
_mediaQuery() {
|
|
const stream = this._tokenStream;
|
|
const expressions = [];
|
|
const mod = stream.match(Tokens.IDENT, ['only', 'not']);
|
|
let type = null;
|
|
this._ws();
|
|
const next = stream.LT(1);
|
|
if (next.type === Tokens.IDENT) {
|
|
type = this._mediaFeature();
|
|
} else if (next.value === '(') {
|
|
expressions.push(this._mediaExpression());
|
|
} else {
|
|
return;
|
|
}
|
|
this._ws();
|
|
const c = stream.match(Tokens.IDENT).value;
|
|
if (c) {
|
|
if (lowerCmp(c, 'and') || !type && lowerCmp(c, 'or')) {
|
|
this._ws();
|
|
expressions.push(this._mediaExpression());
|
|
} else {
|
|
stream.throwUnexpected(undefined, ["'and'", !type && "'or'"].filter(Boolean));
|
|
}
|
|
}
|
|
return new MediaQuery(mod.value || null, type, expressions, mod || next);
|
|
}
|
|
|
|
_mediaExpression() {
|
|
const stream = this._tokenStream;
|
|
stream.mustMatch(Tokens.LPAREN);
|
|
const feature = this._mediaFeature(TT.mediaValue);
|
|
let b;
|
|
for (let pass = 0; ++pass <= 2;) {
|
|
this._ws();
|
|
b = stream.get(true).value;
|
|
if (/^[:=<>]$/.test(b)) {
|
|
const isRange = /[<>]/.test(b);
|
|
if (isRange) stream.match(Tokens.EQUALS);
|
|
this._ws();
|
|
b = this._expression({calc: true});
|
|
if (!isRange) break;
|
|
} else {
|
|
stream.unget();
|
|
b = null;
|
|
break;
|
|
}
|
|
}
|
|
stream.mustMatch(Tokens.RPAREN);
|
|
this._ws();
|
|
return new MediaFeature(feature); // TODO: construct the value properly
|
|
}
|
|
|
|
_mediaFeature(type = Tokens.IDENT) {
|
|
return SyntaxUnit.fromToken(this._tokenStream.mustMatch(type));
|
|
}
|
|
|
|
_page(start) {
|
|
const stream = this._tokenStream;
|
|
this._ws();
|
|
const id = stream.match(Tokens.IDENT).value || null;
|
|
if (id && lowerCmp(id, 'auto')) {
|
|
stream.throwUnexpected();
|
|
}
|
|
const pseudo = stream.match(Tokens.COLON)
|
|
? stream.mustMatch(Tokens.IDENT, false).value
|
|
: null;
|
|
this._ws();
|
|
this.fire({type: 'startpage', id, pseudo}, start);
|
|
this._readDeclarations({readMargins: true});
|
|
this.fire({type: 'endpage', id, pseudo});
|
|
}
|
|
|
|
_margin() {
|
|
const margin = SyntaxUnit.fromToken(this._tokenStream.match(TT.margins));
|
|
if (!margin) return false;
|
|
this.fire({type: 'startpagemargin', margin});
|
|
this._readDeclarations();
|
|
this.fire({type: 'endpagemargin', margin});
|
|
return true;
|
|
}
|
|
|
|
_fontFace(start) {
|
|
this.fire('startfontface', start);
|
|
this._ws();
|
|
this._readDeclarations({Props: ScopedProperties['@font-face']});
|
|
this.fire('endfontface');
|
|
}
|
|
|
|
_fontPaletteValues(start) {
|
|
this.fire({
|
|
type: 'startfontpalettevalues',
|
|
id: this._tokenStream.mustMatch(Tokens.IDENT),
|
|
}, start);
|
|
this._readDeclarations({Props: ScopedProperties['@font-palette-values']});
|
|
this.fire('endfontpalettevalues');
|
|
}
|
|
|
|
_viewport(start) {
|
|
// only viewport-fit is allowed but we're reusing MediaQuery syntax unit,
|
|
// and accept anything for the sake of simplicity since the spec isn't yet final:
|
|
// https://drafts.csswg.org/css-round-display/#extending-viewport-rule
|
|
const descriptors = this._mediaQueryList();
|
|
this.fire({type: 'startviewport', descriptors}, start);
|
|
this._ws();
|
|
this._readDeclarations();
|
|
this.fire({type: 'endviewport', descriptors});
|
|
}
|
|
|
|
_document(start) {
|
|
const stream = this._tokenStream;
|
|
const functions = [];
|
|
const prefix = start.value.split('-')[1] || '';
|
|
do {
|
|
this._ws();
|
|
const uri = stream.match(Tokens.URI);
|
|
const fn = uri ? new PropertyValuePart(uri) : this._function() || stream.LT(1);
|
|
functions.push(fn);
|
|
if (uri) this._ws();
|
|
} while (stream.match(Tokens.COMMA));
|
|
for (const fn of functions) {
|
|
if ((fn.type !== 'function' || !/^(url(-prefix)?|domain|regexp)$/i.test(fn.name)) &&
|
|
fn.type !== 'uri') {
|
|
this.fire({
|
|
type: 'error',
|
|
message: 'Expected url( or url-prefix( or domain( or regexp(, instead saw ' +
|
|
Tokens.name(fn.tokenType || fn.type) + ' ' + (fn.text || fn.value),
|
|
}, fn);
|
|
}
|
|
}
|
|
stream.mustMatch(Tokens.LBRACE);
|
|
this.fire({type: 'startdocument', functions, prefix}, start);
|
|
if (this.options.topDocOnly) {
|
|
stream.readDeclValue({stopOn: '}'});
|
|
stream.mustMatch(Tokens.RBRACE);
|
|
} else {
|
|
/* We allow @import and such inside document sections because the final generated CSS for
|
|
* a given page may be valid e.g. if this section is the first one that matched the URL */
|
|
this._sheetGlobals();
|
|
this._rulesetBlock(start);
|
|
}
|
|
this.fire({type: 'enddocument', functions, prefix});
|
|
this._ws();
|
|
}
|
|
|
|
_documentMisplaced(start) {
|
|
this.fire({
|
|
type: 'error',
|
|
message: 'Nested @document produces broken code',
|
|
}, start);
|
|
this._document(start);
|
|
}
|
|
|
|
_combinator() {
|
|
const token = this._tokenStream.match(TT.combinator);
|
|
if (token) {
|
|
this._ws();
|
|
return new Combinator(token);
|
|
}
|
|
}
|
|
|
|
_property() {
|
|
const stream = this._tokenStream;
|
|
let token = stream.get(true);
|
|
let value = null;
|
|
let hack = null;
|
|
let start;
|
|
if (this.options.starHack && token.type === Tokens.STAR) {
|
|
hack = '*';
|
|
start = token;
|
|
token = stream.get(true);
|
|
}
|
|
if (token.type === Tokens.IDENT) {
|
|
let tokenValue = token.value;
|
|
// check for underscore hack - no error if not allowed because it's valid CSS syntax
|
|
if (this.options.underscoreHack && tokenValue.startsWith('_')) {
|
|
hack = '_';
|
|
tokenValue = tokenValue.slice(1);
|
|
}
|
|
value = new PropertyName(tokenValue, hack, start || token);
|
|
this._ws();
|
|
} else {
|
|
stream.unget();
|
|
}
|
|
return value;
|
|
}
|
|
|
|
_ruleset() {
|
|
const stream = this._tokenStream;
|
|
let braceOpened;
|
|
try {
|
|
stream.skipComment(undefined, true);
|
|
if (parserCache.findBlock()) {
|
|
return true;
|
|
}
|
|
parserCache.startBlock();
|
|
const selectors = this._selectorsGroup();
|
|
if (!selectors) {
|
|
parserCache.cancelBlock();
|
|
return false;
|
|
}
|
|
parserCache.adjustBlockStart(selectors[0]);
|
|
this.fire({type: 'startrule', selectors}, selectors[0]);
|
|
this._readDeclarations({stopAfterBrace: true});
|
|
braceOpened = true;
|
|
this.fire({type: 'endrule', selectors});
|
|
parserCache.endBlock();
|
|
this._ws();
|
|
return true;
|
|
} catch (ex) {
|
|
parserCache.cancelBlock();
|
|
if (!(ex instanceof SyntaxError) || this.options.strict) throw ex;
|
|
this.fire(Object.assign({}, ex, {type: 'error', error: ex}));
|
|
// if there's a right brace, the rule is finished so don't do anything
|
|
// otherwise, rethrow the error because it wasn't handled properly
|
|
if (braceOpened && stream.advance(Tokens.RBRACE) !== Tokens.RBRACE) throw ex;
|
|
// If even a single selector fails to parse, the entire ruleset should be thrown away,
|
|
// so we let the parser continue with the next one
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/** @param {parserlib.Token} start */
|
|
_rulesetBlock(start) {
|
|
const stream = this._tokenStream;
|
|
const map = ParserRoute[start.type];
|
|
this._ws();
|
|
while (true) {
|
|
const fn = map[stream.LT(1).type];
|
|
if (fn) fn.call(this, stream.get(true));
|
|
else if (!this._ruleset()) break;
|
|
stream.skipComment();
|
|
}
|
|
stream.mustMatch(Tokens.RBRACE);
|
|
}
|
|
|
|
_selectorsGroup(relative) {
|
|
const stream = this._tokenStream;
|
|
const selectors = [];
|
|
let comma;
|
|
for (let sel; (sel = this._selector(!sel && relative));) {
|
|
selectors.push(sel);
|
|
this._ws(null, true);
|
|
comma = stream.match(Tokens.COMMA);
|
|
if (!comma) break;
|
|
this._ws(null, true);
|
|
}
|
|
if (comma) stream.throwUnexpected(stream.LT(1));
|
|
return selectors.length ? selectors : null;
|
|
}
|
|
|
|
_selector(relative) {
|
|
const stream = this._tokenStream;
|
|
const sel = [];
|
|
let nextSel = null;
|
|
let combinator = null;
|
|
if (!relative || !TT.combinator.includes(stream.LT(1).type)) {
|
|
nextSel = this._simpleSelectorSequence();
|
|
if (!nextSel) {
|
|
return null;
|
|
}
|
|
sel.push(nextSel);
|
|
}
|
|
while (true) {
|
|
combinator = this._combinator();
|
|
if (combinator) {
|
|
sel.push(combinator);
|
|
nextSel = this._simpleSelectorSequence();
|
|
if (nextSel) {
|
|
sel.push(nextSel);
|
|
continue;
|
|
}
|
|
stream.throwUnexpected(stream.LT(1));
|
|
break;
|
|
}
|
|
if (!this._ws(null, true)) {
|
|
break;
|
|
}
|
|
// make a fallback whitespace combinator
|
|
const ws = new Combinator(stream._token);
|
|
// look for an explicit combinator
|
|
combinator = this._combinator();
|
|
// selector is required if there's a combinator
|
|
nextSel = this._simpleSelectorSequence();
|
|
if (nextSel) {
|
|
sel.push(combinator || ws);
|
|
sel.push(nextSel);
|
|
} else if (combinator) {
|
|
stream.throwUnexpected(stream.LT(1));
|
|
}
|
|
}
|
|
return new Selector(sel, sel[0]);
|
|
}
|
|
|
|
_simpleSelectorSequence() {
|
|
const stream = this._tokenStream;
|
|
const start = stream.LT(1);
|
|
const modifiers = [];
|
|
const seq = [];
|
|
const ns = this._namespacePrefix(start.type);
|
|
const elementName = this._typeSelector(ns) || this._universal(ns);
|
|
if (elementName) {
|
|
seq.push(elementName);
|
|
} else if (ns) {
|
|
stream.unget();
|
|
}
|
|
while (true) {
|
|
const token = stream.get(true);
|
|
const action = ParserRoute.simpleSelectorSequence[token.type];
|
|
const component = action ? action.call(this, token) : (stream.unget(), 0);
|
|
if (!component) break;
|
|
modifiers.push(component);
|
|
seq.push(component);
|
|
}
|
|
const text = fastJoin(seq);
|
|
return text && new SelectorPart(elementName, modifiers, text, start);
|
|
}
|
|
|
|
_typeSelector(ns) {
|
|
const stream = this._tokenStream;
|
|
const nsSupplied = ns !== undefined;
|
|
if (!nsSupplied) ns = this._namespacePrefix();
|
|
const name = stream.match(Tokens.IDENT) &&
|
|
new SelectorSubPart(stream._token.value, 'elementName', stream._token);
|
|
if (!name) {
|
|
if (!nsSupplied && ns && ns.length > 0) stream.unget();
|
|
if (!nsSupplied && ns && ns.length > 1) stream.unget();
|
|
return null;
|
|
}
|
|
if (ns) {
|
|
name.text = ns + name.text;
|
|
name.col -= ns.length;
|
|
}
|
|
return name;
|
|
}
|
|
|
|
_hash(start) {
|
|
return new SelectorSubPart(start.value, 'id', start);
|
|
}
|
|
|
|
_class(start) {
|
|
const name = this._tokenStream.mustMatch(Tokens.IDENT, false).value;
|
|
return new SelectorSubPart('.' + name, 'class', start);
|
|
}
|
|
|
|
_namespacePrefix(next) {
|
|
const stream = this._tokenStream;
|
|
if (!next) next = stream.LA(1);
|
|
return next === Tokens.PIPE ? '|' :
|
|
(next === Tokens.IDENT || next === Tokens.STAR) && stream.LA(2) === Tokens.PIPE
|
|
? stream.get().value + stream.get().value
|
|
: null;
|
|
}
|
|
|
|
_universal(ns = this._namespacePrefix()) {
|
|
return `${ns || ''}${this._tokenStream.match(Tokens.STAR).value || ''}` || null;
|
|
}
|
|
|
|
_attrib(start) {
|
|
const stream = this._tokenStream;
|
|
const value = [
|
|
start.value,
|
|
this._ws(),
|
|
this._namespacePrefix() || '',
|
|
stream.mustMatch(Tokens.IDENT, false).value,
|
|
this._ws(),
|
|
];
|
|
if (stream.match(TT.attrMatch)) {
|
|
value.push(
|
|
stream._token.value,
|
|
this._ws(),
|
|
stream.mustMatch(TT.identString).value,
|
|
this._ws());
|
|
if (stream.match(Tokens.IDENT, ['i', 's'])) {
|
|
value.push(
|
|
stream._token.value,
|
|
this._ws());
|
|
}
|
|
}
|
|
value.push(stream.mustMatch(Tokens.RBRACKET).value);
|
|
return new SelectorSubPart(fastJoin(value), 'attribute', start);
|
|
}
|
|
|
|
_pseudo(start) {
|
|
const stream = this._tokenStream;
|
|
const colons = start.value + (stream.match(Tokens.COLON).value || '');
|
|
const t = stream.mustMatch(TT.pseudo);
|
|
const pseudo = t.type === Tokens.IDENT ? t.value :
|
|
t.value +
|
|
this._ws() +
|
|
(this._expression({list: true}) || '') +
|
|
stream.mustMatch(Tokens.RPAREN).value;
|
|
return new SelectorSubPart(colons + pseudo, 'pseudo', {
|
|
line: t.line,
|
|
col: t.col - colons.length,
|
|
offset: t.offset - colons.length,
|
|
});
|
|
}
|
|
|
|
_expression({calc, list} = {}) {
|
|
const chunks = [];
|
|
const stream = this._tokenStream;
|
|
while (stream.get()) {
|
|
const {type, value} = stream._token;
|
|
if (calc && type === Tokens.FUNCTION) {
|
|
if (!rxCalc.test(value)) {
|
|
stream.throwUnexpected();
|
|
}
|
|
chunks.push(value,
|
|
this._expr('calc').text,
|
|
stream.mustMatch(Tokens.RPAREN).value);
|
|
} else if (TT.expression.includes(type) || list && type === Tokens.COMMA) {
|
|
chunks.push(value, this._ws());
|
|
} else if (type !== Tokens.COMMENT) {
|
|
stream.unget();
|
|
break;
|
|
}
|
|
}
|
|
return fastJoin(chunks) || null;
|
|
}
|
|
|
|
_is(start) {
|
|
let args;
|
|
const value =
|
|
start.value +
|
|
this._ws() +
|
|
(args = this._selectorsGroup(start.type === Tokens.HAS)) +
|
|
this._ws() +
|
|
this._tokenStream.mustMatch(Tokens.RPAREN).value;
|
|
const type = lower(Tokens.name(start.type));
|
|
return Object.assign(new SelectorSubPart(value, type, start), {args});
|
|
}
|
|
|
|
_negation(start) {
|
|
const stream = this._tokenStream;
|
|
const value = [start.value, this._ws()];
|
|
const args = this._selectorsGroup();
|
|
if (!args) stream.throwUnexpected(stream.LT(1));
|
|
value.push(...args, this._ws(), stream.mustMatch(Tokens.RPAREN).value);
|
|
return Object.assign(new SelectorSubPart(fastJoin(value), 'not', start), {args});
|
|
}
|
|
|
|
_declaration(consumeSemicolon, Props) {
|
|
const stream = this._tokenStream;
|
|
const property = this._property();
|
|
if (!property) {
|
|
return false;
|
|
}
|
|
stream.mustMatch(Tokens.COLON);
|
|
const value = property.text.startsWith('--')
|
|
? this._customProperty() // whitespace is a part of custom property value
|
|
: (this._ws(), this._expr());
|
|
// if there's no parts for the value, it's an error
|
|
if (!value || value.length === 0) {
|
|
stream.throwUnexpected(stream.LT(1));
|
|
}
|
|
let invalid;
|
|
if (!this.options.skipValidation) {
|
|
try {
|
|
/* If hacks are allowed, then only check the root property.
|
|
Otherwise treat `_property` or `*property` as invalid */
|
|
const name =
|
|
this.options.starHack && property.hack === '*' ||
|
|
this.options.underscoreHack && property.hack === '_'
|
|
? property.text
|
|
: property.toString();
|
|
validateProperty(name, property, value, Props);
|
|
} catch (ex) {
|
|
if (!(ex instanceof ValidationError)) {
|
|
ex.message = ex.stack;
|
|
}
|
|
invalid = ex;
|
|
}
|
|
}
|
|
const event = {
|
|
type: 'property',
|
|
important: stream.match(Tokens.IMPORTANT),
|
|
property,
|
|
value,
|
|
};
|
|
this._ws();
|
|
if (invalid) {
|
|
event.invalid = invalid;
|
|
event.message = invalid.message;
|
|
}
|
|
this.fire(event, property);
|
|
if (consumeSemicolon) {
|
|
while (stream.match(TT.semiS)) {/*NOP*/}
|
|
this._ws();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
_expr(inFunction, endToken = Tokens.RPAREN) {
|
|
const stream = this._tokenStream;
|
|
const values = [];
|
|
while (true) {
|
|
let value = this._term(inFunction);
|
|
if (!value && !values.length) {
|
|
return null;
|
|
}
|
|
// get everything inside the parens and let validateProperty handle that
|
|
if (!value && inFunction && stream.peek() !== endToken) {
|
|
stream.get();
|
|
value = new PropertyValuePart(stream._token);
|
|
} else if (!value) {
|
|
break;
|
|
}
|
|
// TODO: remove this hack
|
|
const last = values[values.length - 1];
|
|
if (last && last.offset === value.offset && last.text === value.text) {
|
|
break;
|
|
}
|
|
values.push(value);
|
|
this._ws();
|
|
const operator = this._tokenStream.match(inFunction ? TT.opInFunc : TT.op);
|
|
if (operator) {
|
|
this._ws();
|
|
values.push(new PropertyValuePart(operator));
|
|
}
|
|
}
|
|
return values[0] ? new PropertyValue(values, values[0]) : null;
|
|
}
|
|
|
|
_customProperty() {
|
|
const value = this._tokenStream.readDeclValue();
|
|
if (value) {
|
|
const token = this._tokenStream._token;
|
|
token.value = value;
|
|
token.type = Tokens.IDENT;
|
|
return new PropertyValue([new PropertyValuePart(token)], token);
|
|
}
|
|
}
|
|
|
|
_term(inFunction) {
|
|
const stream = this._tokenStream;
|
|
const unary = stream.match(TT.plusMinus) && stream._token;
|
|
const finalize = (token, value) => {
|
|
if (!token && unary) stream.unget();
|
|
if (!token) return null;
|
|
if (token instanceof SyntaxUnit) return token;
|
|
if (unary) {
|
|
token.line = unary.line;
|
|
token.col = unary.col;
|
|
token.value = unary.value + (value || token.value);
|
|
} else if (value) {
|
|
token.value = value;
|
|
}
|
|
return new PropertyValuePart(token);
|
|
};
|
|
const next = this.options.ieFilters && stream.LT(1);
|
|
if (next && next.type === Tokens.IE_FUNCTION) {
|
|
return finalize(next, this._ieFunction());
|
|
}
|
|
// see if it's a simple block
|
|
if (stream.match(inFunction ? TT.LParenBracketBrace : TT.LParenBracket)) {
|
|
const token = stream._token;
|
|
const endToken = Tokens.type(token.endChar);
|
|
token.expr = this._expr(inFunction, endToken);
|
|
stream.mustMatch(endToken);
|
|
return finalize(token, token.value + (token.expr || '') + token.endChar);
|
|
}
|
|
return finalize(
|
|
// see if there's a simple match
|
|
stream.match(TT.term) && stream._token ||
|
|
this._hexcolor() ||
|
|
this._function({asText: Boolean(unary)}));
|
|
}
|
|
|
|
_function({asText} = {}) {
|
|
const stream = this._tokenStream;
|
|
if (!stream.match(Tokens.FUNCTION)) return null;
|
|
const start = stream._token;
|
|
const name = start.value.slice(0, -1);
|
|
this._ws();
|
|
const expr = this._expr(lower(name));
|
|
const ieFilter = this.options.ieFilters && stream.peek() === Tokens.EQUALS ?
|
|
this._functionIeFilter() : '';
|
|
const text = name + '(' + (expr || '') + ieFilter + ')';
|
|
stream.mustMatch(Tokens.RPAREN);
|
|
this._ws();
|
|
if (asText) {
|
|
return text;
|
|
}
|
|
const m = rxVendorPrefix.exec(name) || [];
|
|
return SyntaxUnit.addFuncInfo(
|
|
new SyntaxUnit(text, start, 'function', {
|
|
expr,
|
|
name: m[2] || name,
|
|
prefix: m[1] || '',
|
|
tokenType: Tokens.FUNCTION,
|
|
}));
|
|
}
|
|
|
|
_functionIeFilter() {
|
|
const stream = this._tokenStream;
|
|
const text = [];
|
|
do {
|
|
if (this._ws()) {
|
|
text.push(stream._token.value);
|
|
}
|
|
// might be second time in the loop
|
|
if (stream.LA(0) === Tokens.COMMA) {
|
|
text.push(stream._token.value);
|
|
}
|
|
stream.match(Tokens.IDENT);
|
|
text.push(stream._token.value);
|
|
stream.match(Tokens.EQUALS);
|
|
text.push(stream._token.value);
|
|
let lt = stream.peek();
|
|
while (lt !== Tokens.COMMA &&
|
|
lt !== Tokens.S &&
|
|
lt !== Tokens.RPAREN &&
|
|
lt !== Tokens.EOF) {
|
|
stream.get();
|
|
text.push(stream._token.value);
|
|
lt = stream.peek();
|
|
}
|
|
} while (stream.match([Tokens.COMMA, Tokens.S]));
|
|
return fastJoin(text);
|
|
}
|
|
|
|
_ieFunction() {
|
|
const stream = this._tokenStream;
|
|
const text = [];
|
|
// IE function can begin like a regular function, too
|
|
if (stream.match([Tokens.IE_FUNCTION, Tokens.FUNCTION])) {
|
|
text.push(stream._token.value);
|
|
do {
|
|
text.push(
|
|
stream._token.value === ',' ? ',' : '', // subsequent loops
|
|
this._ws(),
|
|
stream.match(Tokens.IDENT).value || '',
|
|
stream.match(Tokens.EQUALS).value || '');
|
|
// functionText.push(this._term());
|
|
while (!/^([,)]|\s+)$/.test(stream.LT(1).value)) {
|
|
text.push(stream.get(true).value);
|
|
}
|
|
} while (stream.match([Tokens.COMMA, Tokens.S]));
|
|
text.push(stream.match(Tokens.RPAREN).value);
|
|
this._ws();
|
|
}
|
|
return fastJoin(text) || null;
|
|
}
|
|
|
|
_hexcolor() {
|
|
const stream = this._tokenStream;
|
|
if (!stream.match(Tokens.HASH)) return null;
|
|
const token = stream._token;
|
|
const color = token.value;
|
|
const len = color.length;
|
|
if (len !== 4 && len !== 5 && len !== 7 && len !== 9 ||
|
|
!/^#([a-f\d]{3}(?:[a-f\d](?:[a-f\d]{2}){0,2})?)$/i.test(color)) {
|
|
throw new SyntaxError(`Expected a hex color but found '${color}'.`, token);
|
|
}
|
|
this._ws();
|
|
return token;
|
|
}
|
|
|
|
_keyframes(start) {
|
|
const stream = this._tokenStream;
|
|
const prefix = rxVendorPrefix.test(start.value) ? RegExp.$1 : '';
|
|
this._ws();
|
|
const name = this._keyframeName();
|
|
stream.mustMatch(Tokens.LBRACE);
|
|
this.fire({type: 'startkeyframes', name, prefix}, start);
|
|
// check for key
|
|
while (true) {
|
|
this._ws();
|
|
const tt = stream.peek();
|
|
if (tt !== Tokens.IDENT && tt !== Tokens.PERCENTAGE) break;
|
|
this._keyframeRule();
|
|
}
|
|
stream.mustMatch(Tokens.RBRACE);
|
|
this.fire({type: 'endkeyframes', name, prefix});
|
|
this._ws();
|
|
}
|
|
|
|
_keyframeName() {
|
|
const stream = this._tokenStream;
|
|
stream.mustMatch(TT.identString);
|
|
return SyntaxUnit.fromToken(stream._token);
|
|
}
|
|
|
|
_keyframeRule() {
|
|
const keys = this._keyList();
|
|
this.fire({type: 'startkeyframerule', keys}, keys[0]);
|
|
this._readDeclarations();
|
|
this.fire({type: 'endkeyframerule', keys});
|
|
}
|
|
|
|
_keyList() {
|
|
const stream = this._tokenStream;
|
|
const keyList = [];
|
|
// must be least one key
|
|
keyList.push(this._key());
|
|
this._ws();
|
|
while (stream.match(Tokens.COMMA)) {
|
|
this._ws();
|
|
keyList.push(this._key());
|
|
this._ws();
|
|
}
|
|
return keyList;
|
|
}
|
|
|
|
_key() {
|
|
const stream = this._tokenStream;
|
|
if (stream.match(Tokens.PERCENTAGE)) {
|
|
return SyntaxUnit.fromToken(stream._token);
|
|
}
|
|
if (stream.match(Tokens.IDENT)) {
|
|
if (/^(from|to)$/i.test(stream._token.value)) {
|
|
return SyntaxUnit.fromToken(stream._token);
|
|
}
|
|
stream.unget();
|
|
}
|
|
// if it gets here, there wasn't a valid token, so time to explode
|
|
stream.throwUnexpected(stream.LT(1), ['%', "'from'", "'to'"]);
|
|
}
|
|
|
|
_skipCruft() {
|
|
while (this._tokenStream.match(TT.cruft)) { /*NOP*/ }
|
|
}
|
|
|
|
/**
|
|
* @param {{}} [_]
|
|
* @param {Boolean} [_.checkStart] - check for the left brace at the beginning.
|
|
* @param {Boolean} [_.readMargins] - check for margin patterns.
|
|
* @param {Boolean} [_.stopAfterBrace] - stop after the final } without consuming whitespace
|
|
* @param {{}} [_.Props] - definitions of valid properties
|
|
*/
|
|
_readDeclarations({
|
|
checkStart = true,
|
|
readMargins = false,
|
|
stopAfterBrace = false,
|
|
Props,
|
|
} = {}) {
|
|
const stream = this._tokenStream;
|
|
if (checkStart) stream.mustMatch(Tokens.LBRACE);
|
|
let next, tt;
|
|
while ((next = stream.get(true)).value !== '}' && (tt = next.type)) {
|
|
try {
|
|
if (tt === Tokens.SEMICOLON ||
|
|
this._ws(next, true) ||
|
|
readMargins && this._margin() ||
|
|
(stream.unget(), this._declaration(true, Props))) {
|
|
continue;
|
|
}
|
|
break;
|
|
} catch (ex) {
|
|
this._readDeclarationsRecovery(ex, arguments[0]);
|
|
}
|
|
}
|
|
if (next.value !== '}') stream.mustMatch(Tokens.RBRACE);
|
|
if (!stopAfterBrace) this._ws();
|
|
}
|
|
|
|
_readDeclarationsRecovery(ex) {
|
|
if (ex) {
|
|
if (this.options.strict || !(ex instanceof SyntaxError)) {
|
|
throw ex; // if not a syntax error, rethrow it
|
|
}
|
|
this.fire(Object.assign({}, ex, {
|
|
type: ex.type || 'error',
|
|
recoverable: true,
|
|
error: ex,
|
|
}));
|
|
}
|
|
switch (this._tokenStream.advance([Tokens.SEMICOLON, Tokens.RBRACE])) {
|
|
case Tokens.SEMICOLON:
|
|
return true; // continue to the next declaration
|
|
case Tokens.RBRACE:
|
|
this._tokenStream.unget();
|
|
return;
|
|
default:
|
|
throw ex;
|
|
}
|
|
}
|
|
|
|
_ws(start, skipUsoVar) {
|
|
const tt = start && start.type;
|
|
if (tt && !(
|
|
tt === Tokens.S ||
|
|
tt === Tokens.COMMENT ||
|
|
tt === Tokens.USO_VAR && skipUsoVar
|
|
)) {
|
|
return '';
|
|
}
|
|
const stream = this._tokenStream;
|
|
const tokens = skipUsoVar ? TT.usoS : Tokens.S;
|
|
let ws = start ? start.value : '';
|
|
for (let t; (t = stream.LT(1, true)) && t.type === Tokens.S;) {
|
|
ws += stream.get(true).value;
|
|
}
|
|
if (stream._ltIndex === stream._ltAhead) {
|
|
ws += stream._reader.readMatch(/\s+/y) || '';
|
|
if (!stream._reader.peekTest(/\/\*/y)) {
|
|
return ws;
|
|
}
|
|
}
|
|
while (stream.match(tokens)) {
|
|
ws += stream._token.value;
|
|
}
|
|
return ws;
|
|
}
|
|
|
|
_unknownSym(start) {
|
|
if (this.options.strict) {
|
|
throw new SyntaxError('Unknown @ rule.', start);
|
|
}
|
|
const {prelude, block} = this._tokenStream.readUnknownSym();
|
|
this.fire({type: 'unknown-at-rule', name: start.value, prelude, block}, start);
|
|
this._ws();
|
|
}
|
|
|
|
_verifyEnd() {
|
|
const stream = this._tokenStream;
|
|
if (stream.peek() !== Tokens.EOF) {
|
|
stream.throwUnexpected(stream.LT(1));
|
|
}
|
|
}
|
|
|
|
parse(input, {reuseCache} = {}) {
|
|
this._tokenStream = new TokenStream(input);
|
|
parserCache.start(reuseCache && this);
|
|
this._stylesheet();
|
|
}
|
|
|
|
parseStyleSheet(input) {
|
|
return this.parse(input);
|
|
}
|
|
|
|
parseMediaQuery(input, {reuseCache} = {}) {
|
|
this._tokenStream = new TokenStream(input);
|
|
parserCache.start(reuseCache && this);
|
|
const result = this._mediaQuery();
|
|
this._verifyEnd();
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Parses a property value (everything after the semicolon).
|
|
* @return {PropertyValue} The property value.
|
|
* @throws parserlib.util.SyntaxError If an unexpected token is found.
|
|
*/
|
|
parsePropertyValue(input) {
|
|
this._tokenStream = new TokenStream(input);
|
|
this._ws();
|
|
const result = this._expr();
|
|
this._ws();
|
|
this._verifyEnd();
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Parses a complete CSS rule, including selectors and
|
|
* properties.
|
|
* @param {String} input The text to parser.
|
|
* @return {Boolean} True if the parse completed successfully, false if not.
|
|
*/
|
|
parseRule(input, {reuseCache} = {}) {
|
|
this._tokenStream = new TokenStream(input);
|
|
parserCache.start(reuseCache && this);
|
|
this._ws();
|
|
const result = this._ruleset();
|
|
this._ws();
|
|
this._verifyEnd();
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Parses a single CSS selector (no comma)
|
|
* @param {String} input The text to parse as a CSS selector.
|
|
* @return {Selector} An object representing the selector.
|
|
* @throws parserlib.util.SyntaxError If an unexpected token is found.
|
|
*/
|
|
parseSelector(input) {
|
|
this._tokenStream = new TokenStream(input);
|
|
this._ws();
|
|
const result = this._selector();
|
|
this._ws();
|
|
this._verifyEnd();
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Parses an HTML style attribute: a set of CSS declarations
|
|
* separated by semicolons.
|
|
* @param {String} input The text to parse as a style attribute
|
|
* @return {void}
|
|
*/
|
|
parseStyleAttribute(input) {
|
|
// help error recovery in _readDeclarations()
|
|
this._tokenStream = new TokenStream(input + '}');
|
|
this._readDeclarations({checkStart: false});
|
|
}
|
|
}
|
|
|
|
Object.assign(Parser, TYPES);
|
|
Object.assign(Parser.prototype, TYPES);
|
|
Parser.prototype._readWhitespace = Parser.prototype._ws;
|
|
|
|
ParserRoute[Tokens.DOCUMENT_SYM] =
|
|
ParserRoute[Tokens.LAYER_SYM] =
|
|
ParserRoute[Tokens.MEDIA_SYM] =
|
|
ParserRoute[Tokens.SUPPORTS_SYM] = {
|
|
[Tokens.DOCUMENT_SYM]: Parser.prototype._documentMisplaced,
|
|
[Tokens.FONT_FACE_SYM]: Parser.prototype._fontFace,
|
|
[Tokens.FONT_PALETTE_VALUES_SYM]: Parser.prototype._fontPaletteValues,
|
|
[Tokens.KEYFRAMES_SYM]: Parser.prototype._keyframes,
|
|
[Tokens.LAYER_SYM]: Parser.prototype._layer,
|
|
[Tokens.MEDIA_SYM]: Parser.prototype._media,
|
|
[Tokens.PAGE_SYM]: Parser.prototype._page,
|
|
[Tokens.SUPPORTS_SYM]: Parser.prototype._supports,
|
|
[Tokens.UNKNOWN_SYM]: Parser.prototype._unknownSym,
|
|
[Tokens.VIEWPORT_SYM]: Parser.prototype._viewport,
|
|
};
|
|
ParserRoute.stylesheet = Object.assign({}, ParserRoute[Tokens.DOCUMENT_SYM], {
|
|
[Tokens.DOCUMENT_SYM]: Parser.prototype._document,
|
|
[Tokens.S]: Parser.prototype._ws,
|
|
});
|
|
ParserRoute.topDoc = {
|
|
[Tokens.DOCUMENT_SYM]: Parser.prototype._document,
|
|
[Tokens.UNKNOWN_SYM]: Parser.prototype._unknownSym,
|
|
[Tokens.S]: Parser.prototype._ws,
|
|
};
|
|
ParserRoute.simpleSelectorSequence = {
|
|
[Tokens.HASH]: Parser.prototype._hash,
|
|
[Tokens.DOT]: Parser.prototype._class,
|
|
[Tokens.LBRACKET]: Parser.prototype._attrib,
|
|
[Tokens.COLON]: Parser.prototype._pseudo,
|
|
[Tokens.IS]: Parser.prototype._is,
|
|
[Tokens.HAS]: Parser.prototype._is,
|
|
[Tokens.ANY]: Parser.prototype._is,
|
|
[Tokens.WHERE]: Parser.prototype._is,
|
|
[Tokens.NOT]: Parser.prototype._negation,
|
|
};
|
|
|
|
//#endregion
|
|
//#region Helper functions
|
|
|
|
function escapeChar(c) {
|
|
return c === '"' ? '\\' + c : `\\${c.codePointAt(0).toString(16)} `;
|
|
}
|
|
|
|
function fastJoin(arr) {
|
|
return !arr.length ? '' :
|
|
arr.length === 1 ? `${arr[0]}` :
|
|
arr.length === 2 ? `${arr[0]}${arr[1]}` :
|
|
arr.join('');
|
|
}
|
|
|
|
/**
|
|
* vars can span any number of grammar parts so not gonna try to guess. KISS.
|
|
* @param {PropertyValue} value
|
|
*/
|
|
function hasVarParts(value) {
|
|
return value.parts.some(p => p.isVar);
|
|
}
|
|
|
|
function isPseudoElement(pseudo) {
|
|
return pseudo.startsWith('::') ||
|
|
/^:(first-(letter|line)|before|after)$/i.test(pseudo);
|
|
}
|
|
|
|
function lower(text) {
|
|
if (typeof text !== 'string') text = `${text}`;
|
|
let result = lowercaseCache.get(text);
|
|
if (result) return result;
|
|
result = text.toLowerCase();
|
|
lowercaseCache.set(text, result);
|
|
return result;
|
|
}
|
|
|
|
function lowerCmp(a, b) {
|
|
return a.length === b.length && (a === b || lower(a) === lower(b));
|
|
}
|
|
|
|
/** @this {String} */
|
|
function lowerCmpThis(a) {
|
|
return a.length === this.length && (a === this || lower(a) === lower(this));
|
|
}
|
|
|
|
function parseString(str) {
|
|
return str.slice(1, -1) // strip surrounding quotes
|
|
.replace(/\\(\r\n|[^\r0-9a-f]|[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?)/ig, unescapeChar);
|
|
}
|
|
|
|
function serializeString(value) {
|
|
return `"${value.replace(/["\r\n\f]/g, escapeChar)}"`;
|
|
}
|
|
|
|
function unescapeChar(m, c) {
|
|
if (c === '\n' || c === '\r\n' || c === '\r' || c === '\f') {
|
|
return '';
|
|
}
|
|
m = /^[0-9a-f]{1,6}/i.exec(c);
|
|
return m ? String.fromCodePoint(parseInt(m[0], 16)) : c;
|
|
}
|
|
|
|
//#endregion
|
|
//#region PUBLIC API
|
|
|
|
/** @namespace parserlib */
|
|
return {
|
|
css: {
|
|
Colors,
|
|
Combinator,
|
|
GlobalKeywords,
|
|
Matcher,
|
|
MediaFeature,
|
|
MediaQuery,
|
|
Parser,
|
|
Properties,
|
|
PropertyName,
|
|
PropertyValue,
|
|
PropertyValuePart,
|
|
ScopedProperties,
|
|
Selector,
|
|
SelectorPart,
|
|
SelectorSubPart,
|
|
Specificity,
|
|
TokenStream,
|
|
Tokens,
|
|
ValidationError,
|
|
},
|
|
util: {
|
|
EventTarget,
|
|
StringReader,
|
|
SyntaxError,
|
|
SyntaxUnit,
|
|
TokenStreamBase,
|
|
fastJoin,
|
|
isPseudoElement,
|
|
lower,
|
|
rxVendorPrefix,
|
|
describeProp: vtExplode,
|
|
},
|
|
cache: parserCache,
|
|
};
|
|
|
|
//#endregion
|
|
})();
|