'use strict'; 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: // { ..., // term: { // ..., // term: node // } // } if (scanner.charCode() === NUMBERSIGN && scanner.charCodeAt(scanner.pos - 1) === PLUSSIGN) { 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, 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) { const combinators = Object.create(null); const terms = []; let token; let prevToken = null; let prevTokenPos = scanner.pos; while (scanner.charCode() !== stopCharCode && (token = peek(scanner, stopCharCode))) { if (token.type !== 'Spaces') { 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; } } // 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, stopCharCode) { let result; scanner.eat(LEFTSQUAREBRACKET); result = readImplicitGroup(scanner, stopCharCode); scanner.eat(RIGHTSQUAREBRACKET); result.explicit = true; if (scanner.charCode() === EXCLAMATIONMARK) { scanner.pos++; result.disallowEmpty = true; } return result; } function peek(scanner, stopCharCode) { let code = scanner.charCode(); switch (code) { case RIGHTSQUAREBRACKET: // don't eat, stop scan a group break; case LEFTSQUAREBRACKET: return maybeMultiplied(scanner, readGroup(scanner, stopCharCode)); 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;