Spaces:
Paused
Paused
| ; | |
| const scanner = require('./scanner.cjs'); | |
| const TAB = 9; | |
| const N = 10; | |
| const F = 12; | |
| const R = 13; | |
| const SPACE = 32; | |
| const EXCLAMATIONMARK = 33; // ! | |
| const NUMBERSIGN = 35; // # | |
| const AMPERSAND = 38; // & | |
| const APOSTROPHE = 39; // ' | |
| const LEFTPARENTHESIS = 40; // ( | |
| const RIGHTPARENTHESIS = 41; // ) | |
| const ASTERISK = 42; // * | |
| const PLUSSIGN = 43; // + | |
| const COMMA = 44; // , | |
| const HYPERMINUS = 45; // - | |
| const LESSTHANSIGN = 60; // < | |
| const GREATERTHANSIGN = 62; // > | |
| const QUESTIONMARK = 63; // ? | |
| const COMMERCIALAT = 64; // @ | |
| const LEFTSQUAREBRACKET = 91; // [ | |
| const RIGHTSQUAREBRACKET = 93; // ] | |
| const LEFTCURLYBRACKET = 123; // { | |
| const VERTICALLINE = 124; // | | |
| const RIGHTCURLYBRACKET = 125; // } | |
| const INFINITY = 8734; // ∞ | |
| const COMBINATOR_PRECEDENCE = { | |
| ' ': 1, | |
| '&&': 2, | |
| '||': 3, | |
| '|': 4 | |
| }; | |
| function readMultiplierRange(scanner) { | |
| let min = null; | |
| let max = null; | |
| scanner.eat(LEFTCURLYBRACKET); | |
| scanner.skipWs(); | |
| min = scanner.scanNumber(scanner); | |
| scanner.skipWs(); | |
| if (scanner.charCode() === COMMA) { | |
| scanner.pos++; | |
| scanner.skipWs(); | |
| if (scanner.charCode() !== RIGHTCURLYBRACKET) { | |
| max = scanner.scanNumber(scanner); | |
| scanner.skipWs(); | |
| } | |
| } else { | |
| max = min; | |
| } | |
| scanner.eat(RIGHTCURLYBRACKET); | |
| return { | |
| min: Number(min), | |
| max: max ? Number(max) : 0 | |
| }; | |
| } | |
| function readMultiplier(scanner) { | |
| let range = null; | |
| let comma = false; | |
| switch (scanner.charCode()) { | |
| case ASTERISK: | |
| scanner.pos++; | |
| range = { | |
| min: 0, | |
| max: 0 | |
| }; | |
| break; | |
| case PLUSSIGN: | |
| scanner.pos++; | |
| range = { | |
| min: 1, | |
| max: 0 | |
| }; | |
| break; | |
| case QUESTIONMARK: | |
| scanner.pos++; | |
| range = { | |
| min: 0, | |
| max: 1 | |
| }; | |
| break; | |
| case NUMBERSIGN: | |
| scanner.pos++; | |
| comma = true; | |
| if (scanner.charCode() === LEFTCURLYBRACKET) { | |
| range = readMultiplierRange(scanner); | |
| } else if (scanner.charCode() === QUESTIONMARK) { | |
| // https://www.w3.org/TR/css-values-4/#component-multipliers | |
| // > the # and ? multipliers may be stacked as #? | |
| // In this case just treat "#?" as a single multiplier | |
| // { min: 0, max: 0, comma: true } | |
| scanner.pos++; | |
| range = { | |
| min: 0, | |
| max: 0 | |
| }; | |
| } else { | |
| range = { | |
| min: 1, | |
| max: 0 | |
| }; | |
| } | |
| break; | |
| case LEFTCURLYBRACKET: | |
| range = readMultiplierRange(scanner); | |
| break; | |
| default: | |
| return null; | |
| } | |
| return { | |
| type: 'Multiplier', | |
| comma, | |
| min: range.min, | |
| max: range.max, | |
| term: null | |
| }; | |
| } | |
| function maybeMultiplied(scanner, node) { | |
| const multiplier = readMultiplier(scanner); | |
| if (multiplier !== null) { | |
| multiplier.term = node; | |
| // https://www.w3.org/TR/css-values-4/#component-multipliers | |
| // > The + and # multipliers may be stacked as +#; | |
| // Represent "+#" as nested multipliers: | |
| // { ...<multiplier #>, | |
| // term: { | |
| // ...<multipler +>, | |
| // term: node | |
| // } | |
| // } | |
| if (scanner.charCode() === NUMBERSIGN && | |
| scanner.charCodeAt(scanner.pos - 1) === PLUSSIGN) { | |
| return maybeMultiplied(scanner, multiplier); | |
| } | |
| // https://www.w3.org/TR/css-values-4/#component-multipliers | |
| // > the # and ? multipliers, {A} and ? multipliers, and {A,B} and ? multipliers | |
| // > may be stacked as #?, {A}?, and {A,B}?, respectively | |
| // Represent "{}?" as nested multipliers as well as "+#". | |
| // The "#?" case is already handled above, in maybeMultiplied() | |
| if (scanner.charCode() === QUESTIONMARK && | |
| scanner.charCodeAt(scanner.pos - 1) === RIGHTCURLYBRACKET) { | |
| return maybeMultiplied(scanner, multiplier); | |
| } | |
| return multiplier; | |
| } | |
| return node; | |
| } | |
| function maybeToken(scanner) { | |
| const ch = scanner.peek(); | |
| if (ch === '') { | |
| return null; | |
| } | |
| return maybeMultiplied(scanner, { | |
| type: 'Token', | |
| value: ch | |
| }); | |
| } | |
| function readProperty(scanner) { | |
| let name; | |
| scanner.eat(LESSTHANSIGN); | |
| scanner.eat(APOSTROPHE); | |
| name = scanner.scanWord(); | |
| scanner.eat(APOSTROPHE); | |
| scanner.eat(GREATERTHANSIGN); | |
| return maybeMultiplied(scanner, { | |
| type: 'Property', | |
| name | |
| }); | |
| } | |
| // https://drafts.csswg.org/css-values-3/#numeric-ranges | |
| // 4.1. Range Restrictions and Range Definition Notation | |
| // | |
| // Range restrictions can be annotated in the numeric type notation using CSS bracketed | |
| // range notation—[min,max]—within the angle brackets, after the identifying keyword, | |
| // indicating a closed range between (and including) min and max. | |
| // For example, <integer [0, 10]> indicates an integer between 0 and 10, inclusive. | |
| function readTypeRange(scanner) { | |
| // use null for Infinity to make AST format JSON serializable/deserializable | |
| let min = null; // -Infinity | |
| let max = null; // Infinity | |
| let sign = 1; | |
| scanner.eat(LEFTSQUAREBRACKET); | |
| if (scanner.charCode() === HYPERMINUS) { | |
| scanner.peek(); | |
| sign = -1; | |
| } | |
| if (sign == -1 && scanner.charCode() === INFINITY) { | |
| scanner.peek(); | |
| } else { | |
| min = sign * Number(scanner.scanNumber(scanner)); | |
| if (scanner.isNameCharCode()) { | |
| min += scanner.scanWord(); | |
| } | |
| } | |
| scanner.skipWs(); | |
| scanner.eat(COMMA); | |
| scanner.skipWs(); | |
| if (scanner.charCode() === INFINITY) { | |
| scanner.peek(); | |
| } else { | |
| sign = 1; | |
| if (scanner.charCode() === HYPERMINUS) { | |
| scanner.peek(); | |
| sign = -1; | |
| } | |
| max = sign * Number(scanner.scanNumber(scanner)); | |
| if (scanner.isNameCharCode()) { | |
| max += scanner.scanWord(); | |
| } | |
| } | |
| scanner.eat(RIGHTSQUAREBRACKET); | |
| return { | |
| type: 'Range', | |
| min, | |
| max | |
| }; | |
| } | |
| function readType(scanner) { | |
| let name; | |
| let opts = null; | |
| scanner.eat(LESSTHANSIGN); | |
| name = scanner.scanWord(); | |
| // https://drafts.csswg.org/css-values-5/#boolean | |
| if (name === 'boolean-expr') { | |
| scanner.eat(LEFTSQUAREBRACKET); | |
| const implicitGroup = readImplicitGroup(scanner, RIGHTSQUAREBRACKET); | |
| scanner.eat(RIGHTSQUAREBRACKET); | |
| scanner.eat(GREATERTHANSIGN); | |
| return maybeMultiplied(scanner, { | |
| type: 'Boolean', | |
| term: implicitGroup.terms.length === 1 | |
| ? implicitGroup.terms[0] | |
| : implicitGroup | |
| }); | |
| } | |
| if (scanner.charCode() === LEFTPARENTHESIS && | |
| scanner.nextCharCode() === RIGHTPARENTHESIS) { | |
| scanner.pos += 2; | |
| name += '()'; | |
| } | |
| if (scanner.charCodeAt(scanner.findWsEnd(scanner.pos)) === LEFTSQUAREBRACKET) { | |
| scanner.skipWs(); | |
| opts = readTypeRange(scanner); | |
| } | |
| scanner.eat(GREATERTHANSIGN); | |
| return maybeMultiplied(scanner, { | |
| type: 'Type', | |
| name, | |
| opts | |
| }); | |
| } | |
| function readKeywordOrFunction(scanner) { | |
| const name = scanner.scanWord(); | |
| if (scanner.charCode() === LEFTPARENTHESIS) { | |
| scanner.pos++; | |
| return { | |
| type: 'Function', | |
| name | |
| }; | |
| } | |
| return maybeMultiplied(scanner, { | |
| type: 'Keyword', | |
| name | |
| }); | |
| } | |
| function regroupTerms(terms, combinators) { | |
| function createGroup(terms, combinator) { | |
| return { | |
| type: 'Group', | |
| terms, | |
| combinator, | |
| disallowEmpty: false, | |
| explicit: false | |
| }; | |
| } | |
| let combinator; | |
| combinators = Object.keys(combinators) | |
| .sort((a, b) => COMBINATOR_PRECEDENCE[a] - COMBINATOR_PRECEDENCE[b]); | |
| while (combinators.length > 0) { | |
| combinator = combinators.shift(); | |
| let i = 0; | |
| let subgroupStart = 0; | |
| for (; i < terms.length; i++) { | |
| const term = terms[i]; | |
| if (term.type === 'Combinator') { | |
| if (term.value === combinator) { | |
| if (subgroupStart === -1) { | |
| subgroupStart = i - 1; | |
| } | |
| terms.splice(i, 1); | |
| i--; | |
| } else { | |
| if (subgroupStart !== -1 && i - subgroupStart > 1) { | |
| terms.splice( | |
| subgroupStart, | |
| i - subgroupStart, | |
| createGroup(terms.slice(subgroupStart, i), combinator) | |
| ); | |
| i = subgroupStart + 1; | |
| } | |
| subgroupStart = -1; | |
| } | |
| } | |
| } | |
| if (subgroupStart !== -1 && combinators.length) { | |
| terms.splice( | |
| subgroupStart, | |
| i - subgroupStart, | |
| createGroup(terms.slice(subgroupStart, i), combinator) | |
| ); | |
| } | |
| } | |
| return combinator; | |
| } | |
| function readImplicitGroup(scanner, stopCharCode = -1) { | |
| const combinators = Object.create(null); | |
| const terms = []; | |
| let prevToken = null; | |
| let prevTokenPos = scanner.pos; | |
| let prevTokenIsFunction = false; | |
| while (scanner.charCode() !== stopCharCode) { | |
| let token = prevTokenIsFunction | |
| ? readImplicitGroup(scanner, RIGHTPARENTHESIS) | |
| : peek(scanner); | |
| if (!token) { | |
| break; | |
| } | |
| if (token.type === 'Spaces') { | |
| continue; | |
| } | |
| if (prevTokenIsFunction) { | |
| if (token.terms.length === 0) { | |
| prevTokenIsFunction = false; | |
| continue; | |
| } | |
| if (token.combinator === ' ') { | |
| while (token.terms.length > 1) { | |
| combinators[' '] = true; // a b | |
| terms.push({ | |
| type: 'Combinator', | |
| value: ' ' | |
| }, token.terms.shift()); | |
| } | |
| token = token.terms[0]; | |
| } | |
| } | |
| if (token.type === 'Combinator') { | |
| // check for combinator in group beginning and double combinator sequence | |
| if (prevToken === null || prevToken.type === 'Combinator') { | |
| scanner.pos = prevTokenPos; | |
| scanner.error('Unexpected combinator'); | |
| } | |
| combinators[token.value] = true; | |
| } else if (prevToken !== null && prevToken.type !== 'Combinator') { | |
| combinators[' '] = true; // a b | |
| terms.push({ | |
| type: 'Combinator', | |
| value: ' ' | |
| }); | |
| } | |
| terms.push(token); | |
| prevToken = token; | |
| prevTokenPos = scanner.pos; | |
| prevTokenIsFunction = token.type === 'Function'; | |
| } | |
| // check for combinator in group ending | |
| if (prevToken !== null && prevToken.type === 'Combinator') { | |
| scanner.pos -= prevTokenPos; | |
| scanner.error('Unexpected combinator'); | |
| } | |
| return { | |
| type: 'Group', | |
| terms, | |
| combinator: regroupTerms(terms, combinators) || ' ', | |
| disallowEmpty: false, | |
| explicit: false | |
| }; | |
| } | |
| function readGroup(scanner) { | |
| let result; | |
| scanner.eat(LEFTSQUAREBRACKET); | |
| result = readImplicitGroup(scanner, RIGHTSQUAREBRACKET); | |
| scanner.eat(RIGHTSQUAREBRACKET); | |
| result.explicit = true; | |
| if (scanner.charCode() === EXCLAMATIONMARK) { | |
| scanner.pos++; | |
| result.disallowEmpty = true; | |
| } | |
| return result; | |
| } | |
| function peek(scanner) { | |
| let code = scanner.charCode(); | |
| switch (code) { | |
| case RIGHTSQUAREBRACKET: | |
| // don't eat, stop scan a group | |
| break; | |
| case LEFTSQUAREBRACKET: | |
| return maybeMultiplied(scanner, readGroup(scanner)); | |
| case LESSTHANSIGN: | |
| return scanner.nextCharCode() === APOSTROPHE | |
| ? readProperty(scanner) | |
| : readType(scanner); | |
| case VERTICALLINE: | |
| return { | |
| type: 'Combinator', | |
| value: scanner.substringToPos( | |
| scanner.pos + (scanner.nextCharCode() === VERTICALLINE ? 2 : 1) | |
| ) | |
| }; | |
| case AMPERSAND: | |
| scanner.pos++; | |
| scanner.eat(AMPERSAND); | |
| return { | |
| type: 'Combinator', | |
| value: '&&' | |
| }; | |
| case COMMA: | |
| scanner.pos++; | |
| return { | |
| type: 'Comma' | |
| }; | |
| case APOSTROPHE: | |
| return maybeMultiplied(scanner, { | |
| type: 'String', | |
| value: scanner.scanString() | |
| }); | |
| case SPACE: | |
| case TAB: | |
| case N: | |
| case R: | |
| case F: | |
| return { | |
| type: 'Spaces', | |
| value: scanner.scanSpaces() | |
| }; | |
| case COMMERCIALAT: | |
| code = scanner.nextCharCode(); | |
| if (scanner.isNameCharCode(code)) { | |
| scanner.pos++; | |
| return { | |
| type: 'AtKeyword', | |
| name: scanner.scanWord() | |
| }; | |
| } | |
| return maybeToken(scanner); | |
| case ASTERISK: | |
| case PLUSSIGN: | |
| case QUESTIONMARK: | |
| case NUMBERSIGN: | |
| case EXCLAMATIONMARK: | |
| // prohibited tokens (used as a multiplier start) | |
| break; | |
| case LEFTCURLYBRACKET: | |
| // LEFTCURLYBRACKET is allowed since mdn/data uses it w/o quoting | |
| // check next char isn't a number, because it's likely a disjoined multiplier | |
| code = scanner.nextCharCode(); | |
| if (code < 48 || code > 57) { | |
| return maybeToken(scanner); | |
| } | |
| break; | |
| default: | |
| if (scanner.isNameCharCode(code)) { | |
| return readKeywordOrFunction(scanner); | |
| } | |
| return maybeToken(scanner); | |
| } | |
| } | |
| function parse(source) { | |
| const scanner$1 = new scanner.Scanner(source); | |
| const result = readImplicitGroup(scanner$1); | |
| if (scanner$1.pos !== source.length) { | |
| scanner$1.error('Unexpected input'); | |
| } | |
| // reduce redundant groups with single group term | |
| if (result.terms.length === 1 && result.terms[0].type === 'Group') { | |
| return result.terms[0]; | |
| } | |
| return result; | |
| } | |
| exports.parse = parse; | |