diff options
| author | Adam Mathes <adam@adammathes.com> | 2026-02-13 21:34:48 -0800 |
|---|---|---|
| committer | Adam Mathes <adam@adammathes.com> | 2026-02-13 21:34:48 -0800 |
| commit | 76cb9c2a39d477a64824a985ade40507e3bbade1 (patch) | |
| tree | 41e997aa9c6f538d3a136af61dae9424db2005a9 /vanilla/node_modules/css-tree/lib | |
| parent | 819a39a21ac992b1393244a4c283bbb125208c69 (diff) | |
| download | neko-76cb9c2a39d477a64824a985ade40507e3bbade1.tar.gz neko-76cb9c2a39d477a64824a985ade40507e3bbade1.tar.bz2 neko-76cb9c2a39d477a64824a985ade40507e3bbade1.zip | |
feat(vanilla): add testing infrastructure and tests (NK-wjnczv)
Diffstat (limited to 'vanilla/node_modules/css-tree/lib')
134 files changed, 11705 insertions, 0 deletions
diff --git a/vanilla/node_modules/css-tree/lib/convertor/create.js b/vanilla/node_modules/css-tree/lib/convertor/create.js new file mode 100644 index 0000000..56e4333 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/convertor/create.js @@ -0,0 +1,28 @@ +import { List } from '../utils/List.js'; + +export function createConvertor(walk) { + return { + fromPlainObject(ast) { + walk(ast, { + enter(node) { + if (node.children && node.children instanceof List === false) { + node.children = new List().fromArray(node.children); + } + } + }); + + return ast; + }, + toPlainObject(ast) { + walk(ast, { + leave(node) { + if (node.children && node.children instanceof List) { + node.children = node.children.toArray(); + } + } + }); + + return ast; + } + }; +}; diff --git a/vanilla/node_modules/css-tree/lib/convertor/index.js b/vanilla/node_modules/css-tree/lib/convertor/index.js new file mode 100644 index 0000000..0f8d4af --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/convertor/index.js @@ -0,0 +1,4 @@ +import { createConvertor } from './create.js'; +import walker from '../walker/index.js'; + +export default createConvertor(walker); diff --git a/vanilla/node_modules/css-tree/lib/data-patch.js b/vanilla/node_modules/css-tree/lib/data-patch.js new file mode 100644 index 0000000..cd49cbe --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/data-patch.js @@ -0,0 +1,6 @@ +import { createRequire } from 'module'; + +const require = createRequire(import.meta.url); +const patch = require('../data/patch.json'); + +export default patch; diff --git a/vanilla/node_modules/css-tree/lib/data.js b/vanilla/node_modules/css-tree/lib/data.js new file mode 100755 index 0000000..162fa20 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/data.js @@ -0,0 +1,118 @@ +import { createRequire } from 'module'; +import patch from './data-patch.js'; + +const require = createRequire(import.meta.url); +const mdnAtrules = require('mdn-data/css/at-rules.json'); +const mdnProperties = require('mdn-data/css/properties.json'); +const mdnSyntaxes = require('mdn-data/css/syntaxes.json'); + +const hasOwn = Object.hasOwn || ((object, property) => Object.prototype.hasOwnProperty.call(object, property)); +const extendSyntax = /^\s*\|\s*/; + +function preprocessAtrules(dict) { + const result = Object.create(null); + + for (const [atruleName, atrule] of Object.entries(dict)) { + let descriptors = null; + + if (atrule.descriptors) { + descriptors = Object.create(null); + + for (const [name, descriptor] of Object.entries(atrule.descriptors)) { + descriptors[name] = descriptor.syntax; + } + } + + result[atruleName.substr(1)] = { + prelude: atrule.syntax.trim().replace(/\{(.|\s)+\}/, '').match(/^@\S+\s+([^;\{]*)/)[1].trim() || null, + descriptors + }; + } + + return result; +} + +function patchDictionary(dict, patchDict) { + const result = Object.create(null); + + // copy all syntaxes for an original dict + for (const [key, value] of Object.entries(dict)) { + if (value) { + result[key] = value.syntax || value; + } + } + + // apply a patch + for (const key of Object.keys(patchDict)) { + if (hasOwn(dict, key)) { + if (patchDict[key].syntax) { + result[key] = extendSyntax.test(patchDict[key].syntax) + ? result[key] + ' ' + patchDict[key].syntax.trim() + : patchDict[key].syntax; + } else { + delete result[key]; + } + } else { + if (patchDict[key].syntax) { + result[key] = patchDict[key].syntax.replace(extendSyntax, ''); + } + } + } + + return result; +} + +function preprocessPatchAtrulesDescritors(declarations) { + const result = {}; + + for (const [key, value] of Object.entries(declarations || {})) { + result[key] = typeof value === 'string' + ? { syntax: value } + : value; + } + + return result; +} + +function patchAtrules(dict, patchDict) { + const result = {}; + + // copy all syntaxes for an original dict + for (const key in dict) { + if (patchDict[key] === null) { + continue; + } + + const atrulePatch = patchDict[key] || {}; + + result[key] = { + prelude: key in patchDict && 'prelude' in atrulePatch + ? atrulePatch.prelude + : dict[key].prelude || null, + descriptors: patchDictionary( + dict[key].descriptors || {}, + preprocessPatchAtrulesDescritors(atrulePatch.descriptors) + ) + }; + } + + // apply a patch + for (const [key, atrulePatch] of Object.entries(patchDict)) { + if (atrulePatch && !hasOwn(dict, key)) { + result[key] = { + prelude: atrulePatch.prelude || null, + descriptors: atrulePatch.descriptors + ? patchDictionary({}, preprocessPatchAtrulesDescritors(atrulePatch.descriptors)) + : null + }; + } + } + + return result; +} + +export default { + types: patchDictionary(mdnSyntaxes, patch.types), + atrules: patchAtrules(preprocessAtrules(mdnAtrules), patch.atrules), + properties: patchDictionary(mdnProperties, patch.properties) +}; diff --git a/vanilla/node_modules/css-tree/lib/definition-syntax/SyntaxError.js b/vanilla/node_modules/css-tree/lib/definition-syntax/SyntaxError.js new file mode 100644 index 0000000..5f63138 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/definition-syntax/SyntaxError.js @@ -0,0 +1,12 @@ +import { createCustomError } from '../utils/create-custom-error.js'; + +export function SyntaxError(message, input, offset) { + return Object.assign(createCustomError('SyntaxError', message), { + input, + offset, + rawMessage: message, + message: message + '\n' + + ' ' + input + '\n' + + '--' + new Array((offset || input.length) + 1).join('-') + '^' + }); +}; diff --git a/vanilla/node_modules/css-tree/lib/definition-syntax/generate.js b/vanilla/node_modules/css-tree/lib/definition-syntax/generate.js new file mode 100644 index 0000000..b2636d1 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/definition-syntax/generate.js @@ -0,0 +1,135 @@ +function noop(value) { + return value; +} + +function generateMultiplier(multiplier) { + const { min, max, comma } = multiplier; + + if (min === 0 && max === 0) { + return comma ? '#?' : '*'; + } + + if (min === 0 && max === 1) { + return '?'; + } + + if (min === 1 && max === 0) { + return comma ? '#' : '+'; + } + + if (min === 1 && max === 1) { + return ''; + } + + return ( + (comma ? '#' : '') + + (min === max + ? '{' + min + '}' + : '{' + min + ',' + (max !== 0 ? max : '') + '}' + ) + ); +} + +function generateTypeOpts(node) { + switch (node.type) { + case 'Range': + return ( + ' [' + + (node.min === null ? '-∞' : node.min) + + ',' + + (node.max === null ? '∞' : node.max) + + ']' + ); + + default: + throw new Error('Unknown node type `' + node.type + '`'); + } +} + +function generateSequence(node, decorate, forceBraces, compact) { + const combinator = node.combinator === ' ' || compact ? node.combinator : ' ' + node.combinator + ' '; + const result = node.terms + .map(term => internalGenerate(term, decorate, forceBraces, compact)) + .join(combinator); + + if (node.explicit || forceBraces) { + return (compact || result[0] === ',' ? '[' : '[ ') + result + (compact ? ']' : ' ]'); + } + + return result; +} + +function internalGenerate(node, decorate, forceBraces, compact) { + let result; + + switch (node.type) { + case 'Group': + result = + generateSequence(node, decorate, forceBraces, compact) + + (node.disallowEmpty ? '!' : ''); + break; + + case 'Multiplier': + // return since node is a composition + return ( + internalGenerate(node.term, decorate, forceBraces, compact) + + decorate(generateMultiplier(node), node) + ); + + case 'Boolean': + result = '<boolean-expr[' + internalGenerate(node.term, decorate, forceBraces, compact) + ']>'; + break; + + case 'Type': + result = '<' + node.name + (node.opts ? decorate(generateTypeOpts(node.opts), node.opts) : '') + '>'; + break; + + case 'Property': + result = '<\'' + node.name + '\'>'; + break; + + case 'Keyword': + result = node.name; + break; + + case 'AtKeyword': + result = '@' + node.name; + break; + + case 'Function': + result = node.name + '('; + break; + + case 'String': + case 'Token': + result = node.value; + break; + + case 'Comma': + result = ','; + break; + + default: + throw new Error('Unknown node type `' + node.type + '`'); + } + + return decorate(result, node); +} + +export function generate(node, options) { + let decorate = noop; + let forceBraces = false; + let compact = false; + + if (typeof options === 'function') { + decorate = options; + } else if (options) { + forceBraces = Boolean(options.forceBraces); + compact = Boolean(options.compact); + if (typeof options.decorate === 'function') { + decorate = options.decorate; + } + } + + return internalGenerate(node, decorate, forceBraces, compact); +}; diff --git a/vanilla/node_modules/css-tree/lib/definition-syntax/index.js b/vanilla/node_modules/css-tree/lib/definition-syntax/index.js new file mode 100644 index 0000000..0c24dbd --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/definition-syntax/index.js @@ -0,0 +1,4 @@ +export { SyntaxError } from './SyntaxError.js'; +export { generate } from './generate.js'; +export { parse } from './parse.js'; +export { walk } from './walk.js'; diff --git a/vanilla/node_modules/css-tree/lib/definition-syntax/parse.js b/vanilla/node_modules/css-tree/lib/definition-syntax/parse.js new file mode 100644 index 0000000..4e13ad8 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/definition-syntax/parse.js @@ -0,0 +1,552 @@ +import { Scanner } from './scanner.js'; + +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); + } + + 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) { + 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); + } +} + +export function parse(source) { + const scanner = new Scanner(source); + const result = readImplicitGroup(scanner); + + if (scanner.pos !== source.length) { + scanner.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; +}; diff --git a/vanilla/node_modules/css-tree/lib/definition-syntax/scanner.js b/vanilla/node_modules/css-tree/lib/definition-syntax/scanner.js new file mode 100644 index 0000000..d0b1895 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/definition-syntax/scanner.js @@ -0,0 +1,109 @@ +import { SyntaxError } from './SyntaxError.js'; + +const TAB = 9; +const N = 10; +const F = 12; +const R = 13; +const SPACE = 32; +const NAME_CHAR = new Uint8Array(128).map((_, idx) => + /[a-zA-Z0-9\-]/.test(String.fromCharCode(idx)) ? 1 : 0 +); + +export class Scanner { + constructor(str) { + this.str = str; + this.pos = 0; + } + + charCodeAt(pos) { + return pos < this.str.length ? this.str.charCodeAt(pos) : 0; + } + charCode() { + return this.charCodeAt(this.pos); + } + isNameCharCode(code = this.charCode()) { + return code < 128 && NAME_CHAR[code] === 1; + } + nextCharCode() { + return this.charCodeAt(this.pos + 1); + } + nextNonWsCode(pos) { + return this.charCodeAt(this.findWsEnd(pos)); + } + skipWs() { + this.pos = this.findWsEnd(this.pos); + } + findWsEnd(pos) { + for (; pos < this.str.length; pos++) { + const code = this.str.charCodeAt(pos); + if (code !== R && code !== N && code !== F && code !== SPACE && code !== TAB) { + break; + } + } + + return pos; + } + substringToPos(end) { + return this.str.substring(this.pos, this.pos = end); + } + eat(code) { + if (this.charCode() !== code) { + this.error('Expect `' + String.fromCharCode(code) + '`'); + } + + this.pos++; + } + peek() { + return this.pos < this.str.length ? this.str.charAt(this.pos++) : ''; + } + error(message) { + throw new SyntaxError(message, this.str, this.pos); + } + + scanSpaces() { + return this.substringToPos(this.findWsEnd(this.pos)); + } + scanWord() { + let end = this.pos; + + for (; end < this.str.length; end++) { + const code = this.str.charCodeAt(end); + if (code >= 128 || NAME_CHAR[code] === 0) { + break; + } + } + + if (this.pos === end) { + this.error('Expect a keyword'); + } + + return this.substringToPos(end); + } + scanNumber() { + let end = this.pos; + + for (; end < this.str.length; end++) { + const code = this.str.charCodeAt(end); + + if (code < 48 || code > 57) { + break; + } + } + + if (this.pos === end) { + this.error('Expect a number'); + } + + return this.substringToPos(end); + } + scanString() { + const end = this.str.indexOf('\'', this.pos + 1); + + if (end === -1) { + this.pos = this.str.length; + this.error('Expect an apostrophe'); + } + + return this.substringToPos(end + 1); + } +}; diff --git a/vanilla/node_modules/css-tree/lib/definition-syntax/walk.js b/vanilla/node_modules/css-tree/lib/definition-syntax/walk.js new file mode 100644 index 0000000..b8c7957 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/definition-syntax/walk.js @@ -0,0 +1,53 @@ +const noop = function() {}; + +function ensureFunction(value) { + return typeof value === 'function' ? value : noop; +} + +export function walk(node, options, context) { + function walk(node) { + enter.call(context, node); + + switch (node.type) { + case 'Group': + node.terms.forEach(walk); + break; + + case 'Multiplier': + case 'Boolean': + walk(node.term); + break; + + case 'Type': + case 'Property': + case 'Keyword': + case 'AtKeyword': + case 'Function': + case 'String': + case 'Token': + case 'Comma': + break; + + default: + throw new Error('Unknown type: ' + node.type); + } + + leave.call(context, node); + } + + let enter = noop; + let leave = noop; + + if (typeof options === 'function') { + enter = options; + } else if (options) { + enter = ensureFunction(options.enter); + leave = ensureFunction(options.leave); + } + + if (enter === noop && leave === noop) { + throw new Error('Neither `enter` nor `leave` walker handler is set or both aren\'t a function'); + } + + walk(node, context); +}; diff --git a/vanilla/node_modules/css-tree/lib/generator/create.js b/vanilla/node_modules/css-tree/lib/generator/create.js new file mode 100644 index 0000000..c542f4f --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/generator/create.js @@ -0,0 +1,97 @@ +import { tokenize, Delim, WhiteSpace } from '../tokenizer/index.js'; +import { generateSourceMap } from './sourceMap.js'; +import * as tokenBefore from './token-before.js'; + +const REVERSESOLIDUS = 0x005c; // U+005C REVERSE SOLIDUS (\) + +function processChildren(node, delimeter) { + if (typeof delimeter === 'function') { + let prev = null; + + node.children.forEach(node => { + if (prev !== null) { + delimeter.call(this, prev); + } + + this.node(node); + prev = node; + }); + + return; + } + + node.children.forEach(this.node, this); +} + +function processChunk(chunk) { + tokenize(chunk, (type, start, end) => { + this.token(type, chunk.slice(start, end)); + }); +} + +export function createGenerator(config) { + const types = new Map(); + + for (let [name, item] of Object.entries(config.node)) { + const fn = item.generate || item; + + if (typeof fn === 'function') { + types.set(name, item.generate || item); + } + } + + return function(node, options) { + let buffer = ''; + let prevCode = 0; + let handlers = { + node(node) { + if (types.has(node.type)) { + types.get(node.type).call(publicApi, node); + } else { + throw new Error('Unknown node type: ' + node.type); + } + }, + tokenBefore: tokenBefore.safe, + token(type, value) { + prevCode = this.tokenBefore(prevCode, type, value); + + this.emit(value, type, false); + + if (type === Delim && value.charCodeAt(0) === REVERSESOLIDUS) { + this.emit('\n', WhiteSpace, true); + } + }, + emit(value) { + buffer += value; + }, + result() { + return buffer; + } + }; + + if (options) { + if (typeof options.decorator === 'function') { + handlers = options.decorator(handlers); + } + + if (options.sourceMap) { + handlers = generateSourceMap(handlers); + } + + if (options.mode in tokenBefore) { + handlers.tokenBefore = tokenBefore[options.mode]; + } + } + + const publicApi = { + node: (node) => handlers.node(node), + children: processChildren, + token: (type, value) => handlers.token(type, value), + tokenize: processChunk + }; + + handlers.node(node); + + return handlers.result(); + }; +}; diff --git a/vanilla/node_modules/css-tree/lib/generator/index.js b/vanilla/node_modules/css-tree/lib/generator/index.js new file mode 100644 index 0000000..dc05116 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/generator/index.js @@ -0,0 +1,4 @@ +import { createGenerator } from './create.js'; +import config from '../syntax/config/generator.js'; + +export default createGenerator(config); diff --git a/vanilla/node_modules/css-tree/lib/generator/sourceMap.js b/vanilla/node_modules/css-tree/lib/generator/sourceMap.js new file mode 100644 index 0000000..0c5ff27 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/generator/sourceMap.js @@ -0,0 +1,92 @@ +import { SourceMapGenerator } from 'source-map-js/lib/source-map-generator.js'; + +const trackNodes = new Set(['Atrule', 'Selector', 'Declaration']); + +export function generateSourceMap(handlers) { + const map = new SourceMapGenerator(); + const generated = { + line: 1, + column: 0 + }; + const original = { + line: 0, // should be zero to add first mapping + column: 0 + }; + const activatedGenerated = { + line: 1, + column: 0 + }; + const activatedMapping = { + generated: activatedGenerated + }; + let line = 1; + let column = 0; + let sourceMappingActive = false; + + const origHandlersNode = handlers.node; + handlers.node = function(node) { + if (node.loc && node.loc.start && trackNodes.has(node.type)) { + const nodeLine = node.loc.start.line; + const nodeColumn = node.loc.start.column - 1; + + if (original.line !== nodeLine || + original.column !== nodeColumn) { + original.line = nodeLine; + original.column = nodeColumn; + + generated.line = line; + generated.column = column; + + if (sourceMappingActive) { + sourceMappingActive = false; + if (generated.line !== activatedGenerated.line || + generated.column !== activatedGenerated.column) { + map.addMapping(activatedMapping); + } + } + + sourceMappingActive = true; + map.addMapping({ + source: node.loc.source, + original, + generated + }); + } + } + + origHandlersNode.call(this, node); + + if (sourceMappingActive && trackNodes.has(node.type)) { + activatedGenerated.line = line; + activatedGenerated.column = column; + } + }; + + const origHandlersEmit = handlers.emit; + handlers.emit = function(value, type, auto) { + for (let i = 0; i < value.length; i++) { + if (value.charCodeAt(i) === 10) { // \n + line++; + column = 0; + } else { + column++; + } + } + + origHandlersEmit(value, type, auto); + }; + + const origHandlersResult = handlers.result; + handlers.result = function() { + if (sourceMappingActive) { + map.addMapping(activatedMapping); + } + + return { + css: origHandlersResult(), + map + }; + }; + + return handlers; +}; diff --git a/vanilla/node_modules/css-tree/lib/generator/token-before.js b/vanilla/node_modules/css-tree/lib/generator/token-before.js new file mode 100644 index 0000000..da3fed0 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/generator/token-before.js @@ -0,0 +1,182 @@ +import { + WhiteSpace, + Delim, + Ident, + Function as FunctionToken, + Url, + BadUrl, + AtKeyword, + Hash, + Percentage, + Dimension, + Number as NumberToken, + String as StringToken, + Colon, + LeftParenthesis, + RightParenthesis, + CDC +} from '../tokenizer/index.js'; + +const PLUSSIGN = 0x002B; // U+002B PLUS SIGN (+) +const HYPHENMINUS = 0x002D; // U+002D HYPHEN-MINUS (-) + +const code = (type, value) => { + if (type === Delim) { + type = value; + } + + if (typeof type === 'string') { + const charCode = type.charCodeAt(0); + return charCode > 0x7F ? 0x8000 : charCode << 8; + } + + return type; +}; + +// https://www.w3.org/TR/css-syntax-3/#serialization +// The only requirement for serialization is that it must "round-trip" with parsing, +// that is, parsing the stylesheet must produce the same data structures as parsing, +// serializing, and parsing again, except for consecutive <whitespace-token>s, +// which may be collapsed into a single token. + +const specPairs = [ + [Ident, Ident], + [Ident, FunctionToken], + [Ident, Url], + [Ident, BadUrl], + [Ident, '-'], + [Ident, NumberToken], + [Ident, Percentage], + [Ident, Dimension], + [Ident, CDC], + [Ident, LeftParenthesis], + + [AtKeyword, Ident], + [AtKeyword, FunctionToken], + [AtKeyword, Url], + [AtKeyword, BadUrl], + [AtKeyword, '-'], + [AtKeyword, NumberToken], + [AtKeyword, Percentage], + [AtKeyword, Dimension], + [AtKeyword, CDC], + + [Hash, Ident], + [Hash, FunctionToken], + [Hash, Url], + [Hash, BadUrl], + [Hash, '-'], + [Hash, NumberToken], + [Hash, Percentage], + [Hash, Dimension], + [Hash, CDC], + + [Dimension, Ident], + [Dimension, FunctionToken], + [Dimension, Url], + [Dimension, BadUrl], + [Dimension, '-'], + [Dimension, NumberToken], + [Dimension, Percentage], + [Dimension, Dimension], + [Dimension, CDC], + + ['#', Ident], + ['#', FunctionToken], + ['#', Url], + ['#', BadUrl], + ['#', '-'], + ['#', NumberToken], + ['#', Percentage], + ['#', Dimension], + ['#', CDC], // https://github.com/w3c/csswg-drafts/pull/6874 + + ['-', Ident], + ['-', FunctionToken], + ['-', Url], + ['-', BadUrl], + ['-', '-'], + ['-', NumberToken], + ['-', Percentage], + ['-', Dimension], + ['-', CDC], // https://github.com/w3c/csswg-drafts/pull/6874 + + [NumberToken, Ident], + [NumberToken, FunctionToken], + [NumberToken, Url], + [NumberToken, BadUrl], + [NumberToken, NumberToken], + [NumberToken, Percentage], + [NumberToken, Dimension], + [NumberToken, '%'], + [NumberToken, CDC], // https://github.com/w3c/csswg-drafts/pull/6874 + + ['@', Ident], + ['@', FunctionToken], + ['@', Url], + ['@', BadUrl], + ['@', '-'], + ['@', CDC], // https://github.com/w3c/csswg-drafts/pull/6874 + + ['.', NumberToken], + ['.', Percentage], + ['.', Dimension], + + ['+', NumberToken], + ['+', Percentage], + ['+', Dimension], + + ['/', '*'] +]; +// validate with scripts/generate-safe +const safePairs = specPairs.concat([ + [Ident, Hash], + + [Dimension, Hash], + + [Hash, Hash], + + [AtKeyword, LeftParenthesis], + [AtKeyword, StringToken], + [AtKeyword, Colon], + + [Percentage, Percentage], + [Percentage, Dimension], + [Percentage, FunctionToken], + [Percentage, '-'], + + [RightParenthesis, Ident], + [RightParenthesis, FunctionToken], + [RightParenthesis, Percentage], + [RightParenthesis, Dimension], + [RightParenthesis, Hash], + [RightParenthesis, '-'] +]); + +function createMap(pairs) { + const isWhiteSpaceRequired = new Set( + pairs.map(([prev, next]) => (code(prev) << 16 | code(next))) + ); + + return function(prevCode, type, value) { + const nextCode = code(type, value); + const nextCharCode = value.charCodeAt(0); + const emitWs = + (nextCharCode === HYPHENMINUS && + type !== Ident && + type !== FunctionToken && + type !== CDC) || + (nextCharCode === PLUSSIGN) + ? isWhiteSpaceRequired.has(prevCode << 16 | nextCharCode << 8) + : isWhiteSpaceRequired.has(prevCode << 16 | nextCode); + + if (emitWs) { + this.emit(' ', WhiteSpace, true); + } + + return nextCode; + }; +} + +export const spec = createMap(specPairs); +export const safe = createMap(safePairs); diff --git a/vanilla/node_modules/css-tree/lib/index.js b/vanilla/node_modules/css-tree/lib/index.js new file mode 100644 index 0000000..22cf491 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/index.js @@ -0,0 +1,30 @@ +import syntax from './syntax/index.js'; + +export * from './version.js'; +export { default as createSyntax } from './syntax/create.js'; +export { List } from './utils/List.js'; +export { Lexer } from './lexer/Lexer.js'; +export { tokenTypes, tokenNames, TokenStream, OffsetToLocation } from './tokenizer/index.js'; +export * as definitionSyntax from './definition-syntax/index.js'; +export { clone } from './utils/clone.js'; +export * from './utils/names.js'; +export * as ident from './utils/ident.js'; +export * as string from './utils/string.js'; +export * as url from './utils/url.js'; +export const { + tokenize, + parse, + generate, + lexer, + createLexer, + + walk, + find, + findLast, + findAll, + + toPlainObject, + fromPlainObject, + + fork +} = syntax; diff --git a/vanilla/node_modules/css-tree/lib/lexer/Lexer.js b/vanilla/node_modules/css-tree/lib/lexer/Lexer.js new file mode 100644 index 0000000..d09f574 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/lexer/Lexer.js @@ -0,0 +1,511 @@ +import { SyntaxReferenceError, SyntaxMatchError } from './error.js'; +import * as names from '../utils/names.js'; +import { cssWideKeywords } from './generic-const.js'; +import { createGenericTypes } from './generic.js'; +import * as units from './units.js'; +import { parse, generate, walk } from '../definition-syntax/index.js'; +import prepareTokens from './prepare-tokens.js'; +import { buildMatchGraph } from './match-graph.js'; +import { matchAsTree } from './match.js'; +import * as trace from './trace.js'; +import { matchFragments } from './search.js'; +import { getStructureFromConfig } from './structure.js'; + +function dumpMapSyntax(map, compact, syntaxAsAst) { + const result = {}; + + for (const name in map) { + if (map[name].syntax) { + result[name] = syntaxAsAst + ? map[name].syntax + : generate(map[name].syntax, { compact }); + } + } + + return result; +} + +function dumpAtruleMapSyntax(map, compact, syntaxAsAst) { + const result = {}; + + for (const [name, atrule] of Object.entries(map)) { + result[name] = { + prelude: atrule.prelude && ( + syntaxAsAst + ? atrule.prelude.syntax + : generate(atrule.prelude.syntax, { compact }) + ), + descriptors: atrule.descriptors && dumpMapSyntax(atrule.descriptors, compact, syntaxAsAst) + }; + } + + return result; +} + +function valueHasVar(tokens) { + for (let i = 0; i < tokens.length; i++) { + if (tokens[i].value.toLowerCase() === 'var(') { + return true; + } + } + + return false; +} + +function syntaxHasTopLevelCommaMultiplier(syntax) { + const singleTerm = syntax.terms[0]; + + return ( + syntax.explicit === false && + syntax.terms.length === 1 && + singleTerm.type === 'Multiplier' && + singleTerm.comma === true + ); +} + +function buildMatchResult(matched, error, iterations) { + return { + matched, + iterations, + error, + ...trace + }; +} + +function matchSyntax(lexer, syntax, value, useCssWideKeywords) { + const tokens = prepareTokens(value, lexer.syntax); + let result; + + if (valueHasVar(tokens)) { + return buildMatchResult(null, new Error('Matching for a tree with var() is not supported')); + } + + if (useCssWideKeywords) { + result = matchAsTree(tokens, lexer.cssWideKeywordsSyntax, lexer); + } + + if (!useCssWideKeywords || !result.match) { + result = matchAsTree(tokens, syntax.match, lexer); + if (!result.match) { + return buildMatchResult( + null, + new SyntaxMatchError(result.reason, syntax.syntax, value, result), + result.iterations + ); + } + } + + return buildMatchResult(result.match, null, result.iterations); +} + +export class Lexer { + constructor(config, syntax, structure) { + this.cssWideKeywords = cssWideKeywords; + this.syntax = syntax; + this.generic = false; + this.units = { ...units }; + this.atrules = Object.create(null); + this.properties = Object.create(null); + this.types = Object.create(null); + this.structure = structure || getStructureFromConfig(config); + + if (config) { + if (config.cssWideKeywords) { + this.cssWideKeywords = config.cssWideKeywords; + } + + if (config.units) { + for (const group of Object.keys(units)) { + if (Array.isArray(config.units[group])) { + this.units[group] = config.units[group]; + } + } + } + + if (config.types) { + for (const [name, type] of Object.entries(config.types)) { + this.addType_(name, type); + } + } + + if (config.generic) { + this.generic = true; + for (const [name, value] of Object.entries(createGenericTypes(this.units))) { + this.addType_(name, value); + } + } + + if (config.atrules) { + for (const [name, atrule] of Object.entries(config.atrules)) { + this.addAtrule_(name, atrule); + } + } + + if (config.properties) { + for (const [name, property] of Object.entries(config.properties)) { + this.addProperty_(name, property); + } + } + } + + this.cssWideKeywordsSyntax = buildMatchGraph(this.cssWideKeywords.join(' | ')); + } + + checkStructure(ast) { + function collectWarning(node, message) { + warns.push({ node, message }); + } + + const structure = this.structure; + const warns = []; + + this.syntax.walk(ast, function(node) { + if (structure.hasOwnProperty(node.type)) { + structure[node.type].check(node, collectWarning); + } else { + collectWarning(node, 'Unknown node type `' + node.type + '`'); + } + }); + + return warns.length ? warns : false; + } + + createDescriptor(syntax, type, name, parent = null) { + const ref = { + type, + name + }; + const descriptor = { + type, + name, + parent, + serializable: typeof syntax === 'string' || (syntax && typeof syntax.type === 'string'), + syntax: null, + match: null, + matchRef: null // used for properties when a syntax referenced as <'property'> in other syntax definitions + }; + + if (typeof syntax === 'function') { + descriptor.match = buildMatchGraph(syntax, ref); + } else { + if (typeof syntax === 'string') { + // lazy parsing on first access + Object.defineProperty(descriptor, 'syntax', { + get() { + Object.defineProperty(descriptor, 'syntax', { + value: parse(syntax) + }); + + return descriptor.syntax; + } + }); + } else { + descriptor.syntax = syntax; + } + + // lazy graph build on first access + Object.defineProperty(descriptor, 'match', { + get() { + Object.defineProperty(descriptor, 'match', { + value: buildMatchGraph(descriptor.syntax, ref) + }); + + return descriptor.match; + } + }); + + if (type === 'Property') { + Object.defineProperty(descriptor, 'matchRef', { + get() { + const syntax = descriptor.syntax; + const value = syntaxHasTopLevelCommaMultiplier(syntax) + ? buildMatchGraph({ + ...syntax, + terms: [syntax.terms[0].term] + }, ref) + : null; + + Object.defineProperty(descriptor, 'matchRef', { + value + }); + + return value; + } + }); + } + } + + return descriptor; + } + addAtrule_(name, syntax) { + if (!syntax) { + return; + } + + this.atrules[name] = { + type: 'Atrule', + name: name, + prelude: syntax.prelude ? this.createDescriptor(syntax.prelude, 'AtrulePrelude', name) : null, + descriptors: syntax.descriptors + ? Object.keys(syntax.descriptors).reduce( + (map, descName) => { + map[descName] = this.createDescriptor(syntax.descriptors[descName], 'AtruleDescriptor', descName, name); + return map; + }, + Object.create(null) + ) + : null + }; + } + addProperty_(name, syntax) { + if (!syntax) { + return; + } + + this.properties[name] = this.createDescriptor(syntax, 'Property', name); + } + addType_(name, syntax) { + if (!syntax) { + return; + } + + this.types[name] = this.createDescriptor(syntax, 'Type', name); + } + + checkAtruleName(atruleName) { + if (!this.getAtrule(atruleName)) { + return new SyntaxReferenceError('Unknown at-rule', '@' + atruleName); + } + } + checkAtrulePrelude(atruleName, prelude) { + const error = this.checkAtruleName(atruleName); + + if (error) { + return error; + } + + const atrule = this.getAtrule(atruleName); + + if (!atrule.prelude && prelude) { + return new SyntaxError('At-rule `@' + atruleName + '` should not contain a prelude'); + } + + if (atrule.prelude && !prelude) { + if (!matchSyntax(this, atrule.prelude, '', false).matched) { + return new SyntaxError('At-rule `@' + atruleName + '` should contain a prelude'); + } + } + } + checkAtruleDescriptorName(atruleName, descriptorName) { + const error = this.checkAtruleName(atruleName); + + if (error) { + return error; + } + + const atrule = this.getAtrule(atruleName); + const descriptor = names.keyword(descriptorName); + + if (!atrule.descriptors) { + return new SyntaxError('At-rule `@' + atruleName + '` has no known descriptors'); + } + + if (!atrule.descriptors[descriptor.name] && + !atrule.descriptors[descriptor.basename]) { + return new SyntaxReferenceError('Unknown at-rule descriptor', descriptorName); + } + } + checkPropertyName(propertyName) { + if (!this.getProperty(propertyName)) { + return new SyntaxReferenceError('Unknown property', propertyName); + } + } + + matchAtrulePrelude(atruleName, prelude) { + const error = this.checkAtrulePrelude(atruleName, prelude); + + if (error) { + return buildMatchResult(null, error); + } + + const atrule = this.getAtrule(atruleName); + + if (!atrule.prelude) { + return buildMatchResult(null, null); + } + + return matchSyntax(this, atrule.prelude, prelude || '', false); + } + matchAtruleDescriptor(atruleName, descriptorName, value) { + const error = this.checkAtruleDescriptorName(atruleName, descriptorName); + + if (error) { + return buildMatchResult(null, error); + } + + const atrule = this.getAtrule(atruleName); + const descriptor = names.keyword(descriptorName); + + return matchSyntax(this, atrule.descriptors[descriptor.name] || atrule.descriptors[descriptor.basename], value, false); + } + matchDeclaration(node) { + if (node.type !== 'Declaration') { + return buildMatchResult(null, new Error('Not a Declaration node')); + } + + return this.matchProperty(node.property, node.value); + } + matchProperty(propertyName, value) { + // don't match syntax for a custom property at the moment + if (names.property(propertyName).custom) { + return buildMatchResult(null, new Error('Lexer matching doesn\'t applicable for custom properties')); + } + + const error = this.checkPropertyName(propertyName); + + if (error) { + return buildMatchResult(null, error); + } + + return matchSyntax(this, this.getProperty(propertyName), value, true); + } + matchType(typeName, value) { + const typeSyntax = this.getType(typeName); + + if (!typeSyntax) { + return buildMatchResult(null, new SyntaxReferenceError('Unknown type', typeName)); + } + + return matchSyntax(this, typeSyntax, value, false); + } + match(syntax, value) { + if (typeof syntax !== 'string' && (!syntax || !syntax.type)) { + return buildMatchResult(null, new SyntaxReferenceError('Bad syntax')); + } + + if (typeof syntax === 'string' || !syntax.match) { + syntax = this.createDescriptor(syntax, 'Type', 'anonymous'); + } + + return matchSyntax(this, syntax, value, false); + } + + findValueFragments(propertyName, value, type, name) { + return matchFragments(this, value, this.matchProperty(propertyName, value), type, name); + } + findDeclarationValueFragments(declaration, type, name) { + return matchFragments(this, declaration.value, this.matchDeclaration(declaration), type, name); + } + findAllFragments(ast, type, name) { + const result = []; + + this.syntax.walk(ast, { + visit: 'Declaration', + enter: (declaration) => { + result.push.apply(result, this.findDeclarationValueFragments(declaration, type, name)); + } + }); + + return result; + } + + getAtrule(atruleName, fallbackBasename = true) { + const atrule = names.keyword(atruleName); + const atruleEntry = atrule.vendor && fallbackBasename + ? this.atrules[atrule.name] || this.atrules[atrule.basename] + : this.atrules[atrule.name]; + + return atruleEntry || null; + } + getAtrulePrelude(atruleName, fallbackBasename = true) { + const atrule = this.getAtrule(atruleName, fallbackBasename); + + return atrule && atrule.prelude || null; + } + getAtruleDescriptor(atruleName, name) { + return this.atrules.hasOwnProperty(atruleName) && this.atrules.declarators + ? this.atrules[atruleName].declarators[name] || null + : null; + } + getProperty(propertyName, fallbackBasename = true) { + const property = names.property(propertyName); + const propertyEntry = property.vendor && fallbackBasename + ? this.properties[property.name] || this.properties[property.basename] + : this.properties[property.name]; + + return propertyEntry || null; + } + getType(name) { + return hasOwnProperty.call(this.types, name) ? this.types[name] : null; + } + + validate() { + function syntaxRef(name, isType) { + return isType ? `<${name}>` : `<'${name}'>`; + } + + function validate(syntax, name, broken, descriptor) { + if (broken.has(name)) { + return broken.get(name); + } + + broken.set(name, false); + if (descriptor.syntax !== null) { + walk(descriptor.syntax, function(node) { + if (node.type !== 'Type' && node.type !== 'Property') { + return; + } + + const map = node.type === 'Type' ? syntax.types : syntax.properties; + const brokenMap = node.type === 'Type' ? brokenTypes : brokenProperties; + + if (!hasOwnProperty.call(map, node.name)) { + errors.push(`${syntaxRef(name, broken === brokenTypes)} used missed syntax definition ${syntaxRef(node.name, node.type === 'Type')}`); + broken.set(name, true); + } else if (validate(syntax, node.name, brokenMap, map[node.name])) { + errors.push(`${syntaxRef(name, broken === brokenTypes)} used broken syntax definition ${syntaxRef(node.name, node.type === 'Type')}`); + broken.set(name, true); + } + }, this); + } + } + + const errors = []; + let brokenTypes = new Map(); + let brokenProperties = new Map(); + + for (const key in this.types) { + validate(this, key, brokenTypes, this.types[key]); + } + + for (const key in this.properties) { + validate(this, key, brokenProperties, this.properties[key]); + } + + const brokenTypesArray = [...brokenTypes.keys()].filter(name => brokenTypes.get(name)); + const brokenPropertiesArray = [...brokenProperties.keys()].filter(name => brokenProperties.get(name)); + + if (brokenTypesArray.length || brokenPropertiesArray.length) { + return { + errors, + types: brokenTypesArray, + properties: brokenPropertiesArray + }; + } + + return null; + } + dump(syntaxAsAst, pretty) { + return { + generic: this.generic, + cssWideKeywords: this.cssWideKeywords, + units: this.units, + types: dumpMapSyntax(this.types, !pretty, syntaxAsAst), + properties: dumpMapSyntax(this.properties, !pretty, syntaxAsAst), + atrules: dumpAtruleMapSyntax(this.atrules, !pretty, syntaxAsAst) + }; + } + toString() { + return JSON.stringify(this.dump()); + } +}; diff --git a/vanilla/node_modules/css-tree/lib/lexer/error.js b/vanilla/node_modules/css-tree/lib/lexer/error.js new file mode 100644 index 0000000..89a5400 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/lexer/error.js @@ -0,0 +1,123 @@ +import { createCustomError } from '../utils/create-custom-error.js'; +import { generate } from '../definition-syntax/generate.js'; + +const defaultLoc = { offset: 0, line: 1, column: 1 }; + +function locateMismatch(matchResult, node) { + const tokens = matchResult.tokens; + const longestMatch = matchResult.longestMatch; + const mismatchNode = longestMatch < tokens.length ? tokens[longestMatch].node || null : null; + const badNode = mismatchNode !== node ? mismatchNode : null; + let mismatchOffset = 0; + let mismatchLength = 0; + let entries = 0; + let css = ''; + let start; + let end; + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i].value; + + if (i === longestMatch) { + mismatchLength = token.length; + mismatchOffset = css.length; + } + + if (badNode !== null && tokens[i].node === badNode) { + if (i <= longestMatch) { + entries++; + } else { + entries = 0; + } + } + + css += token; + } + + if (longestMatch === tokens.length || entries > 1) { // last + start = fromLoc(badNode || node, 'end') || buildLoc(defaultLoc, css); + end = buildLoc(start); + } else { + start = fromLoc(badNode, 'start') || + buildLoc(fromLoc(node, 'start') || defaultLoc, css.slice(0, mismatchOffset)); + end = fromLoc(badNode, 'end') || + buildLoc(start, css.substr(mismatchOffset, mismatchLength)); + } + + return { + css, + mismatchOffset, + mismatchLength, + start, + end + }; +} + +function fromLoc(node, point) { + const value = node && node.loc && node.loc[point]; + + if (value) { + return 'line' in value ? buildLoc(value) : value; + } + + return null; +} + +function buildLoc({ offset, line, column }, extra) { + const loc = { + offset, + line, + column + }; + + if (extra) { + const lines = extra.split(/\n|\r\n?|\f/); + + loc.offset += extra.length; + loc.line += lines.length - 1; + loc.column = lines.length === 1 ? loc.column + extra.length : lines.pop().length + 1; + } + + return loc; +} + +export const SyntaxReferenceError = function(type, referenceName) { + const error = createCustomError( + 'SyntaxReferenceError', + type + (referenceName ? ' `' + referenceName + '`' : '') + ); + + error.reference = referenceName; + + return error; +}; + +export const SyntaxMatchError = function(message, syntax, node, matchResult) { + const error = createCustomError('SyntaxMatchError', message); + const { + css, + mismatchOffset, + mismatchLength, + start, + end + } = locateMismatch(matchResult, node); + + error.rawMessage = message; + error.syntax = syntax ? generate(syntax) : '<generic>'; + error.css = css; + error.mismatchOffset = mismatchOffset; + error.mismatchLength = mismatchLength; + error.message = message + '\n' + + ' syntax: ' + error.syntax + '\n' + + ' value: ' + (css || '<empty string>') + '\n' + + ' --------' + new Array(error.mismatchOffset + 1).join('-') + '^'; + + Object.assign(error, start); + error.loc = { + source: (node && node.loc && node.loc.source) || '<unknown>', + start, + end + }; + + return error; +}; diff --git a/vanilla/node_modules/css-tree/lib/lexer/generic-an-plus-b.js b/vanilla/node_modules/css-tree/lib/lexer/generic-an-plus-b.js new file mode 100644 index 0000000..347715c --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/lexer/generic-an-plus-b.js @@ -0,0 +1,238 @@ +import { + isDigit, + cmpChar, + Delim, + WhiteSpace, + Comment, + Ident, + Number as NumberToken, + Dimension +} from '../tokenizer/index.js'; + +const PLUSSIGN = 0x002B; // U+002B PLUS SIGN (+) +const HYPHENMINUS = 0x002D; // U+002D HYPHEN-MINUS (-) +const N = 0x006E; // U+006E LATIN SMALL LETTER N (n) +const DISALLOW_SIGN = true; +const ALLOW_SIGN = false; + +function isDelim(token, code) { + return token !== null && token.type === Delim && token.value.charCodeAt(0) === code; +} + +function skipSC(token, offset, getNextToken) { + while (token !== null && (token.type === WhiteSpace || token.type === Comment)) { + token = getNextToken(++offset); + } + + return offset; +} + +function checkInteger(token, valueOffset, disallowSign, offset) { + if (!token) { + return 0; + } + + const code = token.value.charCodeAt(valueOffset); + + if (code === PLUSSIGN || code === HYPHENMINUS) { + if (disallowSign) { + // Number sign is not allowed + return 0; + } + valueOffset++; + } + + for (; valueOffset < token.value.length; valueOffset++) { + if (!isDigit(token.value.charCodeAt(valueOffset))) { + // Integer is expected + return 0; + } + } + + return offset + 1; +} + +// ... <signed-integer> +// ... ['+' | '-'] <signless-integer> +function consumeB(token, offset_, getNextToken) { + let sign = false; + let offset = skipSC(token, offset_, getNextToken); + + token = getNextToken(offset); + + if (token === null) { + return offset_; + } + + if (token.type !== NumberToken) { + if (isDelim(token, PLUSSIGN) || isDelim(token, HYPHENMINUS)) { + sign = true; + offset = skipSC(getNextToken(++offset), offset, getNextToken); + token = getNextToken(offset); + + if (token === null || token.type !== NumberToken) { + return 0; + } + } else { + return offset_; + } + } + + if (!sign) { + const code = token.value.charCodeAt(0); + if (code !== PLUSSIGN && code !== HYPHENMINUS) { + // Number sign is expected + return 0; + } + } + + return checkInteger(token, sign ? 0 : 1, sign, offset); +} + +// An+B microsyntax https://www.w3.org/TR/css-syntax-3/#anb +export default function anPlusB(token, getNextToken) { + /* eslint-disable brace-style*/ + let offset = 0; + + if (!token) { + return 0; + } + + // <integer> + if (token.type === NumberToken) { + return checkInteger(token, 0, ALLOW_SIGN, offset); // b + } + + // -n + // -n <signed-integer> + // -n ['+' | '-'] <signless-integer> + // -n- <signless-integer> + // <dashndashdigit-ident> + else if (token.type === Ident && token.value.charCodeAt(0) === HYPHENMINUS) { + // expect 1st char is N + if (!cmpChar(token.value, 1, N)) { + return 0; + } + + switch (token.value.length) { + // -n + // -n <signed-integer> + // -n ['+' | '-'] <signless-integer> + case 2: + return consumeB(getNextToken(++offset), offset, getNextToken); + + // -n- <signless-integer> + case 3: + if (token.value.charCodeAt(2) !== HYPHENMINUS) { + return 0; + } + + offset = skipSC(getNextToken(++offset), offset, getNextToken); + token = getNextToken(offset); + + return checkInteger(token, 0, DISALLOW_SIGN, offset); + + // <dashndashdigit-ident> + default: + if (token.value.charCodeAt(2) !== HYPHENMINUS) { + return 0; + } + + return checkInteger(token, 3, DISALLOW_SIGN, offset); + } + } + + // '+'? n + // '+'? n <signed-integer> + // '+'? n ['+' | '-'] <signless-integer> + // '+'? n- <signless-integer> + // '+'? <ndashdigit-ident> + else if (token.type === Ident || (isDelim(token, PLUSSIGN) && getNextToken(offset + 1).type === Ident)) { + // just ignore a plus + if (token.type !== Ident) { + token = getNextToken(++offset); + } + + if (token === null || !cmpChar(token.value, 0, N)) { + return 0; + } + + switch (token.value.length) { + // '+'? n + // '+'? n <signed-integer> + // '+'? n ['+' | '-'] <signless-integer> + case 1: + return consumeB(getNextToken(++offset), offset, getNextToken); + + // '+'? n- <signless-integer> + case 2: + if (token.value.charCodeAt(1) !== HYPHENMINUS) { + return 0; + } + + offset = skipSC(getNextToken(++offset), offset, getNextToken); + token = getNextToken(offset); + + return checkInteger(token, 0, DISALLOW_SIGN, offset); + + // '+'? <ndashdigit-ident> + default: + if (token.value.charCodeAt(1) !== HYPHENMINUS) { + return 0; + } + + return checkInteger(token, 2, DISALLOW_SIGN, offset); + } + } + + // <ndashdigit-dimension> + // <ndash-dimension> <signless-integer> + // <n-dimension> + // <n-dimension> <signed-integer> + // <n-dimension> ['+' | '-'] <signless-integer> + else if (token.type === Dimension) { + let code = token.value.charCodeAt(0); + let sign = code === PLUSSIGN || code === HYPHENMINUS ? 1 : 0; + let i = sign; + + for (; i < token.value.length; i++) { + if (!isDigit(token.value.charCodeAt(i))) { + break; + } + } + + if (i === sign) { + // Integer is expected + return 0; + } + + if (!cmpChar(token.value, i, N)) { + return 0; + } + + // <n-dimension> + // <n-dimension> <signed-integer> + // <n-dimension> ['+' | '-'] <signless-integer> + if (i + 1 === token.value.length) { + return consumeB(getNextToken(++offset), offset, getNextToken); + } else { + if (token.value.charCodeAt(i + 1) !== HYPHENMINUS) { + return 0; + } + + // <ndash-dimension> <signless-integer> + if (i + 2 === token.value.length) { + offset = skipSC(getNextToken(++offset), offset, getNextToken); + token = getNextToken(offset); + + return checkInteger(token, 0, DISALLOW_SIGN, offset); + } + // <ndashdigit-dimension> + else { + return checkInteger(token, i + 2, DISALLOW_SIGN, offset); + } + } + } + + return 0; +}; diff --git a/vanilla/node_modules/css-tree/lib/lexer/generic-const.js b/vanilla/node_modules/css-tree/lib/lexer/generic-const.js new file mode 100644 index 0000000..8efe6ae --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/lexer/generic-const.js @@ -0,0 +1,8 @@ +// https://drafts.csswg.org/css-cascade-5/ +export const cssWideKeywords = [ + 'initial', + 'inherit', + 'unset', + 'revert', + 'revert-layer' +]; diff --git a/vanilla/node_modules/css-tree/lib/lexer/generic-urange.js b/vanilla/node_modules/css-tree/lib/lexer/generic-urange.js new file mode 100644 index 0000000..d878409 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/lexer/generic-urange.js @@ -0,0 +1,151 @@ +import { + isHexDigit, + cmpChar, + Ident, + Delim, + Number as NumberToken, + Dimension +} from '../tokenizer/index.js'; + +const PLUSSIGN = 0x002B; // U+002B PLUS SIGN (+) +const HYPHENMINUS = 0x002D; // U+002D HYPHEN-MINUS (-) +const QUESTIONMARK = 0x003F; // U+003F QUESTION MARK (?) +const U = 0x0075; // U+0075 LATIN SMALL LETTER U (u) + +function isDelim(token, code) { + return token !== null && token.type === Delim && token.value.charCodeAt(0) === code; +} + +function startsWith(token, code) { + return token.value.charCodeAt(0) === code; +} + +function hexSequence(token, offset, allowDash) { + let hexlen = 0; + + for (let pos = offset; pos < token.value.length; pos++) { + const code = token.value.charCodeAt(pos); + + if (code === HYPHENMINUS && allowDash && hexlen !== 0) { + hexSequence(token, offset + hexlen + 1, false); + return 6; // dissallow following question marks + } + + if (!isHexDigit(code)) { + return 0; // not a hex digit + } + + if (++hexlen > 6) { + return 0; // too many hex digits + }; + } + + return hexlen; +} + +function withQuestionMarkSequence(consumed, length, getNextToken) { + if (!consumed) { + return 0; // nothing consumed + } + + while (isDelim(getNextToken(length), QUESTIONMARK)) { + if (++consumed > 6) { + return 0; // too many question marks + } + + length++; + } + + return length; +} + +// https://drafts.csswg.org/css-syntax/#urange +// Informally, the <urange> production has three forms: +// U+0001 +// Defines a range consisting of a single code point, in this case the code point "1". +// U+0001-00ff +// Defines a range of codepoints between the first and the second value, in this case +// the range between "1" and "ff" (255 in decimal) inclusive. +// U+00?? +// Defines a range of codepoints where the "?" characters range over all hex digits, +// in this case defining the same as the value U+0000-00ff. +// In each form, a maximum of 6 digits is allowed for each hexadecimal number (if you treat "?" as a hexadecimal digit). +// +// <urange> = +// u '+' <ident-token> '?'* | +// u <dimension-token> '?'* | +// u <number-token> '?'* | +// u <number-token> <dimension-token> | +// u <number-token> <number-token> | +// u '+' '?'+ +export default function urange(token, getNextToken) { + let length = 0; + + // should start with `u` or `U` + if (token === null || token.type !== Ident || !cmpChar(token.value, 0, U)) { + return 0; + } + + token = getNextToken(++length); + if (token === null) { + return 0; + } + + // u '+' <ident-token> '?'* + // u '+' '?'+ + if (isDelim(token, PLUSSIGN)) { + token = getNextToken(++length); + if (token === null) { + return 0; + } + + if (token.type === Ident) { + // u '+' <ident-token> '?'* + return withQuestionMarkSequence(hexSequence(token, 0, true), ++length, getNextToken); + } + + if (isDelim(token, QUESTIONMARK)) { + // u '+' '?'+ + return withQuestionMarkSequence(1, ++length, getNextToken); + } + + // Hex digit or question mark is expected + return 0; + } + + // u <number-token> '?'* + // u <number-token> <dimension-token> + // u <number-token> <number-token> + if (token.type === NumberToken) { + const consumedHexLength = hexSequence(token, 1, true); + if (consumedHexLength === 0) { + return 0; + } + + token = getNextToken(++length); + if (token === null) { + // u <number-token> <eof> + return length; + } + + if (token.type === Dimension || token.type === NumberToken) { + // u <number-token> <dimension-token> + // u <number-token> <number-token> + if (!startsWith(token, HYPHENMINUS) || !hexSequence(token, 1, false)) { + return 0; + } + + return length + 1; + } + + // u <number-token> '?'* + return withQuestionMarkSequence(consumedHexLength, length, getNextToken); + } + + // u <dimension-token> '?'* + if (token.type === Dimension) { + return withQuestionMarkSequence(hexSequence(token, 1, true), ++length, getNextToken); + } + + return 0; +}; diff --git a/vanilla/node_modules/css-tree/lib/lexer/generic.js b/vanilla/node_modules/css-tree/lib/lexer/generic.js new file mode 100644 index 0000000..422e130 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/lexer/generic.js @@ -0,0 +1,622 @@ +import { cssWideKeywords } from './generic-const.js'; +import anPlusB from './generic-an-plus-b.js'; +import urange from './generic-urange.js'; +import { + isIdentifierStart, + isHexDigit, + isDigit, + cmpStr, + consumeNumber, + + Ident, + Function as FunctionToken, + AtKeyword, + Hash, + String as StringToken, + BadString, + Url, + BadUrl, + Delim, + Number as NumberToken, + Percentage, + Dimension, + WhiteSpace, + CDO, + CDC, + Colon, + Semicolon, + Comma, + LeftSquareBracket, + RightSquareBracket, + LeftParenthesis, + RightParenthesis, + LeftCurlyBracket, + RightCurlyBracket +} from '../tokenizer/index.js'; + +const calcFunctionNames = ['calc(', '-moz-calc(', '-webkit-calc(']; +const balancePair = new Map([ + [FunctionToken, RightParenthesis], + [LeftParenthesis, RightParenthesis], + [LeftSquareBracket, RightSquareBracket], + [LeftCurlyBracket, RightCurlyBracket] +]); + +// safe char code getter +function charCodeAt(str, index) { + return index < str.length ? str.charCodeAt(index) : 0; +} + +function eqStr(actual, expected) { + return cmpStr(actual, 0, actual.length, expected); +} + +function eqStrAny(actual, expected) { + for (let i = 0; i < expected.length; i++) { + if (eqStr(actual, expected[i])) { + return true; + } + } + + return false; +} + +// IE postfix hack, i.e. 123\0 or 123px\9 +function isPostfixIeHack(str, offset) { + if (offset !== str.length - 2) { + return false; + } + + return ( + charCodeAt(str, offset) === 0x005C && // U+005C REVERSE SOLIDUS (\) + isDigit(charCodeAt(str, offset + 1)) + ); +} + +function outOfRange(opts, value, numEnd) { + if (opts && opts.type === 'Range') { + const num = Number( + numEnd !== undefined && numEnd !== value.length + ? value.substr(0, numEnd) + : value + ); + + if (isNaN(num)) { + return true; + } + + // FIXME: when opts.min is a string it's a dimension, skip a range validation + // for now since it requires a type covertation which is not implmented yet + if (opts.min !== null && num < opts.min && typeof opts.min !== 'string') { + return true; + } + + // FIXME: when opts.max is a string it's a dimension, skip a range validation + // for now since it requires a type covertation which is not implmented yet + if (opts.max !== null && num > opts.max && typeof opts.max !== 'string') { + return true; + } + } + + return false; +} + +function consumeFunction(token, getNextToken) { + let balanceCloseType = 0; + let balanceStash = []; + let length = 0; + + // balanced token consuming + scan: + do { + switch (token.type) { + case RightCurlyBracket: + case RightParenthesis: + case RightSquareBracket: + if (token.type !== balanceCloseType) { + break scan; + } + + balanceCloseType = balanceStash.pop(); + + if (balanceStash.length === 0) { + length++; + break scan; + } + + break; + + case FunctionToken: + case LeftParenthesis: + case LeftSquareBracket: + case LeftCurlyBracket: + balanceStash.push(balanceCloseType); + balanceCloseType = balancePair.get(token.type); + break; + } + + length++; + } while (token = getNextToken(length)); + + return length; +} + +// TODO: implement +// can be used wherever <length>, <frequency>, <angle>, <time>, <percentage>, <number>, or <integer> values are allowed +// https://drafts.csswg.org/css-values/#calc-notation +function calc(next) { + return function(token, getNextToken, opts) { + if (token === null) { + return 0; + } + + if (token.type === FunctionToken && eqStrAny(token.value, calcFunctionNames)) { + return consumeFunction(token, getNextToken); + } + + return next(token, getNextToken, opts); + }; +} + +function tokenType(expectedTokenType) { + return function(token) { + if (token === null || token.type !== expectedTokenType) { + return 0; + } + + return 1; + }; +} + +// ========================= +// Complex types +// + +// https://drafts.csswg.org/css-values-4/#custom-idents +// 4.2. Author-defined Identifiers: the <custom-ident> type +// Some properties accept arbitrary author-defined identifiers as a component value. +// This generic data type is denoted by <custom-ident>, and represents any valid CSS identifier +// that would not be misinterpreted as a pre-defined keyword in that property’s value definition. +// +// See also: https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident +function customIdent(token) { + if (token === null || token.type !== Ident) { + return 0; + } + + const name = token.value.toLowerCase(); + + // The CSS-wide keywords are not valid <custom-ident>s + if (eqStrAny(name, cssWideKeywords)) { + return 0; + } + + // The default keyword is reserved and is also not a valid <custom-ident> + if (eqStr(name, 'default')) { + return 0; + } + + // TODO: ignore property specific keywords (as described https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident) + // Specifications using <custom-ident> must specify clearly what other keywords + // are excluded from <custom-ident>, if any—for example by saying that any pre-defined keywords + // in that property’s value definition are excluded. Excluded keywords are excluded + // in all ASCII case permutations. + + return 1; +} + +// https://drafts.csswg.org/css-values-4/#dashed-idents +// The <dashed-ident> production is a <custom-ident>, with all the case-sensitivity that implies, +// with the additional restriction that it must start with two dashes (U+002D HYPHEN-MINUS). +function dashedIdent(token) { + if (token === null || token.type !== Ident) { + return 0; + } + + // ... must start with two dashes (U+002D HYPHEN-MINUS) + if (charCodeAt(token.value, 0) !== 0x002D || charCodeAt(token.value, 1) !== 0x002D) { + return 0; + } + + return 1; +} + +// https://drafts.csswg.org/css-variables/#typedef-custom-property-name +// A custom property is any property whose name starts with two dashes (U+002D HYPHEN-MINUS), like --foo. +// The <custom-property-name> production corresponds to this: it’s defined as any <dashed-ident> +// (a valid identifier that starts with two dashes), except -- itself, which is reserved for future use by CSS. +function customPropertyName(token) { + // ... it’s defined as any <dashed-ident> + if (!dashedIdent(token)) { + return 0; + } + + // ... except -- itself, which is reserved for future use by CSS + if (token.value === '--') { + return 0; + } + + return 1; +} + +// https://drafts.csswg.org/css-color-4/#hex-notation +// The syntax of a <hex-color> is a <hash-token> token whose value consists of 3, 4, 6, or 8 hexadecimal digits. +// In other words, a hex color is written as a hash character, "#", followed by some number of digits 0-9 or +// letters a-f (the case of the letters doesn’t matter - #00ff00 is identical to #00FF00). +function hexColor(token) { + if (token === null || token.type !== Hash) { + return 0; + } + + const length = token.value.length; + + // valid values (length): #rgb (4), #rgba (5), #rrggbb (7), #rrggbbaa (9) + if (length !== 4 && length !== 5 && length !== 7 && length !== 9) { + return 0; + } + + for (let i = 1; i < length; i++) { + if (!isHexDigit(charCodeAt(token.value, i))) { + return 0; + } + } + + return 1; +} + +function idSelector(token) { + if (token === null || token.type !== Hash) { + return 0; + } + + if (!isIdentifierStart(charCodeAt(token.value, 1), charCodeAt(token.value, 2), charCodeAt(token.value, 3))) { + return 0; + } + + return 1; +} + +// https://drafts.csswg.org/css-syntax/#any-value +// It represents the entirety of what a valid declaration can have as its value. +function declarationValue(token, getNextToken) { + if (!token) { + return 0; + } + + let balanceCloseType = 0; + let balanceStash = []; + let length = 0; + + // The <declaration-value> production matches any sequence of one or more tokens, + // so long as the sequence does not contain ... + scan: + do { + switch (token.type) { + // ... <bad-string-token>, <bad-url-token>, + case BadString: + case BadUrl: + break scan; + + // ... unmatched <)-token>, <]-token>, or <}-token>, + case RightCurlyBracket: + case RightParenthesis: + case RightSquareBracket: + if (token.type !== balanceCloseType) { + break scan; + } + + balanceCloseType = balanceStash.pop(); + break; + + // ... or top-level <semicolon-token> tokens + case Semicolon: + if (balanceCloseType === 0) { + break scan; + } + + break; + + // ... or <delim-token> tokens with a value of "!" + case Delim: + if (balanceCloseType === 0 && token.value === '!') { + break scan; + } + + break; + + case FunctionToken: + case LeftParenthesis: + case LeftSquareBracket: + case LeftCurlyBracket: + balanceStash.push(balanceCloseType); + balanceCloseType = balancePair.get(token.type); + break; + } + + length++; + } while (token = getNextToken(length)); + + return length; +} + +// https://drafts.csswg.org/css-syntax/#any-value +// The <any-value> production is identical to <declaration-value>, but also +// allows top-level <semicolon-token> tokens and <delim-token> tokens +// with a value of "!". It represents the entirety of what valid CSS can be in any context. +function anyValue(token, getNextToken) { + if (!token) { + return 0; + } + + let balanceCloseType = 0; + let balanceStash = []; + let length = 0; + + // The <any-value> production matches any sequence of one or more tokens, + // so long as the sequence ... + scan: + do { + switch (token.type) { + // ... does not contain <bad-string-token>, <bad-url-token>, + case BadString: + case BadUrl: + break scan; + + // ... unmatched <)-token>, <]-token>, or <}-token>, + case RightCurlyBracket: + case RightParenthesis: + case RightSquareBracket: + if (token.type !== balanceCloseType) { + break scan; + } + + balanceCloseType = balanceStash.pop(); + break; + + case FunctionToken: + case LeftParenthesis: + case LeftSquareBracket: + case LeftCurlyBracket: + balanceStash.push(balanceCloseType); + balanceCloseType = balancePair.get(token.type); + break; + } + + length++; + } while (token = getNextToken(length)); + + return length; +} + +// ========================= +// Dimensions +// + +function dimension(type) { + if (type) { + type = new Set(type); + } + + return function(token, getNextToken, opts) { + if (token === null || token.type !== Dimension) { + return 0; + } + + const numberEnd = consumeNumber(token.value, 0); + + // check unit + if (type !== null) { + // check for IE postfix hack, i.e. 123px\0 or 123px\9 + const reverseSolidusOffset = token.value.indexOf('\\', numberEnd); + const unit = reverseSolidusOffset === -1 || !isPostfixIeHack(token.value, reverseSolidusOffset) + ? token.value.substr(numberEnd) + : token.value.substring(numberEnd, reverseSolidusOffset); + + if (type.has(unit.toLowerCase()) === false) { + return 0; + } + } + + // check range if specified + if (outOfRange(opts, token.value, numberEnd)) { + return 0; + } + + return 1; + }; +} + +// ========================= +// Percentage +// + +// §5.5. Percentages: the <percentage> type +// https://drafts.csswg.org/css-values-4/#percentages +function percentage(token, getNextToken, opts) { + // ... corresponds to the <percentage-token> production + if (token === null || token.type !== Percentage) { + return 0; + } + + // check range if specified + if (outOfRange(opts, token.value, token.value.length - 1)) { + return 0; + } + + return 1; +} + +// ========================= +// Numeric +// + +// https://drafts.csswg.org/css-values-4/#numbers +// The value <zero> represents a literal number with the value 0. Expressions that merely +// evaluate to a <number> with the value 0 (for example, calc(0)) do not match <zero>; +// only literal <number-token>s do. +function zero(next) { + if (typeof next !== 'function') { + next = function() { + return 0; + }; + } + + return function(token, getNextToken, opts) { + if (token !== null && token.type === NumberToken) { + if (Number(token.value) === 0) { + return 1; + } + } + + return next(token, getNextToken, opts); + }; +} + +// § 5.3. Real Numbers: the <number> type +// https://drafts.csswg.org/css-values-4/#numbers +// Number values are denoted by <number>, and represent real numbers, possibly with a fractional component. +// ... It corresponds to the <number-token> production +function number(token, getNextToken, opts) { + if (token === null) { + return 0; + } + + const numberEnd = consumeNumber(token.value, 0); + const isNumber = numberEnd === token.value.length; + if (!isNumber && !isPostfixIeHack(token.value, numberEnd)) { + return 0; + } + + // check range if specified + if (outOfRange(opts, token.value, numberEnd)) { + return 0; + } + + return 1; +} + +// §5.2. Integers: the <integer> type +// https://drafts.csswg.org/css-values-4/#integers +function integer(token, getNextToken, opts) { + // ... corresponds to a subset of the <number-token> production + if (token === null || token.type !== NumberToken) { + return 0; + } + + // The first digit of an integer may be immediately preceded by `-` or `+` to indicate the integer’s sign. + let i = charCodeAt(token.value, 0) === 0x002B || // U+002B PLUS SIGN (+) + charCodeAt(token.value, 0) === 0x002D ? 1 : 0; // U+002D HYPHEN-MINUS (-) + + // When written literally, an integer is one or more decimal digits 0 through 9 ... + for (; i < token.value.length; i++) { + if (!isDigit(charCodeAt(token.value, i))) { + return 0; + } + } + + // check range if specified + if (outOfRange(opts, token.value, i)) { + return 0; + } + + return 1; +} + +// token types +export const tokenTypes = { + 'ident-token': tokenType(Ident), + 'function-token': tokenType(FunctionToken), + 'at-keyword-token': tokenType(AtKeyword), + 'hash-token': tokenType(Hash), + 'string-token': tokenType(StringToken), + 'bad-string-token': tokenType(BadString), + 'url-token': tokenType(Url), + 'bad-url-token': tokenType(BadUrl), + 'delim-token': tokenType(Delim), + 'number-token': tokenType(NumberToken), + 'percentage-token': tokenType(Percentage), + 'dimension-token': tokenType(Dimension), + 'whitespace-token': tokenType(WhiteSpace), + 'CDO-token': tokenType(CDO), + 'CDC-token': tokenType(CDC), + 'colon-token': tokenType(Colon), + 'semicolon-token': tokenType(Semicolon), + 'comma-token': tokenType(Comma), + '[-token': tokenType(LeftSquareBracket), + ']-token': tokenType(RightSquareBracket), + '(-token': tokenType(LeftParenthesis), + ')-token': tokenType(RightParenthesis), + '{-token': tokenType(LeftCurlyBracket), + '}-token': tokenType(RightCurlyBracket) +}; + +// token production types +export const productionTypes = { + // token type aliases + 'string': tokenType(StringToken), + 'ident': tokenType(Ident), + + // percentage + 'percentage': calc(percentage), + + // numeric + 'zero': zero(), + 'number': calc(number), + 'integer': calc(integer), + + // complex types + 'custom-ident': customIdent, + 'dashed-ident': dashedIdent, + 'custom-property-name': customPropertyName, + 'hex-color': hexColor, + 'id-selector': idSelector, // element( <id-selector> ) + 'an-plus-b': anPlusB, + 'urange': urange, + 'declaration-value': declarationValue, + 'any-value': anyValue +}; + +export const unitGroups = [ + 'length', + 'angle', + 'time', + 'frequency', + 'resolution', + 'flex', + 'decibel', + 'semitones' +]; + +// dimensions types depend on units set +export function createDemensionTypes(units) { + const { + angle, + decibel, + frequency, + flex, + length, + resolution, + semitones, + time + } = units || {}; + + return { + 'dimension': calc(dimension(null)), + 'angle': calc(dimension(angle)), + 'decibel': calc(dimension(decibel)), + 'frequency': calc(dimension(frequency)), + 'flex': calc(dimension(flex)), + 'length': calc(zero(dimension(length))), + 'resolution': calc(dimension(resolution)), + 'semitones': calc(dimension(semitones)), + 'time': calc(dimension(time)) + }; +} + +export function createGenericTypes(units) { + return { + ...tokenTypes, + ...productionTypes, + ...createDemensionTypes(units) + }; +}; diff --git a/vanilla/node_modules/css-tree/lib/lexer/index.js b/vanilla/node_modules/css-tree/lib/lexer/index.js new file mode 100644 index 0000000..1661b76 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/lexer/index.js @@ -0,0 +1 @@ +export { Lexer } from './Lexer.js'; diff --git a/vanilla/node_modules/css-tree/lib/lexer/match-graph.js b/vanilla/node_modules/css-tree/lib/lexer/match-graph.js new file mode 100644 index 0000000..5d3d800 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/lexer/match-graph.js @@ -0,0 +1,527 @@ +import { parse } from '../definition-syntax/parse.js'; + +export const MATCH = { type: 'Match' }; +export const MISMATCH = { type: 'Mismatch' }; +export const DISALLOW_EMPTY = { type: 'DisallowEmpty' }; + +const LEFTPARENTHESIS = 40; // ( +const RIGHTPARENTHESIS = 41; // ) + +function createCondition(match, thenBranch, elseBranch) { + // reduce node count + if (thenBranch === MATCH && elseBranch === MISMATCH) { + return match; + } + + if (match === MATCH && thenBranch === MATCH && elseBranch === MATCH) { + return match; + } + + if (match.type === 'If' && match.else === MISMATCH && thenBranch === MATCH) { + thenBranch = match.then; + match = match.match; + } + + return { + type: 'If', + match, + then: thenBranch, + else: elseBranch + }; +} + +function isFunctionType(name) { + return ( + name.length > 2 && + name.charCodeAt(name.length - 2) === LEFTPARENTHESIS && + name.charCodeAt(name.length - 1) === RIGHTPARENTHESIS + ); +} + +function isEnumCapatible(term) { + return ( + term.type === 'Keyword' || + term.type === 'AtKeyword' || + term.type === 'Function' || + term.type === 'Type' && isFunctionType(term.name) + ); +} + +function groupNode(terms, combinator = ' ', explicit = false) { + return { + type: 'Group', + terms, + combinator, + disallowEmpty: false, + explicit + }; +} + +function replaceTypeInGraph(node, replacements, visited = new Set()) { + if (!visited.has(node)) { + visited.add(node); + + switch (node.type) { + case 'If': + node.match = replaceTypeInGraph(node.match, replacements, visited); + node.then = replaceTypeInGraph(node.then, replacements, visited); + node.else = replaceTypeInGraph(node.else, replacements, visited); + break; + + case 'Type': + return replacements[node.name] || node; + } + } + + return node; +} + +function buildGroupMatchGraph(combinator, terms, atLeastOneTermMatched) { + switch (combinator) { + case ' ': { + // Juxtaposing components means that all of them must occur, in the given order. + // + // a b c + // = + // match a + // then match b + // then match c + // then MATCH + // else MISMATCH + // else MISMATCH + // else MISMATCH + let result = MATCH; + + for (let i = terms.length - 1; i >= 0; i--) { + const term = terms[i]; + + result = createCondition( + term, + result, + MISMATCH + ); + }; + + return result; + } + + case '|': { + // A bar (|) separates two or more alternatives: exactly one of them must occur. + // + // a | b | c + // = + // match a + // then MATCH + // else match b + // then MATCH + // else match c + // then MATCH + // else MISMATCH + + let result = MISMATCH; + let map = null; + + for (let i = terms.length - 1; i >= 0; i--) { + let term = terms[i]; + + // reduce sequence of keywords into a Enum + if (isEnumCapatible(term)) { + if (map === null && i > 0 && isEnumCapatible(terms[i - 1])) { + map = Object.create(null); + result = createCondition( + { + type: 'Enum', + map + }, + MATCH, + result + ); + } + + if (map !== null) { + const key = (isFunctionType(term.name) ? term.name.slice(0, -1) : term.name).toLowerCase(); + if (key in map === false) { + map[key] = term; + continue; + } + } + } + + map = null; + + // create a new conditonal node + result = createCondition( + term, + MATCH, + result + ); + }; + + return result; + } + + case '&&': { + // A double ampersand (&&) separates two or more components, + // all of which must occur, in any order. + + // Use MatchOnce for groups with a large number of terms, + // since &&-groups produces at least N!-node trees + if (terms.length > 5) { + return { + type: 'MatchOnce', + terms, + all: true + }; + } + + // Use a combination tree for groups with small number of terms + // + // a && b && c + // = + // match a + // then [b && c] + // else match b + // then [a && c] + // else match c + // then [a && b] + // else MISMATCH + // + // a && b + // = + // match a + // then match b + // then MATCH + // else MISMATCH + // else match b + // then match a + // then MATCH + // else MISMATCH + // else MISMATCH + let result = MISMATCH; + + for (let i = terms.length - 1; i >= 0; i--) { + const term = terms[i]; + let thenClause; + + if (terms.length > 1) { + thenClause = buildGroupMatchGraph( + combinator, + terms.filter(function(newGroupTerm) { + return newGroupTerm !== term; + }), + false + ); + } else { + thenClause = MATCH; + } + + result = createCondition( + term, + thenClause, + result + ); + }; + + return result; + } + + case '||': { + // A double bar (||) separates two or more options: + // one or more of them must occur, in any order. + + // Use MatchOnce for groups with a large number of terms, + // since ||-groups produces at least N!-node trees + if (terms.length > 5) { + return { + type: 'MatchOnce', + terms, + all: false + }; + } + + // Use a combination tree for groups with small number of terms + // + // a || b || c + // = + // match a + // then [b || c] + // else match b + // then [a || c] + // else match c + // then [a || b] + // else MISMATCH + // + // a || b + // = + // match a + // then match b + // then MATCH + // else MATCH + // else match b + // then match a + // then MATCH + // else MATCH + // else MISMATCH + let result = atLeastOneTermMatched ? MATCH : MISMATCH; + + for (let i = terms.length - 1; i >= 0; i--) { + const term = terms[i]; + let thenClause; + + if (terms.length > 1) { + thenClause = buildGroupMatchGraph( + combinator, + terms.filter(function(newGroupTerm) { + return newGroupTerm !== term; + }), + true + ); + } else { + thenClause = MATCH; + } + + result = createCondition( + term, + thenClause, + result + ); + }; + + return result; + } + } +} + +function buildMultiplierMatchGraph(node) { + let result = MATCH; + let matchTerm = buildMatchGraphInternal(node.term); + + if (node.max === 0) { + // disable repeating of empty match to prevent infinite loop + matchTerm = createCondition( + matchTerm, + DISALLOW_EMPTY, + MISMATCH + ); + + // an occurrence count is not limited, make a cycle; + // to collect more terms on each following matching mismatch + result = createCondition( + matchTerm, + null, // will be a loop + MISMATCH + ); + + result.then = createCondition( + MATCH, + MATCH, + result // make a loop + ); + + if (node.comma) { + result.then.else = createCondition( + { type: 'Comma', syntax: node }, + result, + MISMATCH + ); + } + } else { + // create a match node chain for [min .. max] interval with optional matches + for (let i = node.min || 1; i <= node.max; i++) { + if (node.comma && result !== MATCH) { + result = createCondition( + { type: 'Comma', syntax: node }, + result, + MISMATCH + ); + } + + result = createCondition( + matchTerm, + createCondition( + MATCH, + MATCH, + result + ), + MISMATCH + ); + } + } + + if (node.min === 0) { + // allow zero match + result = createCondition( + MATCH, + MATCH, + result + ); + } else { + // create a match node chain to collect [0 ... min - 1] required matches + for (let i = 0; i < node.min - 1; i++) { + if (node.comma && result !== MATCH) { + result = createCondition( + { type: 'Comma', syntax: node }, + result, + MISMATCH + ); + } + + result = createCondition( + matchTerm, + result, + MISMATCH + ); + } + } + + return result; +} + +function buildMatchGraphInternal(node) { + if (typeof node === 'function') { + return { + type: 'Generic', + fn: node + }; + } + + switch (node.type) { + case 'Group': { + let result = buildGroupMatchGraph( + node.combinator, + node.terms.map(buildMatchGraphInternal), + false + ); + + if (node.disallowEmpty) { + result = createCondition( + result, + DISALLOW_EMPTY, + MISMATCH + ); + } + + return result; + } + + case 'Multiplier': + return buildMultiplierMatchGraph(node); + + // https://drafts.csswg.org/css-values-5/#boolean + case 'Boolean': { + const term = buildMatchGraphInternal(node.term); + // <boolean-expr[ <test> ]> = not <boolean-expr-group> | <boolean-expr-group> [ [ and <boolean-expr-group> ]* | [ or <boolean-expr-group> ]* ] + const matchNode = buildMatchGraphInternal(groupNode([ + groupNode([ + { type: 'Keyword', name: 'not' }, + { type: 'Type', name: '!boolean-group' } + ]), + groupNode([ + { type: 'Type', name: '!boolean-group' }, + groupNode([ + { type: 'Multiplier', comma: false, min: 0, max: 0, term: groupNode([ + { type: 'Keyword', name: 'and' }, + { type: 'Type', name: '!boolean-group' } + ]) }, + { type: 'Multiplier', comma: false, min: 0, max: 0, term: groupNode([ + { type: 'Keyword', name: 'or' }, + { type: 'Type', name: '!boolean-group' } + ]) } + ], '|') + ]) + ], '|')); + // <boolean-expr-group> = <test> | ( <boolean-expr[ <test> ]> ) | <general-enclosed> + const booleanGroup = buildMatchGraphInternal( + groupNode([ + { type: 'Type', name: '!term' }, + groupNode([ + { type: 'Token', value: '(' }, + { type: 'Type', name: '!self' }, + { type: 'Token', value: ')' } + ]), + { type: 'Type', name: 'general-enclosed' } + ], '|') + ); + + replaceTypeInGraph(booleanGroup, { '!term': term, '!self': matchNode }); + replaceTypeInGraph(matchNode, { '!boolean-group': booleanGroup }); + + return matchNode; + } + + case 'Type': + case 'Property': + return { + type: node.type, + name: node.name, + syntax: node + }; + + case 'Keyword': + return { + type: node.type, + name: node.name.toLowerCase(), + syntax: node + }; + + case 'AtKeyword': + return { + type: node.type, + name: '@' + node.name.toLowerCase(), + syntax: node + }; + + case 'Function': + return { + type: node.type, + name: node.name.toLowerCase() + '(', + syntax: node + }; + + case 'String': + // convert a one char length String to a Token + if (node.value.length === 3) { + return { + type: 'Token', + value: node.value.charAt(1), + syntax: node + }; + } + + // otherwise use it as is + return { + type: node.type, + value: node.value.substr(1, node.value.length - 2).replace(/\\'/g, '\''), + syntax: node + }; + + case 'Token': + return { + type: node.type, + value: node.value, + syntax: node + }; + + case 'Comma': + return { + type: node.type, + syntax: node + }; + + default: + throw new Error('Unknown node type:', node.type); + } +} + +export function buildMatchGraph(syntaxTree, ref) { + if (typeof syntaxTree === 'string') { + syntaxTree = parse(syntaxTree); + } + + return { + type: 'MatchGraph', + match: buildMatchGraphInternal(syntaxTree), + syntax: ref || null, + source: syntaxTree + }; +} diff --git a/vanilla/node_modules/css-tree/lib/lexer/match.js b/vanilla/node_modules/css-tree/lib/lexer/match.js new file mode 100644 index 0000000..5ac9c48 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/lexer/match.js @@ -0,0 +1,630 @@ +import { MATCH, MISMATCH, DISALLOW_EMPTY } from './match-graph.js'; +import * as TYPE from '../tokenizer/types.js'; + +const { hasOwnProperty } = Object.prototype; +const STUB = 0; +const TOKEN = 1; +const OPEN_SYNTAX = 2; +const CLOSE_SYNTAX = 3; + +const EXIT_REASON_MATCH = 'Match'; +const EXIT_REASON_MISMATCH = 'Mismatch'; +const EXIT_REASON_ITERATION_LIMIT = 'Maximum iteration number exceeded (please fill an issue on https://github.com/csstree/csstree/issues)'; + +const ITERATION_LIMIT = 15000; +export let totalIterationCount = 0; + +function reverseList(list) { + let prev = null; + let next = null; + let item = list; + + while (item !== null) { + next = item.prev; + item.prev = prev; + prev = item; + item = next; + } + + return prev; +} + +function areStringsEqualCaseInsensitive(testStr, referenceStr) { + if (testStr.length !== referenceStr.length) { + return false; + } + + for (let i = 0; i < testStr.length; i++) { + const referenceCode = referenceStr.charCodeAt(i); + let testCode = testStr.charCodeAt(i); + + // testCode.toLowerCase() for U+0041 LATIN CAPITAL LETTER A (A) .. U+005A LATIN CAPITAL LETTER Z (Z). + if (testCode >= 0x0041 && testCode <= 0x005A) { + testCode = testCode | 32; + } + + if (testCode !== referenceCode) { + return false; + } + } + + return true; +} + +function isContextEdgeDelim(token) { + if (token.type !== TYPE.Delim) { + return false; + } + + // Fix matching for unicode-range: U+30??, U+FF00-FF9F + // Probably we need to check out previous match instead + return token.value !== '?'; +} + +function isCommaContextStart(token) { + if (token === null) { + return true; + } + + return ( + token.type === TYPE.Comma || + token.type === TYPE.Function || + token.type === TYPE.LeftParenthesis || + token.type === TYPE.LeftSquareBracket || + token.type === TYPE.LeftCurlyBracket || + isContextEdgeDelim(token) + ); +} + +function isCommaContextEnd(token) { + if (token === null) { + return true; + } + + return ( + token.type === TYPE.RightParenthesis || + token.type === TYPE.RightSquareBracket || + token.type === TYPE.RightCurlyBracket || + (token.type === TYPE.Delim && token.value === '/') + ); +} + +function internalMatch(tokens, state, syntaxes) { + function moveToNextToken() { + do { + tokenIndex++; + token = tokenIndex < tokens.length ? tokens[tokenIndex] : null; + } while (token !== null && (token.type === TYPE.WhiteSpace || token.type === TYPE.Comment)); + } + + function getNextToken(offset) { + const nextIndex = tokenIndex + offset; + + return nextIndex < tokens.length ? tokens[nextIndex] : null; + } + + function stateSnapshotFromSyntax(nextState, prev) { + return { + nextState, + matchStack, + syntaxStack, + thenStack, + tokenIndex, + prev + }; + } + + function pushThenStack(nextState) { + thenStack = { + nextState, + matchStack, + syntaxStack, + prev: thenStack + }; + } + + function pushElseStack(nextState) { + elseStack = stateSnapshotFromSyntax(nextState, elseStack); + } + + function addTokenToMatch() { + matchStack = { + type: TOKEN, + syntax: state.syntax, + token, + prev: matchStack + }; + + moveToNextToken(); + syntaxStash = null; + + if (tokenIndex > longestMatch) { + longestMatch = tokenIndex; + } + } + + function openSyntax() { + syntaxStack = { + syntax: state.syntax, + opts: state.syntax.opts || (syntaxStack !== null && syntaxStack.opts) || null, + prev: syntaxStack + }; + + matchStack = { + type: OPEN_SYNTAX, + syntax: state.syntax, + token: matchStack.token, + prev: matchStack + }; + } + + function closeSyntax() { + if (matchStack.type === OPEN_SYNTAX) { + matchStack = matchStack.prev; + } else { + matchStack = { + type: CLOSE_SYNTAX, + syntax: syntaxStack.syntax, + token: matchStack.token, + prev: matchStack + }; + } + + syntaxStack = syntaxStack.prev; + } + + let syntaxStack = null; + let thenStack = null; + let elseStack = null; + + // null – stashing allowed, nothing stashed + // false – stashing disabled, nothing stashed + // anithing else – fail stashable syntaxes, some syntax stashed + let syntaxStash = null; + + let iterationCount = 0; // count iterations and prevent infinite loop + let exitReason = null; + + let token = null; + let tokenIndex = -1; + let longestMatch = 0; + let matchStack = { + type: STUB, + syntax: null, + token: null, + prev: null + }; + + moveToNextToken(); + + while (exitReason === null && ++iterationCount < ITERATION_LIMIT) { + // function mapList(list, fn) { + // const result = []; + // while (list) { + // result.unshift(fn(list)); + // list = list.prev; + // } + // return result; + // } + // console.log('--\n', + // '#' + iterationCount, + // require('util').inspect({ + // match: mapList(matchStack, x => x.type === TOKEN ? x.token && x.token.value : x.syntax ? ({ [OPEN_SYNTAX]: '<', [CLOSE_SYNTAX]: '</' }[x.type] || x.type) + '!' + x.syntax.name : null), + // token: token && token.value, + // tokenIndex, + // syntax: syntax.type + (syntax.id ? ' #' + syntax.id : '') + // }, { depth: null }) + // ); + switch (state.type) { + case 'Match': + if (thenStack === null) { + // turn to MISMATCH when some tokens left unmatched + if (token !== null) { + // doesn't mismatch if just one token left and it's an IE hack + if (tokenIndex !== tokens.length - 1 || (token.value !== '\\0' && token.value !== '\\9')) { + state = MISMATCH; + break; + } + } + + // break the main loop, return a result - MATCH + exitReason = EXIT_REASON_MATCH; + break; + } + + // go to next syntax (`then` branch) + state = thenStack.nextState; + + // check match is not empty + if (state === DISALLOW_EMPTY) { + if (thenStack.matchStack === matchStack) { + state = MISMATCH; + break; + } else { + state = MATCH; + } + } + + // close syntax if needed + while (thenStack.syntaxStack !== syntaxStack) { + closeSyntax(); + } + + // pop stack + thenStack = thenStack.prev; + break; + + case 'Mismatch': + // when some syntax is stashed + if (syntaxStash !== null && syntaxStash !== false) { + // there is no else branches or a branch reduce match stack + if (elseStack === null || tokenIndex > elseStack.tokenIndex) { + // restore state from the stash + elseStack = syntaxStash; + syntaxStash = false; // disable stashing + } + } else if (elseStack === null) { + // no else branches -> break the main loop + // return a result - MISMATCH + exitReason = EXIT_REASON_MISMATCH; + break; + } + + // go to next syntax (`else` branch) + state = elseStack.nextState; + + // restore all the rest stack states + thenStack = elseStack.thenStack; + syntaxStack = elseStack.syntaxStack; + matchStack = elseStack.matchStack; + tokenIndex = elseStack.tokenIndex; + token = tokenIndex < tokens.length ? tokens[tokenIndex] : null; + + // pop stack + elseStack = elseStack.prev; + break; + + case 'MatchGraph': + state = state.match; + break; + + case 'If': + // IMPORTANT: else stack push must go first, + // since it stores the state of thenStack before changes + if (state.else !== MISMATCH) { + pushElseStack(state.else); + } + + if (state.then !== MATCH) { + pushThenStack(state.then); + } + + state = state.match; + break; + + case 'MatchOnce': + state = { + type: 'MatchOnceBuffer', + syntax: state, + index: 0, + mask: 0 + }; + break; + + case 'MatchOnceBuffer': { + const terms = state.syntax.terms; + + if (state.index === terms.length) { + // no matches at all or it's required all terms to be matched + if (state.mask === 0 || state.syntax.all) { + state = MISMATCH; + break; + } + + // a partial match is ok + state = MATCH; + break; + } + + // all terms are matched + if (state.mask === (1 << terms.length) - 1) { + state = MATCH; + break; + } + + for (; state.index < terms.length; state.index++) { + const matchFlag = 1 << state.index; + + if ((state.mask & matchFlag) === 0) { + // IMPORTANT: else stack push must go first, + // since it stores the state of thenStack before changes + pushElseStack(state); + pushThenStack({ + type: 'AddMatchOnce', + syntax: state.syntax, + mask: state.mask | matchFlag + }); + + // match + state = terms[state.index++]; + break; + } + } + break; + } + + case 'AddMatchOnce': + state = { + type: 'MatchOnceBuffer', + syntax: state.syntax, + index: 0, + mask: state.mask + }; + break; + + case 'Enum': + if (token !== null) { + let name = token.value.toLowerCase(); + + // drop \0 and \9 hack from keyword name + if (name.indexOf('\\') !== -1) { + name = name.replace(/\\[09].*$/, ''); + } + + if (hasOwnProperty.call(state.map, name)) { + state = state.map[name]; + break; + } + } + + state = MISMATCH; + break; + + case 'Generic': { + const opts = syntaxStack !== null ? syntaxStack.opts : null; + const lastTokenIndex = tokenIndex + Math.floor(state.fn(token, getNextToken, opts)); + + if (!isNaN(lastTokenIndex) && lastTokenIndex > tokenIndex) { + while (tokenIndex < lastTokenIndex) { + addTokenToMatch(); + } + + state = MATCH; + } else { + state = MISMATCH; + } + + break; + } + + case 'Type': + case 'Property': { + const syntaxDict = state.type === 'Type' ? 'types' : 'properties'; + const dictSyntax = hasOwnProperty.call(syntaxes, syntaxDict) ? syntaxes[syntaxDict][state.name] : null; + + if (!dictSyntax || !dictSyntax.match) { + throw new Error( + 'Bad syntax reference: ' + + (state.type === 'Type' + ? '<' + state.name + '>' + : '<\'' + state.name + '\'>') + ); + } + + // stash a syntax for types with low priority + if (syntaxStash !== false && token !== null && state.type === 'Type') { + const lowPriorityMatching = + // https://drafts.csswg.org/css-values-4/#custom-idents + // When parsing positionally-ambiguous keywords in a property value, a <custom-ident> production + // can only claim the keyword if no other unfulfilled production can claim it. + (state.name === 'custom-ident' && token.type === TYPE.Ident) || + + // https://drafts.csswg.org/css-values-4/#lengths + // ... if a `0` could be parsed as either a <number> or a <length> in a property (such as line-height), + // it must parse as a <number> + (state.name === 'length' && token.value === '0'); + + if (lowPriorityMatching) { + if (syntaxStash === null) { + syntaxStash = stateSnapshotFromSyntax(state, elseStack); + } + + state = MISMATCH; + break; + } + } + + openSyntax(); + state = dictSyntax.matchRef || dictSyntax.match; + break; + } + + case 'Keyword': { + const name = state.name; + + if (token !== null) { + let keywordName = token.value; + + // drop \0 and \9 hack from keyword name + if (keywordName.indexOf('\\') !== -1) { + keywordName = keywordName.replace(/\\[09].*$/, ''); + } + + if (areStringsEqualCaseInsensitive(keywordName, name)) { + addTokenToMatch(); + state = MATCH; + break; + } + } + + state = MISMATCH; + break; + } + + case 'AtKeyword': + case 'Function': + if (token !== null && areStringsEqualCaseInsensitive(token.value, state.name)) { + addTokenToMatch(); + state = MATCH; + break; + } + + state = MISMATCH; + break; + + case 'Token': + if (token !== null && token.value === state.value) { + addTokenToMatch(); + state = MATCH; + break; + } + + state = MISMATCH; + break; + + case 'Comma': + if (token !== null && token.type === TYPE.Comma) { + if (isCommaContextStart(matchStack.token)) { + state = MISMATCH; + } else { + addTokenToMatch(); + state = isCommaContextEnd(token) ? MISMATCH : MATCH; + } + } else { + state = isCommaContextStart(matchStack.token) || isCommaContextEnd(token) ? MATCH : MISMATCH; + } + + break; + + case 'String': + let string = ''; + let lastTokenIndex = tokenIndex; + + for (; lastTokenIndex < tokens.length && string.length < state.value.length; lastTokenIndex++) { + string += tokens[lastTokenIndex].value; + } + + if (areStringsEqualCaseInsensitive(string, state.value)) { + while (tokenIndex < lastTokenIndex) { + addTokenToMatch(); + } + + state = MATCH; + } else { + state = MISMATCH; + } + + break; + + default: + throw new Error('Unknown node type: ' + state.type); + } + } + + totalIterationCount += iterationCount; + + switch (exitReason) { + case null: + console.warn('[csstree-match] BREAK after ' + ITERATION_LIMIT + ' iterations'); + exitReason = EXIT_REASON_ITERATION_LIMIT; + matchStack = null; + break; + + case EXIT_REASON_MATCH: + while (syntaxStack !== null) { + closeSyntax(); + } + break; + + default: + matchStack = null; + } + + return { + tokens, + reason: exitReason, + iterations: iterationCount, + match: matchStack, + longestMatch + }; +} + +export function matchAsList(tokens, matchGraph, syntaxes) { + const matchResult = internalMatch(tokens, matchGraph, syntaxes || {}); + + if (matchResult.match !== null) { + let item = reverseList(matchResult.match).prev; + + matchResult.match = []; + + while (item !== null) { + switch (item.type) { + case OPEN_SYNTAX: + case CLOSE_SYNTAX: + matchResult.match.push({ + type: item.type, + syntax: item.syntax + }); + break; + + default: + matchResult.match.push({ + token: item.token.value, + node: item.token.node + }); + break; + } + + item = item.prev; + } + } + + return matchResult; +} + +export function matchAsTree(tokens, matchGraph, syntaxes) { + const matchResult = internalMatch(tokens, matchGraph, syntaxes || {}); + + if (matchResult.match === null) { + return matchResult; + } + + let item = matchResult.match; + let host = matchResult.match = { + syntax: matchGraph.syntax || null, + match: [] + }; + const hostStack = [host]; + + // revert a list and start with 2nd item since 1st is a stub item + item = reverseList(item).prev; + + // build a tree + while (item !== null) { + switch (item.type) { + case OPEN_SYNTAX: + host.match.push(host = { + syntax: item.syntax, + match: [] + }); + hostStack.push(host); + break; + + case CLOSE_SYNTAX: + hostStack.pop(); + host = hostStack[hostStack.length - 1]; + break; + + default: + host.match.push({ + syntax: item.syntax || null, + token: item.token.value, + node: item.token.node + }); + } + + item = item.prev; + } + + return matchResult; +} diff --git a/vanilla/node_modules/css-tree/lib/lexer/prepare-tokens.js b/vanilla/node_modules/css-tree/lib/lexer/prepare-tokens.js new file mode 100644 index 0000000..4243fa8 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/lexer/prepare-tokens.js @@ -0,0 +1,50 @@ +import { tokenize } from '../tokenizer/index.js'; + +const astToTokens = { + decorator(handlers) { + const tokens = []; + let curNode = null; + + return { + ...handlers, + node(node) { + const tmp = curNode; + curNode = node; + handlers.node.call(this, node); + curNode = tmp; + }, + emit(value, type, auto) { + tokens.push({ + type, + value, + node: auto ? null : curNode + }); + }, + result() { + return tokens; + } + }; + } +}; + +function stringToTokens(str) { + const tokens = []; + + tokenize(str, (type, start, end) => + tokens.push({ + type, + value: str.slice(start, end), + node: null + }) + ); + + return tokens; +} + +export default function(value, syntax) { + if (typeof value === 'string') { + return stringToTokens(value); + } + + return syntax.generate(value, astToTokens); +}; diff --git a/vanilla/node_modules/css-tree/lib/lexer/search.js b/vanilla/node_modules/css-tree/lib/lexer/search.js new file mode 100644 index 0000000..ee68e2d --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/lexer/search.js @@ -0,0 +1,61 @@ +import { List } from '../utils/List.js'; + +function getFirstMatchNode(matchNode) { + if ('node' in matchNode) { + return matchNode.node; + } + + return getFirstMatchNode(matchNode.match[0]); +} + +function getLastMatchNode(matchNode) { + if ('node' in matchNode) { + return matchNode.node; + } + + return getLastMatchNode(matchNode.match[matchNode.match.length - 1]); +} + +export function matchFragments(lexer, ast, match, type, name) { + function findFragments(matchNode) { + if (matchNode.syntax !== null && + matchNode.syntax.type === type && + matchNode.syntax.name === name) { + const start = getFirstMatchNode(matchNode); + const end = getLastMatchNode(matchNode); + + lexer.syntax.walk(ast, function(node, item, list) { + if (node === start) { + const nodes = new List(); + + do { + nodes.appendData(item.data); + + if (item.data === end) { + break; + } + + item = item.next; + } while (item !== null); + + fragments.push({ + parent: list, + nodes + }); + } + }); + } + + if (Array.isArray(matchNode.match)) { + matchNode.match.forEach(findFragments); + } + } + + const fragments = []; + + if (match.matched !== null) { + findFragments(match.matched); + } + + return fragments; +} diff --git a/vanilla/node_modules/css-tree/lib/lexer/structure.js b/vanilla/node_modules/css-tree/lib/lexer/structure.js new file mode 100644 index 0000000..3bf4ced --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/lexer/structure.js @@ -0,0 +1,169 @@ +import { List } from '../utils/List.js'; + +const { hasOwnProperty } = Object.prototype; + +function isValidNumber(value) { + // Number.isInteger(value) && value >= 0 + return ( + typeof value === 'number' && + isFinite(value) && + Math.floor(value) === value && + value >= 0 + ); +} + +function isValidLocation(loc) { + return ( + Boolean(loc) && + isValidNumber(loc.offset) && + isValidNumber(loc.line) && + isValidNumber(loc.column) + ); +} + +function createNodeStructureChecker(type, fields) { + return function checkNode(node, warn) { + if (!node || node.constructor !== Object) { + return warn(node, 'Type of node should be an Object'); + } + + for (let key in node) { + let valid = true; + + if (hasOwnProperty.call(node, key) === false) { + continue; + } + + if (key === 'type') { + if (node.type !== type) { + warn(node, 'Wrong node type `' + node.type + '`, expected `' + type + '`'); + } + } else if (key === 'loc') { + if (node.loc === null) { + continue; + } else if (node.loc && node.loc.constructor === Object) { + if (typeof node.loc.source !== 'string') { + key += '.source'; + } else if (!isValidLocation(node.loc.start)) { + key += '.start'; + } else if (!isValidLocation(node.loc.end)) { + key += '.end'; + } else { + continue; + } + } + + valid = false; + } else if (fields.hasOwnProperty(key)) { + valid = false; + + for (let i = 0; !valid && i < fields[key].length; i++) { + const fieldType = fields[key][i]; + + switch (fieldType) { + case String: + valid = typeof node[key] === 'string'; + break; + + case Boolean: + valid = typeof node[key] === 'boolean'; + break; + + case null: + valid = node[key] === null; + break; + + default: + if (typeof fieldType === 'string') { + valid = node[key] && node[key].type === fieldType; + } else if (Array.isArray(fieldType)) { + valid = node[key] instanceof List; + } + } + } + } else { + warn(node, 'Unknown field `' + key + '` for ' + type + ' node type'); + } + + if (!valid) { + warn(node, 'Bad value for `' + type + '.' + key + '`'); + } + } + + for (const key in fields) { + if (hasOwnProperty.call(fields, key) && + hasOwnProperty.call(node, key) === false) { + warn(node, 'Field `' + type + '.' + key + '` is missed'); + } + } + }; +} + +function genTypesList(fieldTypes, path) { + const docsTypes = []; + + for (let i = 0; i < fieldTypes.length; i++) { + const fieldType = fieldTypes[i]; + if (fieldType === String || fieldType === Boolean) { + docsTypes.push(fieldType.name.toLowerCase()); + } else if (fieldType === null) { + docsTypes.push('null'); + } else if (typeof fieldType === 'string') { + docsTypes.push(fieldType); + } else if (Array.isArray(fieldType)) { + docsTypes.push('List<' + (genTypesList(fieldType, path) || 'any') + '>'); // TODO: use type enum + } else { + throw new Error('Wrong value `' + fieldType + '` in `' + path + '` structure definition'); + } + } + + return docsTypes.join(' | '); +} + +function processStructure(name, nodeType) { + const structure = nodeType.structure; + const fields = { + type: String, + loc: true + }; + const docs = { + type: '"' + name + '"' + }; + + for (const key in structure) { + if (hasOwnProperty.call(structure, key) === false) { + continue; + } + + const fieldTypes = fields[key] = Array.isArray(structure[key]) + ? structure[key].slice() + : [structure[key]]; + + docs[key] = genTypesList(fieldTypes, name + '.' + key); + } + + return { + docs, + check: createNodeStructureChecker(name, fields) + }; +} + +export function getStructureFromConfig(config) { + const structure = {}; + + if (config.node) { + for (const name in config.node) { + if (hasOwnProperty.call(config.node, name)) { + const nodeType = config.node[name]; + + if (nodeType.structure) { + structure[name] = processStructure(name, nodeType); + } else { + throw new Error('Missed `structure` field in `' + name + '` node type definition'); + } + } + } + } + + return structure; +}; diff --git a/vanilla/node_modules/css-tree/lib/lexer/trace.js b/vanilla/node_modules/css-tree/lib/lexer/trace.js new file mode 100644 index 0000000..959813c --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/lexer/trace.js @@ -0,0 +1,66 @@ +export function getTrace(node) { + function shouldPutToTrace(syntax) { + if (syntax === null) { + return false; + } + + return ( + syntax.type === 'Type' || + syntax.type === 'Property' || + syntax.type === 'Keyword' + ); + } + + function hasMatch(matchNode) { + if (Array.isArray(matchNode.match)) { + // use for-loop for better perfomance + for (let i = 0; i < matchNode.match.length; i++) { + if (hasMatch(matchNode.match[i])) { + if (shouldPutToTrace(matchNode.syntax)) { + result.unshift(matchNode.syntax); + } + + return true; + } + } + } else if (matchNode.node === node) { + result = shouldPutToTrace(matchNode.syntax) + ? [matchNode.syntax] + : []; + + return true; + } + + return false; + } + + let result = null; + + if (this.matched !== null) { + hasMatch(this.matched); + } + + return result; +} + +export function isType(node, type) { + return testNode(this, node, match => match.type === 'Type' && match.name === type); +} + +export function isProperty(node, property) { + return testNode(this, node, match => match.type === 'Property' && match.name === property); +} + +export function isKeyword(node) { + return testNode(this, node, match => match.type === 'Keyword'); +} + +function testNode(match, node, fn) { + const trace = getTrace.call(match, node); + + if (trace === null) { + return false; + } + + return trace.some(fn); +} diff --git a/vanilla/node_modules/css-tree/lib/lexer/units.js b/vanilla/node_modules/css-tree/lib/lexer/units.js new file mode 100644 index 0000000..88b1f69 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/lexer/units.js @@ -0,0 +1,27 @@ +export const length = [ + // absolute length units https://www.w3.org/TR/css-values-3/#lengths + 'cm', 'mm', 'q', 'in', 'pt', 'pc', 'px', + // font-relative length units https://drafts.csswg.org/css-values-4/#font-relative-lengths + 'em', 'rem', + 'ex', 'rex', + 'cap', 'rcap', + 'ch', 'rch', + 'ic', 'ric', + 'lh', 'rlh', + // viewport-percentage lengths https://drafts.csswg.org/css-values-4/#viewport-relative-lengths + 'vw', 'svw', 'lvw', 'dvw', + 'vh', 'svh', 'lvh', 'dvh', + 'vi', 'svi', 'lvi', 'dvi', + 'vb', 'svb', 'lvb', 'dvb', + 'vmin', 'svmin', 'lvmin', 'dvmin', + 'vmax', 'svmax', 'lvmax', 'dvmax', + // container relative lengths https://drafts.csswg.org/css-contain-3/#container-lengths + 'cqw', 'cqh', 'cqi', 'cqb', 'cqmin', 'cqmax' +]; +export const angle = ['deg', 'grad', 'rad', 'turn']; // https://www.w3.org/TR/css-values-3/#angles +export const time = ['s', 'ms']; // https://www.w3.org/TR/css-values-3/#time +export const frequency = ['hz', 'khz']; // https://www.w3.org/TR/css-values-3/#frequency +export const resolution = ['dpi', 'dpcm', 'dppx', 'x']; // https://www.w3.org/TR/css-values-3/#resolution +export const flex = ['fr']; // https://drafts.csswg.org/css-grid/#fr-unit +export const decibel = ['db']; // https://www.w3.org/TR/css3-speech/#mixing-props-voice-volume +export const semitones = ['st']; // https://www.w3.org/TR/css3-speech/#voice-props-voice-pitch diff --git a/vanilla/node_modules/css-tree/lib/parser/SyntaxError.js b/vanilla/node_modules/css-tree/lib/parser/SyntaxError.js new file mode 100644 index 0000000..55e01c1 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/parser/SyntaxError.js @@ -0,0 +1,70 @@ +import { createCustomError } from '../utils/create-custom-error.js'; + +const MAX_LINE_LENGTH = 100; +const OFFSET_CORRECTION = 60; +const TAB_REPLACEMENT = ' '; + +function sourceFragment({ source, line, column, baseLine, baseColumn }, extraLines) { + function processLines(start, end) { + return lines + .slice(start, end) + .map((line, idx) => + String(start + idx + 1).padStart(maxNumLength) + ' |' + line + ).join('\n'); + } + + const prelines = '\n'.repeat(Math.max(baseLine - 1, 0)); + const precolumns = ' '.repeat(Math.max(baseColumn - 1, 0)); + const lines = (prelines + precolumns + source).split(/\r\n?|\n|\f/); + const startLine = Math.max(1, line - extraLines) - 1; + const endLine = Math.min(line + extraLines, lines.length + 1); + const maxNumLength = Math.max(4, String(endLine).length) + 1; + let cutLeft = 0; + + // column correction according to replaced tab before column + column += (TAB_REPLACEMENT.length - 1) * (lines[line - 1].substr(0, column - 1).match(/\t/g) || []).length; + + if (column > MAX_LINE_LENGTH) { + cutLeft = column - OFFSET_CORRECTION + 3; + column = OFFSET_CORRECTION - 2; + } + + for (let i = startLine; i <= endLine; i++) { + if (i >= 0 && i < lines.length) { + lines[i] = lines[i].replace(/\t/g, TAB_REPLACEMENT); + lines[i] = + (cutLeft > 0 && lines[i].length > cutLeft ? '\u2026' : '') + + lines[i].substr(cutLeft, MAX_LINE_LENGTH - 2) + + (lines[i].length > cutLeft + MAX_LINE_LENGTH - 1 ? '\u2026' : ''); + } + } + + return [ + processLines(startLine, line), + new Array(column + maxNumLength + 2).join('-') + '^', + processLines(line, endLine) + ].filter(Boolean) + .join('\n') + .replace(/^(\s+\d+\s+\|\n)+/, '') + .replace(/\n(\s+\d+\s+\|)+$/, ''); +} + +export function SyntaxError(message, source, offset, line, column, baseLine = 1, baseColumn = 1) { + const error = Object.assign(createCustomError('SyntaxError', message), { + source, + offset, + line, + column, + sourceFragment(extraLines) { + return sourceFragment({ source, line, column, baseLine, baseColumn }, isNaN(extraLines) ? 0 : extraLines); + }, + get formattedMessage() { + return ( + `Parse error: ${message}\n` + + sourceFragment({ source, line, column, baseLine, baseColumn }, 2) + ); + } + }); + + return error; +} diff --git a/vanilla/node_modules/css-tree/lib/parser/create.js b/vanilla/node_modules/css-tree/lib/parser/create.js new file mode 100644 index 0000000..8c08d81 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/parser/create.js @@ -0,0 +1,350 @@ +import { List } from '../utils/List.js'; +import { SyntaxError } from './SyntaxError.js'; +import { + tokenize, + OffsetToLocation, + TokenStream, + tokenNames, + + consumeNumber, + findWhiteSpaceStart, + cmpChar, + cmpStr, + + WhiteSpace, + Comment, + Ident, + Function as FunctionToken, + Url, + Hash, + Percentage, + Number as NumberToken +} from '../tokenizer/index.js'; +import { readSequence } from './sequence.js'; + +const NOOP = () => {}; +const EXCLAMATIONMARK = 0x0021; // U+0021 EXCLAMATION MARK (!) +const NUMBERSIGN = 0x0023; // U+0023 NUMBER SIGN (#) +const SEMICOLON = 0x003B; // U+003B SEMICOLON (;) +const LEFTCURLYBRACKET = 0x007B; // U+007B LEFT CURLY BRACKET ({) +const NULL = 0; + +function createParseContext(name) { + return function() { + return this[name](); + }; +} + +function fetchParseValues(dict) { + const result = Object.create(null); + + for (const name of Object.keys(dict)) { + const item = dict[name]; + const fn = item.parse || item; + + if (fn) { + result[name] = fn; + } + } + + return result; +} + +function processConfig(config) { + const parseConfig = { + context: Object.create(null), + features: Object.assign(Object.create(null), config.features), + scope: Object.assign(Object.create(null), config.scope), + atrule: fetchParseValues(config.atrule), + pseudo: fetchParseValues(config.pseudo), + node: fetchParseValues(config.node) + }; + + for (const [name, context] of Object.entries(config.parseContext)) { + switch (typeof context) { + case 'function': + parseConfig.context[name] = context; + break; + + case 'string': + parseConfig.context[name] = createParseContext(context); + break; + } + } + + return { + config: parseConfig, + ...parseConfig, + ...parseConfig.node + }; +} + +export function createParser(config) { + let source = ''; + let filename = '<unknown>'; + let needPositions = false; + let onParseError = NOOP; + let onParseErrorThrow = false; + + const locationMap = new OffsetToLocation(); + const parser = Object.assign(new TokenStream(), processConfig(config || {}), { + parseAtrulePrelude: true, + parseRulePrelude: true, + parseValue: true, + parseCustomProperty: false, + + readSequence, + + consumeUntilBalanceEnd: () => 0, + consumeUntilLeftCurlyBracket(code) { + return code === LEFTCURLYBRACKET ? 1 : 0; + }, + consumeUntilLeftCurlyBracketOrSemicolon(code) { + return code === LEFTCURLYBRACKET || code === SEMICOLON ? 1 : 0; + }, + consumeUntilExclamationMarkOrSemicolon(code) { + return code === EXCLAMATIONMARK || code === SEMICOLON ? 1 : 0; + }, + consumeUntilSemicolonIncluded(code) { + return code === SEMICOLON ? 2 : 0; + }, + + createList() { + return new List(); + }, + createSingleNodeList(node) { + return new List().appendData(node); + }, + getFirstListNode(list) { + return list && list.first; + }, + getLastListNode(list) { + return list && list.last; + }, + + parseWithFallback(consumer, fallback) { + const startIndex = this.tokenIndex; + + try { + return consumer.call(this); + } catch (e) { + if (onParseErrorThrow) { + throw e; + } + + this.skip(startIndex - this.tokenIndex); + const fallbackNode = fallback.call(this); + + onParseErrorThrow = true; + onParseError(e, fallbackNode); + onParseErrorThrow = false; + + return fallbackNode; + } + }, + + lookupNonWSType(offset) { + let type; + + do { + type = this.lookupType(offset++); + if (type !== WhiteSpace && type !== Comment) { + return type; + } + } while (type !== NULL); + + return NULL; + }, + + charCodeAt(offset) { + return offset >= 0 && offset < source.length ? source.charCodeAt(offset) : 0; + }, + substring(offsetStart, offsetEnd) { + return source.substring(offsetStart, offsetEnd); + }, + substrToCursor(start) { + return this.source.substring(start, this.tokenStart); + }, + + cmpChar(offset, charCode) { + return cmpChar(source, offset, charCode); + }, + cmpStr(offsetStart, offsetEnd, str) { + return cmpStr(source, offsetStart, offsetEnd, str); + }, + + consume(tokenType) { + const start = this.tokenStart; + + this.eat(tokenType); + + return this.substrToCursor(start); + }, + consumeFunctionName() { + const name = source.substring(this.tokenStart, this.tokenEnd - 1); + + this.eat(FunctionToken); + + return name; + }, + consumeNumber(type) { + const number = source.substring(this.tokenStart, consumeNumber(source, this.tokenStart)); + + this.eat(type); + + return number; + }, + + eat(tokenType) { + if (this.tokenType !== tokenType) { + const tokenName = tokenNames[tokenType].slice(0, -6).replace(/-/g, ' ').replace(/^./, m => m.toUpperCase()); + let message = `${/[[\](){}]/.test(tokenName) ? `"${tokenName}"` : tokenName} is expected`; + let offset = this.tokenStart; + + // tweak message and offset + switch (tokenType) { + case Ident: + // when identifier is expected but there is a function or url + if (this.tokenType === FunctionToken || this.tokenType === Url) { + offset = this.tokenEnd - 1; + message = 'Identifier is expected but function found'; + } else { + message = 'Identifier is expected'; + } + break; + + case Hash: + if (this.isDelim(NUMBERSIGN)) { + this.next(); + offset++; + message = 'Name is expected'; + } + break; + + case Percentage: + if (this.tokenType === NumberToken) { + offset = this.tokenEnd; + message = 'Percent sign is expected'; + } + break; + } + + this.error(message, offset); + } + + this.next(); + }, + eatIdent(name) { + if (this.tokenType !== Ident || this.lookupValue(0, name) === false) { + this.error(`Identifier "${name}" is expected`); + } + + this.next(); + }, + eatDelim(code) { + if (!this.isDelim(code)) { + this.error(`Delim "${String.fromCharCode(code)}" is expected`); + } + + this.next(); + }, + + getLocation(start, end) { + if (needPositions) { + return locationMap.getLocationRange( + start, + end, + filename + ); + } + + return null; + }, + getLocationFromList(list) { + if (needPositions) { + const head = this.getFirstListNode(list); + const tail = this.getLastListNode(list); + return locationMap.getLocationRange( + head !== null ? head.loc.start.offset - locationMap.startOffset : this.tokenStart, + tail !== null ? tail.loc.end.offset - locationMap.startOffset : this.tokenStart, + filename + ); + } + + return null; + }, + + error(message, offset) { + const location = typeof offset !== 'undefined' && offset < source.length + ? locationMap.getLocation(offset) + : this.eof + ? locationMap.getLocation(findWhiteSpaceStart(source, source.length - 1)) + : locationMap.getLocation(this.tokenStart); + + throw new SyntaxError( + message || 'Unexpected input', + source, + location.offset, + location.line, + location.column, + locationMap.startLine, + locationMap.startColumn + ); + } + }); + + const parse = function(source_, options) { + source = source_; + options = options || {}; + + parser.setSource(source, tokenize); + locationMap.setSource( + source, + options.offset, + options.line, + options.column + ); + + filename = options.filename || '<unknown>'; + needPositions = Boolean(options.positions); + onParseError = typeof options.onParseError === 'function' ? options.onParseError : NOOP; + onParseErrorThrow = false; + + parser.parseAtrulePrelude = 'parseAtrulePrelude' in options ? Boolean(options.parseAtrulePrelude) : true; + parser.parseRulePrelude = 'parseRulePrelude' in options ? Boolean(options.parseRulePrelude) : true; + parser.parseValue = 'parseValue' in options ? Boolean(options.parseValue) : true; + parser.parseCustomProperty = 'parseCustomProperty' in options ? Boolean(options.parseCustomProperty) : false; + + const { context = 'default', onComment } = options; + + if (context in parser.context === false) { + throw new Error('Unknown context `' + context + '`'); + } + + if (typeof onComment === 'function') { + parser.forEachToken((type, start, end) => { + if (type === Comment) { + const loc = parser.getLocation(start, end); + const value = cmpStr(source, end - 2, end, '*/') + ? source.slice(start + 2, end - 2) + : source.slice(start + 2, end); + + onComment(value, loc); + } + }); + } + + const ast = parser.context[context].call(parser, options); + + if (!parser.eof) { + parser.error(); + } + + return ast; + }; + + return Object.assign(parse, { + SyntaxError, + config: parser.config + }); +}; diff --git a/vanilla/node_modules/css-tree/lib/parser/index.js b/vanilla/node_modules/css-tree/lib/parser/index.js new file mode 100644 index 0000000..7cee62e --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/parser/index.js @@ -0,0 +1,4 @@ +import { createParser } from './create.js'; +import config from '../syntax/config/parser.js'; + +export default createParser(config); diff --git a/vanilla/node_modules/css-tree/lib/parser/parse-selector.js b/vanilla/node_modules/css-tree/lib/parser/parse-selector.js new file mode 100644 index 0000000..05b539a --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/parser/parse-selector.js @@ -0,0 +1,4 @@ +import { createParser } from './create.js'; +import config from '../syntax/config/parser-selector.js'; + +export default createParser(config); diff --git a/vanilla/node_modules/css-tree/lib/parser/sequence.js b/vanilla/node_modules/css-tree/lib/parser/sequence.js new file mode 100644 index 0000000..47d1ccd --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/parser/sequence.js @@ -0,0 +1,43 @@ +import { WhiteSpace, Comment } from '../tokenizer/index.js'; + +export function readSequence(recognizer) { + const children = this.createList(); + let space = false; + const context = { + recognizer + }; + + while (!this.eof) { + switch (this.tokenType) { + case Comment: + this.next(); + continue; + + case WhiteSpace: + space = true; + this.next(); + continue; + } + + let child = recognizer.getNode.call(this, context); + + if (child === undefined) { + break; + } + + if (space) { + if (recognizer.onWhiteSpace) { + recognizer.onWhiteSpace.call(this, child, children, context); + } + space = false; + } + + children.push(child); + } + + if (space && recognizer.onWhiteSpace) { + recognizer.onWhiteSpace.call(this, null, children, context); + } + + return children; +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/atrule/container.js b/vanilla/node_modules/css-tree/lib/syntax/atrule/container.js new file mode 100644 index 0000000..aa550de --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/atrule/container.js @@ -0,0 +1,28 @@ +import { Ident } from '../../tokenizer/index.js'; + +// https://drafts.csswg.org/css-contain-3/#container-rule +// The keywords `none`, `and`, `not`, and `or` are excluded from the <custom-ident> above. +const nonContainerNameKeywords = new Set(['none', 'and', 'not', 'or']); + +export default { + parse: { + prelude() { + const children = this.createList(); + + if (this.tokenType === Ident) { + const name = this.substring(this.tokenStart, this.tokenEnd); + + if (!nonContainerNameKeywords.has(name.toLowerCase())) { + children.push(this.Identifier()); + } + } + + children.push(this.Condition('container')); + + return children; + }, + block(nested = false) { + return this.Block(nested); + } + } +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/atrule/font-face.js b/vanilla/node_modules/css-tree/lib/syntax/atrule/font-face.js new file mode 100644 index 0000000..48a0570 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/atrule/font-face.js @@ -0,0 +1,8 @@ +export default { + parse: { + prelude: null, + block() { + return this.Block(true); + } + } +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/atrule/import.js b/vanilla/node_modules/css-tree/lib/syntax/atrule/import.js new file mode 100644 index 0000000..4e5a637 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/atrule/import.js @@ -0,0 +1,104 @@ +import { + String as StringToken, + Ident, + Url, + Function as FunctionToken, + LeftParenthesis, + RightParenthesis +} from '../../tokenizer/index.js'; + +function parseWithFallback(parse, fallback) { + return this.parseWithFallback( + () => { + try { + return parse.call(this); + } finally { + this.skipSC(); + if (this.lookupNonWSType(0) !== RightParenthesis) { + this.error(); + } + } + }, + fallback || (() => this.Raw(null, true)) + ); +} + +const parseFunctions = { + layer() { + this.skipSC(); + + const children = this.createList(); + const node = parseWithFallback.call(this, this.Layer); + + if (node.type !== 'Raw' || node.value !== '') { + children.push(node); + } + + return children; + }, + supports() { + this.skipSC(); + + const children = this.createList(); + const node = parseWithFallback.call( + this, + this.Declaration, + () => parseWithFallback.call(this, () => this.Condition('supports')) + ); + + if (node.type !== 'Raw' || node.value !== '') { + children.push(node); + } + + return children; + } +}; + +export default { + parse: { + prelude() { + const children = this.createList(); + + switch (this.tokenType) { + case StringToken: + children.push(this.String()); + break; + + case Url: + case FunctionToken: + children.push(this.Url()); + break; + + default: + this.error('String or url() is expected'); + } + + this.skipSC(); + + if (this.tokenType === Ident && + this.cmpStr(this.tokenStart, this.tokenEnd, 'layer')) { + children.push(this.Identifier()); + } else if ( + this.tokenType === FunctionToken && + this.cmpStr(this.tokenStart, this.tokenEnd, 'layer(') + ) { + children.push(this.Function(null, parseFunctions)); + } + + this.skipSC(); + + if (this.tokenType === FunctionToken && + this.cmpStr(this.tokenStart, this.tokenEnd, 'supports(')) { + children.push(this.Function(null, parseFunctions)); + } + + if (this.lookupNonWSType(0) === Ident || + this.lookupNonWSType(0) === LeftParenthesis) { + children.push(this.MediaQueryList()); + } + + return children; + }, + block: null + } +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/atrule/index.js b/vanilla/node_modules/css-tree/lib/syntax/atrule/index.js new file mode 100644 index 0000000..46ac7f8 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/atrule/index.js @@ -0,0 +1,23 @@ +import container from './container.js'; +import fontFace from './font-face.js'; +import importAtrule from './import.js'; +import layer from './layer.js'; +import media from './media.js'; +import nest from './nest.js'; +import page from './page.js'; +import scope from './scope.js'; +import startingStyle from './starting-style.js'; +import supports from './supports.js'; + +export default { + container, + 'font-face': fontFace, + import: importAtrule, + layer, + media, + nest, + page, + scope, + 'starting-style': startingStyle, + supports +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/atrule/layer.js b/vanilla/node_modules/css-tree/lib/syntax/atrule/layer.js new file mode 100644 index 0000000..232eb29 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/atrule/layer.js @@ -0,0 +1,12 @@ +export default { + parse: { + prelude() { + return this.createSingleNodeList( + this.LayerList() + ); + }, + block() { + return this.Block(false); + } + } +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/atrule/media.js b/vanilla/node_modules/css-tree/lib/syntax/atrule/media.js new file mode 100644 index 0000000..24d4608 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/atrule/media.js @@ -0,0 +1,12 @@ +export default { + parse: { + prelude() { + return this.createSingleNodeList( + this.MediaQueryList() + ); + }, + block(nested = false) { + return this.Block(nested); + } + } +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/atrule/nest.js b/vanilla/node_modules/css-tree/lib/syntax/atrule/nest.js new file mode 100644 index 0000000..99fc15e --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/atrule/nest.js @@ -0,0 +1,12 @@ +export default { + parse: { + prelude() { + return this.createSingleNodeList( + this.SelectorList() + ); + }, + block() { + return this.Block(true); + } + } +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/atrule/page.js b/vanilla/node_modules/css-tree/lib/syntax/atrule/page.js new file mode 100644 index 0000000..99fc15e --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/atrule/page.js @@ -0,0 +1,12 @@ +export default { + parse: { + prelude() { + return this.createSingleNodeList( + this.SelectorList() + ); + }, + block() { + return this.Block(true); + } + } +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/atrule/scope.js b/vanilla/node_modules/css-tree/lib/syntax/atrule/scope.js new file mode 100644 index 0000000..7185ca1 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/atrule/scope.js @@ -0,0 +1,12 @@ +export default { + parse: { + prelude() { + return this.createSingleNodeList( + this.Scope() + ); + }, + block(nested = false) { + return this.Block(nested); + } + } +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/atrule/starting-style.js b/vanilla/node_modules/css-tree/lib/syntax/atrule/starting-style.js new file mode 100644 index 0000000..b964704 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/atrule/starting-style.js @@ -0,0 +1,8 @@ +export default { + parse: { + prelude: null, + block(nested = false) { + return this.Block(nested); + } + } +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/atrule/supports.js b/vanilla/node_modules/css-tree/lib/syntax/atrule/supports.js new file mode 100644 index 0000000..bdfffce --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/atrule/supports.js @@ -0,0 +1,12 @@ +export default { + parse: { + prelude() { + return this.createSingleNodeList( + this.Condition('supports') + ); + }, + block(nested = false) { + return this.Block(nested); + } + } +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/config/generator.js b/vanilla/node_modules/css-tree/lib/syntax/config/generator.js new file mode 100644 index 0000000..82e874c --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/config/generator.js @@ -0,0 +1,5 @@ +import * as node from '../node/index-generate.js'; + +export default { + node +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/config/lexer.js b/vanilla/node_modules/css-tree/lib/syntax/config/lexer.js new file mode 100644 index 0000000..b479ac5 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/config/lexer.js @@ -0,0 +1,10 @@ +import { cssWideKeywords } from '../../lexer/generic-const.js'; +import definitions from '../../data.js'; +import * as node from '../node/index.js'; + +export default { + generic: true, + cssWideKeywords, + ...definitions, + node +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/config/mix.js b/vanilla/node_modules/css-tree/lib/syntax/config/mix.js new file mode 100644 index 0000000..f722ceb --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/config/mix.js @@ -0,0 +1,123 @@ +function appendOrSet(a, b) { + if (typeof b === 'string' && /^\s*\|/.test(b)) { + return typeof a === 'string' + ? a + b + : b.replace(/^\s*\|\s*/, ''); + } + + return b || null; +} + +function sliceProps(obj, props) { + const result = Object.create(null); + + for (const [key, value] of Object.entries(obj)) { + if (value) { + result[key] = {}; + for (const prop of Object.keys(value)) { + if (props.includes(prop)) { + result[key][prop] = value[prop]; + } + } + } + } + + return result; +} + +export default function mix(dest, src) { + const result = { ...dest }; + + for (const [prop, value] of Object.entries(src)) { + switch (prop) { + case 'generic': + result[prop] = Boolean(value); + break; + + case 'cssWideKeywords': + result[prop] = dest[prop] + ? [...dest[prop], ...value] + : value || []; + break; + + case 'units': + result[prop] = { ...dest[prop] }; + for (const [name, patch] of Object.entries(value)) { + result[prop][name] = Array.isArray(patch) ? patch : []; + } + break; + + case 'atrules': + result[prop] = { ...dest[prop] }; + + for (const [name, atrule] of Object.entries(value)) { + const exists = result[prop][name] || {}; + const current = result[prop][name] = { + prelude: exists.prelude || null, + descriptors: { + ...exists.descriptors + } + }; + + if (!atrule) { + continue; + } + + current.prelude = atrule.prelude + ? appendOrSet(current.prelude, atrule.prelude) + : current.prelude || null; + + for (const [descriptorName, descriptorValue] of Object.entries(atrule.descriptors || {})) { + current.descriptors[descriptorName] = descriptorValue + ? appendOrSet(current.descriptors[descriptorName], descriptorValue) + : null; + } + + if (!Object.keys(current.descriptors).length) { + current.descriptors = null; + } + } + break; + + case 'types': + case 'properties': + result[prop] = { ...dest[prop] }; + for (const [name, syntax] of Object.entries(value)) { + result[prop][name] = appendOrSet(result[prop][name], syntax); + } + break; + + case 'scope': + case 'features': + result[prop] = { ...dest[prop] }; + for (const [name, props] of Object.entries(value)) { + result[prop][name] = { ...result[prop][name], ...props }; + } + break; + + case 'parseContext': + result[prop] = { + ...dest[prop], + ...value + }; + break; + + case 'atrule': + case 'pseudo': + result[prop] = { + ...dest[prop], + ...sliceProps(value, ['parse']) + }; + break; + + case 'node': + result[prop] = { + ...dest[prop], + ...sliceProps(value, ['name', 'structure', 'parse', 'generate', 'walkContext']) + }; + break; + } + } + + return result; +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/config/parser-selector.js b/vanilla/node_modules/css-tree/lib/syntax/config/parser-selector.js new file mode 100644 index 0000000..c9e0ee2 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/config/parser-selector.js @@ -0,0 +1,15 @@ +import { Selector } from '../scope/index.js'; +import pseudo from '../pseudo/index.js'; +import * as node from '../node/index-parse-selector.js'; + +export default { + parseContext: { + default: 'SelectorList', + selectorList: 'SelectorList', + selector: 'Selector' + }, + scope: { Selector }, + atrule: {}, + pseudo, + node +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/config/parser.js b/vanilla/node_modules/css-tree/lib/syntax/config/parser.js new file mode 100644 index 0000000..0b455aa --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/config/parser.js @@ -0,0 +1,45 @@ +import * as scope from '../scope/index.js'; +import atrule from '../atrule/index.js'; +import pseudo from '../pseudo/index.js'; +import * as node from '../node/index-parse.js'; + +export default { + parseContext: { + default: 'StyleSheet', + stylesheet: 'StyleSheet', + atrule: 'Atrule', + atrulePrelude(options) { + return this.AtrulePrelude(options.atrule ? String(options.atrule) : null); + }, + mediaQueryList: 'MediaQueryList', + mediaQuery: 'MediaQuery', + condition(options) { + return this.Condition(options.kind); + }, + rule: 'Rule', + selectorList: 'SelectorList', + selector: 'Selector', + block() { + return this.Block(true); + }, + declarationList: 'DeclarationList', + declaration: 'Declaration', + value: 'Value' + }, + features: { + supports: { + selector() { + return this.Selector(); + } + }, + container: { + style() { + return this.Declaration(); + } + } + }, + scope, + atrule, + pseudo, + node +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/config/walker.js b/vanilla/node_modules/css-tree/lib/syntax/config/walker.js new file mode 100644 index 0000000..215d024 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/config/walker.js @@ -0,0 +1,5 @@ +import * as node from '../node/index.js'; + +export default { + node +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/create.js b/vanilla/node_modules/css-tree/lib/syntax/create.js new file mode 100644 index 0000000..9bb32c5 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/create.js @@ -0,0 +1,55 @@ +import { tokenize } from '../tokenizer/index.js'; +import { createParser } from '../parser/create.js'; +import { createGenerator } from '../generator/create.js'; +import { createConvertor } from '../convertor/create.js'; +import { createWalker } from '../walker/create.js'; +import { Lexer } from '../lexer/Lexer.js'; +import mix from './config/mix.js'; + +function createSyntax(config) { + const parse = createParser(config); + const walk = createWalker(config); + const generate = createGenerator(config); + const { fromPlainObject, toPlainObject } = createConvertor(walk); + + const syntax = { + lexer: null, + createLexer: config => new Lexer(config, syntax, syntax.lexer.structure), + + tokenize, + parse, + generate, + + walk, + find: walk.find, + findLast: walk.findLast, + findAll: walk.findAll, + + fromPlainObject, + toPlainObject, + + fork(extension) { + const base = mix({}, config); // copy of config + + return createSyntax( + typeof extension === 'function' + ? extension(base) // TODO: remove Object.assign as second parameter + : mix(base, extension) + ); + } + }; + + syntax.lexer = new Lexer({ + generic: config.generic, + cssWideKeywords: config.cssWideKeywords, + units: config.units, + types: config.types, + atrules: config.atrules, + properties: config.properties, + node: config.node + }, syntax); + + return syntax; +}; + +export default config => createSyntax(mix({}, config)); diff --git a/vanilla/node_modules/css-tree/lib/syntax/function/expression.js b/vanilla/node_modules/css-tree/lib/syntax/function/expression.js new file mode 100644 index 0000000..040f826 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/function/expression.js @@ -0,0 +1,7 @@ +// legacy IE function +// expression( <any-value> ) +export default function() { + return this.createSingleNodeList( + this.Raw(null, false) + ); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/function/var.js b/vanilla/node_modules/css-tree/lib/syntax/function/var.js new file mode 100644 index 0000000..010dc0a --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/function/var.js @@ -0,0 +1,39 @@ +import { Comma, WhiteSpace } from '../../tokenizer/index.js'; + +// var( <ident> , <value>? ) +export default function() { + const children = this.createList(); + + this.skipSC(); + + // NOTE: Don't check more than a first argument is an ident, rest checks are for lexer + children.push(this.Identifier()); + + this.skipSC(); + + if (this.tokenType === Comma) { + children.push(this.Operator()); + + const startIndex = this.tokenIndex; + const value = this.parseCustomProperty + ? this.Value(null) + : this.Raw(this.consumeUntilExclamationMarkOrSemicolon, false); + + if (value.type === 'Value' && value.children.isEmpty) { + for (let offset = startIndex - this.tokenIndex; offset <= 0; offset++) { + if (this.lookupType(offset) === WhiteSpace) { + value.children.appendData({ + type: 'WhiteSpace', + loc: null, + value: ' ' + }); + break; + } + } + } + + children.push(value); + } + + return children; +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/index.js b/vanilla/node_modules/css-tree/lib/syntax/index.js new file mode 100644 index 0000000..c8c9152 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/index.js @@ -0,0 +1,10 @@ +import createSyntax from './create.js'; +import lexerConfig from './config/lexer.js'; +import parserConfig from './config/parser.js'; +import walkerConfig from './config/walker.js'; + +export default createSyntax({ + ...lexerConfig, + ...parserConfig, + ...walkerConfig +}); diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/AnPlusB.js b/vanilla/node_modules/css-tree/lib/syntax/node/AnPlusB.js new file mode 100644 index 0000000..05c7e44 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/AnPlusB.js @@ -0,0 +1,292 @@ +import { + isDigit, + WhiteSpace, + Comment, + Ident, + Number, + Dimension +} from '../../tokenizer/index.js'; + +const PLUSSIGN = 0x002B; // U+002B PLUS SIGN (+) +const HYPHENMINUS = 0x002D; // U+002D HYPHEN-MINUS (-) +const N = 0x006E; // U+006E LATIN SMALL LETTER N (n) +const DISALLOW_SIGN = true; +const ALLOW_SIGN = false; + +function checkInteger(offset, disallowSign) { + let pos = this.tokenStart + offset; + const code = this.charCodeAt(pos); + + if (code === PLUSSIGN || code === HYPHENMINUS) { + if (disallowSign) { + this.error('Number sign is not allowed'); + } + pos++; + } + + for (; pos < this.tokenEnd; pos++) { + if (!isDigit(this.charCodeAt(pos))) { + this.error('Integer is expected', pos); + } + } +} + +function checkTokenIsInteger(disallowSign) { + return checkInteger.call(this, 0, disallowSign); +} + +function expectCharCode(offset, code) { + if (!this.cmpChar(this.tokenStart + offset, code)) { + let msg = ''; + + switch (code) { + case N: + msg = 'N is expected'; + break; + case HYPHENMINUS: + msg = 'HyphenMinus is expected'; + break; + } + + this.error(msg, this.tokenStart + offset); + } +} + +// ... <signed-integer> +// ... ['+' | '-'] <signless-integer> +function consumeB() { + let offset = 0; + let sign = 0; + let type = this.tokenType; + + while (type === WhiteSpace || type === Comment) { + type = this.lookupType(++offset); + } + + if (type !== Number) { + if (this.isDelim(PLUSSIGN, offset) || + this.isDelim(HYPHENMINUS, offset)) { + sign = this.isDelim(PLUSSIGN, offset) ? PLUSSIGN : HYPHENMINUS; + + do { + type = this.lookupType(++offset); + } while (type === WhiteSpace || type === Comment); + + if (type !== Number) { + this.skip(offset); + checkTokenIsInteger.call(this, DISALLOW_SIGN); + } + } else { + return null; + } + } + + if (offset > 0) { + this.skip(offset); + } + + if (sign === 0) { + type = this.charCodeAt(this.tokenStart); + if (type !== PLUSSIGN && type !== HYPHENMINUS) { + this.error('Number sign is expected'); + } + } + + checkTokenIsInteger.call(this, sign !== 0); + return sign === HYPHENMINUS ? '-' + this.consume(Number) : this.consume(Number); +} + +// An+B microsyntax https://www.w3.org/TR/css-syntax-3/#anb +export const name = 'AnPlusB'; +export const structure = { + a: [String, null], + b: [String, null] +}; + +export function parse() { + /* eslint-disable brace-style*/ + const start = this.tokenStart; + let a = null; + let b = null; + + // <integer> + if (this.tokenType === Number) { + checkTokenIsInteger.call(this, ALLOW_SIGN); + b = this.consume(Number); + } + + // -n + // -n <signed-integer> + // -n ['+' | '-'] <signless-integer> + // -n- <signless-integer> + // <dashndashdigit-ident> + else if (this.tokenType === Ident && this.cmpChar(this.tokenStart, HYPHENMINUS)) { + a = '-1'; + + expectCharCode.call(this, 1, N); + + switch (this.tokenEnd - this.tokenStart) { + // -n + // -n <signed-integer> + // -n ['+' | '-'] <signless-integer> + case 2: + this.next(); + b = consumeB.call(this); + break; + + // -n- <signless-integer> + case 3: + expectCharCode.call(this, 2, HYPHENMINUS); + + this.next(); + this.skipSC(); + + checkTokenIsInteger.call(this, DISALLOW_SIGN); + + b = '-' + this.consume(Number); + break; + + // <dashndashdigit-ident> + default: + expectCharCode.call(this, 2, HYPHENMINUS); + checkInteger.call(this, 3, DISALLOW_SIGN); + this.next(); + + b = this.substrToCursor(start + 2); + } + } + + // '+'? n + // '+'? n <signed-integer> + // '+'? n ['+' | '-'] <signless-integer> + // '+'? n- <signless-integer> + // '+'? <ndashdigit-ident> + else if (this.tokenType === Ident || (this.isDelim(PLUSSIGN) && this.lookupType(1) === Ident)) { + let sign = 0; + a = '1'; + + // just ignore a plus + if (this.isDelim(PLUSSIGN)) { + sign = 1; + this.next(); + } + + expectCharCode.call(this, 0, N); + + switch (this.tokenEnd - this.tokenStart) { + // '+'? n + // '+'? n <signed-integer> + // '+'? n ['+' | '-'] <signless-integer> + case 1: + this.next(); + b = consumeB.call(this); + break; + + // '+'? n- <signless-integer> + case 2: + expectCharCode.call(this, 1, HYPHENMINUS); + + this.next(); + this.skipSC(); + + checkTokenIsInteger.call(this, DISALLOW_SIGN); + + b = '-' + this.consume(Number); + break; + + // '+'? <ndashdigit-ident> + default: + expectCharCode.call(this, 1, HYPHENMINUS); + checkInteger.call(this, 2, DISALLOW_SIGN); + this.next(); + + b = this.substrToCursor(start + sign + 1); + } + } + + // <ndashdigit-dimension> + // <ndash-dimension> <signless-integer> + // <n-dimension> + // <n-dimension> <signed-integer> + // <n-dimension> ['+' | '-'] <signless-integer> + else if (this.tokenType === Dimension) { + const code = this.charCodeAt(this.tokenStart); + const sign = code === PLUSSIGN || code === HYPHENMINUS; + let i = this.tokenStart + sign; + + for (; i < this.tokenEnd; i++) { + if (!isDigit(this.charCodeAt(i))) { + break; + } + } + + if (i === this.tokenStart + sign) { + this.error('Integer is expected', this.tokenStart + sign); + } + + expectCharCode.call(this, i - this.tokenStart, N); + a = this.substring(start, i); + + // <n-dimension> + // <n-dimension> <signed-integer> + // <n-dimension> ['+' | '-'] <signless-integer> + if (i + 1 === this.tokenEnd) { + this.next(); + b = consumeB.call(this); + } else { + expectCharCode.call(this, i - this.tokenStart + 1, HYPHENMINUS); + + // <ndash-dimension> <signless-integer> + if (i + 2 === this.tokenEnd) { + this.next(); + this.skipSC(); + checkTokenIsInteger.call(this, DISALLOW_SIGN); + b = '-' + this.consume(Number); + } + // <ndashdigit-dimension> + else { + checkInteger.call(this, i - this.tokenStart + 2, DISALLOW_SIGN); + this.next(); + b = this.substrToCursor(i + 1); + } + } + } else { + this.error(); + } + + if (a !== null && a.charCodeAt(0) === PLUSSIGN) { + a = a.substr(1); + } + + if (b !== null && b.charCodeAt(0) === PLUSSIGN) { + b = b.substr(1); + } + + return { + type: 'AnPlusB', + loc: this.getLocation(start, this.tokenStart), + a, + b + }; +} + +export function generate(node) { + if (node.a) { + const a = + node.a === '+1' && 'n' || + node.a === '1' && 'n' || + node.a === '-1' && '-n' || + node.a + 'n'; + + if (node.b) { + const b = node.b[0] === '-' || node.b[0] === '+' + ? node.b + : '+' + node.b; + this.tokenize(a + b); + } else { + this.tokenize(a); + } + } else { + this.tokenize(node.b); + } +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Atrule.js b/vanilla/node_modules/css-tree/lib/syntax/node/Atrule.js new file mode 100644 index 0000000..99d1284 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Atrule.js @@ -0,0 +1,100 @@ +import { + AtKeyword, + Semicolon, + LeftCurlyBracket, + RightCurlyBracket +} from '../../tokenizer/index.js'; + +function consumeRaw() { + return this.Raw(this.consumeUntilLeftCurlyBracketOrSemicolon, true); +} + +function isDeclarationBlockAtrule() { + for (let offset = 1, type; type = this.lookupType(offset); offset++) { + if (type === RightCurlyBracket) { + return true; + } + + if (type === LeftCurlyBracket || + type === AtKeyword) { + return false; + } + } + + return false; +} + + +export const name = 'Atrule'; +export const walkContext = 'atrule'; +export const structure = { + name: String, + prelude: ['AtrulePrelude', 'Raw', null], + block: ['Block', null] +}; + +export function parse(isDeclaration = false) { + const start = this.tokenStart; + let name; + let nameLowerCase; + let prelude = null; + let block = null; + + this.eat(AtKeyword); + + name = this.substrToCursor(start + 1); + nameLowerCase = name.toLowerCase(); + this.skipSC(); + + // parse prelude + if (this.eof === false && + this.tokenType !== LeftCurlyBracket && + this.tokenType !== Semicolon) { + if (this.parseAtrulePrelude) { + prelude = this.parseWithFallback(this.AtrulePrelude.bind(this, name, isDeclaration), consumeRaw); + } else { + prelude = consumeRaw.call(this, this.tokenIndex); + } + + this.skipSC(); + } + + switch (this.tokenType) { + case Semicolon: + this.next(); + break; + + case LeftCurlyBracket: + if (hasOwnProperty.call(this.atrule, nameLowerCase) && + typeof this.atrule[nameLowerCase].block === 'function') { + block = this.atrule[nameLowerCase].block.call(this, isDeclaration); + } else { + // TODO: should consume block content as Raw? + block = this.Block(isDeclarationBlockAtrule.call(this)); + } + + break; + } + + return { + type: 'Atrule', + loc: this.getLocation(start, this.tokenStart), + name, + prelude, + block + }; +} + +export function generate(node) { + this.token(AtKeyword, '@' + node.name); + + if (node.prelude !== null) { + this.node(node.prelude); + } + + if (node.block) { + this.node(node.block); + } else { + this.token(Semicolon, ';'); + } +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/AtrulePrelude.js b/vanilla/node_modules/css-tree/lib/syntax/node/AtrulePrelude.js new file mode 100644 index 0000000..5b5645a --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/AtrulePrelude.js @@ -0,0 +1,47 @@ +import { + Semicolon, + LeftCurlyBracket +} from '../../tokenizer/index.js'; + +export const name = 'AtrulePrelude'; +export const walkContext = 'atrulePrelude'; +export const structure = { + children: [[]] +}; + +export function parse(name) { + let children = null; + + if (name !== null) { + name = name.toLowerCase(); + } + + this.skipSC(); + + if (hasOwnProperty.call(this.atrule, name) && + typeof this.atrule[name].prelude === 'function') { + // custom consumer + children = this.atrule[name].prelude.call(this); + } else { + // default consumer + children = this.readSequence(this.scope.AtrulePrelude); + } + + this.skipSC(); + + if (this.eof !== true && + this.tokenType !== LeftCurlyBracket && + this.tokenType !== Semicolon) { + this.error('Semicolon or block is expected'); + } + + return { + type: 'AtrulePrelude', + loc: this.getLocationFromList(children), + children + }; +} + +export function generate(node) { + this.children(node); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/AttributeSelector.js b/vanilla/node_modules/css-tree/lib/syntax/node/AttributeSelector.js new file mode 100644 index 0000000..8578dad --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/AttributeSelector.js @@ -0,0 +1,147 @@ +import { + Ident, + String as StringToken, + Delim, + LeftSquareBracket, + RightSquareBracket +} from '../../tokenizer/index.js'; + +const DOLLARSIGN = 0x0024; // U+0024 DOLLAR SIGN ($) +const ASTERISK = 0x002A; // U+002A ASTERISK (*) +const EQUALSSIGN = 0x003D; // U+003D EQUALS SIGN (=) +const CIRCUMFLEXACCENT = 0x005E; // U+005E (^) +const VERTICALLINE = 0x007C; // U+007C VERTICAL LINE (|) +const TILDE = 0x007E; // U+007E TILDE (~) + +function getAttributeName() { + if (this.eof) { + this.error('Unexpected end of input'); + } + + const start = this.tokenStart; + let expectIdent = false; + + if (this.isDelim(ASTERISK)) { + expectIdent = true; + this.next(); + } else if (!this.isDelim(VERTICALLINE)) { + this.eat(Ident); + } + + if (this.isDelim(VERTICALLINE)) { + if (this.charCodeAt(this.tokenStart + 1) !== EQUALSSIGN) { + this.next(); + this.eat(Ident); + } else if (expectIdent) { + this.error('Identifier is expected', this.tokenEnd); + } + } else if (expectIdent) { + this.error('Vertical line is expected'); + } + + return { + type: 'Identifier', + loc: this.getLocation(start, this.tokenStart), + name: this.substrToCursor(start) + }; +} + +function getOperator() { + const start = this.tokenStart; + const code = this.charCodeAt(start); + + if (code !== EQUALSSIGN && // = + code !== TILDE && // ~= + code !== CIRCUMFLEXACCENT && // ^= + code !== DOLLARSIGN && // $= + code !== ASTERISK && // *= + code !== VERTICALLINE // |= + ) { + this.error('Attribute selector (=, ~=, ^=, $=, *=, |=) is expected'); + } + + this.next(); + + if (code !== EQUALSSIGN) { + if (!this.isDelim(EQUALSSIGN)) { + this.error('Equal sign is expected'); + } + + this.next(); + } + + return this.substrToCursor(start); +} + +// '[' <wq-name> ']' +// '[' <wq-name> <attr-matcher> [ <string-token> | <ident-token> ] <attr-modifier>? ']' +export const name = 'AttributeSelector'; +export const structure = { + name: 'Identifier', + matcher: [String, null], + value: ['String', 'Identifier', null], + flags: [String, null] +}; + +export function parse() { + const start = this.tokenStart; + let name; + let matcher = null; + let value = null; + let flags = null; + + this.eat(LeftSquareBracket); + this.skipSC(); + + name = getAttributeName.call(this); + this.skipSC(); + + if (this.tokenType !== RightSquareBracket) { + // avoid case `[name i]` + if (this.tokenType !== Ident) { + matcher = getOperator.call(this); + + this.skipSC(); + + value = this.tokenType === StringToken + ? this.String() + : this.Identifier(); + + this.skipSC(); + } + + // attribute flags + if (this.tokenType === Ident) { + flags = this.consume(Ident); + + this.skipSC(); + } + } + + this.eat(RightSquareBracket); + + return { + type: 'AttributeSelector', + loc: this.getLocation(start, this.tokenStart), + name, + matcher, + value, + flags + }; +} + +export function generate(node) { + this.token(Delim, '['); + this.node(node.name); + + if (node.matcher !== null) { + this.tokenize(node.matcher); + this.node(node.value); + } + + if (node.flags !== null) { + this.token(Ident, node.flags); + } + + this.token(Delim, ']'); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Block.js b/vanilla/node_modules/css-tree/lib/syntax/node/Block.js new file mode 100644 index 0000000..10bf6fc --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Block.js @@ -0,0 +1,95 @@ +import { + WhiteSpace, + Comment, + Semicolon, + AtKeyword, + LeftCurlyBracket, + RightCurlyBracket +} from '../../tokenizer/index.js'; + +const AMPERSAND = 0x0026; // U+0026 AMPERSAND (&) + +function consumeRaw() { + return this.Raw(null, true); +} +function consumeRule() { + return this.parseWithFallback(this.Rule, consumeRaw); +} +function consumeRawDeclaration() { + return this.Raw(this.consumeUntilSemicolonIncluded, true); +} +function consumeDeclaration() { + if (this.tokenType === Semicolon) { + return consumeRawDeclaration.call(this, this.tokenIndex); + } + + const node = this.parseWithFallback(this.Declaration, consumeRawDeclaration); + + if (this.tokenType === Semicolon) { + this.next(); + } + + return node; +} + +export const name = 'Block'; +export const walkContext = 'block'; +export const structure = { + children: [[ + 'Atrule', + 'Rule', + 'Declaration' + ]] +}; + +export function parse(isStyleBlock) { + const consumer = isStyleBlock ? consumeDeclaration : consumeRule; + const start = this.tokenStart; + let children = this.createList(); + + this.eat(LeftCurlyBracket); + + scan: + while (!this.eof) { + switch (this.tokenType) { + case RightCurlyBracket: + break scan; + + case WhiteSpace: + case Comment: + this.next(); + break; + + case AtKeyword: + children.push(this.parseWithFallback(this.Atrule.bind(this, isStyleBlock), consumeRaw)); + break; + + default: + if (isStyleBlock && this.isDelim(AMPERSAND)) { + children.push(consumeRule.call(this)); + } else { + children.push(consumer.call(this)); + } + } + } + + if (!this.eof) { + this.eat(RightCurlyBracket); + } + + return { + type: 'Block', + loc: this.getLocation(start, this.tokenStart), + children + }; +} + +export function generate(node) { + this.token(LeftCurlyBracket, '{'); + this.children(node, prev => { + if (prev.type === 'Declaration') { + this.token(Semicolon, ';'); + } + }); + this.token(RightCurlyBracket, '}'); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Brackets.js b/vanilla/node_modules/css-tree/lib/syntax/node/Brackets.js new file mode 100644 index 0000000..1d97a4c --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Brackets.js @@ -0,0 +1,35 @@ +import { + Delim, + LeftSquareBracket, + RightSquareBracket +} from '../../tokenizer/index.js'; + +export const name = 'Brackets'; +export const structure = { + children: [[]] +}; + +export function parse(readSequence, recognizer) { + const start = this.tokenStart; + let children = null; + + this.eat(LeftSquareBracket); + + children = readSequence.call(this, recognizer); + + if (!this.eof) { + this.eat(RightSquareBracket); + } + + return { + type: 'Brackets', + loc: this.getLocation(start, this.tokenStart), + children + }; +} + +export function generate(node) { + this.token(Delim, '['); + this.children(node); + this.token(Delim, ']'); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/CDC.js b/vanilla/node_modules/css-tree/lib/syntax/node/CDC.js new file mode 100644 index 0000000..efed4a6 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/CDC.js @@ -0,0 +1,19 @@ +import { CDC } from '../../tokenizer/index.js'; + +export const name = 'CDC'; +export const structure = []; + +export function parse() { + const start = this.tokenStart; + + this.eat(CDC); // --> + + return { + type: 'CDC', + loc: this.getLocation(start, this.tokenStart) + }; +} + +export function generate() { + this.token(CDC, '-->'); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/CDO.js b/vanilla/node_modules/css-tree/lib/syntax/node/CDO.js new file mode 100644 index 0000000..3a9de89 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/CDO.js @@ -0,0 +1,19 @@ +import { CDO } from '../../tokenizer/index.js'; + +export const name = 'CDO'; +export const structure = []; + +export function parse() { + const start = this.tokenStart; + + this.eat(CDO); // <!-- + + return { + type: 'CDO', + loc: this.getLocation(start, this.tokenStart) + }; +} + +export function generate() { + this.token(CDO, '<!--'); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/ClassSelector.js b/vanilla/node_modules/css-tree/lib/syntax/node/ClassSelector.js new file mode 100644 index 0000000..febb0d9 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/ClassSelector.js @@ -0,0 +1,24 @@ +import { Delim, Ident } from '../../tokenizer/index.js'; + +const FULLSTOP = 0x002E; // U+002E FULL STOP (.) + +// '.' ident +export const name = 'ClassSelector'; +export const structure = { + name: String +}; + +export function parse() { + this.eatDelim(FULLSTOP); + + return { + type: 'ClassSelector', + loc: this.getLocation(this.tokenStart - 1, this.tokenEnd), + name: this.consume(Ident) + }; +} + +export function generate(node) { + this.token(Delim, '.'); + this.token(Ident, node.name); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Combinator.js b/vanilla/node_modules/css-tree/lib/syntax/node/Combinator.js new file mode 100644 index 0000000..2e5bb1f --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Combinator.js @@ -0,0 +1,54 @@ +import { WhiteSpace, Delim } from '../../tokenizer/index.js'; + +const PLUSSIGN = 0x002B; // U+002B PLUS SIGN (+) +const SOLIDUS = 0x002F; // U+002F SOLIDUS (/) +const GREATERTHANSIGN = 0x003E; // U+003E GREATER-THAN SIGN (>) +const TILDE = 0x007E; // U+007E TILDE (~) + +export const name = 'Combinator'; +export const structure = { + name: String +}; + +// + | > | ~ | /deep/ +export function parse() { + const start = this.tokenStart; + let name; + + switch (this.tokenType) { + case WhiteSpace: + name = ' '; + break; + + case Delim: + switch (this.charCodeAt(this.tokenStart)) { + case GREATERTHANSIGN: + case PLUSSIGN: + case TILDE: + this.next(); + break; + + case SOLIDUS: + this.next(); + this.eatIdent('deep'); + this.eatDelim(SOLIDUS); + break; + + default: + this.error('Combinator is expected'); + } + + name = this.substrToCursor(start); + break; + } + + return { + type: 'Combinator', + loc: this.getLocation(start, this.tokenStart), + name + }; +} + +export function generate(node) { + this.tokenize(node.name); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Comment.js b/vanilla/node_modules/css-tree/lib/syntax/node/Comment.js new file mode 100644 index 0000000..8a9d291 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Comment.js @@ -0,0 +1,33 @@ +import { Comment } from '../../tokenizer/index.js'; + +const ASTERISK = 0x002A; // U+002A ASTERISK (*) +const SOLIDUS = 0x002F; // U+002F SOLIDUS (/) + + +export const name = 'Comment'; +export const structure = { + value: String +}; + +export function parse() { + const start = this.tokenStart; + let end = this.tokenEnd; + + this.eat(Comment); + + if ((end - start + 2) >= 2 && + this.charCodeAt(end - 2) === ASTERISK && + this.charCodeAt(end - 1) === SOLIDUS) { + end -= 2; + } + + return { + type: 'Comment', + loc: this.getLocation(start, this.tokenStart), + value: this.substring(start + 2, end) + }; +} + +export function generate(node) { + this.token(Comment, '/*' + node.value + '*/'); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Condition.js b/vanilla/node_modules/css-tree/lib/syntax/node/Condition.js new file mode 100644 index 0000000..e4b1e0d --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Condition.js @@ -0,0 +1,123 @@ +import { + WhiteSpace, + Comment, + Ident, + LeftParenthesis, + RightParenthesis, + Function as FunctionToken, + Colon, + EOF +} from '../../tokenizer/index.js'; + +const likelyFeatureToken = new Set([Colon, RightParenthesis, EOF]); + +export const name = 'Condition'; +export const structure = { + kind: String, + children: [[ + 'Identifier', + 'Feature', + 'FeatureFunction', + 'FeatureRange', + 'SupportsDeclaration' + ]] +}; + +function featureOrRange(kind) { + if (this.lookupTypeNonSC(1) === Ident && + likelyFeatureToken.has(this.lookupTypeNonSC(2))) { + return this.Feature(kind); + } + + return this.FeatureRange(kind); +} + +const parentheses = { + media: featureOrRange, + container: featureOrRange, + supports() { + return this.SupportsDeclaration(); + } +}; + +export function parse(kind = 'media') { + const children = this.createList(); + + scan: while (!this.eof) { + switch (this.tokenType) { + case Comment: + case WhiteSpace: + this.next(); + continue; + + case Ident: + children.push(this.Identifier()); + break; + + case LeftParenthesis: { + let term = this.parseWithFallback( + () => parentheses[kind].call(this, kind), + () => null + ); + + if (!term) { + term = this.parseWithFallback( + () => { + this.eat(LeftParenthesis); + const res = this.Condition(kind); + this.eat(RightParenthesis); + return res; + }, + () => { + return this.GeneralEnclosed(kind); + } + ); + } + + children.push(term); + break; + } + + case FunctionToken: { + let term = this.parseWithFallback( + () => this.FeatureFunction(kind), + () => null + ); + + if (!term) { + term = this.GeneralEnclosed(kind); + } + + children.push(term); + break; + } + + default: + break scan; + } + } + + if (children.isEmpty) { + this.error('Condition is expected'); + } + + return { + type: 'Condition', + loc: this.getLocationFromList(children), + kind, + children + }; +} + +export function generate(node) { + node.children.forEach(child => { + if (child.type === 'Condition') { + this.token(LeftParenthesis, '('); + this.node(child); + this.token(RightParenthesis, ')'); + } else { + this.node(child); + } + }); +} + diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Declaration.js b/vanilla/node_modules/css-tree/lib/syntax/node/Declaration.js new file mode 100644 index 0000000..af3ea60 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Declaration.js @@ -0,0 +1,165 @@ +import { isCustomProperty } from '../../utils/names.js'; +import { + Ident, + Hash, + Colon, + Semicolon, + Delim, + WhiteSpace +} from '../../tokenizer/index.js'; + +const EXCLAMATIONMARK = 0x0021; // U+0021 EXCLAMATION MARK (!) +const NUMBERSIGN = 0x0023; // U+0023 NUMBER SIGN (#) +const DOLLARSIGN = 0x0024; // U+0024 DOLLAR SIGN ($) +const AMPERSAND = 0x0026; // U+0026 AMPERSAND (&) +const ASTERISK = 0x002A; // U+002A ASTERISK (*) +const PLUSSIGN = 0x002B; // U+002B PLUS SIGN (+) +const SOLIDUS = 0x002F; // U+002F SOLIDUS (/) + +function consumeValueRaw() { + return this.Raw(this.consumeUntilExclamationMarkOrSemicolon, true); +} + +function consumeCustomPropertyRaw() { + return this.Raw(this.consumeUntilExclamationMarkOrSemicolon, false); +} + +function consumeValue() { + const startValueToken = this.tokenIndex; + const value = this.Value(); + + if (value.type !== 'Raw' && + this.eof === false && + this.tokenType !== Semicolon && + this.isDelim(EXCLAMATIONMARK) === false && + this.isBalanceEdge(startValueToken) === false) { + this.error(); + } + + return value; +} + +export const name = 'Declaration'; +export const walkContext = 'declaration'; +export const structure = { + important: [Boolean, String], + property: String, + value: ['Value', 'Raw'] +}; + +export function parse() { + const start = this.tokenStart; + const startToken = this.tokenIndex; + const property = readProperty.call(this); + const customProperty = isCustomProperty(property); + const parseValue = customProperty ? this.parseCustomProperty : this.parseValue; + const consumeRaw = customProperty ? consumeCustomPropertyRaw : consumeValueRaw; + let important = false; + let value; + + this.skipSC(); + this.eat(Colon); + + const valueStart = this.tokenIndex; + + if (!customProperty) { + this.skipSC(); + } + + if (parseValue) { + value = this.parseWithFallback(consumeValue, consumeRaw); + } else { + value = consumeRaw.call(this, this.tokenIndex); + } + + if (customProperty && value.type === 'Value' && value.children.isEmpty) { + for (let offset = valueStart - this.tokenIndex; offset <= 0; offset++) { + if (this.lookupType(offset) === WhiteSpace) { + value.children.appendData({ + type: 'WhiteSpace', + loc: null, + value: ' ' + }); + break; + } + } + } + + if (this.isDelim(EXCLAMATIONMARK)) { + important = getImportant.call(this); + this.skipSC(); + } + + // Do not include semicolon to range per spec + // https://drafts.csswg.org/css-syntax/#declaration-diagram + + if (this.eof === false && + this.tokenType !== Semicolon && + this.isBalanceEdge(startToken) === false) { + this.error(); + } + + return { + type: 'Declaration', + loc: this.getLocation(start, this.tokenStart), + important, + property, + value + }; +} + +export function generate(node) { + this.token(Ident, node.property); + this.token(Colon, ':'); + this.node(node.value); + + if (node.important) { + this.token(Delim, '!'); + this.token(Ident, node.important === true ? 'important' : node.important); + } +} + +function readProperty() { + const start = this.tokenStart; + + // hacks + if (this.tokenType === Delim) { + switch (this.charCodeAt(this.tokenStart)) { + case ASTERISK: + case DOLLARSIGN: + case PLUSSIGN: + case NUMBERSIGN: + case AMPERSAND: + this.next(); + break; + + // TODO: not sure we should support this hack + case SOLIDUS: + this.next(); + if (this.isDelim(SOLIDUS)) { + this.next(); + } + break; + } + } + + if (this.tokenType === Hash) { + this.eat(Hash); + } else { + this.eat(Ident); + } + + return this.substrToCursor(start); +} + +// ! ws* important +function getImportant() { + this.eat(Delim); + this.skipSC(); + + const important = this.consume(Ident); + + // store original value in case it differ from `important` + // for better original source restoring and hacks like `!ie` support + return important === 'important' ? true : important; +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/DeclarationList.js b/vanilla/node_modules/css-tree/lib/syntax/node/DeclarationList.js new file mode 100644 index 0000000..2b40c99 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/DeclarationList.js @@ -0,0 +1,62 @@ +import { + WhiteSpace, + Comment, + Semicolon, + AtKeyword +} from '../../tokenizer/index.js'; + +const AMPERSAND = 0x0026; // U+0026 AMPERSAND (&) + +function consumeRaw() { + return this.Raw(this.consumeUntilSemicolonIncluded, true); +} + +export const name = 'DeclarationList'; +export const structure = { + children: [[ + 'Declaration', + 'Atrule', + 'Rule' + ]] +}; + +export function parse() { + const children = this.createList(); + + scan: + while (!this.eof) { + switch (this.tokenType) { + case WhiteSpace: + case Comment: + case Semicolon: + this.next(); + break; + + case AtKeyword: + children.push(this.parseWithFallback(this.Atrule.bind(this, true), consumeRaw)); + break; + + default: + if (this.isDelim(AMPERSAND)) { + children.push(this.parseWithFallback(this.Rule, consumeRaw)); + } else { + children.push(this.parseWithFallback(this.Declaration, consumeRaw)); + } + } + } + + return { + type: 'DeclarationList', + loc: this.getLocationFromList(children), + children + }; +} + +export function generate(node) { + this.children(node, prev => { + if (prev.type === 'Declaration') { + this.token(Semicolon, ';'); + } + }); +} + diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Dimension.js b/vanilla/node_modules/css-tree/lib/syntax/node/Dimension.js new file mode 100644 index 0000000..4b9bffc --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Dimension.js @@ -0,0 +1,23 @@ +import { Dimension } from '../../tokenizer/index.js'; + +export const name = 'Dimension'; +export const structure = { + value: String, + unit: String +}; + +export function parse() { + const start = this.tokenStart; + const value = this.consumeNumber(Dimension); + + return { + type: 'Dimension', + loc: this.getLocation(start, this.tokenStart), + value, + unit: this.substring(start + value.length, this.tokenStart) + }; +} + +export function generate(node) { + this.token(Dimension, node.value + node.unit); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Feature.js b/vanilla/node_modules/css-tree/lib/syntax/node/Feature.js new file mode 100644 index 0000000..2f2dc3d --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Feature.js @@ -0,0 +1,103 @@ +import { + Ident, + Number, + Dimension, + Function as FunctionToken, + LeftParenthesis, + RightParenthesis, + Colon, + Delim +} from '../../tokenizer/index.js'; + +const SOLIDUS = 0x002F; // U+002F SOLIDUS (/) + +export const name = 'Feature'; +export const structure = { + kind: String, + name: String, + value: ['Identifier', 'Number', 'Dimension', 'Ratio', 'Function', null] +}; + +export function parse(kind) { + const start = this.tokenStart; + let name; + let value = null; + + this.eat(LeftParenthesis); + this.skipSC(); + + name = this.consume(Ident); + this.skipSC(); + + if (this.tokenType !== RightParenthesis) { + this.eat(Colon); + this.skipSC(); + + switch (this.tokenType) { + case Number: + if (this.lookupNonWSType(1) === Delim) { + value = this.Ratio(); + } else { + value = this.Number(); + } + + break; + + case Dimension: + value = this.Dimension(); + break; + + case Ident: + value = this.Identifier(); + break; + + case FunctionToken: + value = this.parseWithFallback( + () => { + const res = this.Function(this.readSequence, this.scope.Value); + + this.skipSC(); + + if (this.isDelim(SOLIDUS)) { + this.error(); + } + + return res; + }, + () => { + return this.Ratio(); + } + ); + break; + + default: + this.error('Number, dimension, ratio or identifier is expected'); + } + + this.skipSC(); + } + + if (!this.eof) { + this.eat(RightParenthesis); + } + + return { + type: 'Feature', + loc: this.getLocation(start, this.tokenStart), + kind, + name, + value + }; +} + +export function generate(node) { + this.token(LeftParenthesis, '('); + this.token(Ident, node.name); + + if (node.value !== null) { + this.token(Colon, ':'); + this.node(node.value); + } + + this.token(RightParenthesis, ')'); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/FeatureFunction.js b/vanilla/node_modules/css-tree/lib/syntax/node/FeatureFunction.js new file mode 100644 index 0000000..5869479 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/FeatureFunction.js @@ -0,0 +1,63 @@ +import { + Function as FunctionToken, + RightParenthesis +} from '../../tokenizer/index.js'; + +export const name = 'FeatureFunction'; +export const structure = { + kind: String, + feature: String, + value: ['Declaration', 'Selector'] +}; + +function getFeatureParser(kind, name) { + const featuresOfKind = this.features[kind] || {}; + const parser = featuresOfKind[name]; + + if (typeof parser !== 'function') { + this.error(`Unknown feature ${name}()`); + } + + return parser; +} + +export function parse(kind = 'unknown') { + const start = this.tokenStart; + const functionName = this.consumeFunctionName(); + const valueParser = getFeatureParser.call(this, kind, functionName.toLowerCase()); + + this.skipSC(); + + const value = this.parseWithFallback( + () => { + const startValueToken = this.tokenIndex; + const value = valueParser.call(this); + + if (this.eof === false && + this.isBalanceEdge(startValueToken) === false) { + this.error(); + } + + return value; + }, + () => this.Raw(null, false) + ); + + if (!this.eof) { + this.eat(RightParenthesis); + } + + return { + type: 'FeatureFunction', + loc: this.getLocation(start, this.tokenStart), + kind, + feature: functionName, + value + }; +} + +export function generate(node) { + this.token(FunctionToken, node.feature + '('); + this.node(node.value); + this.token(RightParenthesis, ')'); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/FeatureRange.js b/vanilla/node_modules/css-tree/lib/syntax/node/FeatureRange.js new file mode 100644 index 0000000..a83e479 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/FeatureRange.js @@ -0,0 +1,133 @@ +import { + Ident, + Number, + Dimension, + Function as FunctionToken, + LeftParenthesis, + RightParenthesis +} from '../../tokenizer/index.js'; + +const SOLIDUS = 0x002F; // U+002F SOLIDUS (/) +const LESSTHANSIGN = 0x003C; // U+003C LESS-THAN SIGN (<) +const EQUALSSIGN = 0x003D; // U+003D EQUALS SIGN (=) +const GREATERTHANSIGN = 0x003E; // U+003E GREATER-THAN SIGN (>) + +export const name = 'FeatureRange'; +export const structure = { + kind: String, + left: ['Identifier', 'Number', 'Dimension', 'Ratio', 'Function'], + leftComparison: String, + middle: ['Identifier', 'Number', 'Dimension', 'Ratio', 'Function'], + rightComparison: [String, null], + right: ['Identifier', 'Number', 'Dimension', 'Ratio', 'Function', null] +}; + +function readTerm() { + this.skipSC(); + + switch (this.tokenType) { + case Number: + if (this.isDelim(SOLIDUS, this.lookupOffsetNonSC(1))) { + return this.Ratio(); + } else { + return this.Number(); + } + + case Dimension: + return this.Dimension(); + + case Ident: + return this.Identifier(); + + case FunctionToken: + return this.parseWithFallback( + () => { + const res = this.Function(this.readSequence, this.scope.Value); + + this.skipSC(); + + if (this.isDelim(SOLIDUS)) { + this.error(); + } + + return res; + }, + () => { + return this.Ratio(); + } + ); + + default: + this.error('Number, dimension, ratio or identifier is expected'); + } +} + +function readComparison(expectColon) { + this.skipSC(); + + if (this.isDelim(LESSTHANSIGN) || + this.isDelim(GREATERTHANSIGN)) { + const value = this.source[this.tokenStart]; + + this.next(); + + if (this.isDelim(EQUALSSIGN)) { + this.next(); + return value + '='; + } + + return value; + } + + if (this.isDelim(EQUALSSIGN)) { + return '='; + } + + this.error(`Expected ${expectColon ? '":", ' : ''}"<", ">", "=" or ")"`); +} + +export function parse(kind = 'unknown') { + const start = this.tokenStart; + + this.skipSC(); + this.eat(LeftParenthesis); + + const left = readTerm.call(this); + const leftComparison = readComparison.call(this, left.type === 'Identifier'); + const middle = readTerm.call(this); + let rightComparison = null; + let right = null; + + if (this.lookupNonWSType(0) !== RightParenthesis) { + rightComparison = readComparison.call(this); + right = readTerm.call(this); + } + + this.skipSC(); + this.eat(RightParenthesis); + + return { + type: 'FeatureRange', + loc: this.getLocation(start, this.tokenStart), + kind, + left, + leftComparison, + middle, + rightComparison, + right + }; +} + +export function generate(node) { + this.token(LeftParenthesis, '('); + this.node(node.left); + this.tokenize(node.leftComparison); + this.node(node.middle); + + if (node.right) { + this.tokenize(node.rightComparison); + this.node(node.right); + } + + this.token(RightParenthesis, ')'); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Function.js b/vanilla/node_modules/css-tree/lib/syntax/node/Function.js new file mode 100644 index 0000000..1fdc414 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Function.js @@ -0,0 +1,41 @@ +import { + Function as FunctionToken, + RightParenthesis +} from '../../tokenizer/index.js'; + + +export const name = 'Function'; +export const walkContext = 'function'; +export const structure = { + name: String, + children: [[]] +}; + +// <function-token> <sequence> ) +export function parse(readSequence, recognizer) { + const start = this.tokenStart; + const name = this.consumeFunctionName(); + const nameLowerCase = name.toLowerCase(); + let children; + + children = recognizer.hasOwnProperty(nameLowerCase) + ? recognizer[nameLowerCase].call(this, recognizer) + : readSequence.call(this, recognizer); + + if (!this.eof) { + this.eat(RightParenthesis); + } + + return { + type: 'Function', + loc: this.getLocation(start, this.tokenStart), + name, + children + }; +} + +export function generate(node) { + this.token(FunctionToken, node.name + '('); + this.children(node); + this.token(RightParenthesis, ')'); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/GeneralEnclosed.js b/vanilla/node_modules/css-tree/lib/syntax/node/GeneralEnclosed.js new file mode 100644 index 0000000..8ac8cae --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/GeneralEnclosed.js @@ -0,0 +1,66 @@ +import { + Function as FunctionToken, + LeftParenthesis, + RightParenthesis +} from '../../tokenizer/index.js'; + + +export const name = 'GeneralEnclosed'; +export const structure = { + kind: String, + function: [String, null], + children: [[]] +}; + +// <function-token> <any-value> ) +// ( <any-value> ) +export function parse(kind) { + const start = this.tokenStart; + let functionName = null; + + if (this.tokenType === FunctionToken) { + functionName = this.consumeFunctionName(); + } else { + this.eat(LeftParenthesis); + } + + const children = this.parseWithFallback( + () => { + const startValueToken = this.tokenIndex; + const children = this.readSequence(this.scope.Value); + + if (this.eof === false && + this.isBalanceEdge(startValueToken) === false) { + this.error(); + } + + return children; + }, + () => this.createSingleNodeList( + this.Raw(null, false) + ) + ); + + if (!this.eof) { + this.eat(RightParenthesis); + } + + return { + type: 'GeneralEnclosed', + loc: this.getLocation(start, this.tokenStart), + kind, + function: functionName, + children + }; +} + +export function generate(node) { + if (node.function) { + this.token(FunctionToken, node.function + '('); + } else { + this.token(LeftParenthesis, '('); + } + + this.children(node); + this.token(RightParenthesis, ')'); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Hash.js b/vanilla/node_modules/css-tree/lib/syntax/node/Hash.js new file mode 100644 index 0000000..b752752 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Hash.js @@ -0,0 +1,23 @@ +import { Hash } from '../../tokenizer/index.js'; + +// '#' ident +export const xxx = 'XXX'; +export const name = 'Hash'; +export const structure = { + value: String +}; +export function parse() { + const start = this.tokenStart; + + this.eat(Hash); + + return { + type: 'Hash', + loc: this.getLocation(start, this.tokenStart), + value: this.substrToCursor(start + 1) + }; +} +export function generate(node) { + this.token(Hash, '#' + node.value); +} + diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/IdSelector.js b/vanilla/node_modules/css-tree/lib/syntax/node/IdSelector.js new file mode 100644 index 0000000..c85c1b2 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/IdSelector.js @@ -0,0 +1,26 @@ +import { Hash, Delim } from '../../tokenizer/index.js'; + +export const name = 'IdSelector'; +export const structure = { + name: String +}; + +export function parse() { + const start = this.tokenStart; + + // TODO: check value is an ident + this.eat(Hash); + + return { + type: 'IdSelector', + loc: this.getLocation(start, this.tokenStart), + name: this.substrToCursor(start + 1) + }; +} + +export function generate(node) { + // Using Delim instead of Hash is a hack to avoid for a whitespace between ident and id-selector + // in safe mode (e.g. "a#id"), because IE11 doesn't allow a sequence <ident-token> <hash-token> + // without a whitespace in values (e.g. "1px solid#000") + this.token(Delim, '#' + node.name); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Identifier.js b/vanilla/node_modules/css-tree/lib/syntax/node/Identifier.js new file mode 100644 index 0000000..067c2d0 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Identifier.js @@ -0,0 +1,18 @@ +import { Ident } from '../../tokenizer/index.js'; + +export const name = 'Identifier'; +export const structure = { + name: String +}; + +export function parse() { + return { + type: 'Identifier', + loc: this.getLocation(this.tokenStart, this.tokenEnd), + name: this.consume(Ident) + }; +} + +export function generate(node) { + this.token(Ident, node.name); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Layer.js b/vanilla/node_modules/css-tree/lib/syntax/node/Layer.js new file mode 100644 index 0000000..d170dcb --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Layer.js @@ -0,0 +1,28 @@ +import { Ident, Delim } from '../../tokenizer/index.js'; + +const FULLSTOP = 0x002E; // U+002E FULL STOP (.) + +export const name = 'Layer'; +export const structure = { + name: String +}; + +export function parse() { + let tokenStart = this.tokenStart; + let name = this.consume(Ident); + + while (this.isDelim(FULLSTOP)) { + this.eat(Delim); + name += '.' + this.consume(Ident); + } + + return { + type: 'Layer', + loc: this.getLocation(tokenStart, this.tokenStart), + name + }; +} + +export function generate(node) { + this.tokenize(node.name); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/LayerList.js b/vanilla/node_modules/css-tree/lib/syntax/node/LayerList.js new file mode 100644 index 0000000..e8a05cd --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/LayerList.js @@ -0,0 +1,36 @@ +import { Comma } from '../../tokenizer/index.js'; + +export const name = 'LayerList'; +export const structure = { + children: [[ + 'Layer' + ]] +}; + +export function parse() { + const children = this.createList(); + + this.skipSC(); + + while (!this.eof) { + children.push(this.Layer()); + + if (this.lookupTypeNonSC(0) !== Comma) { + break; + } + + this.skipSC(); + this.next(); + this.skipSC(); + } + + return { + type: 'LayerList', + loc: this.getLocationFromList(children), + children + }; +} + +export function generate(node) { + this.children(node, () => this.token(Comma, ',')); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/MediaQuery.js b/vanilla/node_modules/css-tree/lib/syntax/node/MediaQuery.js new file mode 100644 index 0000000..f569c75 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/MediaQuery.js @@ -0,0 +1,102 @@ +import { + Comma, + EOF, + Ident, + LeftCurlyBracket, + LeftParenthesis, + Function as FunctionToken, + Semicolon +} from '../../tokenizer/index.js'; + +export const name = 'MediaQuery'; +export const structure = { + modifier: [String, null], + mediaType: [String, null], + condition: ['Condition', null] +}; + +export function parse() { + const start = this.tokenStart; + let modifier = null; + let mediaType = null; + let condition = null; + + this.skipSC(); + + if (this.tokenType === Ident && this.lookupTypeNonSC(1) !== LeftParenthesis) { + // [ not | only ]? <media-type> + const ident = this.consume(Ident); + const identLowerCase = ident.toLowerCase(); + + if (identLowerCase === 'not' || identLowerCase === 'only') { + this.skipSC(); + modifier = identLowerCase; + mediaType = this.consume(Ident); + } else { + mediaType = ident; + } + + switch (this.lookupTypeNonSC(0)) { + case Ident: { + // and <media-condition-without-or> + this.skipSC(); + this.eatIdent('and'); + condition = this.Condition('media'); + break; + } + + case LeftCurlyBracket: + case Semicolon: + case Comma: + case EOF: + break; + + default: + this.error('Identifier or parenthesis is expected'); + } + } else { + switch (this.tokenType) { + case Ident: + case LeftParenthesis: + case FunctionToken: { + // <media-condition> + condition = this.Condition('media'); + break; + } + + case LeftCurlyBracket: + case Semicolon: + case EOF: + break; + + default: + this.error('Identifier or parenthesis is expected'); + } + } + + return { + type: 'MediaQuery', + loc: this.getLocation(start, this.tokenStart), + modifier, + mediaType, + condition + }; +} + +export function generate(node) { + if (node.mediaType) { + if (node.modifier) { + this.token(Ident, node.modifier); + } + + this.token(Ident, node.mediaType); + + if (node.condition) { + this.token(Ident, 'and'); + this.node(node.condition); + } + } else if (node.condition) { + this.node(node.condition); + } +} + diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/MediaQueryList.js b/vanilla/node_modules/css-tree/lib/syntax/node/MediaQueryList.js new file mode 100644 index 0000000..1c16bcd --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/MediaQueryList.js @@ -0,0 +1,34 @@ +import { Comma } from '../../tokenizer/index.js'; + +export const name = 'MediaQueryList'; +export const structure = { + children: [[ + 'MediaQuery' + ]] +}; + +export function parse() { + const children = this.createList(); + + this.skipSC(); + + while (!this.eof) { + children.push(this.MediaQuery()); + + if (this.tokenType !== Comma) { + break; + } + + this.next(); + } + + return { + type: 'MediaQueryList', + loc: this.getLocationFromList(children), + children + }; +} + +export function generate(node) { + this.children(node, () => this.token(Comma, ',')); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/NestingSelector.js b/vanilla/node_modules/css-tree/lib/syntax/node/NestingSelector.js new file mode 100644 index 0000000..33c8b6a --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/NestingSelector.js @@ -0,0 +1,22 @@ +import { Delim } from '../../tokenizer/index.js'; + +const AMPERSAND = 0x0026; // U+0026 AMPERSAND (&) + +export const name = 'NestingSelector'; +export const structure = { +}; + +export function parse() { + const start = this.tokenStart; + + this.eatDelim(AMPERSAND); + + return { + type: 'NestingSelector', + loc: this.getLocation(start, this.tokenStart) + }; +} + +export function generate() { + this.token(Delim, '&'); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Nth.js b/vanilla/node_modules/css-tree/lib/syntax/node/Nth.js new file mode 100644 index 0000000..bfd74a1 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Nth.js @@ -0,0 +1,47 @@ +import { Ident } from '../../tokenizer/index.js'; + +export const name = 'Nth'; +export const structure = { + nth: ['AnPlusB', 'Identifier'], + selector: ['SelectorList', null] +}; + +export function parse() { + this.skipSC(); + + const start = this.tokenStart; + let end = start; + let selector = null; + let nth; + + if (this.lookupValue(0, 'odd') || this.lookupValue(0, 'even')) { + nth = this.Identifier(); + } else { + nth = this.AnPlusB(); + } + + end = this.tokenStart; + this.skipSC(); + + if (this.lookupValue(0, 'of')) { + this.next(); + + selector = this.SelectorList(); + end = this.tokenStart; + } + + return { + type: 'Nth', + loc: this.getLocation(start, end), + nth, + selector + }; +} + +export function generate(node) { + this.node(node.nth); + if (node.selector !== null) { + this.token(Ident, 'of'); + this.node(node.selector); + } +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Number.js b/vanilla/node_modules/css-tree/lib/syntax/node/Number.js new file mode 100644 index 0000000..a9d8f0a --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Number.js @@ -0,0 +1,18 @@ +import { Number as NumberToken } from '../../tokenizer/index.js'; + +export const name = 'Number'; +export const structure = { + value: String +}; + +export function parse() { + return { + type: 'Number', + loc: this.getLocation(this.tokenStart, this.tokenEnd), + value: this.consume(NumberToken) + }; +} + +export function generate(node) { + this.token(NumberToken, node.value); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Operator.js b/vanilla/node_modules/css-tree/lib/syntax/node/Operator.js new file mode 100644 index 0000000..4f1238b --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Operator.js @@ -0,0 +1,21 @@ +// '/' | '*' | ',' | ':' | '+' | '-' +export const name = 'Operator'; +export const structure = { + value: String +}; + +export function parse() { + const start = this.tokenStart; + + this.next(); + + return { + type: 'Operator', + loc: this.getLocation(start, this.tokenStart), + value: this.substrToCursor(start) + }; +} + +export function generate(node) { + this.tokenize(node.value); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Parentheses.js b/vanilla/node_modules/css-tree/lib/syntax/node/Parentheses.js new file mode 100644 index 0000000..8c6cdb5 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Parentheses.js @@ -0,0 +1,34 @@ +import { + LeftParenthesis, + RightParenthesis +} from '../../tokenizer/index.js'; + +export const name = 'Parentheses'; +export const structure = { + children: [[]] +}; + +export function parse(readSequence, recognizer) { + const start = this.tokenStart; + let children = null; + + this.eat(LeftParenthesis); + + children = readSequence.call(this, recognizer); + + if (!this.eof) { + this.eat(RightParenthesis); + } + + return { + type: 'Parentheses', + loc: this.getLocation(start, this.tokenStart), + children + }; +} + +export function generate(node) { + this.token(LeftParenthesis, '('); + this.children(node); + this.token(RightParenthesis, ')'); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Percentage.js b/vanilla/node_modules/css-tree/lib/syntax/node/Percentage.js new file mode 100644 index 0000000..3c8b628 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Percentage.js @@ -0,0 +1,18 @@ +import { Percentage } from '../../tokenizer/index.js'; + +export const name = 'Percentage'; +export const structure = { + value: String +}; + +export function parse() { + return { + type: 'Percentage', + loc: this.getLocation(this.tokenStart, this.tokenEnd), + value: this.consumeNumber(Percentage) + }; +} + +export function generate(node) { + this.token(Percentage, node.value + '%'); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/PseudoClassSelector.js b/vanilla/node_modules/css-tree/lib/syntax/node/PseudoClassSelector.js new file mode 100644 index 0000000..584546c --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/PseudoClassSelector.js @@ -0,0 +1,65 @@ +import { + Ident, + Function as FunctionToken, + Colon, + RightParenthesis +} from '../../tokenizer/index.js'; + + +export const name = 'PseudoClassSelector'; +export const walkContext = 'function'; +export const structure = { + name: String, + children: [['Raw'], null] +}; + +// : [ <ident> | <function-token> <any-value>? ) ] +export function parse() { + const start = this.tokenStart; + let children = null; + let name; + let nameLowerCase; + + this.eat(Colon); + + if (this.tokenType === FunctionToken) { + name = this.consumeFunctionName(); + nameLowerCase = name.toLowerCase(); + + if (this.lookupNonWSType(0) == RightParenthesis) { + children = this.createList(); + } else if (hasOwnProperty.call(this.pseudo, nameLowerCase)) { + this.skipSC(); + children = this.pseudo[nameLowerCase].call(this); + this.skipSC(); + } else { + children = this.createList(); + children.push( + this.Raw(null, false) + ); + } + + this.eat(RightParenthesis); + } else { + name = this.consume(Ident); + } + + return { + type: 'PseudoClassSelector', + loc: this.getLocation(start, this.tokenStart), + name, + children + }; +} + +export function generate(node) { + this.token(Colon, ':'); + + if (node.children === null) { + this.token(Ident, node.name); + } else { + this.token(FunctionToken, node.name + '('); + this.children(node); + this.token(RightParenthesis, ')'); + } +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/PseudoElementSelector.js b/vanilla/node_modules/css-tree/lib/syntax/node/PseudoElementSelector.js new file mode 100644 index 0000000..b23b9a0 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/PseudoElementSelector.js @@ -0,0 +1,66 @@ +import { + Ident, + Function as FunctionToken, + Colon, + RightParenthesis +} from '../../tokenizer/index.js'; + +export const name = 'PseudoElementSelector'; +export const walkContext = 'function'; +export const structure = { + name: String, + children: [['Raw'], null] +}; + +// :: [ <ident> | <function-token> <any-value>? ) ] +export function parse() { + const start = this.tokenStart; + let children = null; + let name; + let nameLowerCase; + + this.eat(Colon); + this.eat(Colon); + + if (this.tokenType === FunctionToken) { + name = this.consumeFunctionName(); + nameLowerCase = name.toLowerCase(); + + if (this.lookupNonWSType(0) == RightParenthesis) { + children = this.createList(); + } else if (hasOwnProperty.call(this.pseudo, nameLowerCase)) { + this.skipSC(); + children = this.pseudo[nameLowerCase].call(this); + this.skipSC(); + } else { + children = this.createList(); + children.push( + this.Raw(null, false) + ); + } + + this.eat(RightParenthesis); + } else { + name = this.consume(Ident); + } + + return { + type: 'PseudoElementSelector', + loc: this.getLocation(start, this.tokenStart), + name, + children + }; +} + +export function generate(node) { + this.token(Colon, ':'); + this.token(Colon, ':'); + + if (node.children === null) { + this.token(Ident, node.name); + } else { + this.token(FunctionToken, node.name + '('); + this.children(node); + this.token(RightParenthesis, ')'); + } +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Ratio.js b/vanilla/node_modules/css-tree/lib/syntax/node/Ratio.js new file mode 100644 index 0000000..c26cf10 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Ratio.js @@ -0,0 +1,68 @@ +import { + Delim, + Number as NumberToken, + Function as FunctionToken +} from '../../tokenizer/index.js'; + +const SOLIDUS = 0x002F; // U+002F SOLIDUS (/) + +// Media Queries Level 3 defines terms of <ratio> as a positive (not zero or negative) +// integers (see https://drafts.csswg.org/mediaqueries-3/#values) +// However, Media Queries Level 4 removes any definition of values +// (see https://drafts.csswg.org/mediaqueries-4/#values) and refers to +// CSS Values and Units for detail. In CSS Values and Units Level 4 a <ratio> +// definition was added (see https://drafts.csswg.org/css-values-4/#ratios) which +// defines ratio as "<number [0,∞]> [ / <number [0,∞]> ]?" and based on it +// any constrains on terms were removed. Parser also doesn't test numbers +// in any way to make possible for linting and fixing them by the tools using CSSTree. +// An additional syntax examination may be applied by a lexer. +function consumeTerm() { + this.skipSC(); + + switch (this.tokenType) { + case NumberToken: + return this.Number(); + + case FunctionToken: + return this.Function(this.readSequence, this.scope.Value); + + default: + this.error('Number of function is expected'); + } +} + +export const name = 'Ratio'; +export const structure = { + left: ['Number', 'Function'], + right: ['Number', 'Function', null] +}; + +// <number [0,∞]> [ / <number [0,∞]> ]? +export function parse() { + const start = this.tokenStart; + const left = consumeTerm.call(this); + let right = null; + + this.skipSC(); + if (this.isDelim(SOLIDUS)) { + this.eatDelim(SOLIDUS); + right = consumeTerm.call(this); + } + + return { + type: 'Ratio', + loc: this.getLocation(start, this.tokenStart), + left, + right + }; +} + +export function generate(node) { + this.node(node.left); + this.token(Delim, '/'); + if (node.right) { + this.node(node.right); + } else { + this.node(NumberToken, 1); + } +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Raw.js b/vanilla/node_modules/css-tree/lib/syntax/node/Raw.js new file mode 100644 index 0000000..0c2ea69 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Raw.js @@ -0,0 +1,41 @@ +import { WhiteSpace } from '../../tokenizer/index.js'; + +function getOffsetExcludeWS() { + if (this.tokenIndex > 0) { + if (this.lookupType(-1) === WhiteSpace) { + return this.tokenIndex > 1 + ? this.getTokenStart(this.tokenIndex - 1) + : this.firstCharOffset; + } + } + + return this.tokenStart; +} + +export const name = 'Raw'; +export const structure = { + value: String +}; + +export function parse(consumeUntil, excludeWhiteSpace) { + const startOffset = this.getTokenStart(this.tokenIndex); + let endOffset; + + this.skipUntilBalanced(this.tokenIndex, consumeUntil || this.consumeUntilBalanceEnd); + + if (excludeWhiteSpace && this.tokenStart > startOffset) { + endOffset = getOffsetExcludeWS.call(this); + } else { + endOffset = this.tokenStart; + } + + return { + type: 'Raw', + loc: this.getLocation(startOffset, endOffset), + value: this.substring(startOffset, endOffset) + }; +} + +export function generate(node) { + this.tokenize(node.value); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Rule.js b/vanilla/node_modules/css-tree/lib/syntax/node/Rule.js new file mode 100644 index 0000000..89745cf --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Rule.js @@ -0,0 +1,51 @@ +import { LeftCurlyBracket } from '../../tokenizer/index.js'; + +function consumeRaw() { + return this.Raw(this.consumeUntilLeftCurlyBracket, true); +} + +function consumePrelude() { + const prelude = this.SelectorList(); + + if (prelude.type !== 'Raw' && + this.eof === false && + this.tokenType !== LeftCurlyBracket) { + this.error(); + } + + return prelude; +} + +export const name = 'Rule'; +export const walkContext = 'rule'; +export const structure = { + prelude: ['SelectorList', 'Raw'], + block: ['Block'] +}; + +export function parse() { + const startToken = this.tokenIndex; + const startOffset = this.tokenStart; + let prelude; + let block; + + if (this.parseRulePrelude) { + prelude = this.parseWithFallback(consumePrelude, consumeRaw); + } else { + prelude = consumeRaw.call(this, startToken); + } + + block = this.Block(true); + + return { + type: 'Rule', + loc: this.getLocation(startOffset, this.tokenStart), + prelude, + block + }; +} +export function generate(node) { + this.node(node.prelude); + this.node(node.block); +} + diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Scope.js b/vanilla/node_modules/css-tree/lib/syntax/node/Scope.js new file mode 100644 index 0000000..650b035 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Scope.js @@ -0,0 +1,66 @@ +import { + Ident, + LeftParenthesis, + RightParenthesis +} from '../../tokenizer/index.js'; + +export const name = 'Scope'; +export const structure = { + root: ['SelectorList', 'Raw', null], + limit: ['SelectorList', 'Raw', null] +}; + +export function parse() { + let root = null; + let limit = null; + + this.skipSC(); + + const startOffset = this.tokenStart; + if (this.tokenType === LeftParenthesis) { + this.next(); + this.skipSC(); + root = this.parseWithFallback( + this.SelectorList, + () => this.Raw(false, true) + ); + this.skipSC(); + this.eat(RightParenthesis); + } + + if (this.lookupNonWSType(0) === Ident) { + this.skipSC(); + this.eatIdent('to'); + this.skipSC(); + this.eat(LeftParenthesis); + this.skipSC(); + limit = this.parseWithFallback( + this.SelectorList, + () => this.Raw(false, true) + ); + this.skipSC(); + this.eat(RightParenthesis); + } + + return { + type: 'Scope', + loc: this.getLocation(startOffset, this.tokenStart), + root, + limit + }; +} + +export function generate(node) { + if (node.root) { + this.token(LeftParenthesis, '('); + this.node(node.root); + this.token(RightParenthesis, ')'); + } + + if (node.limit) { + this.token(Ident, 'to'); + this.token(LeftParenthesis, '('); + this.node(node.limit); + this.token(RightParenthesis, ')'); + } +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Selector.js b/vanilla/node_modules/css-tree/lib/syntax/node/Selector.js new file mode 100644 index 0000000..36028e0 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Selector.js @@ -0,0 +1,31 @@ +export const name = 'Selector'; +export const structure = { + children: [[ + 'TypeSelector', + 'IdSelector', + 'ClassSelector', + 'AttributeSelector', + 'PseudoClassSelector', + 'PseudoElementSelector', + 'Combinator' + ]] +}; + +export function parse() { + const children = this.readSequence(this.scope.Selector); + + // nothing were consumed + if (this.getFirstListNode(children) === null) { + this.error('Selector is expected'); + } + + return { + type: 'Selector', + loc: this.getLocationFromList(children), + children + }; +} + +export function generate(node) { + this.children(node); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/SelectorList.js b/vanilla/node_modules/css-tree/lib/syntax/node/SelectorList.js new file mode 100644 index 0000000..ebba3cd --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/SelectorList.js @@ -0,0 +1,35 @@ +import { Comma } from '../../tokenizer/index.js'; + +export const name = 'SelectorList'; +export const walkContext = 'selector'; +export const structure = { + children: [[ + 'Selector', + 'Raw' + ]] +}; + +export function parse() { + const children = this.createList(); + + while (!this.eof) { + children.push(this.Selector()); + + if (this.tokenType === Comma) { + this.next(); + continue; + } + + break; + } + + return { + type: 'SelectorList', + loc: this.getLocationFromList(children), + children + }; +} + +export function generate(node) { + this.children(node, () => this.token(Comma, ',')); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/String.js b/vanilla/node_modules/css-tree/lib/syntax/node/String.js new file mode 100644 index 0000000..95e1aaa --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/String.js @@ -0,0 +1,19 @@ +import { String as StringToken } from '../../tokenizer/index.js'; +import { decode, encode } from '../../utils/string.js'; + +export const name = 'String'; +export const structure = { + value: String +}; + +export function parse() { + return { + type: 'String', + loc: this.getLocation(this.tokenStart, this.tokenEnd), + value: decode(this.consume(StringToken)) + }; +} + +export function generate(node) { + this.token(StringToken, encode(node.value)); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/StyleSheet.js b/vanilla/node_modules/css-tree/lib/syntax/node/StyleSheet.js new file mode 100644 index 0000000..3bc5347 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/StyleSheet.js @@ -0,0 +1,82 @@ +import { + WhiteSpace, + Comment, + AtKeyword, + CDO, + CDC +} from '../../tokenizer/index.js'; + +const EXCLAMATIONMARK = 0x0021; // U+0021 EXCLAMATION MARK (!) + +function consumeRaw() { + return this.Raw(null, false); +} + +export const name = 'StyleSheet'; +export const walkContext = 'stylesheet'; +export const structure = { + children: [[ + 'Comment', + 'CDO', + 'CDC', + 'Atrule', + 'Rule', + 'Raw' + ]] +}; + +export function parse() { + const start = this.tokenStart; + const children = this.createList(); + let child; + + scan: + while (!this.eof) { + switch (this.tokenType) { + case WhiteSpace: + this.next(); + continue; + + case Comment: + // ignore comments except exclamation comments (i.e. /*! .. */) on top level + if (this.charCodeAt(this.tokenStart + 2) !== EXCLAMATIONMARK) { + this.next(); + continue; + } + + child = this.Comment(); + break; + + case CDO: // <!-- + child = this.CDO(); + break; + + case CDC: // --> + child = this.CDC(); + break; + + // CSS Syntax Module Level 3 + // §2.2 Error handling + // At the "top level" of a stylesheet, an <at-keyword-token> starts an at-rule. + case AtKeyword: + child = this.parseWithFallback(this.Atrule, consumeRaw); + break; + + // Anything else starts a qualified rule ... + default: + child = this.parseWithFallback(this.Rule, consumeRaw); + } + + children.push(child); + } + + return { + type: 'StyleSheet', + loc: this.getLocation(start, this.tokenStart), + children + }; +} + +export function generate(node) { + this.children(node); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/SupportsDeclaration.js b/vanilla/node_modules/css-tree/lib/syntax/node/SupportsDeclaration.js new file mode 100644 index 0000000..ee816e5 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/SupportsDeclaration.js @@ -0,0 +1,34 @@ +import { + LeftParenthesis, + RightParenthesis +} from '../../tokenizer/index.js'; + +export const name = 'SupportsDeclaration'; +export const structure = { + declaration: 'Declaration' +}; + +export function parse() { + const start = this.tokenStart; + + this.eat(LeftParenthesis); + this.skipSC(); + + const declaration = this.Declaration(); + + if (!this.eof) { + this.eat(RightParenthesis); + } + + return { + type: 'SupportsDeclaration', + loc: this.getLocation(start, this.tokenStart), + declaration + }; +} + +export function generate(node) { + this.token(LeftParenthesis, '('); + this.node(node.declaration); + this.token(RightParenthesis, ')'); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/TypeSelector.js b/vanilla/node_modules/css-tree/lib/syntax/node/TypeSelector.js new file mode 100644 index 0000000..272e195 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/TypeSelector.js @@ -0,0 +1,52 @@ +import { Ident } from '../../tokenizer/index.js'; + +const ASTERISK = 0x002A; // U+002A ASTERISK (*) +const VERTICALLINE = 0x007C; // U+007C VERTICAL LINE (|) + +function eatIdentifierOrAsterisk() { + if (this.tokenType !== Ident && + this.isDelim(ASTERISK) === false) { + this.error('Identifier or asterisk is expected'); + } + + this.next(); +} + +export const name = 'TypeSelector'; +export const structure = { + name: String +}; + +// ident +// ident|ident +// ident|* +// * +// *|ident +// *|* +// |ident +// |* +export function parse() { + const start = this.tokenStart; + + if (this.isDelim(VERTICALLINE)) { + this.next(); + eatIdentifierOrAsterisk.call(this); + } else { + eatIdentifierOrAsterisk.call(this); + + if (this.isDelim(VERTICALLINE)) { + this.next(); + eatIdentifierOrAsterisk.call(this); + } + } + + return { + type: 'TypeSelector', + loc: this.getLocation(start, this.tokenStart), + name: this.substrToCursor(start) + }; +} + +export function generate(node) { + this.tokenize(node.name); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/UnicodeRange.js b/vanilla/node_modules/css-tree/lib/syntax/node/UnicodeRange.js new file mode 100644 index 0000000..95ee8b9 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/UnicodeRange.js @@ -0,0 +1,156 @@ +import { + isHexDigit, + Ident, + Number, + Dimension +} from '../../tokenizer/index.js'; + +const PLUSSIGN = 0x002B; // U+002B PLUS SIGN (+) +const HYPHENMINUS = 0x002D; // U+002D HYPHEN-MINUS (-) +const QUESTIONMARK = 0x003F; // U+003F QUESTION MARK (?) + +function eatHexSequence(offset, allowDash) { + let len = 0; + + for (let pos = this.tokenStart + offset; pos < this.tokenEnd; pos++) { + const code = this.charCodeAt(pos); + + if (code === HYPHENMINUS && allowDash && len !== 0) { + eatHexSequence.call(this, offset + len + 1, false); + return -1; + } + + if (!isHexDigit(code)) { + this.error( + allowDash && len !== 0 + ? 'Hyphen minus' + (len < 6 ? ' or hex digit' : '') + ' is expected' + : (len < 6 ? 'Hex digit is expected' : 'Unexpected input'), + pos + ); + } + + if (++len > 6) { + this.error('Too many hex digits', pos); + }; + } + + this.next(); + return len; +} + +function eatQuestionMarkSequence(max) { + let count = 0; + + while (this.isDelim(QUESTIONMARK)) { + if (++count > max) { + this.error('Too many question marks'); + } + + this.next(); + } +} + +function startsWith(code) { + if (this.charCodeAt(this.tokenStart) !== code) { + this.error((code === PLUSSIGN ? 'Plus sign' : 'Hyphen minus') + ' is expected'); + } +} + +// https://drafts.csswg.org/css-syntax/#urange +// Informally, the <urange> production has three forms: +// U+0001 +// Defines a range consisting of a single code point, in this case the code point "1". +// U+0001-00ff +// Defines a range of codepoints between the first and the second value, in this case +// the range between "1" and "ff" (255 in decimal) inclusive. +// U+00?? +// Defines a range of codepoints where the "?" characters range over all hex digits, +// in this case defining the same as the value U+0000-00ff. +// In each form, a maximum of 6 digits is allowed for each hexadecimal number (if you treat "?" as a hexadecimal digit). +// +// <urange> = +// u '+' <ident-token> '?'* | +// u <dimension-token> '?'* | +// u <number-token> '?'* | +// u <number-token> <dimension-token> | +// u <number-token> <number-token> | +// u '+' '?'+ +function scanUnicodeRange() { + let hexLength = 0; + + switch (this.tokenType) { + case Number: + // u <number-token> '?'* + // u <number-token> <dimension-token> + // u <number-token> <number-token> + hexLength = eatHexSequence.call(this, 1, true); + + if (this.isDelim(QUESTIONMARK)) { + eatQuestionMarkSequence.call(this, 6 - hexLength); + break; + } + + if (this.tokenType === Dimension || + this.tokenType === Number) { + startsWith.call(this, HYPHENMINUS); + eatHexSequence.call(this, 1, false); + break; + } + + break; + + case Dimension: + // u <dimension-token> '?'* + hexLength = eatHexSequence.call(this, 1, true); + + if (hexLength > 0) { + eatQuestionMarkSequence.call(this, 6 - hexLength); + } + + break; + + default: + // u '+' <ident-token> '?'* + // u '+' '?'+ + this.eatDelim(PLUSSIGN); + + if (this.tokenType === Ident) { + hexLength = eatHexSequence.call(this, 0, true); + if (hexLength > 0) { + eatQuestionMarkSequence.call(this, 6 - hexLength); + } + break; + } + + if (this.isDelim(QUESTIONMARK)) { + this.next(); + eatQuestionMarkSequence.call(this, 5); + break; + } + + this.error('Hex digit or question mark is expected'); + } +} + +export const name = 'UnicodeRange'; +export const structure = { + value: String +}; + +export function parse() { + const start = this.tokenStart; + + // U or u + this.eatIdent('u'); + scanUnicodeRange.call(this); + + return { + type: 'UnicodeRange', + loc: this.getLocation(start, this.tokenStart), + value: this.substrToCursor(start) + }; +} + +export function generate(node) { + this.tokenize(node.value); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Url.js b/vanilla/node_modules/css-tree/lib/syntax/node/Url.js new file mode 100644 index 0000000..ac52c9d --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Url.js @@ -0,0 +1,52 @@ +import * as url from '../../utils/url.js'; +import * as string from '../../utils/string.js'; +import { + Function as FunctionToken, + String as StringToken, + Url, + RightParenthesis +} from '../../tokenizer/index.js'; + +export const name = 'Url'; +export const structure = { + value: String +}; + +// <url-token> | <function-token> <string> ) +export function parse() { + const start = this.tokenStart; + let value; + + switch (this.tokenType) { + case Url: + value = url.decode(this.consume(Url)); + break; + + case FunctionToken: + if (!this.cmpStr(this.tokenStart, this.tokenEnd, 'url(')) { + this.error('Function name must be `url`'); + } + + this.eat(FunctionToken); + this.skipSC(); + value = string.decode(this.consume(StringToken)); + this.skipSC(); + if (!this.eof) { + this.eat(RightParenthesis); + } + break; + + default: + this.error('Url or Function is expected'); + } + + return { + type: 'Url', + loc: this.getLocation(start, this.tokenStart), + value + }; +} + +export function generate(node) { + this.token(Url, url.encode(node.value)); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/Value.js b/vanilla/node_modules/css-tree/lib/syntax/node/Value.js new file mode 100644 index 0000000..ba465bc --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/Value.js @@ -0,0 +1,19 @@ +export const name = 'Value'; +export const structure = { + children: [[]] +}; + +export function parse() { + const start = this.tokenStart; + const children = this.readSequence(this.scope.Value); + + return { + type: 'Value', + loc: this.getLocation(start, this.tokenStart), + children + }; +} + +export function generate(node) { + this.children(node); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/WhiteSpace.js b/vanilla/node_modules/css-tree/lib/syntax/node/WhiteSpace.js new file mode 100644 index 0000000..df34e6f --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/WhiteSpace.js @@ -0,0 +1,27 @@ +import { WhiteSpace } from '../../tokenizer/index.js'; + +const SPACE = Object.freeze({ + type: 'WhiteSpace', + loc: null, + value: ' ' +}); + +export const name = 'WhiteSpace'; +export const structure = { + value: String +}; + +export function parse() { + this.eat(WhiteSpace); + return SPACE; + + // return { + // type: 'WhiteSpace', + // loc: this.getLocation(this.tokenStart, this.tokenEnd), + // value: this.consume(WHITESPACE) + // }; +} + +export function generate(node) { + this.token(WhiteSpace, node.value); +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/index-generate.js b/vanilla/node_modules/css-tree/lib/syntax/node/index-generate.js new file mode 100644 index 0000000..568736f --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/index-generate.js @@ -0,0 +1,49 @@ +export { generate as AnPlusB } from './AnPlusB.js'; +export { generate as Atrule } from './Atrule.js'; +export { generate as AtrulePrelude } from './AtrulePrelude.js'; +export { generate as AttributeSelector } from './AttributeSelector.js'; +export { generate as Block } from './Block.js'; +export { generate as Brackets } from './Brackets.js'; +export { generate as CDC } from './CDC.js'; +export { generate as CDO } from './CDO.js'; +export { generate as ClassSelector } from './ClassSelector.js'; +export { generate as Combinator } from './Combinator.js'; +export { generate as Comment } from './Comment.js'; +export { generate as Condition } from './Condition.js'; +export { generate as Declaration } from './Declaration.js'; +export { generate as DeclarationList } from './DeclarationList.js'; +export { generate as Dimension } from './Dimension.js'; +export { generate as Feature } from './Feature.js'; +export { generate as FeatureFunction } from './FeatureFunction.js'; +export { generate as FeatureRange } from './FeatureRange.js'; +export { generate as Function } from './Function.js'; +export { generate as GeneralEnclosed } from './GeneralEnclosed.js'; +export { generate as Hash } from './Hash.js'; +export { generate as Identifier } from './Identifier.js'; +export { generate as IdSelector } from './IdSelector.js'; +export { generate as Layer } from './Layer.js'; +export { generate as LayerList } from './LayerList.js'; +export { generate as MediaQuery } from './MediaQuery.js'; +export { generate as MediaQueryList } from './MediaQueryList.js'; +export { generate as NestingSelector } from './NestingSelector.js'; +export { generate as Nth } from './Nth.js'; +export { generate as Number } from './Number.js'; +export { generate as Operator } from './Operator.js'; +export { generate as Parentheses } from './Parentheses.js'; +export { generate as Percentage } from './Percentage.js'; +export { generate as PseudoClassSelector } from './PseudoClassSelector.js'; +export { generate as PseudoElementSelector } from './PseudoElementSelector.js'; +export { generate as Ratio } from './Ratio.js'; +export { generate as Raw } from './Raw.js'; +export { generate as Rule } from './Rule.js'; +export { generate as Scope } from './Scope.js'; +export { generate as Selector } from './Selector.js'; +export { generate as SelectorList } from './SelectorList.js'; +export { generate as String } from './String.js'; +export { generate as StyleSheet } from './StyleSheet.js'; +export { generate as SupportsDeclaration } from './SupportsDeclaration.js'; +export { generate as TypeSelector } from './TypeSelector.js'; +export { generate as UnicodeRange } from './UnicodeRange.js'; +export { generate as Url } from './Url.js'; +export { generate as Value } from './Value.js'; +export { generate as WhiteSpace } from './WhiteSpace.js'; diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/index-parse-selector.js b/vanilla/node_modules/css-tree/lib/syntax/node/index-parse-selector.js new file mode 100644 index 0000000..3a0a2a3 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/index-parse-selector.js @@ -0,0 +1,17 @@ +export { parse as AnPlusB } from './AnPlusB.js'; +export { parse as AttributeSelector } from './AttributeSelector.js'; +export { parse as ClassSelector } from './ClassSelector.js'; +export { parse as Combinator } from './Combinator.js'; +export { parse as Identifier } from './Identifier.js'; +export { parse as IdSelector } from './IdSelector.js'; +export { parse as NestingSelector } from './NestingSelector.js'; +export { parse as Nth } from './Nth.js'; +export { parse as Operator } from './Operator.js'; +export { parse as Percentage } from './Percentage.js'; +export { parse as PseudoClassSelector } from './PseudoClassSelector.js'; +export { parse as PseudoElementSelector } from './PseudoElementSelector.js'; +export { parse as Raw } from './Raw.js'; +export { parse as Selector } from './Selector.js'; +export { parse as SelectorList } from './SelectorList.js'; +export { parse as String } from './String.js'; +export { parse as TypeSelector } from './TypeSelector.js'; diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/index-parse.js b/vanilla/node_modules/css-tree/lib/syntax/node/index-parse.js new file mode 100644 index 0000000..80136d3 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/index-parse.js @@ -0,0 +1,49 @@ +export { parse as AnPlusB } from './AnPlusB.js'; +export { parse as Atrule } from './Atrule.js'; +export { parse as AtrulePrelude } from './AtrulePrelude.js'; +export { parse as AttributeSelector } from './AttributeSelector.js'; +export { parse as Block } from './Block.js'; +export { parse as Brackets } from './Brackets.js'; +export { parse as CDC } from './CDC.js'; +export { parse as CDO } from './CDO.js'; +export { parse as ClassSelector } from './ClassSelector.js'; +export { parse as Combinator } from './Combinator.js'; +export { parse as Comment } from './Comment.js'; +export { parse as Condition } from './Condition.js'; +export { parse as Declaration } from './Declaration.js'; +export { parse as DeclarationList } from './DeclarationList.js'; +export { parse as Dimension } from './Dimension.js'; +export { parse as Feature } from './Feature.js'; +export { parse as FeatureFunction } from './FeatureFunction.js'; +export { parse as FeatureRange } from './FeatureRange.js'; +export { parse as Function } from './Function.js'; +export { parse as GeneralEnclosed } from './GeneralEnclosed.js'; +export { parse as Hash } from './Hash.js'; +export { parse as Identifier } from './Identifier.js'; +export { parse as IdSelector } from './IdSelector.js'; +export { parse as Layer } from './Layer.js'; +export { parse as LayerList } from './LayerList.js'; +export { parse as MediaQuery } from './MediaQuery.js'; +export { parse as MediaQueryList } from './MediaQueryList.js'; +export { parse as NestingSelector } from './NestingSelector.js'; +export { parse as Nth } from './Nth.js'; +export { parse as Number } from './Number.js'; +export { parse as Operator } from './Operator.js'; +export { parse as Parentheses } from './Parentheses.js'; +export { parse as Percentage } from './Percentage.js'; +export { parse as PseudoClassSelector } from './PseudoClassSelector.js'; +export { parse as PseudoElementSelector } from './PseudoElementSelector.js'; +export { parse as Ratio } from './Ratio.js'; +export { parse as Raw } from './Raw.js'; +export { parse as Rule } from './Rule.js'; +export { parse as Scope } from './Scope.js'; +export { parse as Selector } from './Selector.js'; +export { parse as SelectorList } from './SelectorList.js'; +export { parse as String } from './String.js'; +export { parse as StyleSheet } from './StyleSheet.js'; +export { parse as SupportsDeclaration } from './SupportsDeclaration.js'; +export { parse as TypeSelector } from './TypeSelector.js'; +export { parse as UnicodeRange } from './UnicodeRange.js'; +export { parse as Url } from './Url.js'; +export { parse as Value } from './Value.js'; +export { parse as WhiteSpace } from './WhiteSpace.js'; diff --git a/vanilla/node_modules/css-tree/lib/syntax/node/index.js b/vanilla/node_modules/css-tree/lib/syntax/node/index.js new file mode 100644 index 0000000..4aa2dea --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/node/index.js @@ -0,0 +1,49 @@ +export * as AnPlusB from './AnPlusB.js'; +export * as Atrule from './Atrule.js'; +export * as AtrulePrelude from './AtrulePrelude.js'; +export * as AttributeSelector from './AttributeSelector.js'; +export * as Block from './Block.js'; +export * as Brackets from './Brackets.js'; +export * as CDC from './CDC.js'; +export * as CDO from './CDO.js'; +export * as ClassSelector from './ClassSelector.js'; +export * as Combinator from './Combinator.js'; +export * as Comment from './Comment.js'; +export * as Condition from './Condition.js'; +export * as Declaration from './Declaration.js'; +export * as DeclarationList from './DeclarationList.js'; +export * as Dimension from './Dimension.js'; +export * as Feature from './Feature.js'; +export * as FeatureFunction from './FeatureFunction.js'; +export * as FeatureRange from './FeatureRange.js'; +export * as Function from './Function.js'; +export * as GeneralEnclosed from './GeneralEnclosed.js'; +export * as Hash from './Hash.js'; +export * as Identifier from './Identifier.js'; +export * as IdSelector from './IdSelector.js'; +export * as Layer from './Layer.js'; +export * as LayerList from './LayerList.js'; +export * as MediaQuery from './MediaQuery.js'; +export * as MediaQueryList from './MediaQueryList.js'; +export * as NestingSelector from './NestingSelector.js'; +export * as Nth from './Nth.js'; +export * as Number from './Number.js'; +export * as Operator from './Operator.js'; +export * as Parentheses from './Parentheses.js'; +export * as Percentage from './Percentage.js'; +export * as PseudoClassSelector from './PseudoClassSelector.js'; +export * as PseudoElementSelector from './PseudoElementSelector.js'; +export * as Ratio from './Ratio.js'; +export * as Raw from './Raw.js'; +export * as Rule from './Rule.js'; +export * as Scope from './Scope.js'; +export * as Selector from './Selector.js'; +export * as SelectorList from './SelectorList.js'; +export * as String from './String.js'; +export * as StyleSheet from './StyleSheet.js'; +export * as SupportsDeclaration from './SupportsDeclaration.js'; +export * as TypeSelector from './TypeSelector.js'; +export * as UnicodeRange from './UnicodeRange.js'; +export * as Url from './Url.js'; +export * as Value from './Value.js'; +export * as WhiteSpace from './WhiteSpace.js'; diff --git a/vanilla/node_modules/css-tree/lib/syntax/pseudo/index.js b/vanilla/node_modules/css-tree/lib/syntax/pseudo/index.js new file mode 100644 index 0000000..7d75fe1 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/pseudo/index.js @@ -0,0 +1,56 @@ +import { parseLanguageRangeList } from './lang.js'; + +const selectorList = { + parse() { + return this.createSingleNodeList( + this.SelectorList() + ); + } +}; + +const selector = { + parse() { + return this.createSingleNodeList( + this.Selector() + ); + } +}; + +const identList = { + parse() { + return this.createSingleNodeList( + this.Identifier() + ); + } +}; + +const langList = { + parse: parseLanguageRangeList +}; + +const nth = { + parse() { + return this.createSingleNodeList( + this.Nth() + ); + } +}; + +export default { + 'dir': identList, + 'has': selectorList, + 'lang': langList, + 'matches': selectorList, + 'is': selectorList, + '-moz-any': selectorList, + '-webkit-any': selectorList, + 'where': selectorList, + 'not': selectorList, + 'nth-child': nth, + 'nth-last-child': nth, + 'nth-last-of-type': nth, + 'nth-of-type': nth, + 'slotted': selector, + 'host': selector, + 'host-context': selector +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/pseudo/lang.js b/vanilla/node_modules/css-tree/lib/syntax/pseudo/lang.js new file mode 100644 index 0000000..3adfdb8 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/pseudo/lang.js @@ -0,0 +1,33 @@ +import { Comma, String as StringToken, Ident, RightParenthesis } from '../../tokenizer/index.js'; + +export function parseLanguageRangeList() { + const children = this.createList(); + + this.skipSC(); + + loop: while (!this.eof) { + switch (this.tokenType) { + case Ident: + children.push(this.Identifier()); + break; + + case StringToken: + children.push(this.String()); + break; + + case Comma: + children.push(this.Operator()); + break; + + case RightParenthesis: + break loop; + + default: + this.error('Identifier, string or comma is expected'); + } + + this.skipSC(); + } + + return children; +} diff --git a/vanilla/node_modules/css-tree/lib/syntax/scope/atrulePrelude.js b/vanilla/node_modules/css-tree/lib/syntax/scope/atrulePrelude.js new file mode 100644 index 0000000..c903555 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/scope/atrulePrelude.js @@ -0,0 +1,5 @@ +import getNode from './default.js'; + +export default { + getNode +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/scope/default.js b/vanilla/node_modules/css-tree/lib/syntax/scope/default.js new file mode 100644 index 0000000..8f14035 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/scope/default.js @@ -0,0 +1,85 @@ +import { + Ident, + String as StringToken, + Number as NumberToken, + Function as FunctionToken, + Url, + Hash, + Dimension, + Percentage, + LeftParenthesis, + LeftSquareBracket, + Comma, + Delim +} from '../../tokenizer/index.js'; + +const NUMBERSIGN = 0x0023; // U+0023 NUMBER SIGN (#) +const ASTERISK = 0x002A; // U+002A ASTERISK (*) +const PLUSSIGN = 0x002B; // U+002B PLUS SIGN (+) +const HYPHENMINUS = 0x002D; // U+002D HYPHEN-MINUS (-) +const SOLIDUS = 0x002F; // U+002F SOLIDUS (/) +const U = 0x0075; // U+0075 LATIN SMALL LETTER U (u) + +export default function defaultRecognizer(context) { + switch (this.tokenType) { + case Hash: + return this.Hash(); + + case Comma: + return this.Operator(); + + case LeftParenthesis: + return this.Parentheses(this.readSequence, context.recognizer); + + case LeftSquareBracket: + return this.Brackets(this.readSequence, context.recognizer); + + case StringToken: + return this.String(); + + case Dimension: + return this.Dimension(); + + case Percentage: + return this.Percentage(); + + case NumberToken: + return this.Number(); + + case FunctionToken: + return this.cmpStr(this.tokenStart, this.tokenEnd, 'url(') + ? this.Url() + : this.Function(this.readSequence, context.recognizer); + + case Url: + return this.Url(); + + case Ident: + // check for unicode range, it should start with u+ or U+ + if (this.cmpChar(this.tokenStart, U) && + this.cmpChar(this.tokenStart + 1, PLUSSIGN)) { + return this.UnicodeRange(); + } else { + return this.Identifier(); + } + + case Delim: { + const code = this.charCodeAt(this.tokenStart); + + if (code === SOLIDUS || + code === ASTERISK || + code === PLUSSIGN || + code === HYPHENMINUS) { + return this.Operator(); // TODO: replace with Delim + } + + // TODO: produce a node with Delim node type + + if (code === NUMBERSIGN) { + this.error('Hex or identifier is expected', this.tokenStart + 1); + } + + break; + } + } +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/scope/index.js b/vanilla/node_modules/css-tree/lib/syntax/scope/index.js new file mode 100644 index 0000000..6dabbbe --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/scope/index.js @@ -0,0 +1,3 @@ +export { default as AtrulePrelude } from './atrulePrelude.js'; +export { default as Selector } from './selector.js'; +export { default as Value } from './value.js'; diff --git a/vanilla/node_modules/css-tree/lib/syntax/scope/selector.js b/vanilla/node_modules/css-tree/lib/syntax/scope/selector.js new file mode 100644 index 0000000..a8efcd5 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/scope/selector.js @@ -0,0 +1,94 @@ +import { + Delim, + Ident, + Dimension, + Percentage, + Number as NumberToken, + Hash, + Colon, + LeftSquareBracket +} from '../../tokenizer/index.js'; + +const NUMBERSIGN = 0x0023; // U+0023 NUMBER SIGN (#) +const AMPERSAND = 0x0026; // U+0026 AMPERSAND (&) +const ASTERISK = 0x002A; // U+002A ASTERISK (*) +const PLUSSIGN = 0x002B; // U+002B PLUS SIGN (+) +const SOLIDUS = 0x002F; // U+002F SOLIDUS (/) +const FULLSTOP = 0x002E; // U+002E FULL STOP (.) +const GREATERTHANSIGN = 0x003E; // U+003E GREATER-THAN SIGN (>) +const VERTICALLINE = 0x007C; // U+007C VERTICAL LINE (|) +const TILDE = 0x007E; // U+007E TILDE (~) + +function onWhiteSpace(next, children) { + if (children.last !== null && children.last.type !== 'Combinator' && + next !== null && next.type !== 'Combinator') { + children.push({ // FIXME: this.Combinator() should be used instead + type: 'Combinator', + loc: null, + name: ' ' + }); + } +} + +function getNode() { + switch (this.tokenType) { + case LeftSquareBracket: + return this.AttributeSelector(); + + case Hash: + return this.IdSelector(); + + case Colon: + if (this.lookupType(1) === Colon) { + return this.PseudoElementSelector(); + } else { + return this.PseudoClassSelector(); + } + + case Ident: + return this.TypeSelector(); + + case NumberToken: + case Percentage: + return this.Percentage(); + + case Dimension: + // throws when .123ident + if (this.charCodeAt(this.tokenStart) === FULLSTOP) { + this.error('Identifier is expected', this.tokenStart + 1); + } + break; + + case Delim: { + const code = this.charCodeAt(this.tokenStart); + + switch (code) { + case PLUSSIGN: + case GREATERTHANSIGN: + case TILDE: + case SOLIDUS: // /deep/ + return this.Combinator(); + + case FULLSTOP: + return this.ClassSelector(); + + case ASTERISK: + case VERTICALLINE: + return this.TypeSelector(); + + case NUMBERSIGN: + return this.IdSelector(); + + case AMPERSAND: + return this.NestingSelector(); + } + + break; + } + } +}; + +export default { + onWhiteSpace, + getNode +}; diff --git a/vanilla/node_modules/css-tree/lib/syntax/scope/value.js b/vanilla/node_modules/css-tree/lib/syntax/scope/value.js new file mode 100644 index 0000000..dc94219 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/syntax/scope/value.js @@ -0,0 +1,25 @@ +import getNode from './default.js'; +import expressionFn from '../function/expression.js'; +import varFn from '../function/var.js'; + +function isPlusMinusOperator(node) { + return ( + node !== null && + node.type === 'Operator' && + (node.value[node.value.length - 1] === '-' || node.value[node.value.length - 1] === '+') + ); +} + +export default { + getNode, + onWhiteSpace(next, children) { + if (isPlusMinusOperator(next)) { + next.value = ' ' + next.value; + } + if (isPlusMinusOperator(children.last)) { + children.last.value += ' '; + } + }, + 'expression': expressionFn, + 'var': varFn +}; diff --git a/vanilla/node_modules/css-tree/lib/tokenizer/OffsetToLocation.js b/vanilla/node_modules/css-tree/lib/tokenizer/OffsetToLocation.js new file mode 100644 index 0000000..cc584c0 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/tokenizer/OffsetToLocation.js @@ -0,0 +1,87 @@ +import { adoptBuffer } from './adopt-buffer.js'; +import { isBOM } from './char-code-definitions.js'; + +const N = 10; +const F = 12; +const R = 13; + +function computeLinesAndColumns(host) { + const source = host.source; + const sourceLength = source.length; + const startOffset = source.length > 0 ? isBOM(source.charCodeAt(0)) : 0; + const lines = adoptBuffer(host.lines, sourceLength); + const columns = adoptBuffer(host.columns, sourceLength); + let line = host.startLine; + let column = host.startColumn; + + for (let i = startOffset; i < sourceLength; i++) { + const code = source.charCodeAt(i); + + lines[i] = line; + columns[i] = column++; + + if (code === N || code === R || code === F) { + if (code === R && i + 1 < sourceLength && source.charCodeAt(i + 1) === N) { + i++; + lines[i] = line; + columns[i] = column; + } + + line++; + column = 1; + } + } + + lines[sourceLength] = line; + columns[sourceLength] = column; + + host.lines = lines; + host.columns = columns; + host.computed = true; +} + +export class OffsetToLocation { + constructor(source, startOffset, startLine, startColumn) { + this.setSource(source, startOffset, startLine, startColumn); + this.lines = null; + this.columns = null; + } + setSource(source = '', startOffset = 0, startLine = 1, startColumn = 1) { + this.source = source; + this.startOffset = startOffset; + this.startLine = startLine; + this.startColumn = startColumn; + this.computed = false; + } + getLocation(offset, filename) { + if (!this.computed) { + computeLinesAndColumns(this); + } + + return { + source: filename, + offset: this.startOffset + offset, + line: this.lines[offset], + column: this.columns[offset] + }; + } + getLocationRange(start, end, filename) { + if (!this.computed) { + computeLinesAndColumns(this); + } + + return { + source: filename, + start: { + offset: this.startOffset + start, + line: this.lines[start], + column: this.columns[start] + }, + end: { + offset: this.startOffset + end, + line: this.lines[end], + column: this.columns[end] + } + }; + } +}; diff --git a/vanilla/node_modules/css-tree/lib/tokenizer/TokenStream.js b/vanilla/node_modules/css-tree/lib/tokenizer/TokenStream.js new file mode 100644 index 0000000..96d48b7 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/tokenizer/TokenStream.js @@ -0,0 +1,316 @@ +import { adoptBuffer } from './adopt-buffer.js'; +import { cmpStr } from './utils.js'; +import tokenNames from './names.js'; +import { + WhiteSpace, + Comment, + Delim, + EOF, + Function as FunctionToken, + LeftParenthesis, + RightParenthesis, + LeftSquareBracket, + RightSquareBracket, + LeftCurlyBracket, + RightCurlyBracket +} from './types.js'; + +const OFFSET_MASK = 0x00FFFFFF; +const TYPE_SHIFT = 24; +const balancePair = new Uint8Array(32); // 32b of memory ought to be enough for anyone (any number of tokens) +balancePair[FunctionToken] = RightParenthesis; +balancePair[LeftParenthesis] = RightParenthesis; +balancePair[LeftSquareBracket] = RightSquareBracket; +balancePair[LeftCurlyBracket] = RightCurlyBracket; + +function isBlockOpenerToken(tokenType) { + return balancePair[tokenType] !== 0; +} + +export class TokenStream { + constructor(source, tokenize) { + this.setSource(source, tokenize); + } + reset() { + this.eof = false; + this.tokenIndex = -1; + this.tokenType = 0; + this.tokenStart = this.firstCharOffset; + this.tokenEnd = this.firstCharOffset; + } + setSource(source = '', tokenize = () => {}) { + source = String(source || ''); + + const sourceLength = source.length; + const offsetAndType = adoptBuffer(this.offsetAndType, source.length + 1); // +1 because of eof-token + const balance = adoptBuffer(this.balance, source.length + 1); + let tokenCount = 0; + let firstCharOffset = -1; + let balanceCloseType = 0; + let balanceStart = source.length; + + // capture buffers + this.offsetAndType = null; + this.balance = null; + balance.fill(0); + + tokenize(source, (type, start, end) => { + const index = tokenCount++; + + // type & offset + offsetAndType[index] = (type << TYPE_SHIFT) | end; + + if (firstCharOffset === -1) { + firstCharOffset = start; + } + + // balance + balance[index] = balanceStart; + + if (type === balanceCloseType) { + const prevBalanceStart = balance[balanceStart]; + + // set reference to balance end for a block opener + balance[balanceStart] = index; + + // pop state + balanceStart = prevBalanceStart; + balanceCloseType = balancePair[offsetAndType[prevBalanceStart] >> TYPE_SHIFT]; + } else if (isBlockOpenerToken(type)) { // check for FunctionToken, <(-token>, <[-token> and <{-token> + // push state + balanceStart = index; + balanceCloseType = balancePair[type]; + } + }); + + // finalize buffers + offsetAndType[tokenCount] = (EOF << TYPE_SHIFT) | sourceLength; // <EOF-token> + balance[tokenCount] = tokenCount; // prevents false positive balance match with any token + + // reverse references from balance start to end + // tokens + // token: a ( [ b c ] d e ) { + // index: 0 1 2 3 4 5 6 7 8 9 + // before + // balance: 0 8 5 2 2 2 1 1 1 0 + // - > > < < < < < < - + // after + // balance: 9 8 5 5 5 2 8 8 1 9 + // > > > > > < > > < > + for (let i = 0; i < tokenCount; i++) { + const balanceStart = balance[i]; + + if (balanceStart <= i) { + const balanceEnd = balance[balanceStart]; + + if (balanceEnd !== i) { + balance[i] = balanceEnd; + } + } else if (balanceStart > tokenCount) { + balance[i] = tokenCount; + } + } + + // balance[0] = tokenCount; + + this.source = source; + this.firstCharOffset = firstCharOffset === -1 ? 0 : firstCharOffset; + this.tokenCount = tokenCount; + this.offsetAndType = offsetAndType; + this.balance = balance; + + this.reset(); + this.next(); + } + + lookupType(offset) { + offset += this.tokenIndex; + + if (offset < this.tokenCount) { + return this.offsetAndType[offset] >> TYPE_SHIFT; + } + + return EOF; + } + lookupTypeNonSC(idx) { + for (let offset = this.tokenIndex; offset < this.tokenCount; offset++) { + const tokenType = this.offsetAndType[offset] >> TYPE_SHIFT; + + if (tokenType !== WhiteSpace && tokenType !== Comment) { + if (idx-- === 0) { + return tokenType; + } + } + } + + return EOF; + } + lookupOffset(offset) { + offset += this.tokenIndex; + + if (offset < this.tokenCount) { + return this.offsetAndType[offset - 1] & OFFSET_MASK; + } + + return this.source.length; + } + lookupOffsetNonSC(idx) { + for (let offset = this.tokenIndex; offset < this.tokenCount; offset++) { + const tokenType = this.offsetAndType[offset] >> TYPE_SHIFT; + + if (tokenType !== WhiteSpace && tokenType !== Comment) { + if (idx-- === 0) { + return offset - this.tokenIndex; + } + } + } + + return EOF; + } + lookupValue(offset, referenceStr) { + offset += this.tokenIndex; + + if (offset < this.tokenCount) { + return cmpStr( + this.source, + this.offsetAndType[offset - 1] & OFFSET_MASK, + this.offsetAndType[offset] & OFFSET_MASK, + referenceStr + ); + } + + return false; + } + getTokenStart(tokenIndex) { + if (tokenIndex === this.tokenIndex) { + return this.tokenStart; + } + + if (tokenIndex > 0) { + return tokenIndex < this.tokenCount + ? this.offsetAndType[tokenIndex - 1] & OFFSET_MASK + : this.offsetAndType[this.tokenCount] & OFFSET_MASK; + } + + return this.firstCharOffset; + } + substrToCursor(start) { + return this.source.substring(start, this.tokenStart); + } + + isBalanceEdge(pos) { + return this.balance[this.tokenIndex] < pos; + // return this.balance[this.balance[pos]] !== this.tokenIndex; + } + isDelim(code, offset) { + if (offset) { + return ( + this.lookupType(offset) === Delim && + this.source.charCodeAt(this.lookupOffset(offset)) === code + ); + } + + return ( + this.tokenType === Delim && + this.source.charCodeAt(this.tokenStart) === code + ); + } + + skip(tokenCount) { + let next = this.tokenIndex + tokenCount; + + if (next < this.tokenCount) { + this.tokenIndex = next; + this.tokenStart = this.offsetAndType[next - 1] & OFFSET_MASK; + next = this.offsetAndType[next]; + this.tokenType = next >> TYPE_SHIFT; + this.tokenEnd = next & OFFSET_MASK; + } else { + this.tokenIndex = this.tokenCount; + this.next(); + } + } + next() { + let next = this.tokenIndex + 1; + + if (next < this.tokenCount) { + this.tokenIndex = next; + this.tokenStart = this.tokenEnd; + next = this.offsetAndType[next]; + this.tokenType = next >> TYPE_SHIFT; + this.tokenEnd = next & OFFSET_MASK; + } else { + this.eof = true; + this.tokenIndex = this.tokenCount; + this.tokenType = EOF; + this.tokenStart = this.tokenEnd = this.source.length; + } + } + skipSC() { + while (this.tokenType === WhiteSpace || this.tokenType === Comment) { + this.next(); + } + } + skipUntilBalanced(startToken, stopConsume) { + let cursor = startToken; + let balanceEnd = 0; + let offset = 0; + + loop: + for (; cursor < this.tokenCount; cursor++) { + balanceEnd = this.balance[cursor]; + + // stop scanning on balance edge that points to offset before start token + if (balanceEnd < startToken) { + break loop; + } + + offset = cursor > 0 ? this.offsetAndType[cursor - 1] & OFFSET_MASK : this.firstCharOffset; + + // check stop condition + switch (stopConsume(this.source.charCodeAt(offset))) { + case 1: // just stop + break loop; + + case 2: // stop & included + cursor++; + break loop; + + default: + // fast forward to the end of balanced block for an open block tokens + if (isBlockOpenerToken(this.offsetAndType[cursor] >> TYPE_SHIFT)) { + cursor = balanceEnd; + } + } + } + + this.skip(cursor - this.tokenIndex); + } + + forEachToken(fn) { + for (let i = 0, offset = this.firstCharOffset; i < this.tokenCount; i++) { + const start = offset; + const item = this.offsetAndType[i]; + const end = item & OFFSET_MASK; + const type = item >> TYPE_SHIFT; + + offset = end; + + fn(type, start, end, i); + } + } + dump() { + const tokens = new Array(this.tokenCount); + + this.forEachToken((type, start, end, index) => { + tokens[index] = { + idx: index, + type: tokenNames[type], + chunk: this.source.substring(start, end), + balance: this.balance[index] + }; + }); + + return tokens; + } +}; diff --git a/vanilla/node_modules/css-tree/lib/tokenizer/adopt-buffer.js b/vanilla/node_modules/css-tree/lib/tokenizer/adopt-buffer.js new file mode 100644 index 0000000..ab4566d --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/tokenizer/adopt-buffer.js @@ -0,0 +1,9 @@ +const MIN_SIZE = 16 * 1024; + +export function adoptBuffer(buffer = null, size) { + if (buffer === null || buffer.length < size) { + return new Uint32Array(Math.max(size + 1024, MIN_SIZE)); + } + + return buffer; +}; diff --git a/vanilla/node_modules/css-tree/lib/tokenizer/char-code-definitions.js b/vanilla/node_modules/css-tree/lib/tokenizer/char-code-definitions.js new file mode 100644 index 0000000..715572a --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/tokenizer/char-code-definitions.js @@ -0,0 +1,212 @@ +const EOF = 0; + +// https://drafts.csswg.org/css-syntax-3/ +// § 4.2. Definitions + +// digit +// A code point between U+0030 DIGIT ZERO (0) and U+0039 DIGIT NINE (9). +export function isDigit(code) { + return code >= 0x0030 && code <= 0x0039; +} + +// hex digit +// A digit, or a code point between U+0041 LATIN CAPITAL LETTER A (A) and U+0046 LATIN CAPITAL LETTER F (F), +// or a code point between U+0061 LATIN SMALL LETTER A (a) and U+0066 LATIN SMALL LETTER F (f). +export function isHexDigit(code) { + return ( + isDigit(code) || // 0 .. 9 + (code >= 0x0041 && code <= 0x0046) || // A .. F + (code >= 0x0061 && code <= 0x0066) // a .. f + ); +} + +// uppercase letter +// A code point between U+0041 LATIN CAPITAL LETTER A (A) and U+005A LATIN CAPITAL LETTER Z (Z). +export function isUppercaseLetter(code) { + return code >= 0x0041 && code <= 0x005A; +} + +// lowercase letter +// A code point between U+0061 LATIN SMALL LETTER A (a) and U+007A LATIN SMALL LETTER Z (z). +export function isLowercaseLetter(code) { + return code >= 0x0061 && code <= 0x007A; +} + +// letter +// An uppercase letter or a lowercase letter. +export function isLetter(code) { + return isUppercaseLetter(code) || isLowercaseLetter(code); +} + +// non-ASCII code point +// A code point with a value equal to or greater than U+0080 <control>. +// +// 2024-09-02: The latest spec narrows the range for non-ASCII characters (see https://github.com/csstree/csstree/issues/188). +// However, all modern browsers support a wider range, and strictly following the latest spec could result +// in some CSS being parsed incorrectly, even though it works in the browser. Therefore, this function adheres +// to the previous, broader definition of non-ASCII characters. +export function isNonAscii(code) { + return code >= 0x0080; +} + +// name-start code point +// A letter, a non-ASCII code point, or U+005F LOW LINE (_). +export function isNameStart(code) { + return isLetter(code) || isNonAscii(code) || code === 0x005F; +} + +// name code point +// A name-start code point, a digit, or U+002D HYPHEN-MINUS (-). +export function isName(code) { + return isNameStart(code) || isDigit(code) || code === 0x002D; +} + +// non-printable code point +// A code point between U+0000 NULL and U+0008 BACKSPACE, or U+000B LINE TABULATION, +// or a code point between U+000E SHIFT OUT and U+001F INFORMATION SEPARATOR ONE, or U+007F DELETE. +export function isNonPrintable(code) { + return ( + (code >= 0x0000 && code <= 0x0008) || + (code === 0x000B) || + (code >= 0x000E && code <= 0x001F) || + (code === 0x007F) + ); +} + +// newline +// U+000A LINE FEED. Note that U+000D CARRIAGE RETURN and U+000C FORM FEED are not included in this definition, +// as they are converted to U+000A LINE FEED during preprocessing. +// TODO: we doesn't do a preprocessing, so check a code point for U+000D CARRIAGE RETURN and U+000C FORM FEED +export function isNewline(code) { + return code === 0x000A || code === 0x000D || code === 0x000C; +} + +// whitespace +// A newline, U+0009 CHARACTER TABULATION, or U+0020 SPACE. +export function isWhiteSpace(code) { + return isNewline(code) || code === 0x0020 || code === 0x0009; +} + +// § 4.3.8. Check if two code points are a valid escape +export function isValidEscape(first, second) { + // If the first code point is not U+005C REVERSE SOLIDUS (\), return false. + if (first !== 0x005C) { + return false; + } + + // Otherwise, if the second code point is a newline or EOF, return false. + if (isNewline(second) || second === EOF) { + return false; + } + + // Otherwise, return true. + return true; +} + +// § 4.3.9. Check if three code points would start an identifier +export function isIdentifierStart(first, second, third) { + // Look at the first code point: + + // U+002D HYPHEN-MINUS + if (first === 0x002D) { + // If the second code point is a name-start code point or a U+002D HYPHEN-MINUS, + // or the second and third code points are a valid escape, return true. Otherwise, return false. + return ( + isNameStart(second) || + second === 0x002D || + isValidEscape(second, third) + ); + } + + // name-start code point + if (isNameStart(first)) { + // Return true. + return true; + } + + // U+005C REVERSE SOLIDUS (\) + if (first === 0x005C) { + // If the first and second code points are a valid escape, return true. Otherwise, return false. + return isValidEscape(first, second); + } + + // anything else + // Return false. + return false; +} + +// § 4.3.10. Check if three code points would start a number +export function isNumberStart(first, second, third) { + // Look at the first code point: + + // U+002B PLUS SIGN (+) + // U+002D HYPHEN-MINUS (-) + if (first === 0x002B || first === 0x002D) { + // If the second code point is a digit, return true. + if (isDigit(second)) { + return 2; + } + + // Otherwise, if the second code point is a U+002E FULL STOP (.) + // and the third code point is a digit, return true. + // Otherwise, return false. + return second === 0x002E && isDigit(third) ? 3 : 0; + } + + // U+002E FULL STOP (.) + if (first === 0x002E) { + // If the second code point is a digit, return true. Otherwise, return false. + return isDigit(second) ? 2 : 0; + } + + // digit + if (isDigit(first)) { + // Return true. + return 1; + } + + // anything else + // Return false. + return 0; +} + +// +// Misc +// + +// detect BOM (https://en.wikipedia.org/wiki/Byte_order_mark) +export function isBOM(code) { + // UTF-16BE + if (code === 0xFEFF) { + return 1; + } + + // UTF-16LE + if (code === 0xFFFE) { + return 1; + } + + return 0; +} + +// Fast code category +// Only ASCII code points has a special meaning, that's why we define a maps for 0..127 codes only +const CATEGORY = new Array(0x80); +export const EofCategory = 0x80; +export const WhiteSpaceCategory = 0x82; +export const DigitCategory = 0x83; +export const NameStartCategory = 0x84; +export const NonPrintableCategory = 0x85; + +for (let i = 0; i < CATEGORY.length; i++) { + CATEGORY[i] = + isWhiteSpace(i) && WhiteSpaceCategory || + isDigit(i) && DigitCategory || + isNameStart(i) && NameStartCategory || + isNonPrintable(i) && NonPrintableCategory || + i || EofCategory; +} + +export function charCodeCategory(code) { + return code < 0x80 ? CATEGORY[code] : NameStartCategory; +} diff --git a/vanilla/node_modules/css-tree/lib/tokenizer/index.js b/vanilla/node_modules/css-tree/lib/tokenizer/index.js new file mode 100644 index 0000000..16df44c --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/tokenizer/index.js @@ -0,0 +1,513 @@ +import * as TYPE from './types.js'; +import { + isNewline, + isName, + isValidEscape, + isNumberStart, + isIdentifierStart, + isBOM, + charCodeCategory, + WhiteSpaceCategory, + DigitCategory, + NameStartCategory, + NonPrintableCategory +} from './char-code-definitions.js'; +import { + cmpStr, + getNewlineLength, + findWhiteSpaceEnd, + consumeEscaped, + consumeName, + consumeNumber, + consumeBadUrlRemnants +} from './utils.js'; + +export function tokenize(source, onToken) { + function getCharCode(offset) { + return offset < sourceLength ? source.charCodeAt(offset) : 0; + } + + // § 4.3.3. Consume a numeric token + function consumeNumericToken() { + // Consume a number and let number be the result. + offset = consumeNumber(source, offset); + + // If the next 3 input code points would start an identifier, then: + if (isIdentifierStart(getCharCode(offset), getCharCode(offset + 1), getCharCode(offset + 2))) { + // Create a <dimension-token> with the same value and type flag as number, and a unit set initially to the empty string. + // Consume a name. Set the <dimension-token>’s unit to the returned value. + // Return the <dimension-token>. + type = TYPE.Dimension; + offset = consumeName(source, offset); + return; + } + + // Otherwise, if the next input code point is U+0025 PERCENTAGE SIGN (%), consume it. + if (getCharCode(offset) === 0x0025) { + // Create a <percentage-token> with the same value as number, and return it. + type = TYPE.Percentage; + offset++; + return; + } + + // Otherwise, create a <number-token> with the same value and type flag as number, and return it. + type = TYPE.Number; + } + + // § 4.3.4. Consume an ident-like token + function consumeIdentLikeToken() { + const nameStartOffset = offset; + + // Consume a name, and let string be the result. + offset = consumeName(source, offset); + + // If string’s value is an ASCII case-insensitive match for "url", + // and the next input code point is U+0028 LEFT PARENTHESIS ((), consume it. + if (cmpStr(source, nameStartOffset, offset, 'url') && getCharCode(offset) === 0x0028) { + // While the next two input code points are whitespace, consume the next input code point. + offset = findWhiteSpaceEnd(source, offset + 1); + + // If the next one or two input code points are U+0022 QUOTATION MARK ("), U+0027 APOSTROPHE ('), + // or whitespace followed by U+0022 QUOTATION MARK (") or U+0027 APOSTROPHE ('), + // then create a <function-token> with its value set to string and return it. + if (getCharCode(offset) === 0x0022 || + getCharCode(offset) === 0x0027) { + type = TYPE.Function; + offset = nameStartOffset + 4; + return; + } + + // Otherwise, consume a url token, and return it. + consumeUrlToken(); + return; + } + + // Otherwise, if the next input code point is U+0028 LEFT PARENTHESIS ((), consume it. + // Create a <function-token> with its value set to string and return it. + if (getCharCode(offset) === 0x0028) { + type = TYPE.Function; + offset++; + return; + } + + // Otherwise, create an <ident-token> with its value set to string and return it. + type = TYPE.Ident; + } + + // § 4.3.5. Consume a string token + function consumeStringToken(endingCodePoint) { + // This algorithm may be called with an ending code point, which denotes the code point + // that ends the string. If an ending code point is not specified, + // the current input code point is used. + if (!endingCodePoint) { + endingCodePoint = getCharCode(offset++); + } + + // Initially create a <string-token> with its value set to the empty string. + type = TYPE.String; + + // Repeatedly consume the next input code point from the stream: + for (; offset < source.length; offset++) { + const code = source.charCodeAt(offset); + + switch (charCodeCategory(code)) { + // ending code point + case endingCodePoint: + // Return the <string-token>. + offset++; + return; + + // EOF + // case EofCategory: + // This is a parse error. Return the <string-token>. + // return; + + // newline + case WhiteSpaceCategory: + if (isNewline(code)) { + // This is a parse error. Reconsume the current input code point, + // create a <bad-string-token>, and return it. + offset += getNewlineLength(source, offset, code); + type = TYPE.BadString; + return; + } + break; + + // U+005C REVERSE SOLIDUS (\) + case 0x005C: + // If the next input code point is EOF, do nothing. + if (offset === source.length - 1) { + break; + } + + const nextCode = getCharCode(offset + 1); + + // Otherwise, if the next input code point is a newline, consume it. + if (isNewline(nextCode)) { + offset += getNewlineLength(source, offset + 1, nextCode); + } else if (isValidEscape(code, nextCode)) { + // Otherwise, (the stream starts with a valid escape) consume + // an escaped code point and append the returned code point to + // the <string-token>’s value. + offset = consumeEscaped(source, offset) - 1; + } + break; + + // anything else + // Append the current input code point to the <string-token>’s value. + } + } + } + + // § 4.3.6. Consume a url token + // Note: This algorithm assumes that the initial "url(" has already been consumed. + // This algorithm also assumes that it’s being called to consume an "unquoted" value, like url(foo). + // A quoted value, like url("foo"), is parsed as a <function-token>. Consume an ident-like token + // automatically handles this distinction; this algorithm shouldn’t be called directly otherwise. + function consumeUrlToken() { + // Initially create a <url-token> with its value set to the empty string. + type = TYPE.Url; + + // Consume as much whitespace as possible. + offset = findWhiteSpaceEnd(source, offset); + + // Repeatedly consume the next input code point from the stream: + for (; offset < source.length; offset++) { + const code = source.charCodeAt(offset); + + switch (charCodeCategory(code)) { + // U+0029 RIGHT PARENTHESIS ()) + case 0x0029: + // Return the <url-token>. + offset++; + return; + + // EOF + // case EofCategory: + // This is a parse error. Return the <url-token>. + // return; + + // whitespace + case WhiteSpaceCategory: + // Consume as much whitespace as possible. + offset = findWhiteSpaceEnd(source, offset); + + // If the next input code point is U+0029 RIGHT PARENTHESIS ()) or EOF, + // consume it and return the <url-token> + // (if EOF was encountered, this is a parse error); + if (getCharCode(offset) === 0x0029 || offset >= source.length) { + if (offset < source.length) { + offset++; + } + return; + } + + // otherwise, consume the remnants of a bad url, create a <bad-url-token>, + // and return it. + offset = consumeBadUrlRemnants(source, offset); + type = TYPE.BadUrl; + return; + + // U+0022 QUOTATION MARK (") + // U+0027 APOSTROPHE (') + // U+0028 LEFT PARENTHESIS (() + // non-printable code point + case 0x0022: + case 0x0027: + case 0x0028: + case NonPrintableCategory: + // This is a parse error. Consume the remnants of a bad url, + // create a <bad-url-token>, and return it. + offset = consumeBadUrlRemnants(source, offset); + type = TYPE.BadUrl; + return; + + // U+005C REVERSE SOLIDUS (\) + case 0x005C: + // If the stream starts with a valid escape, consume an escaped code point and + // append the returned code point to the <url-token>’s value. + if (isValidEscape(code, getCharCode(offset + 1))) { + offset = consumeEscaped(source, offset) - 1; + break; + } + + // Otherwise, this is a parse error. Consume the remnants of a bad url, + // create a <bad-url-token>, and return it. + offset = consumeBadUrlRemnants(source, offset); + type = TYPE.BadUrl; + return; + + // anything else + // Append the current input code point to the <url-token>’s value. + } + } + } + + // ensure source is a string + source = String(source || ''); + + const sourceLength = source.length; + let start = isBOM(getCharCode(0)); + let offset = start; + let type; + + // https://drafts.csswg.org/css-syntax-3/#consume-token + // § 4.3.1. Consume a token + while (offset < sourceLength) { + const code = source.charCodeAt(offset); + + switch (charCodeCategory(code)) { + // whitespace + case WhiteSpaceCategory: + // Consume as much whitespace as possible. Return a <whitespace-token>. + type = TYPE.WhiteSpace; + offset = findWhiteSpaceEnd(source, offset + 1); + break; + + // U+0022 QUOTATION MARK (") + case 0x0022: + // Consume a string token and return it. + consumeStringToken(); + break; + + // U+0023 NUMBER SIGN (#) + case 0x0023: + // If the next input code point is a name code point or the next two input code points are a valid escape, then: + if (isName(getCharCode(offset + 1)) || isValidEscape(getCharCode(offset + 1), getCharCode(offset + 2))) { + // Create a <hash-token>. + type = TYPE.Hash; + + // If the next 3 input code points would start an identifier, set the <hash-token>’s type flag to "id". + // if (isIdentifierStart(getCharCode(offset + 1), getCharCode(offset + 2), getCharCode(offset + 3))) { + // // TODO: set id flag + // } + + // Consume a name, and set the <hash-token>’s value to the returned string. + offset = consumeName(source, offset + 1); + + // Return the <hash-token>. + } else { + // Otherwise, return a <delim-token> with its value set to the current input code point. + type = TYPE.Delim; + offset++; + } + + break; + + // U+0027 APOSTROPHE (') + case 0x0027: + // Consume a string token and return it. + consumeStringToken(); + break; + + // U+0028 LEFT PARENTHESIS (() + case 0x0028: + // Return a <(-token>. + type = TYPE.LeftParenthesis; + offset++; + break; + + // U+0029 RIGHT PARENTHESIS ()) + case 0x0029: + // Return a <)-token>. + type = TYPE.RightParenthesis; + offset++; + break; + + // U+002B PLUS SIGN (+) + case 0x002B: + // If the input stream starts with a number, ... + if (isNumberStart(code, getCharCode(offset + 1), getCharCode(offset + 2))) { + // ... reconsume the current input code point, consume a numeric token, and return it. + consumeNumericToken(); + } else { + // Otherwise, return a <delim-token> with its value set to the current input code point. + type = TYPE.Delim; + offset++; + } + break; + + // U+002C COMMA (,) + case 0x002C: + // Return a <comma-token>. + type = TYPE.Comma; + offset++; + break; + + // U+002D HYPHEN-MINUS (-) + case 0x002D: + // If the input stream starts with a number, reconsume the current input code point, consume a numeric token, and return it. + if (isNumberStart(code, getCharCode(offset + 1), getCharCode(offset + 2))) { + consumeNumericToken(); + } else { + // Otherwise, if the next 2 input code points are U+002D HYPHEN-MINUS U+003E GREATER-THAN SIGN (->), consume them and return a <CDC-token>. + if (getCharCode(offset + 1) === 0x002D && + getCharCode(offset + 2) === 0x003E) { + type = TYPE.CDC; + offset = offset + 3; + } else { + // Otherwise, if the input stream starts with an identifier, ... + if (isIdentifierStart(code, getCharCode(offset + 1), getCharCode(offset + 2))) { + // ... reconsume the current input code point, consume an ident-like token, and return it. + consumeIdentLikeToken(); + } else { + // Otherwise, return a <delim-token> with its value set to the current input code point. + type = TYPE.Delim; + offset++; + } + } + } + break; + + // U+002E FULL STOP (.) + case 0x002E: + // If the input stream starts with a number, ... + if (isNumberStart(code, getCharCode(offset + 1), getCharCode(offset + 2))) { + // ... reconsume the current input code point, consume a numeric token, and return it. + consumeNumericToken(); + } else { + // Otherwise, return a <delim-token> with its value set to the current input code point. + type = TYPE.Delim; + offset++; + } + + break; + + // U+002F SOLIDUS (/) + case 0x002F: + // If the next two input code point are U+002F SOLIDUS (/) followed by a U+002A ASTERISK (*), + if (getCharCode(offset + 1) === 0x002A) { + // ... consume them and all following code points up to and including the first U+002A ASTERISK (*) + // followed by a U+002F SOLIDUS (/), or up to an EOF code point. + type = TYPE.Comment; + offset = source.indexOf('*/', offset + 2); + offset = offset === -1 ? source.length : offset + 2; + } else { + type = TYPE.Delim; + offset++; + } + break; + + // U+003A COLON (:) + case 0x003A: + // Return a <colon-token>. + type = TYPE.Colon; + offset++; + break; + + // U+003B SEMICOLON (;) + case 0x003B: + // Return a <semicolon-token>. + type = TYPE.Semicolon; + offset++; + break; + + // U+003C LESS-THAN SIGN (<) + case 0x003C: + // If the next 3 input code points are U+0021 EXCLAMATION MARK U+002D HYPHEN-MINUS U+002D HYPHEN-MINUS (!--), ... + if (getCharCode(offset + 1) === 0x0021 && + getCharCode(offset + 2) === 0x002D && + getCharCode(offset + 3) === 0x002D) { + // ... consume them and return a <CDO-token>. + type = TYPE.CDO; + offset = offset + 4; + } else { + // Otherwise, return a <delim-token> with its value set to the current input code point. + type = TYPE.Delim; + offset++; + } + + break; + + // U+0040 COMMERCIAL AT (@) + case 0x0040: + // If the next 3 input code points would start an identifier, ... + if (isIdentifierStart(getCharCode(offset + 1), getCharCode(offset + 2), getCharCode(offset + 3))) { + // ... consume a name, create an <at-keyword-token> with its value set to the returned value, and return it. + type = TYPE.AtKeyword; + offset = consumeName(source, offset + 1); + } else { + // Otherwise, return a <delim-token> with its value set to the current input code point. + type = TYPE.Delim; + offset++; + } + + break; + + // U+005B LEFT SQUARE BRACKET ([) + case 0x005B: + // Return a <[-token>. + type = TYPE.LeftSquareBracket; + offset++; + break; + + // U+005C REVERSE SOLIDUS (\) + case 0x005C: + // If the input stream starts with a valid escape, ... + if (isValidEscape(code, getCharCode(offset + 1))) { + // ... reconsume the current input code point, consume an ident-like token, and return it. + consumeIdentLikeToken(); + } else { + // Otherwise, this is a parse error. Return a <delim-token> with its value set to the current input code point. + type = TYPE.Delim; + offset++; + } + break; + + // U+005D RIGHT SQUARE BRACKET (]) + case 0x005D: + // Return a <]-token>. + type = TYPE.RightSquareBracket; + offset++; + break; + + // U+007B LEFT CURLY BRACKET ({) + case 0x007B: + // Return a <{-token>. + type = TYPE.LeftCurlyBracket; + offset++; + break; + + // U+007D RIGHT CURLY BRACKET (}) + case 0x007D: + // Return a <}-token>. + type = TYPE.RightCurlyBracket; + offset++; + break; + + // digit + case DigitCategory: + // Reconsume the current input code point, consume a numeric token, and return it. + consumeNumericToken(); + break; + + // name-start code point + case NameStartCategory: + // Reconsume the current input code point, consume an ident-like token, and return it. + consumeIdentLikeToken(); + break; + + // EOF + // case EofCategory: + // Return an <EOF-token>. + // break; + + // anything else + default: + // Return a <delim-token> with its value set to the current input code point. + type = TYPE.Delim; + offset++; + } + + // put token to stream + onToken(type, start, start = offset); + } +} + +export * from './types.js'; +export * as tokenTypes from './types.js'; +export { default as tokenNames } from './names.js'; +export * from './char-code-definitions.js'; +export * from './utils.js'; +export * from './OffsetToLocation.js'; +export * from './TokenStream.js'; diff --git a/vanilla/node_modules/css-tree/lib/tokenizer/names.js b/vanilla/node_modules/css-tree/lib/tokenizer/names.js new file mode 100644 index 0000000..54831bd --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/tokenizer/names.js @@ -0,0 +1,28 @@ +export default [ + 'EOF-token', + 'ident-token', + 'function-token', + 'at-keyword-token', + 'hash-token', + 'string-token', + 'bad-string-token', + 'url-token', + 'bad-url-token', + 'delim-token', + 'number-token', + 'percentage-token', + 'dimension-token', + 'whitespace-token', + 'CDO-token', + 'CDC-token', + 'colon-token', + 'semicolon-token', + 'comma-token', + '[-token', + ']-token', + '(-token', + ')-token', + '{-token', + '}-token', + 'comment-token' +]; diff --git a/vanilla/node_modules/css-tree/lib/tokenizer/types.js b/vanilla/node_modules/css-tree/lib/tokenizer/types.js new file mode 100644 index 0000000..5018569 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/tokenizer/types.js @@ -0,0 +1,28 @@ +// CSS Syntax Module Level 3 +// https://www.w3.org/TR/css-syntax-3/ +export const EOF = 0; // <EOF-token> +export const Ident = 1; // <ident-token> +export const Function = 2; // <function-token> +export const AtKeyword = 3; // <at-keyword-token> +export const Hash = 4; // <hash-token> +export const String = 5; // <string-token> +export const BadString = 6; // <bad-string-token> +export const Url = 7; // <url-token> +export const BadUrl = 8; // <bad-url-token> +export const Delim = 9; // <delim-token> +export const Number = 10; // <number-token> +export const Percentage = 11; // <percentage-token> +export const Dimension = 12; // <dimension-token> +export const WhiteSpace = 13; // <whitespace-token> +export const CDO = 14; // <CDO-token> +export const CDC = 15; // <CDC-token> +export const Colon = 16; // <colon-token> : +export const Semicolon = 17; // <semicolon-token> ; +export const Comma = 18; // <comma-token> , +export const LeftSquareBracket = 19; // <[-token> +export const RightSquareBracket = 20; // <]-token> +export const LeftParenthesis = 21; // <(-token> +export const RightParenthesis = 22; // <)-token> +export const LeftCurlyBracket = 23; // <{-token> +export const RightCurlyBracket = 24; // <}-token> +export const Comment = 25; diff --git a/vanilla/node_modules/css-tree/lib/tokenizer/utils.js b/vanilla/node_modules/css-tree/lib/tokenizer/utils.js new file mode 100644 index 0000000..c131ec5 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/tokenizer/utils.js @@ -0,0 +1,254 @@ +import { + isDigit, + isHexDigit, + isUppercaseLetter, + isName, + isWhiteSpace, + isValidEscape +} from './char-code-definitions.js'; + +function getCharCode(source, offset) { + return offset < source.length ? source.charCodeAt(offset) : 0; +} + +export function getNewlineLength(source, offset, code) { + if (code === 13 /* \r */ && getCharCode(source, offset + 1) === 10 /* \n */) { + return 2; + } + + return 1; +} + +export function cmpChar(testStr, offset, referenceCode) { + let code = testStr.charCodeAt(offset); + + // code.toLowerCase() for A..Z + if (isUppercaseLetter(code)) { + code = code | 32; + } + + return code === referenceCode; +} + +export function cmpStr(testStr, start, end, referenceStr) { + if (end - start !== referenceStr.length) { + return false; + } + + if (start < 0 || end > testStr.length) { + return false; + } + + for (let i = start; i < end; i++) { + const referenceCode = referenceStr.charCodeAt(i - start); + let testCode = testStr.charCodeAt(i); + + // testCode.toLowerCase() for A..Z + if (isUppercaseLetter(testCode)) { + testCode = testCode | 32; + } + + if (testCode !== referenceCode) { + return false; + } + } + + return true; +} + +export function findWhiteSpaceStart(source, offset) { + for (; offset >= 0; offset--) { + if (!isWhiteSpace(source.charCodeAt(offset))) { + break; + } + } + + return offset + 1; +} + +export function findWhiteSpaceEnd(source, offset) { + for (; offset < source.length; offset++) { + if (!isWhiteSpace(source.charCodeAt(offset))) { + break; + } + } + + return offset; +} + +export function findDecimalNumberEnd(source, offset) { + for (; offset < source.length; offset++) { + if (!isDigit(source.charCodeAt(offset))) { + break; + } + } + + return offset; +} + +// § 4.3.7. Consume an escaped code point +export function consumeEscaped(source, offset) { + // It assumes that the U+005C REVERSE SOLIDUS (\) has already been consumed and + // that the next input code point has already been verified to be part of a valid escape. + offset += 2; + + // hex digit + if (isHexDigit(getCharCode(source, offset - 1))) { + // Consume as many hex digits as possible, but no more than 5. + // Note that this means 1-6 hex digits have been consumed in total. + for (const maxOffset = Math.min(source.length, offset + 5); offset < maxOffset; offset++) { + if (!isHexDigit(getCharCode(source, offset))) { + break; + } + } + + // If the next input code point is whitespace, consume it as well. + const code = getCharCode(source, offset); + if (isWhiteSpace(code)) { + offset += getNewlineLength(source, offset, code); + } + } + + return offset; +} + +// §4.3.11. Consume a name +// Note: This algorithm does not do the verification of the first few code points that are necessary +// to ensure the returned code points would constitute an <ident-token>. If that is the intended use, +// ensure that the stream starts with an identifier before calling this algorithm. +export function consumeName(source, offset) { + // Let result initially be an empty string. + // Repeatedly consume the next input code point from the stream: + for (; offset < source.length; offset++) { + const code = source.charCodeAt(offset); + + // name code point + if (isName(code)) { + // Append the code point to result. + continue; + } + + // the stream starts with a valid escape + if (isValidEscape(code, getCharCode(source, offset + 1))) { + // Consume an escaped code point. Append the returned code point to result. + offset = consumeEscaped(source, offset) - 1; + continue; + } + + // anything else + // Reconsume the current input code point. Return result. + break; + } + + return offset; +} + +// §4.3.12. Consume a number +export function consumeNumber(source, offset) { + let code = source.charCodeAt(offset); + + // 2. If the next input code point is U+002B PLUS SIGN (+) or U+002D HYPHEN-MINUS (-), + // consume it and append it to repr. + if (code === 0x002B || code === 0x002D) { + code = source.charCodeAt(offset += 1); + } + + // 3. While the next input code point is a digit, consume it and append it to repr. + if (isDigit(code)) { + offset = findDecimalNumberEnd(source, offset + 1); + code = source.charCodeAt(offset); + } + + // 4. If the next 2 input code points are U+002E FULL STOP (.) followed by a digit, then: + if (code === 0x002E && isDigit(source.charCodeAt(offset + 1))) { + // 4.1 Consume them. + // 4.2 Append them to repr. + offset += 2; + + // 4.3 Set type to "number". + // TODO + + // 4.4 While the next input code point is a digit, consume it and append it to repr. + + offset = findDecimalNumberEnd(source, offset); + } + + // 5. If the next 2 or 3 input code points are U+0045 LATIN CAPITAL LETTER E (E) + // or U+0065 LATIN SMALL LETTER E (e), ... , followed by a digit, then: + if (cmpChar(source, offset, 101 /* e */)) { + let sign = 0; + code = source.charCodeAt(offset + 1); + + // ... optionally followed by U+002D HYPHEN-MINUS (-) or U+002B PLUS SIGN (+) ... + if (code === 0x002D || code === 0x002B) { + sign = 1; + code = source.charCodeAt(offset + 2); + } + + // ... followed by a digit + if (isDigit(code)) { + // 5.1 Consume them. + // 5.2 Append them to repr. + + // 5.3 Set type to "number". + // TODO + + // 5.4 While the next input code point is a digit, consume it and append it to repr. + offset = findDecimalNumberEnd(source, offset + 1 + sign + 1); + } + } + + return offset; +} + +// § 4.3.14. Consume the remnants of a bad url +// ... its sole use is to consume enough of the input stream to reach a recovery point +// where normal tokenizing can resume. +export function consumeBadUrlRemnants(source, offset) { + // Repeatedly consume the next input code point from the stream: + for (; offset < source.length; offset++) { + const code = source.charCodeAt(offset); + + // U+0029 RIGHT PARENTHESIS ()) + // EOF + if (code === 0x0029) { + // Return. + offset++; + break; + } + + if (isValidEscape(code, getCharCode(source, offset + 1))) { + // Consume an escaped code point. + // Note: This allows an escaped right parenthesis ("\)") to be encountered + // without ending the <bad-url-token>. This is otherwise identical to + // the "anything else" clause. + offset = consumeEscaped(source, offset); + } + } + + return offset; +} + +// § 4.3.7. Consume an escaped code point +// Note: This algorithm assumes that escaped is valid without leading U+005C REVERSE SOLIDUS (\) +export function decodeEscaped(escaped) { + // Single char escaped that's not a hex digit + if (escaped.length === 1 && !isHexDigit(escaped.charCodeAt(0))) { + return escaped[0]; + } + + // Interpret the hex digits as a hexadecimal number. + let code = parseInt(escaped, 16); + + if ( + (code === 0) || // If this number is zero, + (code >= 0xD800 && code <= 0xDFFF) || // or is for a surrogate, + (code > 0x10FFFF) // or is greater than the maximum allowed code point + ) { + // ... return U+FFFD REPLACEMENT CHARACTER + code = 0xFFFD; + } + + // Otherwise, return the code point with that value. + return String.fromCodePoint(code); +} diff --git a/vanilla/node_modules/css-tree/lib/utils/List.js b/vanilla/node_modules/css-tree/lib/utils/List.js new file mode 100644 index 0000000..8953264 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/utils/List.js @@ -0,0 +1,469 @@ +// +// list +// ┌──────┐ +// ┌──────────────┼─head │ +// │ │ tail─┼──────────────┐ +// │ └──────┘ │ +// ▼ ▼ +// item item item item +// ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ +// null ◀──┼─prev │◀───┼─prev │◀───┼─prev │◀───┼─prev │ +// │ next─┼───▶│ next─┼───▶│ next─┼───▶│ next─┼──▶ null +// ├──────┤ ├──────┤ ├──────┤ ├──────┤ +// │ data │ │ data │ │ data │ │ data │ +// └──────┘ └──────┘ └──────┘ └──────┘ +// + +let releasedCursors = null; + +export class List { + static createItem(data) { + return { + prev: null, + next: null, + data + }; + } + + constructor() { + this.head = null; + this.tail = null; + this.cursor = null; + } + createItem(data) { + return List.createItem(data); + } + + // cursor helpers + allocateCursor(prev, next) { + let cursor; + + if (releasedCursors !== null) { + cursor = releasedCursors; + releasedCursors = releasedCursors.cursor; + cursor.prev = prev; + cursor.next = next; + cursor.cursor = this.cursor; + } else { + cursor = { + prev, + next, + cursor: this.cursor + }; + } + + this.cursor = cursor; + + return cursor; + } + releaseCursor() { + const { cursor } = this; + + this.cursor = cursor.cursor; + cursor.prev = null; + cursor.next = null; + cursor.cursor = releasedCursors; + releasedCursors = cursor; + } + updateCursors(prevOld, prevNew, nextOld, nextNew) { + let { cursor } = this; + + while (cursor !== null) { + if (cursor.prev === prevOld) { + cursor.prev = prevNew; + } + + if (cursor.next === nextOld) { + cursor.next = nextNew; + } + + cursor = cursor.cursor; + } + } + *[Symbol.iterator]() { + for (let cursor = this.head; cursor !== null; cursor = cursor.next) { + yield cursor.data; + } + } + + // getters + get size() { + let size = 0; + + for (let cursor = this.head; cursor !== null; cursor = cursor.next) { + size++; + } + + return size; + } + get isEmpty() { + return this.head === null; + } + get first() { + return this.head && this.head.data; + } + get last() { + return this.tail && this.tail.data; + } + + // convertors + fromArray(array) { + let cursor = null; + this.head = null; + + for (let data of array) { + const item = List.createItem(data); + + if (cursor !== null) { + cursor.next = item; + } else { + this.head = item; + } + + item.prev = cursor; + cursor = item; + } + + this.tail = cursor; + return this; + } + toArray() { + return [...this]; + } + toJSON() { + return [...this]; + } + + // array-like methods + forEach(fn, thisArg = this) { + // push cursor + const cursor = this.allocateCursor(null, this.head); + + while (cursor.next !== null) { + const item = cursor.next; + cursor.next = item.next; + fn.call(thisArg, item.data, item, this); + } + + // pop cursor + this.releaseCursor(); + } + forEachRight(fn, thisArg = this) { + // push cursor + const cursor = this.allocateCursor(this.tail, null); + + while (cursor.prev !== null) { + const item = cursor.prev; + cursor.prev = item.prev; + fn.call(thisArg, item.data, item, this); + } + + // pop cursor + this.releaseCursor(); + } + reduce(fn, initialValue, thisArg = this) { + // push cursor + let cursor = this.allocateCursor(null, this.head); + let acc = initialValue; + let item; + + while (cursor.next !== null) { + item = cursor.next; + cursor.next = item.next; + + acc = fn.call(thisArg, acc, item.data, item, this); + } + + // pop cursor + this.releaseCursor(); + + return acc; + } + reduceRight(fn, initialValue, thisArg = this) { + // push cursor + let cursor = this.allocateCursor(this.tail, null); + let acc = initialValue; + let item; + + while (cursor.prev !== null) { + item = cursor.prev; + cursor.prev = item.prev; + + acc = fn.call(thisArg, acc, item.data, item, this); + } + + // pop cursor + this.releaseCursor(); + + return acc; + } + some(fn, thisArg = this) { + for (let cursor = this.head; cursor !== null; cursor = cursor.next) { + if (fn.call(thisArg, cursor.data, cursor, this)) { + return true; + } + } + + return false; + } + map(fn, thisArg = this) { + const result = new List(); + + for (let cursor = this.head; cursor !== null; cursor = cursor.next) { + result.appendData(fn.call(thisArg, cursor.data, cursor, this)); + } + + return result; + } + filter(fn, thisArg = this) { + const result = new List(); + + for (let cursor = this.head; cursor !== null; cursor = cursor.next) { + if (fn.call(thisArg, cursor.data, cursor, this)) { + result.appendData(cursor.data); + } + } + + return result; + } + + nextUntil(start, fn, thisArg = this) { + if (start === null) { + return; + } + + // push cursor + const cursor = this.allocateCursor(null, start); + + while (cursor.next !== null) { + const item = cursor.next; + cursor.next = item.next; + if (fn.call(thisArg, item.data, item, this)) { + break; + } + } + + // pop cursor + this.releaseCursor(); + } + prevUntil(start, fn, thisArg = this) { + if (start === null) { + return; + } + + // push cursor + const cursor = this.allocateCursor(start, null); + + while (cursor.prev !== null) { + const item = cursor.prev; + cursor.prev = item.prev; + if (fn.call(thisArg, item.data, item, this)) { + break; + } + } + + // pop cursor + this.releaseCursor(); + } + + // mutation + clear() { + this.head = null; + this.tail = null; + } + copy() { + const result = new List(); + + for (let data of this) { + result.appendData(data); + } + + return result; + } + prepend(item) { + // head + // ^ + // item + this.updateCursors(null, item, this.head, item); + + // insert to the beginning of the list + if (this.head !== null) { + // new item <- first item + this.head.prev = item; + // new item -> first item + item.next = this.head; + } else { + // if list has no head, then it also has no tail + // in this case tail points to the new item + this.tail = item; + } + + // head always points to new item + this.head = item; + return this; + } + prependData(data) { + return this.prepend(List.createItem(data)); + } + append(item) { + return this.insert(item); + } + appendData(data) { + return this.insert(List.createItem(data)); + } + insert(item, before = null) { + if (before !== null) { + // prev before + // ^ + // item + this.updateCursors(before.prev, item, before, item); + + if (before.prev === null) { + // insert to the beginning of list + if (this.head !== before) { + throw new Error('before doesn\'t belong to list'); + } + // since head points to before therefore list doesn't empty + // no need to check tail + this.head = item; + before.prev = item; + item.next = before; + this.updateCursors(null, item); + } else { + // insert between two items + before.prev.next = item; + item.prev = before.prev; + before.prev = item; + item.next = before; + } + } else { + // tail + // ^ + // item + this.updateCursors(this.tail, item, null, item); + + // insert to the ending of the list + if (this.tail !== null) { + // last item -> new item + this.tail.next = item; + // last item <- new item + item.prev = this.tail; + } else { + // if list has no tail, then it also has no head + // in this case head points to new item + this.head = item; + } + + // tail always points to new item + this.tail = item; + } + + return this; + } + insertData(data, before) { + return this.insert(List.createItem(data), before); + } + remove(item) { + // item + // ^ + // prev next + this.updateCursors(item, item.prev, item, item.next); + + if (item.prev !== null) { + item.prev.next = item.next; + } else { + if (this.head !== item) { + throw new Error('item doesn\'t belong to list'); + } + + this.head = item.next; + } + + if (item.next !== null) { + item.next.prev = item.prev; + } else { + if (this.tail !== item) { + throw new Error('item doesn\'t belong to list'); + } + + this.tail = item.prev; + } + + item.prev = null; + item.next = null; + + return item; + } + push(data) { + this.insert(List.createItem(data)); + } + pop() { + return this.tail !== null ? this.remove(this.tail) : null; + } + unshift(data) { + this.prepend(List.createItem(data)); + } + shift() { + return this.head !== null ? this.remove(this.head) : null; + } + prependList(list) { + return this.insertList(list, this.head); + } + appendList(list) { + return this.insertList(list); + } + insertList(list, before) { + // ignore empty lists + if (list.head === null) { + return this; + } + + if (before !== undefined && before !== null) { + this.updateCursors(before.prev, list.tail, before, list.head); + + // insert in the middle of dist list + if (before.prev !== null) { + // before.prev <-> list.head + before.prev.next = list.head; + list.head.prev = before.prev; + } else { + this.head = list.head; + } + + before.prev = list.tail; + list.tail.next = before; + } else { + this.updateCursors(this.tail, list.tail, null, list.head); + + // insert to end of the list + if (this.tail !== null) { + // if destination list has a tail, then it also has a head, + // but head doesn't change + // dest tail -> source head + this.tail.next = list.head; + // dest tail <- source head + list.head.prev = this.tail; + } else { + // if list has no a tail, then it also has no a head + // in this case points head to new item + this.head = list.head; + } + + // tail always start point to new item + this.tail = list.tail; + } + + list.head = null; + list.tail = null; + return this; + } + replace(oldItem, newItemOrList) { + if ('head' in newItemOrList) { + this.insertList(newItemOrList, oldItem); + } else { + this.insert(newItemOrList, oldItem); + } + + this.remove(oldItem); + } +} diff --git a/vanilla/node_modules/css-tree/lib/utils/clone.js b/vanilla/node_modules/css-tree/lib/utils/clone.js new file mode 100644 index 0000000..84819c0 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/utils/clone.js @@ -0,0 +1,21 @@ +import { List } from './List.js'; + +export function clone(node) { + const result = {}; + + for (const key of Object.keys(node)) { + let value = node[key]; + + if (value) { + if (Array.isArray(value) || value instanceof List) { + value = value.map(clone); + } else if (value.constructor === Object) { + value = clone(value); + } + } + + result[key] = value; + } + + return result; +} diff --git a/vanilla/node_modules/css-tree/lib/utils/create-custom-error.js b/vanilla/node_modules/css-tree/lib/utils/create-custom-error.js new file mode 100644 index 0000000..dba122f --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/utils/create-custom-error.js @@ -0,0 +1,14 @@ +export function createCustomError(name, message) { + // use Object.create(), because some VMs prevent setting line/column otherwise + // (iOS Safari 10 even throws an exception) + const error = Object.create(SyntaxError.prototype); + const errorStack = new Error(); + + return Object.assign(error, { + name, + message, + get stack() { + return (errorStack.stack || '').replace(/^(.+\n){1,3}/, `${name}: ${message}\n`); + } + }); +}; diff --git a/vanilla/node_modules/css-tree/lib/utils/ident.js b/vanilla/node_modules/css-tree/lib/utils/ident.js new file mode 100644 index 0000000..9cbe0f8 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/utils/ident.js @@ -0,0 +1,101 @@ +import { + isName, + isValidEscape, + consumeEscaped, + decodeEscaped +} from '../tokenizer/index.js'; + +const REVERSE_SOLIDUS = 0x005c; // U+005C REVERSE SOLIDUS (\) + +export function decode(str) { + const end = str.length - 1; + let decoded = ''; + + for (let i = 0; i < str.length; i++) { + let code = str.charCodeAt(i); + + if (code === REVERSE_SOLIDUS) { + // special case at the ending + if (i === end) { + // if the next input code point is EOF, do nothing + break; + } + + code = str.charCodeAt(++i); + + // consume escaped + if (isValidEscape(REVERSE_SOLIDUS, code)) { + const escapeStart = i - 1; + const escapeEnd = consumeEscaped(str, escapeStart); + + i = escapeEnd - 1; + decoded += decodeEscaped(str.substring(escapeStart + 1, escapeEnd)); + } else { + // \r\n + if (code === 0x000d && str.charCodeAt(i + 1) === 0x000a) { + i++; + } + } + } else { + decoded += str[i]; + } + } + + return decoded; +} + +// https://drafts.csswg.org/cssom/#serialize-an-identifier +// § 2.1. Common Serializing Idioms +export function encode(str) { + let encoded = ''; + + // If the character is the first character and is a "-" (U+002D), + // and there is no second character, then the escaped character. + // Note: That's means a single dash string "-" return as escaped dash, + // so move the condition out of the main loop + if (str.length === 1 && str.charCodeAt(0) === 0x002D) { + return '\\-'; + } + + // To serialize an identifier means to create a string represented + // by the concatenation of, for each character of the identifier: + for (let i = 0; i < str.length; i++) { + const code = str.charCodeAt(i); + + // If the character is NULL (U+0000), then the REPLACEMENT CHARACTER (U+FFFD). + if (code === 0x0000) { + encoded += '\uFFFD'; + continue; + } + + if ( + // If the character is in the range [\1-\1f] (U+0001 to U+001F) or is U+007F ... + // Note: Do not compare with 0x0001 since 0x0000 is precessed before + code <= 0x001F || code === 0x007F || + // [or] ... is in the range [0-9] (U+0030 to U+0039), + (code >= 0x0030 && code <= 0x0039 && ( + // If the character is the first character ... + i === 0 || + // If the character is the second character ... and the first character is a "-" (U+002D) + i === 1 && str.charCodeAt(0) === 0x002D + )) + ) { + // ... then the character escaped as code point. + encoded += '\\' + code.toString(16) + ' '; + continue; + } + + // If the character is not handled by one of the above rules and is greater + // than or equal to U+0080, is "-" (U+002D) or "_" (U+005F), or is in one + // of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to U+005A), + // or \[a-z] (U+0061 to U+007A), then the character itself. + if (isName(code)) { + encoded += str.charAt(i); + } else { + // Otherwise, the escaped character. + encoded += '\\' + str.charAt(i); + } + } + + return encoded; +} diff --git a/vanilla/node_modules/css-tree/lib/utils/index.js b/vanilla/node_modules/css-tree/lib/utils/index.js new file mode 100644 index 0000000..07bf0f9 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/utils/index.js @@ -0,0 +1,6 @@ +export * from './clone.js'; +export * as ident from './ident.js'; +export * from './List.js'; +export * from './names.js'; +export * as string from './string.js'; +export * as url from './url.js'; diff --git a/vanilla/node_modules/css-tree/lib/utils/names.js b/vanilla/node_modules/css-tree/lib/utils/names.js new file mode 100644 index 0000000..b4f74b9 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/utils/names.js @@ -0,0 +1,106 @@ +const keywords = new Map(); +const properties = new Map(); +const HYPHENMINUS = 45; // '-'.charCodeAt() + +export const keyword = getKeywordDescriptor; +export const property = getPropertyDescriptor; +export const vendorPrefix = getVendorPrefix; +export function isCustomProperty(str, offset) { + offset = offset || 0; + + return str.length - offset >= 2 && + str.charCodeAt(offset) === HYPHENMINUS && + str.charCodeAt(offset + 1) === HYPHENMINUS; +} + +function getVendorPrefix(str, offset) { + offset = offset || 0; + + // verdor prefix should be at least 3 chars length + if (str.length - offset >= 3) { + // vendor prefix starts with hyper minus following non-hyper minus + if (str.charCodeAt(offset) === HYPHENMINUS && + str.charCodeAt(offset + 1) !== HYPHENMINUS) { + // vendor prefix should contain a hyper minus at the ending + const secondDashIndex = str.indexOf('-', offset + 2); + + if (secondDashIndex !== -1) { + return str.substring(offset, secondDashIndex + 1); + } + } + } + + return ''; +} + +function getKeywordDescriptor(keyword) { + if (keywords.has(keyword)) { + return keywords.get(keyword); + } + + const name = keyword.toLowerCase(); + let descriptor = keywords.get(name); + + if (descriptor === undefined) { + const custom = isCustomProperty(name, 0); + const vendor = !custom ? getVendorPrefix(name, 0) : ''; + descriptor = Object.freeze({ + basename: name.substr(vendor.length), + name, + prefix: vendor, + vendor, + custom + }); + } + + keywords.set(keyword, descriptor); + + return descriptor; +} + +function getPropertyDescriptor(property) { + if (properties.has(property)) { + return properties.get(property); + } + + let name = property; + let hack = property[0]; + + if (hack === '/') { + hack = property[1] === '/' ? '//' : '/'; + } else if (hack !== '_' && + hack !== '*' && + hack !== '$' && + hack !== '#' && + hack !== '+' && + hack !== '&') { + hack = ''; + } + + const custom = isCustomProperty(name, hack.length); + + // re-use result when possible (the same as for lower case) + if (!custom) { + name = name.toLowerCase(); + if (properties.has(name)) { + const descriptor = properties.get(name); + properties.set(property, descriptor); + return descriptor; + } + } + + const vendor = !custom ? getVendorPrefix(name, hack.length) : ''; + const prefix = name.substr(0, hack.length + vendor.length); + const descriptor = Object.freeze({ + basename: name.substr(prefix.length), + name: name.substr(hack.length), + hack, + vendor, + prefix, + custom + }); + + properties.set(property, descriptor); + + return descriptor; +} diff --git a/vanilla/node_modules/css-tree/lib/utils/string.js b/vanilla/node_modules/css-tree/lib/utils/string.js new file mode 100644 index 0000000..928a85b --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/utils/string.js @@ -0,0 +1,99 @@ +import { + isHexDigit, + isWhiteSpace, + isValidEscape, + consumeEscaped, + decodeEscaped +} from '../tokenizer/index.js'; + +const REVERSE_SOLIDUS = 0x005c; // U+005C REVERSE SOLIDUS (\) +const QUOTATION_MARK = 0x0022; // " +const APOSTROPHE = 0x0027; // ' + +export function decode(str) { + const len = str.length; + const firstChar = str.charCodeAt(0); + const start = firstChar === QUOTATION_MARK || firstChar === APOSTROPHE ? 1 : 0; + const end = start === 1 && len > 1 && str.charCodeAt(len - 1) === firstChar ? len - 2 : len - 1; + let decoded = ''; + + for (let i = start; i <= end; i++) { + let code = str.charCodeAt(i); + + if (code === REVERSE_SOLIDUS) { + // special case at the ending + if (i === end) { + // if the next input code point is EOF, do nothing + // otherwise include last quote as escaped + if (i !== len - 1) { + decoded = str.substr(i + 1); + } + break; + } + + code = str.charCodeAt(++i); + + // consume escaped + if (isValidEscape(REVERSE_SOLIDUS, code)) { + const escapeStart = i - 1; + const escapeEnd = consumeEscaped(str, escapeStart); + + i = escapeEnd - 1; + decoded += decodeEscaped(str.substring(escapeStart + 1, escapeEnd)); + } else { + // \r\n + if (code === 0x000d && str.charCodeAt(i + 1) === 0x000a) { + i++; + } + } + } else { + decoded += str[i]; + } + } + + return decoded; +} + +// https://drafts.csswg.org/cssom/#serialize-a-string +// § 2.1. Common Serializing Idioms +export function encode(str, apostrophe) { + const quote = apostrophe ? '\'' : '"'; + const quoteCode = apostrophe ? APOSTROPHE : QUOTATION_MARK; + let encoded = ''; + let wsBeforeHexIsNeeded = false; + + for (let i = 0; i < str.length; i++) { + const code = str.charCodeAt(i); + + // If the character is NULL (U+0000), then the REPLACEMENT CHARACTER (U+FFFD). + if (code === 0x0000) { + encoded += '\uFFFD'; + continue; + } + + // If the character is in the range [\1-\1f] (U+0001 to U+001F) or is U+007F, + // the character escaped as code point. + // Note: Do not compare with 0x0001 since 0x0000 is precessed before + if (code <= 0x001f || code === 0x007F) { + encoded += '\\' + code.toString(16); + wsBeforeHexIsNeeded = true; + continue; + } + + // If the character is '"' (U+0022) or "\" (U+005C), the escaped character. + if (code === quoteCode || code === REVERSE_SOLIDUS) { + encoded += '\\' + str.charAt(i); + wsBeforeHexIsNeeded = false; + } else { + if (wsBeforeHexIsNeeded && (isHexDigit(code) || isWhiteSpace(code))) { + encoded += ' '; + } + + // Otherwise, the character itself. + encoded += str.charAt(i); + wsBeforeHexIsNeeded = false; + } + } + + return quote + encoded + quote; +} diff --git a/vanilla/node_modules/css-tree/lib/utils/url.js b/vanilla/node_modules/css-tree/lib/utils/url.js new file mode 100644 index 0000000..cce5709 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/utils/url.js @@ -0,0 +1,108 @@ +import { + isHexDigit, + isWhiteSpace, + isValidEscape, + consumeEscaped, + decodeEscaped +} from '../tokenizer/index.js'; + +const SPACE = 0x0020; // U+0020 SPACE +const REVERSE_SOLIDUS = 0x005c; // U+005C REVERSE SOLIDUS (\) +const QUOTATION_MARK = 0x0022; // " +const APOSTROPHE = 0x0027; // ' +const LEFTPARENTHESIS = 0x0028; // U+0028 LEFT PARENTHESIS (() +const RIGHTPARENTHESIS = 0x0029; // U+0029 RIGHT PARENTHESIS ()) + +export function decode(str) { + const len = str.length; + let start = 4; // length of "url(" + let end = str.charCodeAt(len - 1) === RIGHTPARENTHESIS ? len - 2 : len - 1; + let decoded = ''; + + while (start < end && isWhiteSpace(str.charCodeAt(start))) { + start++; + } + + while (start < end && isWhiteSpace(str.charCodeAt(end))) { + end--; + } + + for (let i = start; i <= end; i++) { + let code = str.charCodeAt(i); + + if (code === REVERSE_SOLIDUS) { + // special case at the ending + if (i === end) { + // if the next input code point is EOF, do nothing + // otherwise include last left parenthesis as escaped + if (i !== len - 1) { + decoded = str.substr(i + 1); + } + break; + } + + code = str.charCodeAt(++i); + + // consume escaped + if (isValidEscape(REVERSE_SOLIDUS, code)) { + const escapeStart = i - 1; + const escapeEnd = consumeEscaped(str, escapeStart); + + i = escapeEnd - 1; + decoded += decodeEscaped(str.substring(escapeStart + 1, escapeEnd)); + } else { + // \r\n + if (code === 0x000d && str.charCodeAt(i + 1) === 0x000a) { + i++; + } + } + } else { + decoded += str[i]; + } + } + + return decoded; +} + +export function encode(str) { + let encoded = ''; + let wsBeforeHexIsNeeded = false; + + for (let i = 0; i < str.length; i++) { + const code = str.charCodeAt(i); + + // If the character is NULL (U+0000), then the REPLACEMENT CHARACTER (U+FFFD). + if (code === 0x0000) { + encoded += '\uFFFD'; + continue; + } + + // If the character is in the range [\1-\1f] (U+0001 to U+001F) or is U+007F, + // the character escaped as code point. + // Note: Do not compare with 0x0001 since 0x0000 is precessed before + if (code <= 0x001f || code === 0x007F) { + encoded += '\\' + code.toString(16); + wsBeforeHexIsNeeded = true; + continue; + } + + if (code === SPACE || + code === REVERSE_SOLIDUS || + code === QUOTATION_MARK || + code === APOSTROPHE || + code === LEFTPARENTHESIS || + code === RIGHTPARENTHESIS) { + encoded += '\\' + str.charAt(i); + wsBeforeHexIsNeeded = false; + } else { + if (wsBeforeHexIsNeeded && isHexDigit(code)) { + encoded += ' '; + } + + encoded += str.charAt(i); + wsBeforeHexIsNeeded = false; + } + } + + return 'url(' + encoded + ')'; +} diff --git a/vanilla/node_modules/css-tree/lib/version.js b/vanilla/node_modules/css-tree/lib/version.js new file mode 100644 index 0000000..dde3a83 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/version.js @@ -0,0 +1,5 @@ +import { createRequire } from 'module'; + +const require = createRequire(import.meta.url); + +export const { version } = require('../package.json'); diff --git a/vanilla/node_modules/css-tree/lib/walker/create.js b/vanilla/node_modules/css-tree/lib/walker/create.js new file mode 100644 index 0000000..ca76e03 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/walker/create.js @@ -0,0 +1,287 @@ +const { hasOwnProperty } = Object.prototype; +const noop = function() {}; + +function ensureFunction(value) { + return typeof value === 'function' ? value : noop; +} + +function invokeForType(fn, type) { + return function(node, item, list) { + if (node.type === type) { + fn.call(this, node, item, list); + } + }; +} + +function getWalkersFromStructure(name, nodeType) { + const structure = nodeType.structure; + const walkers = []; + + for (const key in structure) { + if (hasOwnProperty.call(structure, key) === false) { + continue; + } + + let fieldTypes = structure[key]; + const walker = { + name: key, + type: false, + nullable: false + }; + + if (!Array.isArray(fieldTypes)) { + fieldTypes = [fieldTypes]; + } + + for (const fieldType of fieldTypes) { + if (fieldType === null) { + walker.nullable = true; + } else if (typeof fieldType === 'string') { + walker.type = 'node'; + } else if (Array.isArray(fieldType)) { + walker.type = 'list'; + } + } + + if (walker.type) { + walkers.push(walker); + } + } + + if (walkers.length) { + return { + context: nodeType.walkContext, + fields: walkers + }; + } + + return null; +} + +function getTypesFromConfig(config) { + const types = {}; + + for (const name in config.node) { + if (hasOwnProperty.call(config.node, name)) { + const nodeType = config.node[name]; + + if (!nodeType.structure) { + throw new Error('Missed `structure` field in `' + name + '` node type definition'); + } + + types[name] = getWalkersFromStructure(name, nodeType); + } + } + + return types; +} + +function createTypeIterator(config, reverse) { + const fields = config.fields.slice(); + const contextName = config.context; + const useContext = typeof contextName === 'string'; + + if (reverse) { + fields.reverse(); + } + + return function(node, context, walk, walkReducer) { + let prevContextValue; + + if (useContext) { + prevContextValue = context[contextName]; + context[contextName] = node; + } + + for (const field of fields) { + const ref = node[field.name]; + + if (!field.nullable || ref) { + if (field.type === 'list') { + const breakWalk = reverse + ? ref.reduceRight(walkReducer, false) + : ref.reduce(walkReducer, false); + + if (breakWalk) { + return true; + } + } else if (walk(ref)) { + return true; + } + } + } + + if (useContext) { + context[contextName] = prevContextValue; + } + }; +} + +function createFastTraveralMap({ + StyleSheet, + Atrule, + Rule, + Block, + DeclarationList +}) { + return { + Atrule: { + StyleSheet, + Atrule, + Rule, + Block + }, + Rule: { + StyleSheet, + Atrule, + Rule, + Block + }, + Declaration: { + StyleSheet, + Atrule, + Rule, + Block, + DeclarationList + } + }; +} + +export function createWalker(config) { + const types = getTypesFromConfig(config); + const iteratorsNatural = {}; + const iteratorsReverse = {}; + const breakWalk = Symbol('break-walk'); + const skipNode = Symbol('skip-node'); + + for (const name in types) { + if (hasOwnProperty.call(types, name) && types[name] !== null) { + iteratorsNatural[name] = createTypeIterator(types[name], false); + iteratorsReverse[name] = createTypeIterator(types[name], true); + } + } + + const fastTraversalIteratorsNatural = createFastTraveralMap(iteratorsNatural); + const fastTraversalIteratorsReverse = createFastTraveralMap(iteratorsReverse); + + const walk = function(root, options) { + function walkNode(node, item, list) { + const enterRet = enter.call(context, node, item, list); + + if (enterRet === breakWalk) { + return true; + } + + if (enterRet === skipNode) { + return false; + } + + if (iterators.hasOwnProperty(node.type)) { + if (iterators[node.type](node, context, walkNode, walkReducer)) { + return true; + } + } + + if (leave.call(context, node, item, list) === breakWalk) { + return true; + } + + return false; + } + + let enter = noop; + let leave = noop; + let iterators = iteratorsNatural; + let walkReducer = (ret, data, item, list) => ret || walkNode(data, item, list); + const context = { + break: breakWalk, + skip: skipNode, + + root, + stylesheet: null, + atrule: null, + atrulePrelude: null, + rule: null, + selector: null, + block: null, + declaration: null, + function: null + }; + + if (typeof options === 'function') { + enter = options; + } else if (options) { + enter = ensureFunction(options.enter); + leave = ensureFunction(options.leave); + + if (options.reverse) { + iterators = iteratorsReverse; + } + + if (options.visit) { + if (fastTraversalIteratorsNatural.hasOwnProperty(options.visit)) { + iterators = options.reverse + ? fastTraversalIteratorsReverse[options.visit] + : fastTraversalIteratorsNatural[options.visit]; + } else if (!types.hasOwnProperty(options.visit)) { + throw new Error('Bad value `' + options.visit + '` for `visit` option (should be: ' + Object.keys(types).sort().join(', ') + ')'); + } + + enter = invokeForType(enter, options.visit); + leave = invokeForType(leave, options.visit); + } + } + + if (enter === noop && leave === noop) { + throw new Error('Neither `enter` nor `leave` walker handler is set or both aren\'t a function'); + } + + walkNode(root); + }; + + walk.break = breakWalk; + walk.skip = skipNode; + + walk.find = function(ast, fn) { + let found = null; + + walk(ast, function(node, item, list) { + if (fn.call(this, node, item, list)) { + found = node; + return breakWalk; + } + }); + + return found; + }; + + walk.findLast = function(ast, fn) { + let found = null; + + walk(ast, { + reverse: true, + enter(node, item, list) { + if (fn.call(this, node, item, list)) { + found = node; + return breakWalk; + } + } + }); + + return found; + }; + + walk.findAll = function(ast, fn) { + const found = []; + + walk(ast, function(node, item, list) { + if (fn.call(this, node, item, list)) { + found.push(node); + } + }); + + return found; + }; + + return walk; +}; diff --git a/vanilla/node_modules/css-tree/lib/walker/index.js b/vanilla/node_modules/css-tree/lib/walker/index.js new file mode 100644 index 0000000..24cb477 --- /dev/null +++ b/vanilla/node_modules/css-tree/lib/walker/index.js @@ -0,0 +1,4 @@ +import { createWalker } from './create.js'; +import config from '../syntax/config/walker.js'; + +export default createWalker(config); |
