format: prettify entire project

This commit is contained in:
Rim
2025-04-02 06:50:39 -04:00
parent 86f0782a98
commit 7ccc0be712
1711 changed files with 755867 additions and 235931 deletions

View File

@ -1,28 +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;
return {
fromPlainObject(ast) {
walk(ast, {
enter(node) {
if (node.children && node.children instanceof List === false) {
node.children = new List().fromArray(node.children);
}
},
toPlainObject(ast) {
walk(ast, {
leave(node) {
if (node.children && node.children instanceof List) {
node.children = node.children.toArray();
}
}
});
});
return ast;
}
};
};
return ast;
},
toPlainObject(ast) {
walk(ast, {
leave(node) {
if (node.children && node.children instanceof List) {
node.children = node.children.toArray();
}
},
});
return ast;
},
};
}

137
node_modules/css-tree/lib/data.js generated vendored
View File

@ -9,87 +9,100 @@ const mdnSyntaxes = require('mdn-data/css/syntaxes.json');
const extendSyntax = /^\s*\|\s*/;
function preprocessAtrules(dict) {
const result = Object.create(null);
const result = Object.create(null);
for (const atruleName in dict) {
const atrule = dict[atruleName];
let descriptors = null;
for (const atruleName in dict) {
const atrule = dict[atruleName];
let descriptors = null;
if (atrule.descriptors) {
descriptors = Object.create(null);
if (atrule.descriptors) {
descriptors = Object.create(null);
for (const descriptor in atrule.descriptors) {
descriptors[descriptor] = atrule.descriptors[descriptor].syntax;
}
}
result[atruleName.substr(1)] = {
prelude: atrule.syntax.trim().replace(/\{(.|\s)+\}/, '').match(/^@\S+\s+([^;\{]*)/)[1].trim() || null,
descriptors
};
for (const descriptor in atrule.descriptors) {
descriptors[descriptor] = atrule.descriptors[descriptor].syntax;
}
}
return result;
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 = {};
const result = {};
// copy all syntaxes for an original dict
for (const key in dict) {
result[key] = dict[key].syntax || dict[key];
// copy all syntaxes for an original dict
for (const key in dict) {
result[key] = dict[key].syntax || dict[key];
}
// apply a patch
for (const key in patchDict) {
if (key in dict) {
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, '');
}
}
}
// apply a patch
for (const key in patchDict) {
if (key in dict) {
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;
return result;
}
function patchAtrules(dict, patchDict) {
const result = {};
const result = {};
// copy all syntaxes for an original dict
for (const key in dict) {
const patchDescriptors = (patchDict[key] && patchDict[key].descriptors) || null;
// copy all syntaxes for an original dict
for (const key in dict) {
const patchDescriptors =
(patchDict[key] && patchDict[key].descriptors) || null;
result[key] = {
prelude: key in patchDict && 'prelude' in patchDict[key]
? patchDict[key].prelude
: dict[key].prelude || null,
descriptors: patchDictionary(dict[key].descriptors || {}, patchDescriptors || {})
};
result[key] = {
prelude:
key in patchDict && 'prelude' in patchDict[key] ?
patchDict[key].prelude
: dict[key].prelude || null,
descriptors: patchDictionary(
dict[key].descriptors || {},
patchDescriptors || {}
),
};
}
// apply a patch
for (const key in patchDict) {
if (!hasOwnProperty.call(dict, key)) {
result[key] = {
prelude: patchDict[key].prelude || null,
descriptors:
patchDict[key].descriptors &&
patchDictionary({}, patchDict[key].descriptors),
};
}
}
// apply a patch
for (const key in patchDict) {
if (!hasOwnProperty.call(dict, key)) {
result[key] = {
prelude: patchDict[key].prelude || null,
descriptors: patchDict[key].descriptors && patchDictionary({}, patchDict[key].descriptors)
};
}
}
return result;
return result;
}
export default {
types: patchDictionary(mdnSyntaxes, patch.types),
atrules: patchAtrules(preprocessAtrules(mdnAtrules), patch.atrules),
properties: patchDictionary(mdnProperties, patch.properties)
types: patchDictionary(mdnSyntaxes, patch.types),
atrules: patchAtrules(preprocessAtrules(mdnAtrules), patch.atrules),
properties: patchDictionary(mdnProperties, patch.properties),
};

View File

@ -1,12 +1,18 @@
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('-') + '^'
});
};
return Object.assign(createCustomError('SyntaxError', message), {
input,
offset,
rawMessage: message,
message:
message +
'\n' +
' ' +
input +
'\n' +
'--' +
new Array((offset || input.length) + 1).join('-') +
'^',
});
}

View File

@ -1,131 +1,141 @@
function noop(value) {
return value;
return value;
}
function generateMultiplier(multiplier) {
const { min, max, comma } = multiplier;
const { min, max, comma } = multiplier;
if (min === 0 && max === 0) {
return comma ? '#?' : '*';
}
if (min === 0 && max === 0) {
return comma ? '#?' : '*';
}
if (min === 0 && max === 1) {
return '?';
}
if (min === 0 && max === 1) {
return '?';
}
if (min === 1 && max === 0) {
return comma ? '#' : '+';
}
if (min === 1 && max === 0) {
return comma ? '#' : '+';
}
if (min === 1 && max === 1) {
return '';
}
if (min === 1 && max === 1) {
return '';
}
return (
(comma ? '#' : '') +
(min === max
? '{' + min + '}'
: '{' + min + ',' + (max !== 0 ? max : '') + '}'
)
);
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) +
']'
);
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 + '`');
}
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);
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 ? ']' : ' ]');
}
if (node.explicit || forceBraces) {
return (
(compact || result[0] === ',' ? '[' : '[ ') +
result +
(compact ? ']' : ' ]')
);
}
return result;
return result;
}
function internalGenerate(node, decorate, forceBraces, compact) {
let result;
let result;
switch (node.type) {
case 'Group':
result =
generateSequence(node, decorate, forceBraces, compact) +
(node.disallowEmpty ? '!' : '');
break;
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 'Multiplier':
// return since node is a composition
return (
internalGenerate(node.term, decorate, forceBraces, compact) +
decorate(generateMultiplier(node), node)
);
case 'Type':
result = '<' + node.name + (node.opts ? decorate(generateTypeOpts(node.opts), node.opts) : '') + '>';
break;
case 'Type':
result =
'<' +
node.name +
(node.opts ? decorate(generateTypeOpts(node.opts), node.opts) : '') +
'>';
break;
case 'Property':
result = '<\'' + node.name + '\'>';
break;
case 'Property':
result = "<'" + node.name + "'>";
break;
case 'Keyword':
result = node.name;
break;
case 'Keyword':
result = node.name;
break;
case 'AtKeyword':
result = '@' + node.name;
break;
case 'AtKeyword':
result = '@' + node.name;
break;
case 'Function':
result = node.name + '(';
break;
case 'Function':
result = node.name + '(';
break;
case 'String':
case 'Token':
result = node.value;
break;
case 'String':
case 'Token':
result = node.value;
break;
case 'Comma':
result = ',';
break;
case 'Comma':
result = ',';
break;
default:
throw new Error('Unknown node type `' + node.type + '`');
}
default:
throw new Error('Unknown node type `' + node.type + '`');
}
return decorate(result, node);
return decorate(result, node);
}
export function generate(node, options) {
let decorate = noop;
let forceBraces = false;
let compact = false;
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;
}
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);
};
return internalGenerate(node, decorate, forceBraces, compact);
}

View File

@ -5,244 +5,244 @@ 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 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 LEFTCURLYBRACKET = 123; // {
const VERTICALLINE = 124; // |
const RIGHTCURLYBRACKET = 125; // }
const INFINITY = 8734; // ∞
const INFINITY = 8734; // ∞
const NAME_CHAR = new Uint8Array(128).map((_, idx) =>
/[a-zA-Z0-9\-]/.test(String.fromCharCode(idx)) ? 1 : 0
/[a-zA-Z0-9\-]/.test(String.fromCharCode(idx)) ? 1 : 0
);
const COMBINATOR_PRECEDENCE = {
' ': 1,
'&&': 2,
'||': 3,
'|': 4
' ': 1,
'&&': 2,
'||': 3,
'|': 4,
};
function scanSpaces(tokenizer) {
return tokenizer.substringToPos(
tokenizer.findWsEnd(tokenizer.pos)
);
return tokenizer.substringToPos(tokenizer.findWsEnd(tokenizer.pos));
}
function scanWord(tokenizer) {
let end = tokenizer.pos;
let end = tokenizer.pos;
for (; end < tokenizer.str.length; end++) {
const code = tokenizer.str.charCodeAt(end);
if (code >= 128 || NAME_CHAR[code] === 0) {
break;
}
for (; end < tokenizer.str.length; end++) {
const code = tokenizer.str.charCodeAt(end);
if (code >= 128 || NAME_CHAR[code] === 0) {
break;
}
}
if (tokenizer.pos === end) {
tokenizer.error('Expect a keyword');
}
if (tokenizer.pos === end) {
tokenizer.error('Expect a keyword');
}
return tokenizer.substringToPos(end);
return tokenizer.substringToPos(end);
}
function scanNumber(tokenizer) {
let end = tokenizer.pos;
let end = tokenizer.pos;
for (; end < tokenizer.str.length; end++) {
const code = tokenizer.str.charCodeAt(end);
if (code < 48 || code > 57) {
break;
}
for (; end < tokenizer.str.length; end++) {
const code = tokenizer.str.charCodeAt(end);
if (code < 48 || code > 57) {
break;
}
}
if (tokenizer.pos === end) {
tokenizer.error('Expect a number');
}
if (tokenizer.pos === end) {
tokenizer.error('Expect a number');
}
return tokenizer.substringToPos(end);
return tokenizer.substringToPos(end);
}
function scanString(tokenizer) {
const end = tokenizer.str.indexOf('\'', tokenizer.pos + 1);
const end = tokenizer.str.indexOf("'", tokenizer.pos + 1);
if (end === -1) {
tokenizer.pos = tokenizer.str.length;
tokenizer.error('Expect an apostrophe');
}
if (end === -1) {
tokenizer.pos = tokenizer.str.length;
tokenizer.error('Expect an apostrophe');
}
return tokenizer.substringToPos(end + 1);
return tokenizer.substringToPos(end + 1);
}
function readMultiplierRange(tokenizer) {
let min = null;
let max = null;
let min = null;
let max = null;
tokenizer.eat(LEFTCURLYBRACKET);
tokenizer.eat(LEFTCURLYBRACKET);
min = scanNumber(tokenizer);
min = scanNumber(tokenizer);
if (tokenizer.charCode() === COMMA) {
tokenizer.pos++;
if (tokenizer.charCode() !== RIGHTCURLYBRACKET) {
max = scanNumber(tokenizer);
}
} else {
max = min;
if (tokenizer.charCode() === COMMA) {
tokenizer.pos++;
if (tokenizer.charCode() !== RIGHTCURLYBRACKET) {
max = scanNumber(tokenizer);
}
} else {
max = min;
}
tokenizer.eat(RIGHTCURLYBRACKET);
tokenizer.eat(RIGHTCURLYBRACKET);
return {
min: Number(min),
max: max ? Number(max) : 0
};
return {
min: Number(min),
max: max ? Number(max) : 0,
};
}
function readMultiplier(tokenizer) {
let range = null;
let comma = false;
let range = null;
let comma = false;
switch (tokenizer.charCode()) {
case ASTERISK:
tokenizer.pos++;
switch (tokenizer.charCode()) {
case ASTERISK:
tokenizer.pos++;
range = {
min: 0,
max: 0
};
range = {
min: 0,
max: 0,
};
break;
break;
case PLUSSIGN:
tokenizer.pos++;
case PLUSSIGN:
tokenizer.pos++;
range = {
min: 1,
max: 0
};
range = {
min: 1,
max: 0,
};
break;
break;
case QUESTIONMARK:
tokenizer.pos++;
case QUESTIONMARK:
tokenizer.pos++;
range = {
min: 0,
max: 1
};
range = {
min: 0,
max: 1,
};
break;
break;
case NUMBERSIGN:
tokenizer.pos++;
case NUMBERSIGN:
tokenizer.pos++;
comma = true;
comma = true;
if (tokenizer.charCode() === LEFTCURLYBRACKET) {
range = readMultiplierRange(tokenizer);
} else if (tokenizer.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 }
tokenizer.pos++;
range = {
min: 0,
max: 0
};
} else {
range = {
min: 1,
max: 0
};
}
if (tokenizer.charCode() === LEFTCURLYBRACKET) {
range = readMultiplierRange(tokenizer);
} else if (tokenizer.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 }
tokenizer.pos++;
range = {
min: 0,
max: 0,
};
} else {
range = {
min: 1,
max: 0,
};
}
break;
break;
case LEFTCURLYBRACKET:
range = readMultiplierRange(tokenizer);
break;
case LEFTCURLYBRACKET:
range = readMultiplierRange(tokenizer);
break;
default:
return null;
}
default:
return null;
}
return {
type: 'Multiplier',
comma,
min: range.min,
max: range.max,
term: null
};
return {
type: 'Multiplier',
comma,
min: range.min,
max: range.max,
term: null,
};
}
function maybeMultiplied(tokenizer, node) {
const multiplier = readMultiplier(tokenizer);
const multiplier = readMultiplier(tokenizer);
if (multiplier !== null) {
multiplier.term = node;
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 (tokenizer.charCode() === NUMBERSIGN &&
tokenizer.charCodeAt(tokenizer.pos - 1) === PLUSSIGN) {
return maybeMultiplied(tokenizer, multiplier);
}
return multiplier;
// 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 (
tokenizer.charCode() === NUMBERSIGN &&
tokenizer.charCodeAt(tokenizer.pos - 1) === PLUSSIGN
) {
return maybeMultiplied(tokenizer, multiplier);
}
return node;
return multiplier;
}
return node;
}
function maybeToken(tokenizer) {
const ch = tokenizer.peek();
const ch = tokenizer.peek();
if (ch === '') {
return null;
}
if (ch === '') {
return null;
}
return {
type: 'Token',
value: ch
};
return {
type: 'Token',
value: ch,
};
}
function readProperty(tokenizer) {
let name;
let name;
tokenizer.eat(LESSTHANSIGN);
tokenizer.eat(APOSTROPHE);
tokenizer.eat(LESSTHANSIGN);
tokenizer.eat(APOSTROPHE);
name = scanWord(tokenizer);
name = scanWord(tokenizer);
tokenizer.eat(APOSTROPHE);
tokenizer.eat(GREATERTHANSIGN);
tokenizer.eat(APOSTROPHE);
tokenizer.eat(GREATERTHANSIGN);
return maybeMultiplied(tokenizer, {
type: 'Property',
name
});
return maybeMultiplied(tokenizer, {
type: 'Property',
name,
});
}
// https://drafts.csswg.org/css-values-3/#numeric-ranges
@ -253,332 +253,338 @@ function readProperty(tokenizer) {
// 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(tokenizer) {
// use null for Infinity to make AST format JSON serializable/deserializable
let min = null; // -Infinity
let max = null; // Infinity
let sign = 1;
// use null for Infinity to make AST format JSON serializable/deserializable
let min = null; // -Infinity
let max = null; // Infinity
let sign = 1;
tokenizer.eat(LEFTSQUAREBRACKET);
tokenizer.eat(LEFTSQUAREBRACKET);
if (tokenizer.charCode() === HYPERMINUS) {
tokenizer.peek();
sign = -1;
}
if (sign == -1 && tokenizer.charCode() === INFINITY) {
tokenizer.peek();
} else {
min = sign * Number(scanNumber(tokenizer));
if (NAME_CHAR[tokenizer.charCode()] !== 0) {
min += scanWord(tokenizer);
}
}
scanSpaces(tokenizer);
tokenizer.eat(COMMA);
scanSpaces(tokenizer);
if (tokenizer.charCode() === INFINITY) {
tokenizer.peek();
} else {
sign = 1;
if (tokenizer.charCode() === HYPERMINUS) {
tokenizer.peek();
sign = -1;
tokenizer.peek();
sign = -1;
}
if (sign == -1 && tokenizer.charCode() === INFINITY) {
tokenizer.peek();
} else {
min = sign * Number(scanNumber(tokenizer));
max = sign * Number(scanNumber(tokenizer));
if (NAME_CHAR[tokenizer.charCode()] !== 0) {
min += scanWord(tokenizer);
}
if (NAME_CHAR[tokenizer.charCode()] !== 0) {
max += scanWord(tokenizer);
}
}
scanSpaces(tokenizer);
tokenizer.eat(COMMA);
scanSpaces(tokenizer);
tokenizer.eat(RIGHTSQUAREBRACKET);
if (tokenizer.charCode() === INFINITY) {
tokenizer.peek();
} else {
sign = 1;
if (tokenizer.charCode() === HYPERMINUS) {
tokenizer.peek();
sign = -1;
}
max = sign * Number(scanNumber(tokenizer));
if (NAME_CHAR[tokenizer.charCode()] !== 0) {
max += scanWord(tokenizer);
}
}
tokenizer.eat(RIGHTSQUAREBRACKET);
return {
type: 'Range',
min,
max
};
return {
type: 'Range',
min,
max,
};
}
function readType(tokenizer) {
let name;
let opts = null;
let name;
let opts = null;
tokenizer.eat(LESSTHANSIGN);
name = scanWord(tokenizer);
tokenizer.eat(LESSTHANSIGN);
name = scanWord(tokenizer);
if (tokenizer.charCode() === LEFTPARENTHESIS &&
tokenizer.nextCharCode() === RIGHTPARENTHESIS) {
tokenizer.pos += 2;
name += '()';
}
if (
tokenizer.charCode() === LEFTPARENTHESIS &&
tokenizer.nextCharCode() === RIGHTPARENTHESIS
) {
tokenizer.pos += 2;
name += '()';
}
if (tokenizer.charCodeAt(tokenizer.findWsEnd(tokenizer.pos)) === LEFTSQUAREBRACKET) {
scanSpaces(tokenizer);
opts = readTypeRange(tokenizer);
}
if (
tokenizer.charCodeAt(tokenizer.findWsEnd(tokenizer.pos)) ===
LEFTSQUAREBRACKET
) {
scanSpaces(tokenizer);
opts = readTypeRange(tokenizer);
}
tokenizer.eat(GREATERTHANSIGN);
tokenizer.eat(GREATERTHANSIGN);
return maybeMultiplied(tokenizer, {
type: 'Type',
name,
opts
});
return maybeMultiplied(tokenizer, {
type: 'Type',
name,
opts,
});
}
function readKeywordOrFunction(tokenizer) {
const name = scanWord(tokenizer);
const name = scanWord(tokenizer);
if (tokenizer.charCode() === LEFTPARENTHESIS) {
tokenizer.pos++;
if (tokenizer.charCode() === LEFTPARENTHESIS) {
tokenizer.pos++;
return {
type: 'Function',
name
};
}
return {
type: 'Function',
name,
};
}
return maybeMultiplied(tokenizer, {
type: 'Keyword',
name
});
return maybeMultiplied(tokenizer, {
type: 'Keyword',
name,
});
}
function regroupTerms(terms, combinators) {
function createGroup(terms, combinator) {
return {
type: 'Group',
terms,
combinator,
disallowEmpty: false,
explicit: false
};
}
function createGroup(terms, combinator) {
return {
type: 'Group',
terms,
combinator,
disallowEmpty: false,
explicit: false,
};
}
let combinator;
let combinator;
combinators = Object.keys(combinators)
.sort((a, b) => COMBINATOR_PRECEDENCE[a] - COMBINATOR_PRECEDENCE[b]);
combinators = Object.keys(combinators).sort(
(a, b) => COMBINATOR_PRECEDENCE[a] - COMBINATOR_PRECEDENCE[b]
);
while (combinators.length > 0) {
combinator = combinators.shift();
while (combinators.length > 0) {
combinator = combinators.shift();
let i = 0;
let subgroupStart = 0;
let i = 0;
let subgroupStart = 0;
for (; i < terms.length; i++) {
const term = terms[i];
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) {
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)
subgroupStart,
i - subgroupStart,
createGroup(terms.slice(subgroupStart, i), combinator)
);
i = subgroupStart + 1;
}
subgroupStart = -1;
}
}
}
return combinator;
if (subgroupStart !== -1 && combinators.length) {
terms.splice(
subgroupStart,
i - subgroupStart,
createGroup(terms.slice(subgroupStart, i), combinator)
);
}
}
return combinator;
}
function readImplicitGroup(tokenizer) {
const terms = [];
const combinators = {};
let token;
let prevToken = null;
let prevTokenPos = tokenizer.pos;
const terms = [];
const combinators = {};
let token;
let prevToken = null;
let prevTokenPos = tokenizer.pos;
while (token = peek(tokenizer)) {
if (token.type !== 'Spaces') {
if (token.type === 'Combinator') {
// check for combinator in group beginning and double combinator sequence
if (prevToken === null || prevToken.type === 'Combinator') {
tokenizer.pos = prevTokenPos;
tokenizer.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 = tokenizer.pos;
while ((token = peek(tokenizer))) {
if (token.type !== 'Spaces') {
if (token.type === 'Combinator') {
// check for combinator in group beginning and double combinator sequence
if (prevToken === null || prevToken.type === 'Combinator') {
tokenizer.pos = prevTokenPos;
tokenizer.error('Unexpected combinator');
}
}
// check for combinator in group ending
if (prevToken !== null && prevToken.type === 'Combinator') {
tokenizer.pos -= prevTokenPos;
tokenizer.error('Unexpected combinator');
}
combinators[token.value] = true;
} else if (prevToken !== null && prevToken.type !== 'Combinator') {
combinators[' '] = true; // a b
terms.push({
type: 'Combinator',
value: ' ',
});
}
return {
type: 'Group',
terms,
combinator: regroupTerms(terms, combinators) || ' ',
disallowEmpty: false,
explicit: false
};
terms.push(token);
prevToken = token;
prevTokenPos = tokenizer.pos;
}
}
// check for combinator in group ending
if (prevToken !== null && prevToken.type === 'Combinator') {
tokenizer.pos -= prevTokenPos;
tokenizer.error('Unexpected combinator');
}
return {
type: 'Group',
terms,
combinator: regroupTerms(terms, combinators) || ' ',
disallowEmpty: false,
explicit: false,
};
}
function readGroup(tokenizer) {
let result;
let result;
tokenizer.eat(LEFTSQUAREBRACKET);
result = readImplicitGroup(tokenizer);
tokenizer.eat(RIGHTSQUAREBRACKET);
tokenizer.eat(LEFTSQUAREBRACKET);
result = readImplicitGroup(tokenizer);
tokenizer.eat(RIGHTSQUAREBRACKET);
result.explicit = true;
result.explicit = true;
if (tokenizer.charCode() === EXCLAMATIONMARK) {
tokenizer.pos++;
result.disallowEmpty = true;
}
if (tokenizer.charCode() === EXCLAMATIONMARK) {
tokenizer.pos++;
result.disallowEmpty = true;
}
return result;
return result;
}
function peek(tokenizer) {
let code = tokenizer.charCode();
let code = tokenizer.charCode();
if (code < 128 && NAME_CHAR[code] === 1) {
return readKeywordOrFunction(tokenizer);
}
if (code < 128 && NAME_CHAR[code] === 1) {
return readKeywordOrFunction(tokenizer);
}
switch (code) {
case RIGHTSQUAREBRACKET:
// don't eat, stop scan a group
break;
switch (code) {
case RIGHTSQUAREBRACKET:
// don't eat, stop scan a group
break;
case LEFTSQUAREBRACKET:
return maybeMultiplied(tokenizer, readGroup(tokenizer));
case LEFTSQUAREBRACKET:
return maybeMultiplied(tokenizer, readGroup(tokenizer));
case LESSTHANSIGN:
return tokenizer.nextCharCode() === APOSTROPHE
? readProperty(tokenizer)
: readType(tokenizer);
case LESSTHANSIGN:
return tokenizer.nextCharCode() === APOSTROPHE ?
readProperty(tokenizer)
: readType(tokenizer);
case VERTICALLINE:
return {
type: 'Combinator',
value: tokenizer.substringToPos(
tokenizer.pos + (tokenizer.nextCharCode() === VERTICALLINE ? 2 : 1)
)
};
case VERTICALLINE:
return {
type: 'Combinator',
value: tokenizer.substringToPos(
tokenizer.pos + (tokenizer.nextCharCode() === VERTICALLINE ? 2 : 1)
),
};
case AMPERSAND:
tokenizer.pos++;
tokenizer.eat(AMPERSAND);
case AMPERSAND:
tokenizer.pos++;
tokenizer.eat(AMPERSAND);
return {
type: 'Combinator',
value: '&&'
};
return {
type: 'Combinator',
value: '&&',
};
case COMMA:
tokenizer.pos++;
return {
type: 'Comma'
};
case COMMA:
tokenizer.pos++;
return {
type: 'Comma',
};
case APOSTROPHE:
return maybeMultiplied(tokenizer, {
type: 'String',
value: scanString(tokenizer)
});
case APOSTROPHE:
return maybeMultiplied(tokenizer, {
type: 'String',
value: scanString(tokenizer),
});
case SPACE:
case TAB:
case N:
case R:
case F:
return {
type: 'Spaces',
value: scanSpaces(tokenizer)
};
case SPACE:
case TAB:
case N:
case R:
case F:
return {
type: 'Spaces',
value: scanSpaces(tokenizer),
};
case COMMERCIALAT:
code = tokenizer.nextCharCode();
case COMMERCIALAT:
code = tokenizer.nextCharCode();
if (code < 128 && NAME_CHAR[code] === 1) {
tokenizer.pos++;
return {
type: 'AtKeyword',
name: scanWord(tokenizer)
};
}
if (code < 128 && NAME_CHAR[code] === 1) {
tokenizer.pos++;
return {
type: 'AtKeyword',
name: scanWord(tokenizer),
};
}
return maybeToken(tokenizer);
return maybeToken(tokenizer);
case ASTERISK:
case PLUSSIGN:
case QUESTIONMARK:
case NUMBERSIGN:
case EXCLAMATIONMARK:
// prohibited tokens (used as a multiplier start)
break;
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 = tokenizer.nextCharCode();
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 = tokenizer.nextCharCode();
if (code < 48 || code > 57) {
return maybeToken(tokenizer);
}
if (code < 48 || code > 57) {
return maybeToken(tokenizer);
}
break;
break;
default:
return maybeToken(tokenizer);
}
default:
return maybeToken(tokenizer);
}
}
export function parse(source) {
const tokenizer = new Tokenizer(source);
const result = readImplicitGroup(tokenizer);
const tokenizer = new Tokenizer(source);
const result = readImplicitGroup(tokenizer);
if (tokenizer.pos !== source.length) {
tokenizer.error('Unexpected input');
}
if (tokenizer.pos !== source.length) {
tokenizer.error('Unexpected input');
}
// reduce redundant groups with single group term
if (result.terms.length === 1 && result.terms[0].type === 'Group') {
return result.terms[0];
}
// reduce redundant groups with single group term
if (result.terms.length === 1 && result.terms[0].type === 'Group') {
return result.terms[0];
}
return result;
};
return result;
}

View File

@ -7,46 +7,52 @@ const R = 13;
const SPACE = 32;
export class Tokenizer {
constructor(str) {
this.str = str;
this.pos = 0;
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);
}
nextCharCode() {
return this.charCodeAt(this.pos + 1);
}
nextNonWsCode(pos) {
return this.charCodeAt(this.findWsEnd(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;
}
}
charCodeAt(pos) {
return pos < this.str.length ? this.str.charCodeAt(pos) : 0;
}
charCode() {
return this.charCodeAt(this.pos);
}
nextCharCode() {
return this.charCodeAt(this.pos + 1);
}
nextNonWsCode(pos) {
return this.charCodeAt(this.findWsEnd(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;
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) + '`');
}
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);
}
};
this.pos++;
}
peek() {
return this.pos < this.str.length ? this.str.charAt(this.pos++) : '';
}
error(message) {
throw new SyntaxError(message, this.str, this.pos);
}
}

View File

@ -1,52 +1,54 @@
const noop = function() {};
const noop = function () {};
function ensureFunction(value) {
return typeof value === 'function' ? value : noop;
return typeof value === 'function' ? value : noop;
}
export function walk(node, options, context) {
function walk(node) {
enter.call(context, node);
function walk(node) {
enter.call(context, node);
switch (node.type) {
case 'Group':
node.terms.forEach(walk);
break;
switch (node.type) {
case 'Group':
node.terms.forEach(walk);
break;
case 'Multiplier':
walk(node.term);
break;
case 'Multiplier':
walk(node.term);
break;
case 'Type':
case 'Property':
case 'Keyword':
case 'AtKeyword':
case 'Function':
case 'String':
case 'Token':
case 'Comma':
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);
default:
throw new Error('Unknown type: ' + node.type);
}
let enter = noop;
let leave = noop;
leave.call(context, node);
}
if (typeof options === 'function') {
enter = options;
} else if (options) {
enter = ensureFunction(options.enter);
leave = ensureFunction(options.leave);
}
let enter = noop;
let leave = noop;
if (enter === noop && leave === noop) {
throw new Error('Neither `enter` nor `leave` walker handler is set or both aren\'t a function');
}
if (typeof options === 'function') {
enter = options;
} else if (options) {
enter = ensureFunction(options.enter);
leave = ensureFunction(options.leave);
}
walk(node, context);
};
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);
}

View File

@ -5,94 +5,94 @@ 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;
if (typeof delimeter === 'function') {
let prev = null;
node.children.forEach(node => {
if (prev !== null) {
delimeter.call(this, prev);
}
node.children.forEach((node) => {
if (prev !== null) {
delimeter.call(this, prev);
}
this.node(node);
prev = node;
});
this.node(node);
prev = node;
});
return;
}
return;
}
node.children.forEach(this.node, this);
node.children.forEach(this.node, this);
}
function processChunk(chunk) {
tokenize(chunk, (type, start, end) => {
this.token(type, chunk.slice(start, end));
});
tokenize(chunk, (type, start, end) => {
this.token(type, chunk.slice(start, end));
});
}
export function createGenerator(config) {
const types = new Map();
const types = new Map();
for (let name in config.node) {
const item = config.node[name];
const fn = item.generate || item;
for (let name in config.node) {
const item = config.node[name];
const fn = item.generate || item;
if (typeof fn === 'function') {
types.set(name, 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];
}
}
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();
const publicApi = {
node: (node) => handlers.node(node),
children: processChildren,
token: (type, value) => handlers.token(type, value),
tokenize: processChunk,
};
};
handlers.node(node);
return handlers.result();
};
}

View File

@ -3,90 +3,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 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;
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;
if (original.line !== nodeLine || original.column !== nodeColumn) {
original.line = nodeLine;
original.column = nodeColumn;
generated.line = line;
generated.column = column;
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) {
sourceMappingActive = false;
if (
generated.line !== activatedGenerated.line ||
generated.column !== activatedGenerated.column
) {
map.addMapping(activatedMapping);
}
}
return {
css: origHandlersResult(),
map
};
};
sourceMappingActive = true;
map.addMapping({
source: node.loc.source,
original,
generated,
});
}
}
return handlers;
};
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;
}

View File

@ -1,36 +1,36 @@
import {
WhiteSpace,
Delim,
Ident,
Function as FunctionToken,
Url,
BadUrl,
AtKeyword,
Hash,
Percentage,
Dimension,
Number as NumberToken,
String as StringToken,
Colon,
LeftParenthesis,
RightParenthesis,
CDC
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 PLUSSIGN = 0x002b; // U+002B PLUS SIGN (+)
const HYPHENMINUS = 0x002d; // U+002D HYPHEN-MINUS (-)
const code = (type, value) => {
if (type === Delim) {
type = value;
}
if (type === Delim) {
type = value;
}
if (typeof type === 'string') {
const charCode = type.charCodeAt(0);
return charCode > 0x7F ? 0x8000 : charCode << 8;
}
if (typeof type === 'string') {
const charCode = type.charCodeAt(0);
return charCode > 0x7f ? 0x8000 : charCode << 8;
}
return type;
return type;
};
// https://www.w3.org/TR/css-syntax-3/#serialization
@ -40,142 +40,144 @@ const code = (type, value) => {
// 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],
[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],
[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],
[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],
[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
['-', 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
[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
['@', Ident],
['@', FunctionToken],
['@', Url],
['@', BadUrl],
['@', '-'],
['@', CDC], // https://github.com/w3c/csswg-drafts/pull/6874
['.', NumberToken],
['.', Percentage],
['.', Dimension],
['.', NumberToken],
['.', Percentage],
['.', Dimension],
['+', NumberToken],
['+', Percentage],
['+', Dimension],
['+', NumberToken],
['+', Percentage],
['+', Dimension],
['/', '*']
['/', '*'],
];
// validate with scripts/generate-safe
const safePairs = specPairs.concat([
[Ident, Hash],
[Ident, Hash],
[Dimension, Hash],
[Dimension, Hash],
[Hash, Hash],
[Hash, Hash],
[AtKeyword, LeftParenthesis],
[AtKeyword, StringToken],
[AtKeyword, Colon],
[AtKeyword, LeftParenthesis],
[AtKeyword, StringToken],
[AtKeyword, Colon],
[Percentage, Percentage],
[Percentage, Dimension],
[Percentage, FunctionToken],
[Percentage, '-'],
[Percentage, Percentage],
[Percentage, Dimension],
[Percentage, FunctionToken],
[Percentage, '-'],
[RightParenthesis, Ident],
[RightParenthesis, FunctionToken],
[RightParenthesis, Percentage],
[RightParenthesis, Dimension],
[RightParenthesis, Hash],
[RightParenthesis, '-']
[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)))
);
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);
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);
}
if (emitWs) {
this.emit(' ', WhiteSpace, true);
}
return nextCode;
};
return nextCode;
};
}
export const spec = createMap(specPairs);

24
node_modules/css-tree/lib/index.js generated vendored
View File

@ -12,19 +12,19 @@ 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,
tokenize,
parse,
generate,
lexer,
createLexer,
walk,
find,
findLast,
findAll,
walk,
find,
findLast,
findAll,
toPlainObject,
fromPlainObject,
toPlainObject,
fromPlainObject,
fork
fork,
} = syntax;

View File

@ -13,441 +13,507 @@ import { getStructureFromConfig } from './structure.js';
const cssWideKeywordsSyntax = buildMatchGraph(cssWideKeywords.join(' | '));
function dumpMapSyntax(map, compact, syntaxAsAst) {
const result = {};
const result = {};
for (const name in map) {
if (map[name].syntax) {
result[name] = syntaxAsAst
? map[name].syntax
: generate(map[name].syntax, { compact });
}
for (const name in map) {
if (map[name].syntax) {
result[name] =
syntaxAsAst ?
map[name].syntax
: generate(map[name].syntax, { compact });
}
}
return result;
return result;
}
function dumpAtruleMapSyntax(map, compact, syntaxAsAst) {
const result = {};
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)
};
}
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;
return result;
}
function valueHasVar(tokens) {
for (let i = 0; i < tokens.length; i++) {
if (tokens[i].value.toLowerCase() === 'var(') {
return true;
}
for (let i = 0; i < tokens.length; i++) {
if (tokens[i].value.toLowerCase() === 'var(') {
return true;
}
}
return false;
return false;
}
function buildMatchResult(matched, error, iterations) {
return {
matched,
iterations,
error,
...trace
};
return {
matched,
iterations,
error,
...trace,
};
}
function matchSyntax(lexer, syntax, value, useCssWideKeywords) {
const tokens = prepareTokens(value, lexer.syntax);
let result;
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 (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
);
}
}
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);
return buildMatchResult(result.match, null, result.iterations);
}
export class Lexer {
constructor(config, syntax, structure) {
this.cssWideKeywordsSyntax = cssWideKeywordsSyntax;
this.syntax = syntax;
this.generic = false;
this.atrules = Object.create(null);
this.properties = Object.create(null);
this.types = Object.create(null);
this.structure = structure || getStructureFromConfig(config);
constructor(config, syntax, structure) {
this.cssWideKeywordsSyntax = cssWideKeywordsSyntax;
this.syntax = syntax;
this.generic = false;
this.atrules = Object.create(null);
this.properties = Object.create(null);
this.types = Object.create(null);
this.structure = structure || getStructureFromConfig(config);
if (config) {
if (config.types) {
for (const name in config.types) {
this.addType_(name, config.types[name]);
}
}
if (config.generic) {
this.generic = true;
for (const name in generic) {
this.addType_(name, generic[name]);
}
}
if (config.atrules) {
for (const name in config.atrules) {
this.addAtrule_(name, config.atrules[name]);
}
}
if (config.properties) {
for (const name in config.properties) {
this.addProperty_(name, config.properties[name]);
}
}
if (config) {
if (config.types) {
for (const name in config.types) {
this.addType_(name, config.types[name]);
}
}
if (config.generic) {
this.generic = true;
for (const name in generic) {
this.addType_(name, generic[name]);
}
}
if (config.atrules) {
for (const name in config.atrules) {
this.addAtrule_(name, config.atrules[name]);
}
}
if (config.properties) {
for (const name in config.properties) {
this.addProperty_(name, config.properties[name]);
}
}
}
}
checkStructure(ast) {
function collectWarning(node, message) {
warns.push({ node, message });
}
checkStructure(ast) {
function collectWarning(node, message) {
warns.push({ node, message });
}
const structure = this.structure;
const warns = [];
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 + '`');
}
});
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;
}
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,
};
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
};
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 (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;
}
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 descriptor.syntax;
},
});
} else {
descriptor.syntax = syntax;
}
return result;
// lazy graph build on first access
Object.defineProperty(descriptor, 'match', {
get() {
Object.defineProperty(descriptor, 'match', {
value: buildMatchGraph(descriptor.syntax, ref),
});
return descriptor.match;
},
});
}
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;
return descriptor;
}
addAtrule_(name, syntax) {
if (!syntax) {
return;
}
validate() {
function validate(syntax, name, broken, descriptor) {
if (broken.has(name)) {
return broken.get(name);
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 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;
}
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;
const map = node.type === 'Type' ? syntax.types : syntax.properties;
const brokenMap = node.type === 'Type' ? brokenTypes : brokenProperties;
if (!hasOwnProperty.call(map, node.name) || validate(syntax, node.name, brokenMap, map[node.name])) {
broken.set(name, true);
}
}, this);
if (
!hasOwnProperty.call(map, node.name) ||
validate(syntax, node.name, brokenMap, map[node.name])
) {
broken.set(name, true);
}
}
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]);
}
brokenTypes = [...brokenTypes.keys()].filter(name => brokenTypes.get(name));
brokenProperties = [...brokenProperties.keys()].filter(name => brokenProperties.get(name));
if (brokenTypes.length || brokenProperties.length) {
return {
types: brokenTypes,
properties: brokenProperties
};
}
return null;
},
this
);
}
}
dump(syntaxAsAst, pretty) {
return {
generic: this.generic,
types: dumpMapSyntax(this.types, !pretty, syntaxAsAst),
properties: dumpMapSyntax(this.properties, !pretty, syntaxAsAst),
atrules: dumpAtruleMapSyntax(this.atrules, !pretty, syntaxAsAst)
};
let brokenTypes = new Map();
let brokenProperties = new Map();
for (const key in this.types) {
validate(this, key, brokenTypes, this.types[key]);
}
toString() {
return JSON.stringify(this.dump());
for (const key in this.properties) {
validate(this, key, brokenProperties, this.properties[key]);
}
};
brokenTypes = [...brokenTypes.keys()].filter((name) =>
brokenTypes.get(name)
);
brokenProperties = [...brokenProperties.keys()].filter((name) =>
brokenProperties.get(name)
);
if (brokenTypes.length || brokenProperties.length) {
return {
types: brokenTypes,
properties: brokenProperties,
};
}
return null;
}
dump(syntaxAsAst, pretty) {
return {
generic: this.generic,
types: dumpMapSyntax(this.types, !pretty, syntaxAsAst),
properties: dumpMapSyntax(this.properties, !pretty, syntaxAsAst),
atrules: dumpAtruleMapSyntax(this.atrules, !pretty, syntaxAsAst),
};
}
toString() {
return JSON.stringify(this.dump());
}
}

View File

@ -4,120 +4,133 @@ 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;
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;
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 (i === longestMatch) {
mismatchLength = token.length;
mismatchOffset = css.length;
}
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));
if (badNode !== null && tokens[i].node === badNode) {
if (i <= longestMatch) {
entries++;
} else {
entries = 0;
}
}
return {
css,
mismatchOffset,
mismatchLength,
start,
end
};
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];
const value = node && node.loc && node.loc[point];
if (value) {
return 'line' in value ? buildLoc(value) : value;
}
if (value) {
return 'line' in value ? buildLoc(value) : value;
}
return null;
return null;
}
function buildLoc({ offset, line, column }, extra) {
const loc = {
offset,
line,
column
};
const loc = {
offset,
line,
column,
};
if (extra) {
const lines = extra.split(/\n|\r\n?|\f/);
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;
}
loc.offset += extra.length;
loc.line += lines.length - 1;
loc.column =
lines.length === 1 ? loc.column + extra.length : lines.pop().length + 1;
}
return loc;
return loc;
}
export const SyntaxReferenceError = function(type, referenceName) {
const error = createCustomError(
'SyntaxReferenceError',
type + (referenceName ? ' `' + referenceName + '`' : '')
);
export const SyntaxReferenceError = function (type, referenceName) {
const error = createCustomError(
'SyntaxReferenceError',
type + (referenceName ? ' `' + referenceName + '`' : '')
);
error.reference = referenceName;
error.reference = referenceName;
return error;
return error;
};
export const SyntaxMatchError = function(message, syntax, node, matchResult) {
const error = createCustomError('SyntaxMatchError', message);
const {
css,
mismatchOffset,
mismatchLength,
start,
end
} = locateMismatch(matchResult, node);
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('-') + '^';
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
};
Object.assign(error, start);
error.loc = {
source: (node && node.loc && node.loc.source) || '<unknown>',
start,
end,
};
return error;
return error;
};

View File

@ -1,238 +1,246 @@
import {
isDigit,
cmpChar,
Delim,
WhiteSpace,
Comment,
Ident,
Number as NumberToken,
Dimension
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 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;
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);
}
while (
token !== null &&
(token.type === WhiteSpace || token.type === Comment)
) {
token = getNextToken(++offset);
}
return offset;
return offset;
}
function checkInteger(token, valueOffset, disallowSign, offset) {
if (!token) {
return 0;
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++;
}
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;
}
}
for (; valueOffset < token.value.length; valueOffset++) {
if (!isDigit(token.value.charCodeAt(valueOffset))) {
// Integer is expected
return 0;
}
}
return offset + 1;
return offset + 1;
}
// ... <signed-integer>
// ... ['+' | '-'] <signless-integer>
function consumeB(token, offset_, getNextToken) {
let sign = false;
let offset = skipSC(token, offset_, getNextToken);
let sign = false;
let offset = skipSC(token, offset_, getNextToken);
token = getNextToken(offset);
token = getNextToken(offset);
if (token === null) {
return 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 (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;
}
}
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);
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;
/* eslint-disable brace-style*/
let offset = 0;
if (!token) {
return 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;
}
// <integer>
if (token.type === NumberToken) {
return checkInteger(token, 0, ALLOW_SIGN, offset); // b
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);
}
// -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);
}
if (token === null || !cmpChar(token.value, 0, N)) {
return 0;
}
// '+'? 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);
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;
}
if (token === null || !cmpChar(token.value, 0, N)) {
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;
}
switch (token.value.length) {
// '+'? n
// '+'? n <signed-integer>
// '+'? n ['+' | '-'] <signless-integer>
case 1:
return consumeB(getNextToken(++offset), offset, getNextToken);
return checkInteger(token, 2, DISALLOW_SIGN, offset);
}
}
// '+'? n- <signless-integer>
case 2:
if (token.value.charCodeAt(1) !== HYPHENMINUS) {
return 0;
}
// <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;
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);
}
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;
}
// <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;
if (i + 1 === token.value.length) {
return consumeB(getNextToken(++offset), offset, getNextToken);
} else {
if (token.value.charCodeAt(i + 1) !== HYPHENMINUS) {
return 0;
}
for (; i < token.value.length; i++) {
if (!isDigit(token.value.charCodeAt(i))) {
break;
}
}
// <ndash-dimension> <signless-integer>
if (i + 2 === token.value.length) {
offset = skipSC(getNextToken(++offset), offset, getNextToken);
token = getNextToken(offset);
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 checkInteger(token, 0, DISALLOW_SIGN, offset);
}
// <ndashdigit-dimension>
else {
return checkInteger(token, i + 2, DISALLOW_SIGN, offset);
}
}
}
return 0;
};
return 0;
}

View File

@ -1,8 +1,8 @@
// https://drafts.csswg.org/css-cascade-5/
export const cssWideKeywords = [
'initial',
'inherit',
'unset',
'revert',
'revert-layer'
'initial',
'inherit',
'unset',
'revert',
'revert-layer',
];

View File

@ -1,62 +1,64 @@
import {
isHexDigit,
cmpChar,
Ident,
Delim,
Number as NumberToken,
Dimension
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)
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;
return (
token !== null && token.type === Delim && token.value.charCodeAt(0) === code
);
}
function startsWith(token, code) {
return token.value.charCodeAt(0) === code;
return token.value.charCodeAt(0) === code;
}
function hexSequence(token, offset, allowDash) {
let hexlen = 0;
let hexlen = 0;
for (let pos = offset; pos < token.value.length; pos++) {
const code = token.value.charCodeAt(pos);
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
};
if (code === HYPHENMINUS && allowDash && hexlen !== 0) {
hexSequence(token, offset + hexlen + 1, false);
return 6; // dissallow following question marks
}
return hexlen;
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
if (!consumed) {
return 0; // nothing consumed
}
while (isDelim(getNextToken(length), QUESTIONMARK)) {
if (++consumed > 6) {
return 0; // too many question marks
}
while (isDelim(getNextToken(length), QUESTIONMARK)) {
if (++consumed > 6) {
return 0; // too many question marks
}
length++;
}
length++;
}
return length;
return length;
}
// https://drafts.csswg.org/css-syntax/#urange
@ -79,73 +81,81 @@ function withQuestionMarkSequence(consumed, length, getNextToken) {
// u <number-token> <number-token> |
// u '+' '?'+
export default function urange(token, getNextToken) {
let length = 0;
let length = 0;
// should start with `u` or `U`
if (token === null || token.type !== Ident || !cmpChar(token.value, 0, U)) {
return 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) {
return 0;
// u <number-token> <eof>
return length;
}
// 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
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> '?'*
// 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;
}
return withQuestionMarkSequence(consumedHexLength, length, getNextToken);
}
token = getNextToken(++length);
if (token === null) {
// u <number-token> <eof>
return length;
}
// u <dimension-token> '?'*
if (token.type === Dimension) {
return withQuestionMarkSequence(
hexSequence(token, 1, true),
++length,
getNextToken
);
}
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;
};
return 0;
}

File diff suppressed because it is too large Load Diff

View File

@ -4,453 +4,420 @@ export const MATCH = { type: 'Match' };
export const MISMATCH = { type: 'Mismatch' };
export const DISALLOW_EMPTY = { type: 'DisallowEmpty' };
const LEFTPARENTHESIS = 40; // (
const LEFTPARENTHESIS = 40; // (
const RIGHTPARENTHESIS = 41; // )
function createCondition(match, thenBranch, elseBranch) {
// reduce node count
if (thenBranch === MATCH && elseBranch === MISMATCH) {
return match;
}
// reduce node count
if (thenBranch === MATCH && elseBranch === MISMATCH) {
return match;
}
if (match === MATCH && thenBranch === MATCH && elseBranch === MATCH) {
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;
}
if (match.type === 'If' && match.else === MISMATCH && thenBranch === MATCH) {
thenBranch = match.then;
match = match.match;
}
return {
type: 'If',
match,
then: thenBranch,
else: elseBranch
};
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
);
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)
);
return (
term.type === 'Keyword' ||
term.type === 'AtKeyword' ||
term.type === 'Function' ||
(term.type === 'Type' && isFunctionType(term.name))
);
}
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;
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];
for (let i = terms.length - 1; i >= 0; i--) {
const term = terms[i];
result = createCondition(
term,
result,
MISMATCH
);
};
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;
}
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);
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
);
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
);
// 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
);
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.comma) {
result.then.else = createCondition(
{ type: 'Comma', syntax: node },
result,
MISMATCH
);
}
if (node.min === 0) {
// allow zero match
} 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(
MATCH,
MATCH,
result
{ type: 'Comma', syntax: node },
result,
MISMATCH
);
} 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
);
}
result = createCondition(
matchTerm,
createCondition(MATCH, MATCH, result),
MISMATCH
);
}
}
return result;
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') {
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);
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: 'Generic',
fn: node
type: 'Token',
value: node.value.charAt(1),
syntax: node,
};
}
}
switch (node.type) {
case 'Group': {
let result = buildGroupMatchGraph(
node.combinator,
node.terms.map(buildMatchGraphInternal),
false
);
// otherwise use it as is
return {
type: node.type,
value: node.value.substr(1, node.value.length - 2).replace(/\\'/g, "'"),
syntax: node,
};
if (node.disallowEmpty) {
result = createCondition(
result,
DISALLOW_EMPTY,
MISMATCH
);
}
case 'Token':
return {
type: node.type,
value: node.value,
syntax: node,
};
return result;
}
case 'Comma':
return {
type: node.type,
syntax: node,
};
case 'Multiplier':
return buildMultiplierMatchGraph(node);
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);
}
default:
throw new Error('Unknown node type:', node.type);
}
}
export function buildMatchGraph(syntaxTree, ref) {
if (typeof syntaxTree === 'string') {
syntaxTree = parse(syntaxTree);
}
if (typeof syntaxTree === 'string') {
syntaxTree = parse(syntaxTree);
}
return {
type: 'MatchGraph',
match: buildMatchGraphInternal(syntaxTree),
syntax: ref || null,
source: syntaxTree
};
return {
type: 'MatchGraph',
match: buildMatchGraphInternal(syntaxTree),
syntax: ref || null,
source: syntaxTree,
};
}

File diff suppressed because it is too large Load Diff

View File

@ -1,50 +1,50 @@
import { tokenize } from '../tokenizer/index.js';
const astToTokens = {
decorator(handlers) {
const tokens = [];
let curNode = null;
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;
}
};
}
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 = [];
const tokens = [];
tokenize(str, (type, start, end) =>
tokens.push({
type,
value: str.slice(start, end),
node: null
})
);
tokenize(str, (type, start, end) =>
tokens.push({
type,
value: str.slice(start, end),
node: null,
})
);
return tokens;
return tokens;
}
export default function(value, syntax) {
if (typeof value === 'string') {
return stringToTokens(value);
}
export default function (value, syntax) {
if (typeof value === 'string') {
return stringToTokens(value);
}
return syntax.generate(value, astToTokens);
};
return syntax.generate(value, astToTokens);
}

View File

@ -1,61 +1,63 @@
import { List } from '../utils/List.js';
function getFirstMatchNode(matchNode) {
if ('node' in matchNode) {
return matchNode.node;
}
if ('node' in matchNode) {
return matchNode.node;
}
return getFirstMatchNode(matchNode.match[0]);
return getFirstMatchNode(matchNode.match[0]);
}
function getLastMatchNode(matchNode) {
if ('node' in matchNode) {
return matchNode.node;
}
if ('node' in matchNode) {
return matchNode.node;
}
return getLastMatchNode(matchNode.match[matchNode.match.length - 1]);
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);
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();
lexer.syntax.walk(ast, function (node, item, list) {
if (node === start) {
const nodes = new List();
do {
nodes.appendData(item.data);
do {
nodes.appendData(item.data);
if (item.data === end) {
break;
}
if (item.data === end) {
break;
}
item = item.next;
} while (item !== null);
item = item.next;
} while (item !== null);
fragments.push({
parent: list,
nodes
});
}
});
}
if (Array.isArray(matchNode.match)) {
matchNode.match.forEach(findFragments);
fragments.push({
parent: list,
nodes,
});
}
});
}
const fragments = [];
if (match.matched !== null) {
findFragments(match.matched);
if (Array.isArray(matchNode.match)) {
matchNode.match.forEach(findFragments);
}
}
return fragments;
const fragments = [];
if (match.matched !== null) {
findFragments(match.matched);
}
return fragments;
}

View File

@ -3,162 +3,178 @@ 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
);
// 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)
);
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');
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;
}
}
for (let key in node) {
let valid = true;
valid = false;
} else if (fields.hasOwnProperty(key)) {
valid = false;
if (hasOwnProperty.call(node, key) === false) {
continue;
}
for (let i = 0; !valid && i < fields[key].length; i++) {
const fieldType = fields[key][i];
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;
}
}
switch (fieldType) {
case String:
valid = typeof node[key] === 'string';
break;
valid = false;
} else if (fields.hasOwnProperty(key)) {
valid = false;
case Boolean:
valid = typeof node[key] === 'boolean';
break;
for (let i = 0; !valid && i < fields[key].length; i++) {
const fieldType = fields[key][i];
case null:
valid = node[key] === null;
break;
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 + '`');
}
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');
}
for (const key in fields) {
if (hasOwnProperty.call(fields, key) &&
hasOwnProperty.call(node, key) === false) {
warn(node, 'Field `' + type + '.' + key + '` is missed');
}
}
};
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 processStructure(name, nodeType) {
const structure = nodeType.structure;
const fields = {
type: String,
loc: true
};
const docs = {
type: '"' + name + '"'
};
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 docsTypes = [];
const fieldTypes = fields[key] = Array.isArray(structure[key])
? structure[key].slice()
: [structure[key]];
for (let i = 0; i < fieldTypes.length; i++) {
const fieldType = fieldTypes[i];
if (fieldType === String || fieldType === Boolean) {
docsTypes.push(fieldType.name);
} else if (fieldType === null) {
docsTypes.push('null');
} else if (typeof fieldType === 'string') {
docsTypes.push('<' + fieldType + '>');
} else if (Array.isArray(fieldType)) {
docsTypes.push('List'); // TODO: use type enum
} else {
throw new Error('Wrong value `' + fieldType + '` in `' + name + '.' + key + '` structure definition');
}
}
docs[key] = docsTypes.join(' | ');
for (const key in structure) {
if (hasOwnProperty.call(structure, key) === false) {
continue;
}
return {
docs,
check: createNodeStructureChecker(name, fields)
};
const docsTypes = [];
const fieldTypes = (fields[key] =
Array.isArray(structure[key]) ?
structure[key].slice()
: [structure[key]]);
for (let i = 0; i < fieldTypes.length; i++) {
const fieldType = fieldTypes[i];
if (fieldType === String || fieldType === Boolean) {
docsTypes.push(fieldType.name);
} else if (fieldType === null) {
docsTypes.push('null');
} else if (typeof fieldType === 'string') {
docsTypes.push('<' + fieldType + '>');
} else if (Array.isArray(fieldType)) {
docsTypes.push('List'); // TODO: use type enum
} else {
throw new Error(
'Wrong value `' +
fieldType +
'` in `' +
name +
'.' +
key +
'` structure definition'
);
}
}
docs[key] = docsTypes.join(' | ');
}
return {
docs,
check: createNodeStructureChecker(name, fields),
};
}
export function getStructureFromConfig(config) {
const structure = {};
const structure = {};
if (config.node) {
for (const name in config.node) {
if (hasOwnProperty.call(config.node, name)) {
const nodeType = config.node[name];
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');
}
}
if (nodeType.structure) {
structure[name] = processStructure(name, nodeType);
} else {
throw new Error(
'Missed `structure` field in `' + name + '` node type definition'
);
}
}
}
}
return structure;
};
return structure;
}

View File

@ -1,66 +1,72 @@
export function getTrace(node) {
function shouldPutToTrace(syntax) {
if (syntax === null) {
return false;
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 (
syntax.type === 'Type' ||
syntax.type === 'Property' ||
syntax.type === 'Keyword'
);
return true;
}
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 false;
}
return true;
}
}
} else if (matchNode.node === node) {
result = shouldPutToTrace(matchNode.syntax)
? [matchNode.syntax]
: [];
let result = null;
return true;
}
if (this.matched !== null) {
hasMatch(this.matched);
}
return false;
}
let result = null;
if (this.matched !== null) {
hasMatch(this.matched);
}
return result;
return result;
}
export function isType(node, type) {
return testNode(this, node, match => match.type === 'Type' && match.name === 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);
return testNode(
this,
node,
(match) => match.type === 'Property' && match.name === property
);
}
export function isKeyword(node) {
return testNode(this, node, match => match.type === 'Keyword');
return testNode(this, node, (match) => match.type === 'Keyword');
}
function testNode(match, node, fn) {
const trace = getTrace.call(match, node);
const trace = getTrace.call(match, node);
if (trace === null) {
return false;
}
if (trace === null) {
return false;
}
return trace.some(fn);
return trace.some(fn);
}

View File

@ -5,61 +5,70 @@ const OFFSET_CORRECTION = 60;
const TAB_REPLACEMENT = ' ';
function sourceFragment({ source, line, column }, extraLines) {
function processLines(start, end) {
return lines
.slice(start, end)
.map((line, idx) =>
String(start + idx + 1).padStart(maxNumLength) + ' |' + line
).join('\n');
function processLines(start, end) {
return lines
.slice(start, end)
.map(
(line, idx) =>
String(start + idx + 1).padStart(maxNumLength) + ' |' + line
)
.join('\n');
}
const lines = 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' : '');
}
}
const lines = 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');
return [
processLines(startLine, line),
new Array(column + maxNumLength + 2).join('-') + '^',
processLines(line, endLine),
]
.filter(Boolean)
.join('\n');
}
export function SyntaxError(message, source, offset, line, column) {
const error = Object.assign(createCustomError('SyntaxError', message), {
source,
offset,
line,
column,
sourceFragment(extraLines) {
return sourceFragment({ source, line, column }, isNaN(extraLines) ? 0 : extraLines);
},
get formattedMessage() {
return (
`Parse error: ${message}\n` +
sourceFragment({ source, line, column }, 2)
);
}
});
const error = Object.assign(createCustomError('SyntaxError', message), {
source,
offset,
line,
column,
sourceFragment(extraLines) {
return sourceFragment(
{ source, line, column },
isNaN(extraLines) ? 0 : extraLines
);
},
get formattedMessage() {
return (
`Parse error: ${message}\n` +
sourceFragment({ source, line, column }, 2)
);
},
});
return error;
return error;
}

View File

@ -1,346 +1,362 @@
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
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 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]();
};
return function () {
return this[name]();
};
}
function fetchParseValues(dict) {
const result = Object.create(null);
const result = Object.create(null);
for (const name in dict) {
const item = dict[name];
const fn = item.parse || item;
for (const name in dict) {
const item = dict[name];
const fn = item.parse || item;
if (fn) {
result[name] = fn;
}
if (fn) {
result[name] = fn;
}
}
return result;
return result;
}
function processConfig(config) {
const parseConfig = {
context: Object.create(null),
scope: Object.assign(Object.create(null), config.scope),
atrule: fetchParseValues(config.atrule),
pseudo: fetchParseValues(config.pseudo),
node: fetchParseValues(config.node)
};
const parseConfig = {
context: Object.create(null),
scope: Object.assign(Object.create(null), config.scope),
atrule: fetchParseValues(config.atrule),
pseudo: fetchParseValues(config.pseudo),
node: fetchParseValues(config.node),
};
for (const name in config.parseContext) {
switch (typeof config.parseContext[name]) {
case 'function':
parseConfig.context[name] = config.parseContext[name];
break;
for (const name in config.parseContext) {
switch (typeof config.parseContext[name]) {
case 'function':
parseConfig.context[name] = config.parseContext[name];
break;
case 'string':
parseConfig.context[name] = createParseContext(config.parseContext[name]);
break;
}
case 'string':
parseConfig.context[name] = createParseContext(
config.parseContext[name]
);
break;
}
}
return {
config: parseConfig,
...parseConfig,
...parseConfig.node
};
return {
config: parseConfig,
...parseConfig,
...parseConfig.node,
};
}
export function createParser(config) {
let source = '';
let filename = '<unknown>';
let needPositions = false;
let onParseError = NOOP;
let onParseErrorThrow = false;
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,
const locationMap = new OffsetToLocation();
const parser = Object.assign(new TokenStream(), processConfig(config || {}), {
parseAtrulePrelude: true,
parseRulePrelude: true,
parseValue: true,
parseCustomProperty: false,
readSequence,
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;
},
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;
},
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 startToken = this.tokenIndex;
parseWithFallback(consumer, fallback) {
const startToken = this.tokenIndex;
try {
return consumer.call(this);
} catch (e) {
if (onParseErrorThrow) {
throw e;
}
const fallbackNode = fallback.call(this, startToken);
onParseErrorThrow = true;
onParseError(e, fallbackNode);
onParseErrorThrow = false;
return fallbackNode;
}
},
lookupNonWSType(offset) {
let type;
do {
type = this.lookupType(offset++);
if (type !== WhiteSpace) {
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
);
try {
return consumer.call(this);
} catch (e) {
if (onParseErrorThrow) {
throw e;
}
});
const parse = function(source_, options) {
source = source_;
options = options || {};
const fallbackNode = fallback.call(this, startToken);
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 = true;
onParseError(e, fallbackNode);
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;
return fallbackNode;
}
},
const { context = 'default', onComment } = options;
lookupNonWSType(offset) {
let type;
if (context in parser.context === false) {
throw new Error('Unknown context `' + context + '`');
do {
type = this.lookupType(offset++);
if (type !== WhiteSpace) {
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;
}
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);
this.error(message, offset);
}
onComment(value, loc);
}
});
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
);
},
});
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);
const ast = parser.context[context].call(parser, options);
if (!parser.eof) {
parser.error();
}
if (!parser.eof) {
parser.error();
}
return ast;
};
return ast;
};
return Object.assign(parse, {
SyntaxError,
config: parser.config
});
};
return Object.assign(parse, {
SyntaxError,
config: parser.config,
});
}

View File

@ -1,43 +1,43 @@
import { WhiteSpace, Comment } from '../tokenizer/index.js';
export function readSequence(recognizer) {
const children = this.createList();
let space = false;
const context = {
recognizer
};
const children = this.createList();
let space = false;
const context = {
recognizer,
};
while (!this.eof) {
switch (this.tokenType) {
case Comment:
this.next();
continue;
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);
case WhiteSpace:
space = true;
this.next();
continue;
}
if (space && recognizer.onWhiteSpace) {
recognizer.onWhiteSpace.call(this, null, children, context);
let child = recognizer.getNode.call(this, context);
if (child === undefined) {
break;
}
return children;
};
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;
}

View File

@ -1,8 +1,8 @@
export default {
parse: {
prelude: null,
block() {
return this.Block(true);
}
}
parse: {
prelude: null,
block() {
return this.Block(true);
},
},
};

View File

@ -1,39 +1,41 @@
import {
String as StringToken,
Ident,
Url,
Function as FunctionToken,
LeftParenthesis
String as StringToken,
Ident,
Url,
Function as FunctionToken,
LeftParenthesis,
} from '../../tokenizer/index.js';
export default {
parse: {
prelude() {
const children = this.createList();
parse: {
prelude() {
const children = this.createList();
this.skipSC();
this.skipSC();
switch (this.tokenType) {
case StringToken:
children.push(this.String());
break;
switch (this.tokenType) {
case StringToken:
children.push(this.String());
break;
case Url:
case FunctionToken:
children.push(this.Url());
break;
case Url:
case FunctionToken:
children.push(this.Url());
break;
default:
this.error('String or url() is expected');
}
default:
this.error('String or url() is expected');
}
if (this.lookupNonWSType(0) === Ident ||
this.lookupNonWSType(0) === LeftParenthesis) {
children.push(this.MediaQueryList());
}
if (
this.lookupNonWSType(0) === Ident ||
this.lookupNonWSType(0) === LeftParenthesis
) {
children.push(this.MediaQueryList());
}
return children;
},
block: null
}
return children;
},
block: null,
},
};

View File

@ -5,9 +5,9 @@ import page from './page.js';
import supports from './supports.js';
export default {
'font-face': fontFace,
'import': importAtrule,
media,
page,
supports
'font-face': fontFace,
import: importAtrule,
media,
page,
supports,
};

View File

@ -1,12 +1,10 @@
export default {
parse: {
prelude() {
return this.createSingleNodeList(
this.MediaQueryList()
);
},
block() {
return this.Block(false);
}
}
parse: {
prelude() {
return this.createSingleNodeList(this.MediaQueryList());
},
block() {
return this.Block(false);
},
},
};

View File

@ -1,12 +1,10 @@
export default {
parse: {
prelude() {
return this.createSingleNodeList(
this.SelectorList()
);
},
block() {
return this.Block(true);
}
}
parse: {
prelude() {
return this.createSingleNodeList(this.SelectorList());
},
block() {
return this.Block(true);
},
},
};

View File

@ -1,80 +1,74 @@
import {
WhiteSpace,
Comment,
Ident,
Function,
Colon,
LeftParenthesis
WhiteSpace,
Comment,
Ident,
Function,
Colon,
LeftParenthesis,
} from '../../tokenizer/index.js';
function consumeRaw() {
return this.createSingleNodeList(
this.Raw(this.tokenIndex, null, false)
);
return this.createSingleNodeList(this.Raw(this.tokenIndex, null, false));
}
function parentheses() {
this.skipSC();
this.skipSC();
if (this.tokenType === Ident &&
this.lookupNonWSType(1) === Colon) {
return this.createSingleNodeList(
this.Declaration()
);
}
if (this.tokenType === Ident && this.lookupNonWSType(1) === Colon) {
return this.createSingleNodeList(this.Declaration());
}
return readSequence.call(this);
return readSequence.call(this);
}
function readSequence() {
const children = this.createList();
let child;
const children = this.createList();
let child;
this.skipSC();
this.skipSC();
scan:
while (!this.eof) {
switch (this.tokenType) {
case Comment:
case WhiteSpace:
this.next();
continue;
scan: while (!this.eof) {
switch (this.tokenType) {
case Comment:
case WhiteSpace:
this.next();
continue;
case Function:
child = this.Function(consumeRaw, this.scope.AtrulePrelude);
break;
case Function:
child = this.Function(consumeRaw, this.scope.AtrulePrelude);
break;
case Ident:
child = this.Identifier();
break;
case Ident:
child = this.Identifier();
break;
case LeftParenthesis:
child = this.Parentheses(parentheses, this.scope.AtrulePrelude);
break;
case LeftParenthesis:
child = this.Parentheses(parentheses, this.scope.AtrulePrelude);
break;
default:
break scan;
}
children.push(child);
default:
break scan;
}
return children;
children.push(child);
}
return children;
}
export default {
parse: {
prelude() {
const children = readSequence.call(this);
parse: {
prelude() {
const children = readSequence.call(this);
if (this.getFirstListNode(children) === null) {
this.error('Condition is expected');
}
if (this.getFirstListNode(children) === null) {
this.error('Condition is expected');
}
return children;
},
block() {
return this.Block(false);
}
}
return children;
},
block() {
return this.Block(false);
},
},
};

View File

@ -1,5 +1,5 @@
import * as node from '../node/index-generate.js';
export default {
node
node,
};

View File

@ -2,7 +2,7 @@ import definitions from '../../data.js';
import * as node from '../node/index.js';
export default {
generic: true,
...definitions,
node
generic: true,
...definitions,
node,
};

View File

@ -1,138 +1,135 @@
const { hasOwnProperty } = Object.prototype;
const shape = {
generic: true,
types: appendOrAssign,
atrules: {
prelude: appendOrAssignOrNull,
descriptors: appendOrAssignOrNull
},
properties: appendOrAssign,
parseContext: assign,
scope: deepAssign,
atrule: ['parse'],
pseudo: ['parse'],
node: ['name', 'structure', 'parse', 'generate', 'walkContext']
generic: true,
types: appendOrAssign,
atrules: {
prelude: appendOrAssignOrNull,
descriptors: appendOrAssignOrNull,
},
properties: appendOrAssign,
parseContext: assign,
scope: deepAssign,
atrule: ['parse'],
pseudo: ['parse'],
node: ['name', 'structure', 'parse', 'generate', 'walkContext'],
};
function isObject(value) {
return value && value.constructor === Object;
return value && value.constructor === Object;
}
function copy(value) {
return isObject(value)
? { ...value }
: value;
return isObject(value) ? { ...value } : value;
}
function assign(dest, src) {
return Object.assign(dest, src);
return Object.assign(dest, src);
}
function deepAssign(dest, src) {
for (const key in src) {
if (hasOwnProperty.call(src, key)) {
if (isObject(dest[key])) {
deepAssign(dest[key], src[key]);
} else {
dest[key] = copy(src[key]);
}
}
for (const key in src) {
if (hasOwnProperty.call(src, key)) {
if (isObject(dest[key])) {
deepAssign(dest[key], src[key]);
} else {
dest[key] = copy(src[key]);
}
}
}
return dest;
return dest;
}
function append(a, b) {
if (typeof b === 'string' && /^\s*\|/.test(b)) {
return typeof a === 'string'
? a + b
: b.replace(/^\s*\|\s*/, '');
}
if (typeof b === 'string' && /^\s*\|/.test(b)) {
return typeof a === 'string' ? a + b : b.replace(/^\s*\|\s*/, '');
}
return b || null;
return b || null;
}
function appendOrAssign(a, b) {
if (typeof b === 'string') {
return append(a, b);
}
if (typeof b === 'string') {
return append(a, b);
}
const result = { ...a };
for (let key in b) {
if (hasOwnProperty.call(b, key)) {
result[key] = append(hasOwnProperty.call(a, key) ? a[key] : undefined, b[key]);
}
const result = { ...a };
for (let key in b) {
if (hasOwnProperty.call(b, key)) {
result[key] = append(
hasOwnProperty.call(a, key) ? a[key] : undefined,
b[key]
);
}
}
return result;
return result;
}
function appendOrAssignOrNull(a, b) {
const result = appendOrAssign(a, b);
const result = appendOrAssign(a, b);
return !isObject(result) || Object.keys(result).length
? result
: null;
return !isObject(result) || Object.keys(result).length ? result : null;
}
function mix(dest, src, shape) {
for (const key in shape) {
if (hasOwnProperty.call(shape, key) === false) {
continue;
}
if (shape[key] === true) {
if (hasOwnProperty.call(src, key)) {
dest[key] = copy(src[key]);
}
} else if (shape[key]) {
if (typeof shape[key] === 'function') {
const fn = shape[key];
dest[key] = fn({}, dest[key]);
dest[key] = fn(dest[key] || {}, src[key]);
} else if (isObject(shape[key])) {
const result = {};
for (let name in dest[key]) {
result[name] = mix({}, dest[key][name], shape[key]);
}
for (let name in src[key]) {
result[name] = mix(result[name] || {}, src[key][name], shape[key]);
}
dest[key] = result;
} else if (Array.isArray(shape[key])) {
const res = {};
const innerShape = shape[key].reduce(function(s, k) {
s[k] = true;
return s;
}, {});
for (const [name, value] of Object.entries(dest[key] || {})) {
res[name] = {};
if (value) {
mix(res[name], value, innerShape);
}
}
for (const name in src[key]) {
if (hasOwnProperty.call(src[key], name)) {
if (!res[name]) {
res[name] = {};
}
if (src[key] && src[key][name]) {
mix(res[name], src[key][name], innerShape);
}
}
}
dest[key] = res;
}
}
for (const key in shape) {
if (hasOwnProperty.call(shape, key) === false) {
continue;
}
return dest;
if (shape[key] === true) {
if (hasOwnProperty.call(src, key)) {
dest[key] = copy(src[key]);
}
} else if (shape[key]) {
if (typeof shape[key] === 'function') {
const fn = shape[key];
dest[key] = fn({}, dest[key]);
dest[key] = fn(dest[key] || {}, src[key]);
} else if (isObject(shape[key])) {
const result = {};
for (let name in dest[key]) {
result[name] = mix({}, dest[key][name], shape[key]);
}
for (let name in src[key]) {
result[name] = mix(result[name] || {}, src[key][name], shape[key]);
}
dest[key] = result;
} else if (Array.isArray(shape[key])) {
const res = {};
const innerShape = shape[key].reduce(function (s, k) {
s[k] = true;
return s;
}, {});
for (const [name, value] of Object.entries(dest[key] || {})) {
res[name] = {};
if (value) {
mix(res[name], value, innerShape);
}
}
for (const name in src[key]) {
if (hasOwnProperty.call(src[key], name)) {
if (!res[name]) {
res[name] = {};
}
if (src[key] && src[key][name]) {
mix(res[name], src[key][name], innerShape);
}
}
}
dest[key] = res;
}
}
}
return dest;
}
export default (dest, src) => mix(dest, src, shape);

View File

@ -3,13 +3,13 @@ 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
parseContext: {
default: 'SelectorList',
selectorList: 'SelectorList',
selector: 'Selector',
},
scope: { Selector },
atrule: {},
pseudo,
node,
};

View File

@ -4,27 +4,27 @@ 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',
rule: 'Rule',
selectorList: 'SelectorList',
selector: 'Selector',
block() {
return this.Block(true);
},
declarationList: 'DeclarationList',
declaration: 'Declaration',
value: 'Value'
parseContext: {
default: 'StyleSheet',
stylesheet: 'StyleSheet',
atrule: 'Atrule',
atrulePrelude(options) {
return this.AtrulePrelude(options.atrule ? String(options.atrule) : null);
},
scope,
atrule,
pseudo,
node
mediaQueryList: 'MediaQueryList',
mediaQuery: 'MediaQuery',
rule: 'Rule',
selectorList: 'SelectorList',
selector: 'Selector',
block() {
return this.Block(true);
},
declarationList: 'DeclarationList',
declaration: 'Declaration',
value: 'Value',
},
scope,
atrule,
pseudo,
node,
};

View File

@ -1,5 +1,5 @@
import * as node from '../node/index.js';
export default {
node
node,
};

View File

@ -7,47 +7,50 @@ 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 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),
const syntax = {
lexer: null,
createLexer: (config) => new Lexer(config, syntax, syntax.lexer.structure),
tokenize,
parse,
generate,
tokenize,
parse,
generate,
walk,
find: walk.find,
findLast: walk.findLast,
findAll: walk.findAll,
walk,
find: walk.find,
findLast: walk.findLast,
findAll: walk.findAll,
fromPlainObject,
toPlainObject,
fromPlainObject,
toPlainObject,
fork(extension) {
const base = mix({}, config); // copy of config
fork(extension) {
const base = mix({}, config); // copy of config
return createSyntax(
typeof extension === 'function'
? extension(base, Object.assign)
: mix(base, extension)
);
}
};
return createSyntax(
typeof extension === 'function' ?
extension(base, Object.assign)
: mix(base, extension)
);
},
};
syntax.lexer = new Lexer({
generic: true,
types: config.types,
atrules: config.atrules,
properties: config.properties,
node: config.node
}, syntax);
syntax.lexer = new Lexer(
{
generic: true,
types: config.types,
atrules: config.atrules,
properties: config.properties,
node: config.node,
},
syntax
);
return syntax;
};
return syntax;
}
export default config => createSyntax(mix({}, config));
export default (config) => createSyntax(mix({}, config));

View File

@ -1,7 +1,5 @@
// legacy IE function
// expression( <any-value> )
export default function() {
return this.createSingleNodeList(
this.Raw(this.tokenIndex, null, false)
);
export default function () {
return this.createSingleNodeList(this.Raw(this.tokenIndex, null, false));
}

View File

@ -1,39 +1,44 @@
import { Comma, WhiteSpace } from '../../tokenizer/index.js';
// var( <ident> , <value>? )
export default function() {
const children = this.createList();
export default function () {
const children = this.createList();
this.skipSC();
this.skipSC();
// NOTE: Don't check more than a first argument is an ident, rest checks are for lexer
children.push(this.Identifier());
// NOTE: Don't check more than a first argument is an ident, rest checks are for lexer
children.push(this.Identifier());
this.skipSC();
this.skipSC();
if (this.tokenType === Comma) {
children.push(this.Operator());
if (this.tokenType === Comma) {
children.push(this.Operator());
const startIndex = this.tokenIndex;
const value = this.parseCustomProperty
? this.Value(null)
: this.Raw(this.tokenIndex, this.consumeUntilExclamationMarkOrSemicolon, false);
const startIndex = this.tokenIndex;
const value =
this.parseCustomProperty ?
this.Value(null)
: this.Raw(
this.tokenIndex,
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;
}
}
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;
};
children.push(value);
}
return children;
}

View File

@ -4,7 +4,7 @@ import parserConfig from './config/parser.js';
import walkerConfig from './config/walker.js';
export default createSyntax({
...lexerConfig,
...parserConfig,
...walkerConfig
...lexerConfig,
...parserConfig,
...walkerConfig,
});

View File

@ -1,292 +1,297 @@
import {
isDigit,
WhiteSpace,
Comment,
Ident,
Number,
Dimension
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 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);
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++;
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);
}
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);
return checkInteger.call(this, 0, disallowSign);
}
function expectCharCode(offset, code) {
if (!this.cmpChar(this.tokenStart + offset, code)) {
let msg = '';
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);
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;
let offset = 0;
let sign = 0;
let type = this.tokenType;
while (type === WhiteSpace || type === Comment) {
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) {
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) {
if (type !== Number) {
this.skip(offset);
checkTokenIsInteger.call(this, DISALLOW_SIGN);
}
} else {
return null;
}
}
if (sign === 0) {
type = this.charCodeAt(this.tokenStart);
if (type !== PLUSSIGN && type !== HYPHENMINUS) {
this.error('Number sign is expected');
}
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);
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]
a: [String, null],
b: [String, null],
};
export function parse() {
/* eslint-disable brace-style*/
const start = this.tokenStart;
let a = null;
let b = null;
/* 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);
// <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();
}
// -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, 0, N);
expectCharCode.call(this, 1, N);
switch (this.tokenEnd - this.tokenStart) {
// '+'? n
// '+'? n <signed-integer>
// '+'? n ['+' | '-'] <signless-integer>
case 1:
this.next();
b = consumeB.call(this);
break;
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 2:
expectCharCode.call(this, 1, HYPHENMINUS);
// -n- <signless-integer>
case 3:
expectCharCode.call(this, 2, HYPHENMINUS);
this.next();
this.skipSC();
this.next();
this.skipSC();
checkTokenIsInteger.call(this, DISALLOW_SIGN);
checkTokenIsInteger.call(this, DISALLOW_SIGN);
b = '-' + this.consume(Number);
break;
b = '-' + this.consume(Number);
break;
// '+'? <ndashdigit-ident>
default:
expectCharCode.call(this, 1, HYPHENMINUS);
checkInteger.call(this, 2, DISALLOW_SIGN);
this.next();
// <dashndashdigit-ident>
default:
expectCharCode.call(this, 2, HYPHENMINUS);
checkInteger.call(this, 3, DISALLOW_SIGN);
this.next();
b = this.substrToCursor(start + sign + 1);
}
}
b = this.substrToCursor(start + 2);
}
// <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;
}
}
// '+'? 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);
}
if (i === this.tokenStart + sign) {
this.error('Integer is expected', this.tokenStart + sign);
}
// <ndashdigit-dimension>
// <ndash-dimension> <signless-integer>
expectCharCode.call(this, i - this.tokenStart, N);
a = this.substring(start, i);
// <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);
}
}
if (i + 1 === this.tokenEnd) {
this.next();
b = consumeB.call(this);
} else {
this.error();
}
expectCharCode.call(this, i - this.tokenStart + 1, HYPHENMINUS);
if (a !== null && a.charCodeAt(0) === PLUSSIGN) {
a = a.substr(1);
// <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 (b !== null && b.charCodeAt(0) === PLUSSIGN) {
b = b.substr(1);
}
if (a !== null && a.charCodeAt(0) === PLUSSIGN) {
a = a.substr(1);
}
return {
type: 'AnPlusB',
loc: this.getLocation(start, this.tokenStart),
a,
b
};
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.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);
}
if (node.b) {
const b = node.b[0] === '-' || node.b[0] === '+' ? node.b : '+' + node.b;
this.tokenize(a + b);
} else {
this.tokenize(node.b);
this.tokenize(a);
}
} else {
this.tokenize(node.b);
}
}

View File

@ -1,100 +1,109 @@
import {
AtKeyword,
Semicolon,
LeftCurlyBracket,
RightCurlyBracket
AtKeyword,
Semicolon,
LeftCurlyBracket,
RightCurlyBracket,
} from '../../tokenizer/index.js';
function consumeRaw(startToken) {
return this.Raw(startToken, this.consumeUntilLeftCurlyBracketOrSemicolon, true);
return this.Raw(
startToken,
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;
}
for (let offset = 1, type; (type = this.lookupType(offset)); offset++) {
if (type === RightCurlyBracket) {
return true;
}
return false;
}
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]
name: String,
prelude: ['AtrulePrelude', 'Raw', null],
block: ['Block', null],
};
export function parse() {
const start = this.tokenStart;
let name;
let nameLowerCase;
let prelude = null;
let block = null;
const start = this.tokenStart;
let name;
let nameLowerCase;
let prelude = null;
let block = null;
this.eat(AtKeyword);
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),
consumeRaw
);
} else {
prelude = consumeRaw.call(this, this.tokenIndex);
}
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), consumeRaw);
} else {
prelude = consumeRaw.call(this, this.tokenIndex);
}
switch (this.tokenType) {
case Semicolon:
this.next();
break;
this.skipSC();
}
case LeftCurlyBracket:
if (
hasOwnProperty.call(this.atrule, nameLowerCase) &&
typeof this.atrule[nameLowerCase].block === 'function'
) {
block = this.atrule[nameLowerCase].block.call(this);
} else {
// TODO: should consume block content as Raw?
block = this.Block(isDeclarationBlockAtrule.call(this));
}
switch (this.tokenType) {
case Semicolon:
this.next();
break;
break;
}
case LeftCurlyBracket:
if (hasOwnProperty.call(this.atrule, nameLowerCase) &&
typeof this.atrule[nameLowerCase].block === 'function') {
block = this.atrule[nameLowerCase].block.call(this);
} 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
};
return {
type: 'Atrule',
loc: this.getLocation(start, this.tokenStart),
name,
prelude,
block,
};
}
export function generate(node) {
this.token(AtKeyword, '@' + node.name);
this.token(AtKeyword, '@' + node.name);
if (node.prelude !== null) {
this.node(node.prelude);
}
if (node.prelude !== null) {
this.node(node.prelude);
}
if (node.block) {
this.node(node.block);
} else {
this.token(Semicolon, ';');
}
if (node.block) {
this.node(node.block);
} else {
this.token(Semicolon, ';');
}
}

View File

@ -1,47 +1,48 @@
import {
Semicolon,
LeftCurlyBracket
} from '../../tokenizer/index.js';
import { Semicolon, LeftCurlyBracket } from '../../tokenizer/index.js';
export const name = 'AtrulePrelude';
export const walkContext = 'atrulePrelude';
export const structure = {
children: [[]]
children: [[]],
};
export function parse(name) {
let children = null;
let children = null;
if (name !== null) {
name = name.toLowerCase();
}
if (name !== null) {
name = name.toLowerCase();
}
this.skipSC();
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);
}
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();
this.skipSC();
if (this.eof !== true &&
this.tokenType !== LeftCurlyBracket &&
this.tokenType !== Semicolon) {
this.error('Semicolon or block is expected');
}
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
};
return {
type: 'AtrulePrelude',
loc: this.getLocationFromList(children),
children,
};
}
export function generate(node) {
this.children(node);
this.children(node);
}

View File

@ -1,147 +1,147 @@
import {
Ident,
String as StringToken,
Delim,
LeftSquareBracket,
RightSquareBracket
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 (~)
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');
}
if (this.eof) {
this.error('Unexpected end of input');
}
const start = this.tokenStart;
let expectIdent = false;
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(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);
}
if (this.isDelim(VERTICALLINE)) {
if (this.charCodeAt(this.tokenStart + 1) !== EQUALSSIGN) {
this.next();
this.eat(Ident);
} else if (expectIdent) {
this.error('Vertical line is expected');
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)
};
return {
type: 'Identifier',
loc: this.getLocation(start, this.tokenStart),
name: this.substrToCursor(start),
};
}
function getOperator() {
const start = this.tokenStart;
const code = this.charCodeAt(start);
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');
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();
}
if (code !== EQUALSSIGN) {
if (!this.isDelim(EQUALSSIGN)) {
this.error('Equal sign is expected');
}
this.next();
}
return this.substrToCursor(start);
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]
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;
const start = this.tokenStart;
let name;
let matcher = null;
let value = null;
let flags = null;
this.eat(LeftSquareBracket);
this.skipSC();
this.eat(LeftSquareBracket);
this.skipSC();
name = getAttributeName.call(this);
this.skipSC();
name = getAttributeName.call(this);
this.skipSC();
if (this.tokenType !== RightSquareBracket) {
// avoid case `[name i]`
if (this.tokenType !== Ident) {
matcher = getOperator.call(this);
if (this.tokenType !== RightSquareBracket) {
// avoid case `[name i]`
if (this.tokenType !== Ident) {
matcher = getOperator.call(this);
this.skipSC();
this.skipSC();
value = this.tokenType === StringToken
? this.String()
: this.Identifier();
value =
this.tokenType === StringToken ? this.String() : this.Identifier();
this.skipSC();
}
// attribute flags
if (this.tokenType === Ident) {
flags = this.consume(Ident);
this.skipSC();
}
this.skipSC();
}
this.eat(RightSquareBracket);
// attribute flags
if (this.tokenType === Ident) {
flags = this.consume(Ident);
return {
type: 'AttributeSelector',
loc: this.getLocation(start, this.tokenStart),
name,
matcher,
value,
flags
};
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);
this.token(Delim, '[');
this.node(node.name);
if (node.matcher !== null) {
this.tokenize(node.matcher);
this.node(node.value);
}
if (node.matcher !== null) {
this.tokenize(node.matcher);
this.node(node.value);
}
if (node.flags !== null) {
this.token(Ident, node.flags);
}
if (node.flags !== null) {
this.token(Ident, node.flags);
}
this.token(Delim, ']');
this.token(Delim, ']');
}

View File

@ -1,89 +1,84 @@
import {
WhiteSpace,
Comment,
Semicolon,
AtKeyword,
LeftCurlyBracket,
RightCurlyBracket
WhiteSpace,
Comment,
Semicolon,
AtKeyword,
LeftCurlyBracket,
RightCurlyBracket,
} from '../../tokenizer/index.js';
function consumeRaw(startToken) {
return this.Raw(startToken, null, true);
return this.Raw(startToken, null, true);
}
function consumeRule() {
return this.parseWithFallback(this.Rule, consumeRaw);
return this.parseWithFallback(this.Rule, consumeRaw);
}
function consumeRawDeclaration(startToken) {
return this.Raw(startToken, this.consumeUntilSemicolonIncluded, true);
return this.Raw(startToken, this.consumeUntilSemicolonIncluded, true);
}
function consumeDeclaration() {
if (this.tokenType === Semicolon) {
return consumeRawDeclaration.call(this, this.tokenIndex);
}
if (this.tokenType === Semicolon) {
return consumeRawDeclaration.call(this, this.tokenIndex);
}
const node = this.parseWithFallback(this.Declaration, consumeRawDeclaration);
const node = this.parseWithFallback(this.Declaration, consumeRawDeclaration);
if (this.tokenType === Semicolon) {
this.next();
}
if (this.tokenType === Semicolon) {
this.next();
}
return node;
return node;
}
export const name = 'Block';
export const walkContext = 'block';
export const structure = {
children: [[
'Atrule',
'Rule',
'Declaration'
]]
children: [['Atrule', 'Rule', 'Declaration']],
};
export function parse(isDeclaration) {
const consumer = isDeclaration ? consumeDeclaration : consumeRule;
const start = this.tokenStart;
let children = this.createList();
const consumer = isDeclaration ? consumeDeclaration : consumeRule;
const start = this.tokenStart;
let children = this.createList();
this.eat(LeftCurlyBracket);
this.eat(LeftCurlyBracket);
scan:
while (!this.eof) {
switch (this.tokenType) {
case RightCurlyBracket:
break scan;
scan: while (!this.eof) {
switch (this.tokenType) {
case RightCurlyBracket:
break scan;
case WhiteSpace:
case Comment:
this.next();
break;
case WhiteSpace:
case Comment:
this.next();
break;
case AtKeyword:
children.push(this.parseWithFallback(this.Atrule, consumeRaw));
break;
case AtKeyword:
children.push(this.parseWithFallback(this.Atrule, consumeRaw));
break;
default:
children.push(consumer.call(this));
}
default:
children.push(consumer.call(this));
}
}
if (!this.eof) {
this.eat(RightCurlyBracket);
}
if (!this.eof) {
this.eat(RightCurlyBracket);
}
return {
type: 'Block',
loc: this.getLocation(start, this.tokenStart),
children
};
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, '}');
this.token(LeftCurlyBracket, '{');
this.children(node, (prev) => {
if (prev.type === 'Declaration') {
this.token(Semicolon, ';');
}
});
this.token(RightCurlyBracket, '}');
}

View File

@ -1,35 +1,35 @@
import {
Delim,
LeftSquareBracket,
RightSquareBracket
Delim,
LeftSquareBracket,
RightSquareBracket,
} from '../../tokenizer/index.js';
export const name = 'Brackets';
export const structure = {
children: [[]]
children: [[]],
};
export function parse(readSequence, recognizer) {
const start = this.tokenStart;
let children = null;
const start = this.tokenStart;
let children = null;
this.eat(LeftSquareBracket);
this.eat(LeftSquareBracket);
children = readSequence.call(this, recognizer);
children = readSequence.call(this, recognizer);
if (!this.eof) {
this.eat(RightSquareBracket);
}
if (!this.eof) {
this.eat(RightSquareBracket);
}
return {
type: 'Brackets',
loc: this.getLocation(start, this.tokenStart),
children
};
return {
type: 'Brackets',
loc: this.getLocation(start, this.tokenStart),
children,
};
}
export function generate(node) {
this.token(Delim, '[');
this.children(node);
this.token(Delim, ']');
this.token(Delim, '[');
this.children(node);
this.token(Delim, ']');
}

View File

@ -4,16 +4,16 @@ export const name = 'CDC';
export const structure = [];
export function parse() {
const start = this.tokenStart;
const start = this.tokenStart;
this.eat(CDC); // -->
this.eat(CDC); // -->
return {
type: 'CDC',
loc: this.getLocation(start, this.tokenStart)
};
return {
type: 'CDC',
loc: this.getLocation(start, this.tokenStart),
};
}
export function generate() {
this.token(CDC, '-->');
this.token(CDC, '-->');
}

View File

@ -4,16 +4,16 @@ export const name = 'CDO';
export const structure = [];
export function parse() {
const start = this.tokenStart;
const start = this.tokenStart;
this.eat(CDO); // <!--
this.eat(CDO); // <!--
return {
type: 'CDO',
loc: this.getLocation(start, this.tokenStart)
};
return {
type: 'CDO',
loc: this.getLocation(start, this.tokenStart),
};
}
export function generate() {
this.token(CDO, '<!--');
this.token(CDO, '<!--');
}

View File

@ -1,24 +1,24 @@
import { Delim, Ident } from '../../tokenizer/index.js';
const FULLSTOP = 0x002E; // U+002E FULL STOP (.)
const FULLSTOP = 0x002e; // U+002E FULL STOP (.)
// '.' ident
export const name = 'ClassSelector';
export const structure = {
name: String
name: String,
};
export function parse() {
this.eatDelim(FULLSTOP);
this.eatDelim(FULLSTOP);
return {
type: 'ClassSelector',
loc: this.getLocation(this.tokenStart - 1, this.tokenEnd),
name: this.consume(Ident)
};
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);
this.token(Delim, '.');
this.token(Ident, node.name);
}

View File

@ -1,54 +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 (~)
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
name: String,
};
// + | > | ~ | /deep/
export function parse() {
const start = this.tokenStart;
let name;
const start = this.tokenStart;
let name;
switch (this.tokenType) {
case WhiteSpace:
name = ' ';
break;
switch (this.tokenType) {
case WhiteSpace:
name = ' ';
break;
case Delim:
switch (this.charCodeAt(this.tokenStart)) {
case GREATERTHANSIGN:
case PLUSSIGN:
case TILDE:
this.next();
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;
case SOLIDUS:
this.next();
this.eatIdent('deep');
this.eatDelim(SOLIDUS);
break;
default:
this.error('Combinator is expected');
}
default:
this.error('Combinator is expected');
}
name = this.substrToCursor(start);
break;
}
name = this.substrToCursor(start);
break;
}
return {
type: 'Combinator',
loc: this.getLocation(start, this.tokenStart),
name
};
return {
type: 'Combinator',
loc: this.getLocation(start, this.tokenStart),
name,
};
}
export function generate(node) {
this.tokenize(node.name);
this.tokenize(node.name);
}

View File

@ -1,33 +1,34 @@
import { Comment } from '../../tokenizer/index.js';
const ASTERISK = 0x002A; // U+002A ASTERISK (*)
const SOLIDUS = 0x002F; // U+002F SOLIDUS (/)
const ASTERISK = 0x002a; // U+002A ASTERISK (*)
const SOLIDUS = 0x002f; // U+002F SOLIDUS (/)
export const name = 'Comment';
export const structure = {
value: String
value: String,
};
export function parse() {
const start = this.tokenStart;
let end = this.tokenEnd;
const start = this.tokenStart;
let end = this.tokenEnd;
this.eat(Comment);
this.eat(Comment);
if ((end - start + 2) >= 2 &&
this.charCodeAt(end - 2) === ASTERISK &&
this.charCodeAt(end - 1) === SOLIDUS) {
end -= 2;
}
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)
};
return {
type: 'Comment',
loc: this.getLocation(start, this.tokenStart),
value: this.substring(start + 2, end),
};
}
export function generate(node) {
this.token(Comment, '/*' + node.value + '*/');
this.token(Comment, '/*' + node.value + '*/');
}

View File

@ -1,165 +1,179 @@
import { isCustomProperty } from '../../utils/names.js';
import {
Ident,
Hash,
Colon,
Semicolon,
Delim,
WhiteSpace
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 (/)
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(startToken) {
return this.Raw(startToken, this.consumeUntilExclamationMarkOrSemicolon, true);
return this.Raw(
startToken,
this.consumeUntilExclamationMarkOrSemicolon,
true
);
}
function consumeCustomPropertyRaw(startToken) {
return this.Raw(startToken, this.consumeUntilExclamationMarkOrSemicolon, false);
return this.Raw(
startToken,
this.consumeUntilExclamationMarkOrSemicolon,
false
);
}
function consumeValue() {
const startValueToken = this.tokenIndex;
const value = this.Value();
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();
}
if (
value.type !== 'Raw' &&
this.eof === false &&
this.tokenType !== Semicolon &&
this.isDelim(EXCLAMATIONMARK) === false &&
this.isBalanceEdge(startValueToken) === false
) {
this.error();
}
return value;
return value;
}
export const name = 'Declaration';
export const walkContext = 'declaration';
export const structure = {
important: [Boolean, String],
property: String,
value: ['Value', 'Raw']
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;
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();
this.eat(Colon);
}
const valueStart = this.tokenIndex;
if (parseValue) {
value = this.parseWithFallback(consumeValue, consumeRaw);
} else {
value = consumeRaw.call(this, this.tokenIndex);
}
if (!customProperty) {
this.skipSC();
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 (parseValue) {
value = this.parseWithFallback(consumeValue, consumeRaw);
} else {
value = consumeRaw.call(this, this.tokenIndex);
}
if (this.isDelim(EXCLAMATIONMARK)) {
important = getImportant.call(this);
this.skipSC();
}
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;
}
}
}
// Do not include semicolon to range per spec
// https://drafts.csswg.org/css-syntax/#declaration-diagram
if (this.isDelim(EXCLAMATIONMARK)) {
important = getImportant.call(this);
this.skipSC();
}
if (
this.eof === false &&
this.tokenType !== Semicolon &&
this.isBalanceEdge(startToken) === false
) {
this.error();
}
// 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
};
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);
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);
}
if (node.important) {
this.token(Delim, '!');
this.token(Ident, node.important === true ? 'important' : node.important);
}
}
function readProperty() {
const start = this.tokenStart;
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;
// 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;
// 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);
}
if (this.tokenType === Hash) {
this.eat(Hash);
} else {
this.eat(Ident);
}
return this.substrToCursor(start);
return this.substrToCursor(start);
}
// ! ws* important
function getImportant() {
this.eat(Delim);
this.skipSC();
this.eat(Delim);
this.skipSC();
const important = this.consume(Ident);
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;
// store original value in case it differ from `important`
// for better original source restoring and hacks like `!ie` support
return important === 'important' ? true : important;
}

View File

@ -1,49 +1,41 @@
import {
WhiteSpace,
Comment,
Semicolon
} from '../../tokenizer/index.js';
import { WhiteSpace, Comment, Semicolon } from '../../tokenizer/index.js';
function consumeRaw(startToken) {
return this.Raw(startToken, this.consumeUntilSemicolonIncluded, true);
return this.Raw(startToken, this.consumeUntilSemicolonIncluded, true);
}
export const name = 'DeclarationList';
export const structure = {
children: [[
'Declaration'
]]
children: [['Declaration']],
};
export function parse() {
const children = this.createList();
const children = this.createList();
scan:
while (!this.eof) {
switch (this.tokenType) {
case WhiteSpace:
case Comment:
case Semicolon:
this.next();
break;
scan: while (!this.eof) {
switch (this.tokenType) {
case WhiteSpace:
case Comment:
case Semicolon:
this.next();
break;
default:
children.push(this.parseWithFallback(this.Declaration, consumeRaw));
}
default:
children.push(this.parseWithFallback(this.Declaration, consumeRaw));
}
}
return {
type: 'DeclarationList',
loc: this.getLocationFromList(children),
children
};
return {
type: 'DeclarationList',
loc: this.getLocationFromList(children),
children,
};
}
export function generate(node) {
this.children(node, prev => {
if (prev.type === 'Declaration') {
this.token(Semicolon, ';');
}
});
this.children(node, (prev) => {
if (prev.type === 'Declaration') {
this.token(Semicolon, ';');
}
});
}

View File

@ -2,22 +2,22 @@ import { Dimension } from '../../tokenizer/index.js';
export const name = 'Dimension';
export const structure = {
value: String,
unit: String
value: String,
unit: String,
};
export function parse() {
const start = this.tokenStart;
const value = this.consumeNumber(Dimension);
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)
};
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);
this.token(Dimension, node.value + node.unit);
}

View File

@ -1,41 +1,41 @@
import {
Function as FunctionToken,
RightParenthesis
Function as FunctionToken,
RightParenthesis,
} from '../../tokenizer/index.js';
export const name = 'Function';
export const walkContext = 'function';
export const structure = {
name: String,
children: [[]]
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;
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);
children =
recognizer.hasOwnProperty(nameLowerCase) ?
recognizer[nameLowerCase].call(this, recognizer)
: readSequence.call(this, recognizer);
if (!this.eof) {
this.eat(RightParenthesis);
}
if (!this.eof) {
this.eat(RightParenthesis);
}
return {
type: 'Function',
loc: this.getLocation(start, this.tokenStart),
name,
children
};
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, ')');
this.token(FunctionToken, node.name + '(');
this.children(node);
this.token(RightParenthesis, ')');
}

View File

@ -4,20 +4,19 @@ import { Hash } from '../../tokenizer/index.js';
export const xxx = 'XXX';
export const name = 'Hash';
export const structure = {
value: String
value: String,
};
export function parse() {
const start = this.tokenStart;
const start = this.tokenStart;
this.eat(Hash);
this.eat(Hash);
return {
type: 'Hash',
loc: this.getLocation(start, this.tokenStart),
value: this.substrToCursor(start + 1)
};
return {
type: 'Hash',
loc: this.getLocation(start, this.tokenStart),
value: this.substrToCursor(start + 1),
};
}
export function generate(node) {
this.token(Hash, '#' + node.value);
this.token(Hash, '#' + node.value);
}

View File

@ -2,25 +2,25 @@ import { Hash, Delim } from '../../tokenizer/index.js';
export const name = 'IdSelector';
export const structure = {
name: String
name: String,
};
export function parse() {
const start = this.tokenStart;
const start = this.tokenStart;
// TODO: check value is an ident
this.eat(Hash);
// TODO: check value is an ident
this.eat(Hash);
return {
type: 'IdSelector',
loc: this.getLocation(start, this.tokenStart),
name: this.substrToCursor(start + 1)
};
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);
// 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);
}

View File

@ -2,17 +2,17 @@ import { Ident } from '../../tokenizer/index.js';
export const name = 'Identifier';
export const structure = {
name: String
name: String,
};
export function parse() {
return {
type: 'Identifier',
loc: this.getLocation(this.tokenStart, this.tokenEnd),
name: this.consume(Ident)
};
return {
type: 'Identifier',
loc: this.getLocation(this.tokenStart, this.tokenEnd),
name: this.consume(Ident),
};
}
export function generate(node) {
this.token(Ident, node.name);
this.token(Ident, node.name);
}

View File

@ -1,77 +1,77 @@
import {
Ident,
Number,
Dimension,
LeftParenthesis,
RightParenthesis,
Colon,
Delim
Ident,
Number,
Dimension,
LeftParenthesis,
RightParenthesis,
Colon,
Delim,
} from '../../tokenizer/index.js';
export const name = 'MediaFeature';
export const structure = {
name: String,
value: ['Identifier', 'Number', 'Dimension', 'Ratio', null]
name: String,
value: ['Identifier', 'Number', 'Dimension', 'Ratio', null],
};
export function parse() {
const start = this.tokenStart;
let name;
let value = null;
const start = this.tokenStart;
let name;
let value = null;
this.eat(LeftParenthesis);
this.eat(LeftParenthesis);
this.skipSC();
name = this.consume(Ident);
this.skipSC();
if (this.tokenType !== RightParenthesis) {
this.eat(Colon);
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;
default:
this.error('Number, dimension, ratio or identifier is expected');
switch (this.tokenType) {
case Number:
if (this.lookupNonWSType(1) === Delim) {
value = this.Ratio();
} else {
value = this.Number();
}
this.skipSC();
break;
case Dimension:
value = this.Dimension();
break;
case Ident:
value = this.Identifier();
break;
default:
this.error('Number, dimension, ratio or identifier is expected');
}
this.eat(RightParenthesis);
this.skipSC();
}
return {
type: 'MediaFeature',
loc: this.getLocation(start, this.tokenStart),
name,
value
};
this.eat(RightParenthesis);
return {
type: 'MediaFeature',
loc: this.getLocation(start, this.tokenStart),
name,
value,
};
}
export function generate(node) {
this.token(LeftParenthesis, '(');
this.token(Ident, node.name);
this.token(LeftParenthesis, '(');
this.token(Ident, node.name);
if (node.value !== null) {
this.token(Colon, ':');
this.node(node.value);
}
if (node.value !== null) {
this.token(Colon, ':');
this.node(node.value);
}
this.token(RightParenthesis, ')');
this.token(RightParenthesis, ')');
}

View File

@ -1,60 +1,54 @@
import {
WhiteSpace,
Comment,
Ident,
LeftParenthesis
WhiteSpace,
Comment,
Ident,
LeftParenthesis,
} from '../../tokenizer/index.js';
export const name = 'MediaQuery';
export const structure = {
children: [[
'Identifier',
'MediaFeature',
'WhiteSpace'
]]
children: [['Identifier', 'MediaFeature', 'WhiteSpace']],
};
export function parse() {
const children = this.createList();
let child = null;
const children = this.createList();
let child = null;
this.skipSC();
this.skipSC();
scan:
while (!this.eof) {
switch (this.tokenType) {
case Comment:
case WhiteSpace:
this.next();
continue;
scan: while (!this.eof) {
switch (this.tokenType) {
case Comment:
case WhiteSpace:
this.next();
continue;
case Ident:
child = this.Identifier();
break;
case Ident:
child = this.Identifier();
break;
case LeftParenthesis:
child = this.MediaFeature();
break;
case LeftParenthesis:
child = this.MediaFeature();
break;
default:
break scan;
}
children.push(child);
default:
break scan;
}
if (child === null) {
this.error('Identifier or parenthesis is expected');
}
children.push(child);
}
return {
type: 'MediaQuery',
loc: this.getLocationFromList(children),
children
};
if (child === null) {
this.error('Identifier or parenthesis is expected');
}
return {
type: 'MediaQuery',
loc: this.getLocationFromList(children),
children,
};
}
export function generate(node) {
this.children(node);
this.children(node);
}

View File

@ -2,33 +2,31 @@ import { Comma } from '../../tokenizer/index.js';
export const name = 'MediaQueryList';
export const structure = {
children: [[
'MediaQuery'
]]
children: [['MediaQuery']],
};
export function parse() {
const children = this.createList();
const children = this.createList();
this.skipSC();
this.skipSC();
while (!this.eof) {
children.push(this.MediaQuery());
while (!this.eof) {
children.push(this.MediaQuery());
if (this.tokenType !== Comma) {
break;
}
this.next();
if (this.tokenType !== Comma) {
break;
}
return {
type: 'MediaQueryList',
loc: this.getLocationFromList(children),
children
};
this.next();
}
return {
type: 'MediaQueryList',
loc: this.getLocationFromList(children),
children,
};
}
export function generate(node) {
this.children(node, () => this.token(Comma, ','));
this.children(node, () => this.token(Comma, ','));
}

View File

@ -2,46 +2,46 @@ import { Ident } from '../../tokenizer/index.js';
export const name = 'Nth';
export const structure = {
nth: ['AnPlusB', 'Identifier'],
selector: ['SelectorList', null]
nth: ['AnPlusB', 'Identifier'],
selector: ['SelectorList', null],
};
export function parse() {
this.skipSC();
this.skipSC();
const start = this.tokenStart;
let end = start;
let selector = null;
let nth;
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();
}
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;
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
};
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);
}
this.node(node.nth);
if (node.selector !== null) {
this.token(Ident, 'of');
this.node(node.selector);
}
}

View File

@ -2,17 +2,17 @@ import { Number as NumberToken } from '../../tokenizer/index.js';
export const name = 'Number';
export const structure = {
value: String
value: String,
};
export function parse() {
return {
type: 'Number',
loc: this.getLocation(this.tokenStart, this.tokenEnd),
value: this.consume(NumberToken)
};
return {
type: 'Number',
loc: this.getLocation(this.tokenStart, this.tokenEnd),
value: this.consume(NumberToken),
};
}
export function generate(node) {
this.token(NumberToken, node.value);
this.token(NumberToken, node.value);
}

View File

@ -1,21 +1,21 @@
// '/' | '*' | ',' | ':' | '+' | '-'
export const name = 'Operator';
export const structure = {
value: String
value: String,
};
export function parse() {
const start = this.tokenStart;
const start = this.tokenStart;
this.next();
this.next();
return {
type: 'Operator',
loc: this.getLocation(start, this.tokenStart),
value: this.substrToCursor(start)
};
return {
type: 'Operator',
loc: this.getLocation(start, this.tokenStart),
value: this.substrToCursor(start),
};
}
export function generate(node) {
this.tokenize(node.value);
this.tokenize(node.value);
}

View File

@ -1,34 +1,31 @@
import {
LeftParenthesis,
RightParenthesis
} from '../../tokenizer/index.js';
import { LeftParenthesis, RightParenthesis } from '../../tokenizer/index.js';
export const name = 'Parentheses';
export const structure = {
children: [[]]
children: [[]],
};
export function parse(readSequence, recognizer) {
const start = this.tokenStart;
let children = null;
const start = this.tokenStart;
let children = null;
this.eat(LeftParenthesis);
this.eat(LeftParenthesis);
children = readSequence.call(this, recognizer);
children = readSequence.call(this, recognizer);
if (!this.eof) {
this.eat(RightParenthesis);
}
if (!this.eof) {
this.eat(RightParenthesis);
}
return {
type: 'Parentheses',
loc: this.getLocation(start, this.tokenStart),
children
};
return {
type: 'Parentheses',
loc: this.getLocation(start, this.tokenStart),
children,
};
}
export function generate(node) {
this.token(LeftParenthesis, '(');
this.children(node);
this.token(RightParenthesis, ')');
this.token(LeftParenthesis, '(');
this.children(node);
this.token(RightParenthesis, ')');
}

View File

@ -2,17 +2,17 @@ import { Percentage } from '../../tokenizer/index.js';
export const name = 'Percentage';
export const structure = {
value: String
value: String,
};
export function parse() {
return {
type: 'Percentage',
loc: this.getLocation(this.tokenStart, this.tokenEnd),
value: this.consumeNumber(Percentage)
};
return {
type: 'Percentage',
loc: this.getLocation(this.tokenStart, this.tokenEnd),
value: this.consumeNumber(Percentage),
};
}
export function generate(node) {
this.token(Percentage, node.value + '%');
this.token(Percentage, node.value + '%');
}

View File

@ -1,63 +1,60 @@
import {
Ident,
Function as FunctionToken,
Colon,
RightParenthesis
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]
name: String,
children: [['Raw'], null],
};
// : [ <ident> | <function-token> <any-value>? ) ]
export function parse() {
const start = this.tokenStart;
let children = null;
let name;
let nameLowerCase;
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.tokenType === FunctionToken) {
name = this.consumeFunctionName();
nameLowerCase = name.toLowerCase();
if (hasOwnProperty.call(this.pseudo, nameLowerCase)) {
this.skipSC();
children = this.pseudo[nameLowerCase].call(this);
this.skipSC();
} else {
children = this.createList();
children.push(
this.Raw(this.tokenIndex, null, false)
);
}
this.eat(RightParenthesis);
if (hasOwnProperty.call(this.pseudo, nameLowerCase)) {
this.skipSC();
children = this.pseudo[nameLowerCase].call(this);
this.skipSC();
} else {
name = this.consume(Ident);
children = this.createList();
children.push(this.Raw(this.tokenIndex, null, false));
}
return {
type: 'PseudoClassSelector',
loc: this.getLocation(start, this.tokenStart),
name,
children
};
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, ':');
this.token(Colon, ':');
if (node.children === null) {
this.token(Ident, node.name);
} else {
this.token(FunctionToken, node.name + '(');
this.children(node);
this.token(RightParenthesis, ')');
}
if (node.children === null) {
this.token(Ident, node.name);
} else {
this.token(FunctionToken, node.name + '(');
this.children(node);
this.token(RightParenthesis, ')');
}
}

View File

@ -1,64 +1,62 @@
import {
Ident,
Function as FunctionToken,
Colon,
RightParenthesis
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]
name: String,
children: [['Raw'], null],
};
// :: [ <ident> | <function-token> <any-value>? ) ]
export function parse() {
const start = this.tokenStart;
let children = null;
let name;
let nameLowerCase;
const start = this.tokenStart;
let children = null;
let name;
let nameLowerCase;
this.eat(Colon);
this.eat(Colon);
this.eat(Colon);
this.eat(Colon);
if (this.tokenType === FunctionToken) {
name = this.consumeFunctionName();
nameLowerCase = name.toLowerCase();
if (this.tokenType === FunctionToken) {
name = this.consumeFunctionName();
nameLowerCase = name.toLowerCase();
if (hasOwnProperty.call(this.pseudo, nameLowerCase)) {
this.skipSC();
children = this.pseudo[nameLowerCase].call(this);
this.skipSC();
} else {
children = this.createList();
children.push(
this.Raw(this.tokenIndex, null, false)
);
}
this.eat(RightParenthesis);
if (hasOwnProperty.call(this.pseudo, nameLowerCase)) {
this.skipSC();
children = this.pseudo[nameLowerCase].call(this);
this.skipSC();
} else {
name = this.consume(Ident);
children = this.createList();
children.push(this.Raw(this.tokenIndex, null, false));
}
return {
type: 'PseudoElementSelector',
loc: this.getLocation(start, this.tokenStart),
name,
children
};
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, ':');
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, ')');
}
if (node.children === null) {
this.token(Ident, node.name);
} else {
this.token(FunctionToken, node.name + '(');
this.children(node);
this.token(RightParenthesis, ')');
}
}

View File

@ -1,7 +1,11 @@
import { isDigit, Delim, Number as NumberToken } from '../../tokenizer/index.js';
import {
isDigit,
Delim,
Number as NumberToken,
} from '../../tokenizer/index.js';
const SOLIDUS = 0x002F; // U+002F SOLIDUS (/)
const FULLSTOP = 0x002E; // U+002E FULL STOP (.)
const SOLIDUS = 0x002f; // U+002F SOLIDUS (/)
const FULLSTOP = 0x002e; // U+002E FULL STOP (.)
// Terms of <ratio> should be a positive numbers (not zero or negative)
// (see https://drafts.csswg.org/mediaqueries-3/#values)
@ -10,50 +14,53 @@ const FULLSTOP = 0x002E; // U+002E FULL STOP (.)
// to test a term is unsigned number without an exponent part.
// Additional checking may be applied on lexer validation.
function consumeNumber() {
this.skipSC();
this.skipSC();
const value = this.consume(NumberToken);
const value = this.consume(NumberToken);
for (let i = 0; i < value.length; i++) {
const code = value.charCodeAt(i);
if (!isDigit(code) && code !== FULLSTOP) {
this.error('Unsigned number is expected', this.tokenStart - value.length + i);
}
for (let i = 0; i < value.length; i++) {
const code = value.charCodeAt(i);
if (!isDigit(code) && code !== FULLSTOP) {
this.error(
'Unsigned number is expected',
this.tokenStart - value.length + i
);
}
}
if (Number(value) === 0) {
this.error('Zero number is not allowed', this.tokenStart - value.length);
}
if (Number(value) === 0) {
this.error('Zero number is not allowed', this.tokenStart - value.length);
}
return value;
return value;
}
export const name = 'Ratio';
export const structure = {
left: String,
right: String
left: String,
right: String,
};
// <positive-integer> S* '/' S* <positive-integer>
export function parse() {
const start = this.tokenStart;
const left = consumeNumber.call(this);
let right;
const start = this.tokenStart;
const left = consumeNumber.call(this);
let right;
this.skipSC();
this.eatDelim(SOLIDUS);
right = consumeNumber.call(this);
this.skipSC();
this.eatDelim(SOLIDUS);
right = consumeNumber.call(this);
return {
type: 'Ratio',
loc: this.getLocation(start, this.tokenStart),
left,
right
};
return {
type: 'Ratio',
loc: this.getLocation(start, this.tokenStart),
left,
right,
};
}
export function generate(node) {
this.token(NumberToken, node.left);
this.token(Delim, '/');
this.token(NumberToken, node.right);
this.token(NumberToken, node.left);
this.token(Delim, '/');
this.token(NumberToken, node.right);
}

View File

@ -1,41 +1,44 @@
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;
}
if (this.tokenIndex > 0) {
if (this.lookupType(-1) === WhiteSpace) {
return this.tokenIndex > 1 ?
this.getTokenStart(this.tokenIndex - 1)
: this.firstCharOffset;
}
}
return this.tokenStart;
return this.tokenStart;
}
export const name = 'Raw';
export const structure = {
value: String
value: String,
};
export function parse(startToken, consumeUntil, excludeWhiteSpace) {
const startOffset = this.getTokenStart(startToken);
let endOffset;
const startOffset = this.getTokenStart(startToken);
let endOffset;
this.skipUntilBalanced(startToken, consumeUntil || this.consumeUntilBalanceEnd);
this.skipUntilBalanced(
startToken,
consumeUntil || this.consumeUntilBalanceEnd
);
if (excludeWhiteSpace && this.tokenStart > startOffset) {
endOffset = getOffsetExcludeWS.call(this);
} else {
endOffset = this.tokenStart;
}
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)
};
return {
type: 'Raw',
loc: this.getLocation(startOffset, endOffset),
value: this.substring(startOffset, endOffset),
};
}
export function generate(node) {
this.tokenize(node.value);
this.tokenize(node.value);
}

View File

@ -1,51 +1,52 @@
import { LeftCurlyBracket } from '../../tokenizer/index.js';
function consumeRaw(startToken) {
return this.Raw(startToken, this.consumeUntilLeftCurlyBracket, true);
return this.Raw(startToken, this.consumeUntilLeftCurlyBracket, true);
}
function consumePrelude() {
const prelude = this.SelectorList();
const prelude = this.SelectorList();
if (prelude.type !== 'Raw' &&
this.eof === false &&
this.tokenType !== LeftCurlyBracket) {
this.error();
}
if (
prelude.type !== 'Raw' &&
this.eof === false &&
this.tokenType !== LeftCurlyBracket
) {
this.error();
}
return prelude;
return prelude;
}
export const name = 'Rule';
export const walkContext = 'rule';
export const structure = {
prelude: ['SelectorList', 'Raw'],
block: ['Block']
prelude: ['SelectorList', 'Raw'],
block: ['Block'],
};
export function parse() {
const startToken = this.tokenIndex;
const startOffset = this.tokenStart;
let prelude;
let block;
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);
}
if (this.parseRulePrelude) {
prelude = this.parseWithFallback(consumePrelude, consumeRaw);
} else {
prelude = consumeRaw.call(this, startToken);
}
block = this.Block(true);
block = this.Block(true);
return {
type: 'Rule',
loc: this.getLocation(startOffset, this.tokenStart),
prelude,
block
};
return {
type: 'Rule',
loc: this.getLocation(startOffset, this.tokenStart),
prelude,
block,
};
}
export function generate(node) {
this.node(node.prelude);
this.node(node.block);
this.node(node.prelude);
this.node(node.block);
}

View File

@ -1,32 +1,34 @@
export const name = 'Selector';
export const structure = {
children: [[
'TypeSelector',
'IdSelector',
'ClassSelector',
'AttributeSelector',
'PseudoClassSelector',
'PseudoElementSelector',
'Combinator',
'WhiteSpace'
]]
children: [
[
'TypeSelector',
'IdSelector',
'ClassSelector',
'AttributeSelector',
'PseudoClassSelector',
'PseudoElementSelector',
'Combinator',
'WhiteSpace',
],
],
};
export function parse() {
const children = this.readSequence(this.scope.Selector);
const children = this.readSequence(this.scope.Selector);
// nothing were consumed
if (this.getFirstListNode(children) === null) {
this.error('Selector is expected');
}
// nothing were consumed
if (this.getFirstListNode(children) === null) {
this.error('Selector is expected');
}
return {
type: 'Selector',
loc: this.getLocationFromList(children),
children
};
return {
type: 'Selector',
loc: this.getLocationFromList(children),
children,
};
}
export function generate(node) {
this.children(node);
this.children(node);
}

View File

@ -3,33 +3,30 @@ import { Comma } from '../../tokenizer/index.js';
export const name = 'SelectorList';
export const walkContext = 'selector';
export const structure = {
children: [[
'Selector',
'Raw'
]]
children: [['Selector', 'Raw']],
};
export function parse() {
const children = this.createList();
const children = this.createList();
while (!this.eof) {
children.push(this.Selector());
while (!this.eof) {
children.push(this.Selector());
if (this.tokenType === Comma) {
this.next();
continue;
}
break;
if (this.tokenType === Comma) {
this.next();
continue;
}
return {
type: 'SelectorList',
loc: this.getLocationFromList(children),
children
};
break;
}
return {
type: 'SelectorList',
loc: this.getLocationFromList(children),
children,
};
}
export function generate(node) {
this.children(node, () => this.token(Comma, ','));
this.children(node, () => this.token(Comma, ','));
}

View File

@ -3,17 +3,17 @@ import { decode, encode } from '../../utils/string.js';
export const name = 'String';
export const structure = {
value: String
value: String,
};
export function parse() {
return {
type: 'String',
loc: this.getLocation(this.tokenStart, this.tokenEnd),
value: decode(this.consume(StringToken))
};
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));
this.token(StringToken, encode(node.value));
}

View File

@ -1,82 +1,74 @@
import {
WhiteSpace,
Comment,
AtKeyword,
CDO,
CDC
WhiteSpace,
Comment,
AtKeyword,
CDO,
CDC,
} from '../../tokenizer/index.js';
const EXCLAMATIONMARK = 0x0021; // U+0021 EXCLAMATION MARK (!)
function consumeRaw(startToken) {
return this.Raw(startToken, null, false);
return this.Raw(startToken, null, false);
}
export const name = 'StyleSheet';
export const walkContext = 'stylesheet';
export const structure = {
children: [[
'Comment',
'CDO',
'CDC',
'Atrule',
'Rule',
'Raw'
]]
children: [['Comment', 'CDO', 'CDC', 'Atrule', 'Rule', 'Raw']],
};
export function parse() {
const start = this.tokenStart;
const children = this.createList();
let child;
const start = this.tokenStart;
const children = this.createList();
let child;
scan:
while (!this.eof) {
switch (this.tokenType) {
case WhiteSpace:
this.next();
continue;
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);
case Comment:
// ignore comments except exclamation comments (i.e. /*! .. */) on top level
if (this.charCodeAt(this.tokenStart + 2) !== EXCLAMATIONMARK) {
this.next();
continue;
}
children.push(child);
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);
}
return {
type: 'StyleSheet',
loc: this.getLocation(start, this.tokenStart),
children
};
children.push(child);
}
return {
type: 'StyleSheet',
loc: this.getLocation(start, this.tokenStart),
children,
};
}
export function generate(node) {
this.children(node);
this.children(node);
}

View File

@ -1,20 +1,19 @@
import { Ident } from '../../tokenizer/index.js';
const ASTERISK = 0x002A; // U+002A ASTERISK (*)
const VERTICALLINE = 0x007C; // U+007C VERTICAL LINE (|)
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');
}
if (this.tokenType !== Ident && this.isDelim(ASTERISK) === false) {
this.error('Identifier or asterisk is expected');
}
this.next();
this.next();
}
export const name = 'TypeSelector';
export const structure = {
name: String
name: String,
};
// ident
@ -26,27 +25,27 @@ export const structure = {
// |ident
// |*
export function parse() {
const start = this.tokenStart;
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);
} else {
eatIdentifierOrAsterisk.call(this);
if (this.isDelim(VERTICALLINE)) {
this.next();
eatIdentifierOrAsterisk.call(this);
}
this.next();
eatIdentifierOrAsterisk.call(this);
}
}
return {
type: 'TypeSelector',
loc: this.getLocation(start, this.tokenStart),
name: this.substrToCursor(start)
};
return {
type: 'TypeSelector',
loc: this.getLocation(start, this.tokenStart),
name: this.substrToCursor(start),
};
}
export function generate(node) {
this.tokenize(node.name);
this.tokenize(node.name);
}

View File

@ -1,59 +1,57 @@
import {
isHexDigit,
Ident,
Number,
Dimension
} from '../../tokenizer/index.js';
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 (?)
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;
let len = 0;
for (let pos = this.tokenStart + offset; pos < this.tokenEnd; pos++) {
const code = this.charCodeAt(pos);
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);
};
if (code === HYPHENMINUS && allowDash && len !== 0) {
eatHexSequence.call(this, offset + len + 1, false);
return -1;
}
this.next();
return len;
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;
let count = 0;
while (this.isDelim(QUESTIONMARK)) {
if (++count > max) {
this.error('Too many question marks');
}
this.next();
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');
}
if (this.charCodeAt(this.tokenStart) !== code) {
this.error(
(code === PLUSSIGN ? 'Plus sign' : 'Hyphen minus') + ' is expected'
);
}
}
// https://drafts.csswg.org/css-syntax/#urange
@ -76,81 +74,80 @@ function startsWith(code) {
// u <number-token> <number-token> |
// u '+' '?'+
function scanUnicodeRange() {
let hexLength = 0;
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);
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.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;
}
if (this.tokenType === Dimension || this.tokenType === Number) {
startsWith.call(this, HYPHENMINUS);
eatHexSequence.call(this, 1, false);
break;
}
break;
break;
case Dimension:
// u <dimension-token> '?'*
hexLength = eatHexSequence.call(this, 1, true);
case Dimension:
// u <dimension-token> '?'*
hexLength = eatHexSequence.call(this, 1, true);
if (hexLength > 0) {
eatQuestionMarkSequence.call(this, 6 - hexLength);
}
if (hexLength > 0) {
eatQuestionMarkSequence.call(this, 6 - hexLength);
}
break;
break;
default:
// u '+' <ident-token> '?'*
// u '+' '?'+
this.eatDelim(PLUSSIGN);
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.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;
}
if (this.isDelim(QUESTIONMARK)) {
this.next();
eatQuestionMarkSequence.call(this, 5);
break;
}
this.error('Hex digit or question mark is expected');
}
this.error('Hex digit or question mark is expected');
}
}
export const name = 'UnicodeRange';
export const structure = {
value: String
value: String,
};
export function parse() {
const start = this.tokenStart;
const start = this.tokenStart;
// U or u
this.eatIdent('u');
scanUnicodeRange.call(this);
// U or u
this.eatIdent('u');
scanUnicodeRange.call(this);
return {
type: 'UnicodeRange',
loc: this.getLocation(start, this.tokenStart),
value: this.substrToCursor(start)
};
return {
type: 'UnicodeRange',
loc: this.getLocation(start, this.tokenStart),
value: this.substrToCursor(start),
};
}
export function generate(node) {
this.tokenize(node.value);
this.tokenize(node.value);
}

View File

@ -1,52 +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
Function as FunctionToken,
String as StringToken,
Url,
RightParenthesis,
} from '../../tokenizer/index.js';
export const name = 'Url';
export const structure = {
value: String
value: String,
};
// <url-token> | <function-token> <string> )
export function parse() {
const start = this.tokenStart;
let value;
const start = this.tokenStart;
let value;
switch (this.tokenType) {
case Url:
value = url.decode(this.consume(Url));
break;
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`');
}
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;
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');
}
default:
this.error('Url or Function is expected');
}
return {
type: 'Url',
loc: this.getLocation(start, this.tokenStart),
value
};
return {
type: 'Url',
loc: this.getLocation(start, this.tokenStart),
value,
};
}
export function generate(node) {
this.token(Url, url.encode(node.value));
this.token(Url, url.encode(node.value));
}

View File

@ -1,19 +1,19 @@
export const name = 'Value';
export const structure = {
children: [[]]
children: [[]],
};
export function parse() {
const start = this.tokenStart;
const children = this.readSequence(this.scope.Value);
const start = this.tokenStart;
const children = this.readSequence(this.scope.Value);
return {
type: 'Value',
loc: this.getLocation(start, this.tokenStart),
children
};
return {
type: 'Value',
loc: this.getLocation(start, this.tokenStart),
children,
};
}
export function generate(node) {
this.children(node);
this.children(node);
}

View File

@ -1,27 +1,27 @@
import { WhiteSpace } from '../../tokenizer/index.js';
const SPACE = Object.freeze({
type: 'WhiteSpace',
loc: null,
value: ' '
type: 'WhiteSpace',
loc: null,
value: ' ',
});
export const name = 'WhiteSpace';
export const structure = {
value: String
value: String,
};
export function parse() {
this.eat(WhiteSpace);
return SPACE;
this.eat(WhiteSpace);
return SPACE;
// return {
// type: 'WhiteSpace',
// loc: this.getLocation(this.tokenStart, this.tokenEnd),
// value: this.consume(WHITESPACE)
// };
// return {
// type: 'WhiteSpace',
// loc: this.getLocation(this.tokenStart, this.tokenEnd),
// value: this.consume(WHITESPACE)
// };
}
export function generate(node) {
this.token(WhiteSpace, node.value);
this.token(WhiteSpace, node.value);
}

View File

@ -1,48 +1,40 @@
const selectorList = {
parse() {
return this.createSingleNodeList(
this.SelectorList()
);
}
parse() {
return this.createSingleNodeList(this.SelectorList());
},
};
const selector = {
parse() {
return this.createSingleNodeList(
this.Selector()
);
}
parse() {
return this.createSingleNodeList(this.Selector());
},
};
const identList = {
parse() {
return this.createSingleNodeList(
this.Identifier()
);
}
parse() {
return this.createSingleNodeList(this.Identifier());
},
};
const nth = {
parse() {
return this.createSingleNodeList(
this.Nth()
);
}
parse() {
return this.createSingleNodeList(this.Nth());
},
};
export default {
'dir': identList,
'has': selectorList,
'lang': identList,
'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
dir: identList,
has: selectorList,
lang: identList,
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,
};

View File

@ -1,5 +1,5 @@
import getNode from './default.js';
export default {
getNode
getNode,
};

View File

@ -1,85 +1,89 @@
import {
Ident,
String as StringToken,
Number as NumberToken,
Function as FunctionToken,
Url,
Hash,
Dimension,
Percentage,
LeftParenthesis,
LeftSquareBracket,
Comma,
Delim
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)
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();
switch (this.tokenType) {
case Hash:
return this.Hash();
case Comma:
return this.Operator();
case Comma:
return this.Operator();
case LeftParenthesis:
return this.Parentheses(this.readSequence, context.recognizer);
case LeftParenthesis:
return this.Parentheses(this.readSequence, context.recognizer);
case LeftSquareBracket:
return this.Brackets(this.readSequence, context.recognizer);
case LeftSquareBracket:
return this.Brackets(this.readSequence, context.recognizer);
case StringToken:
return this.String();
case StringToken:
return this.String();
case Dimension:
return this.Dimension();
case Dimension:
return this.Dimension();
case Percentage:
return this.Percentage();
case Percentage:
return this.Percentage();
case NumberToken:
return this.Number();
case NumberToken:
return this.Number();
case FunctionToken:
return this.cmpStr(this.tokenStart, this.tokenEnd, 'url(')
? this.Url()
: this.Function(this.readSequence, context.recognizer);
case FunctionToken:
return this.cmpStr(this.tokenStart, this.tokenEnd, 'url(') ?
this.Url()
: this.Function(this.readSequence, context.recognizer);
case Url:
return this.Url();
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 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);
case Delim: {
const code = this.charCodeAt(this.tokenStart);
if (code === SOLIDUS ||
code === ASTERISK ||
code === PLUSSIGN ||
code === HYPHENMINUS) {
return this.Operator(); // TODO: replace with Delim
}
if (
code === SOLIDUS ||
code === ASTERISK ||
code === PLUSSIGN ||
code === HYPHENMINUS
) {
return this.Operator(); // TODO: replace with Delim
}
// TODO: produce a node with Delim node type
// TODO: produce a node with Delim node type
if (code === NUMBERSIGN) {
this.error('Hex or identifier is expected', this.tokenStart + 1);
}
if (code === NUMBERSIGN) {
this.error('Hex or identifier is expected', this.tokenStart + 1);
}
break;
}
break;
}
};
}
}

View File

@ -1,90 +1,95 @@
import {
Delim,
Ident,
Dimension,
Percentage,
Number as NumberToken,
Hash,
Colon,
LeftSquareBracket
Delim,
Ident,
Dimension,
Percentage,
Number as NumberToken,
Hash,
Colon,
LeftSquareBracket,
} 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 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 (~)
const NUMBERSIGN = 0x0023; // U+0023 NUMBER SIGN (#)
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: ' '
});
}
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();
switch (this.tokenType) {
case LeftSquareBracket:
return this.AttributeSelector();
case Hash:
return this.IdSelector();
case Hash:
return this.IdSelector();
case Colon:
if (this.lookupType(1) === Colon) {
return this.PseudoElementSelector();
} else {
return this.PseudoClassSelector();
}
case Colon:
if (this.lookupType(1) === Colon) {
return this.PseudoElementSelector();
} else {
return this.PseudoClassSelector();
}
case Ident:
return this.TypeSelector();
case Ident:
return this.TypeSelector();
case NumberToken:
case Percentage:
return this.Percentage();
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 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);
case Delim: {
const code = this.charCodeAt(this.tokenStart);
switch (code) {
case PLUSSIGN:
case GREATERTHANSIGN:
case TILDE:
case SOLIDUS: // /deep/
return this.Combinator();
switch (code) {
case PLUSSIGN:
case GREATERTHANSIGN:
case TILDE:
case SOLIDUS: // /deep/
return this.Combinator();
case FULLSTOP:
return this.ClassSelector();
case FULLSTOP:
return this.ClassSelector();
case ASTERISK:
case VERTICALLINE:
return this.TypeSelector();
case ASTERISK:
case VERTICALLINE:
return this.TypeSelector();
case NUMBERSIGN:
return this.IdSelector();
}
case NUMBERSIGN:
return this.IdSelector();
}
break;
}
break;
}
};
}
}
export default {
onWhiteSpace,
getNode
onWhiteSpace,
getNode,
};

View File

@ -3,23 +3,24 @@ 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] === '+')
);
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
getNode,
onWhiteSpace(next, children) {
if (isPlusMinusOperator(next)) {
next.value = ' ' + next.value;
}
if (isPlusMinusOperator(children.last)) {
children.last.value += ' ';
}
},
expression: expressionFn,
var: varFn,
};

View File

@ -6,82 +6,86 @@ 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;
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);
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++;
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;
}
line++;
column = 1;
}
}
lines[sourceLength] = line;
columns[sourceLength] = column;
lines[sourceLength] = line;
columns[sourceLength] = column;
host.lines = lines;
host.columns = columns;
host.computed = true;
host.lines = lines;
host.columns = columns;
host.computed = true;
}
export class OffsetToLocation {
constructor() {
this.lines = null;
this.columns = null;
this.computed = false;
constructor() {
this.lines = null;
this.columns = null;
this.computed = false;
}
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);
}
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]
};
return {
source: filename,
offset: this.startOffset + offset,
line: this.lines[offset],
column: this.columns[offset],
};
}
getLocationRange(start, end, filename) {
if (!this.computed) {
computeLinesAndColumns(this);
}
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]
}
};
}
};
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],
},
};
}
}

View File

@ -2,271 +2,273 @@ 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
WhiteSpace,
Comment,
Delim,
EOF,
Function as FunctionToken,
LeftParenthesis,
RightParenthesis,
LeftSquareBracket,
RightSquareBracket,
LeftCurlyBracket,
RightCurlyBracket,
} from './types.js';
const OFFSET_MASK = 0x00FFFFFF;
const OFFSET_MASK = 0x00ffffff;
const TYPE_SHIFT = 24;
const balancePair = new Map([
[FunctionToken, RightParenthesis],
[LeftParenthesis, RightParenthesis],
[LeftSquareBracket, RightSquareBracket],
[LeftCurlyBracket, RightCurlyBracket]
[FunctionToken, RightParenthesis],
[LeftParenthesis, RightParenthesis],
[LeftSquareBracket, RightSquareBracket],
[LeftCurlyBracket, RightCurlyBracket],
]);
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 || '');
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 balanceCloseType = 0;
let balanceStart = 0;
let firstCharOffset = -1;
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 balanceCloseType = 0;
let balanceStart = 0;
let firstCharOffset = -1;
// capture buffers
this.offsetAndType = null;
this.balance = null;
// capture buffers
this.offsetAndType = null;
this.balance = null;
tokenize(source, (type, start, end) => {
switch (type) {
default:
balance[tokenCount] = sourceLength;
break;
tokenize(source, (type, start, end) => {
switch (type) {
default:
balance[tokenCount] = sourceLength;
break;
case balanceCloseType: {
let balancePrev = balanceStart & OFFSET_MASK;
balanceStart = balance[balancePrev];
balanceCloseType = balanceStart >> TYPE_SHIFT;
balance[tokenCount] = balancePrev;
balance[balancePrev++] = tokenCount;
for (; balancePrev < tokenCount; balancePrev++) {
if (balance[balancePrev] === sourceLength) {
balance[balancePrev] = tokenCount;
}
}
break;
}
case LeftParenthesis:
case FunctionToken:
case LeftSquareBracket:
case LeftCurlyBracket:
balance[tokenCount] = balanceStart;
balanceCloseType = balancePair.get(type);
balanceStart = (balanceCloseType << TYPE_SHIFT) | tokenCount;
break;
case balanceCloseType: {
let balancePrev = balanceStart & OFFSET_MASK;
balanceStart = balance[balancePrev];
balanceCloseType = balanceStart >> TYPE_SHIFT;
balance[tokenCount] = balancePrev;
balance[balancePrev++] = tokenCount;
for (; balancePrev < tokenCount; balancePrev++) {
if (balance[balancePrev] === sourceLength) {
balance[balancePrev] = tokenCount;
}
offsetAndType[tokenCount++] = (type << TYPE_SHIFT) | end;
if (firstCharOffset === -1) {
firstCharOffset = start;
}
});
// finalize buffers
offsetAndType[tokenCount] = (EOF << TYPE_SHIFT) | sourceLength; // <EOF-token>
balance[tokenCount] = sourceLength;
balance[sourceLength] = sourceLength; // prevents false positive balance match with any token
while (balanceStart !== 0) {
const balancePrev = balanceStart & OFFSET_MASK;
balanceStart = balance[balancePrev];
balance[balancePrev] = sourceLength;
}
break;
}
this.source = source;
this.firstCharOffset = firstCharOffset === -1 ? 0 : firstCharOffset;
this.tokenCount = tokenCount;
this.offsetAndType = offsetAndType;
this.balance = balance;
case LeftParenthesis:
case FunctionToken:
case LeftSquareBracket:
case LeftCurlyBracket:
balance[tokenCount] = balanceStart;
balanceCloseType = balancePair.get(type);
balanceStart = (balanceCloseType << TYPE_SHIFT) | tokenCount;
break;
}
this.reset();
this.next();
offsetAndType[tokenCount++] = (type << TYPE_SHIFT) | end;
if (firstCharOffset === -1) {
firstCharOffset = start;
}
});
// finalize buffers
offsetAndType[tokenCount] = (EOF << TYPE_SHIFT) | sourceLength; // <EOF-token>
balance[tokenCount] = sourceLength;
balance[sourceLength] = sourceLength; // prevents false positive balance match with any token
while (balanceStart !== 0) {
const balancePrev = balanceStart & OFFSET_MASK;
balanceStart = balance[balancePrev];
balance[balancePrev] = sourceLength;
}
lookupType(offset) {
offset += this.tokenIndex;
this.source = source;
this.firstCharOffset = firstCharOffset === -1 ? 0 : firstCharOffset;
this.tokenCount = tokenCount;
this.offsetAndType = offsetAndType;
this.balance = balance;
if (offset < this.tokenCount) {
return this.offsetAndType[offset] >> TYPE_SHIFT;
}
this.reset();
this.next();
}
return EOF;
}
lookupOffset(offset) {
offset += this.tokenIndex;
lookupType(offset) {
offset += this.tokenIndex;
if (offset < this.tokenCount) {
return this.offsetAndType[offset - 1] & OFFSET_MASK;
}
return this.source.length;
}
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);
if (offset < this.tokenCount) {
return this.offsetAndType[offset] >> TYPE_SHIFT;
}
isBalanceEdge(pos) {
return this.balance[this.tokenIndex] < pos;
}
isDelim(code, offset) {
if (offset) {
return (
this.lookupType(offset) === Delim &&
this.source.charCodeAt(this.lookupOffset(offset)) === code
);
}
return EOF;
}
lookupOffset(offset) {
offset += this.tokenIndex;
return (
this.tokenType === Delim &&
this.source.charCodeAt(this.tokenStart) === code
);
if (offset < this.tokenCount) {
return this.offsetAndType[offset - 1] & OFFSET_MASK;
}
skip(tokenCount) {
let next = this.tokenIndex + tokenCount;
return this.source.length;
}
lookupValue(offset, referenceStr) {
offset += this.tokenIndex;
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;
let offset;
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
if (this.balance[balanceEnd] === cursor) {
cursor = balanceEnd;
}
}
}
this.skip(cursor - this.tokenIndex);
if (offset < this.tokenCount) {
return cmpStr(
this.source,
this.offsetAndType[offset - 1] & OFFSET_MASK,
this.offsetAndType[offset] & OFFSET_MASK,
referenceStr
);
}
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);
}
return false;
}
getTokenStart(tokenIndex) {
if (tokenIndex === this.tokenIndex) {
return this.tokenStart;
}
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;
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;
}
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;
let offset;
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
if (this.balance[balanceEnd] === cursor) {
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;
}
}

View File

@ -1,9 +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));
}
if (buffer === null || buffer.length < size) {
return new Uint32Array(Math.max(size + 1024, MIN_SIZE));
}
return buffer;
};
return buffer;
}

View File

@ -6,66 +6,66 @@ const EOF = 0;
// 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;
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
);
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;
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;
return code >= 0x0061 && code <= 0x007a;
}
// letter
// An uppercase letter or a lowercase letter.
export function isLetter(code) {
return isUppercaseLetter(code) || isLowercaseLetter(code);
return isUppercaseLetter(code) || isLowercaseLetter(code);
}
// non-ASCII code point
// A code point with a value equal to or greater than U+0080 <control>.
export function isNonAscii(code) {
return code >= 0x0080;
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;
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;
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)
);
return (
(code >= 0x0000 && code <= 0x0008) ||
code === 0x000b ||
(code >= 0x000e && code <= 0x001f) ||
code === 0x007f
);
}
// newline
@ -73,96 +73,94 @@ export function isNonPrintable(code) {
// 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;
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;
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;
}
// 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, if the second code point is a newline or EOF, return false.
if (isNewline(second) || second === EOF) {
return false;
}
// Otherwise, return true.
return true;
// 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:
// 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)
);
}
// 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;
}
// 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);
}
// 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;
// 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:
// 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+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;
}
// 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;
}
// 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;
}
// digit
if (isDigit(first)) {
// Return true.
return 1;
}
// 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;
}
// anything else
// Return false.
return 0;
// digit
if (isDigit(first)) {
// Return true.
return 1;
}
// anything else
// Return false.
return 0;
}
//
@ -171,17 +169,17 @@ export function isNumberStart(first, second, third) {
// detect BOM (https://en.wikipedia.org/wiki/Byte_order_mark)
export function isBOM(code) {
// UTF-16BE
if (code === 0xFEFF) {
return 1;
}
// UTF-16BE
if (code === 0xfeff) {
return 1;
}
// UTF-16LE
if (code === 0xFFFE) {
return 1;
}
// UTF-16LE
if (code === 0xfffe) {
return 1;
}
return 0;
return 0;
}
// Fast code category
@ -194,14 +192,15 @@ 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;
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;
return code < 0x80 ? CATEGORY[code] : NameStartCategory;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,27 @@
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'
'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',
];

View File

@ -1,28 +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 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 LeftParenthesis = 21; // <(-token>
export const RightParenthesis = 22; // <)-token>
export const LeftCurlyBracket = 23; // <{-token>
export const RightCurlyBracket = 24; // <}-token>
export const Comment = 25;

View File

@ -1,115 +1,119 @@
import {
isDigit,
isHexDigit,
isUppercaseLetter,
isName,
isWhiteSpace,
isValidEscape
isDigit,
isHexDigit,
isUppercaseLetter,
isName,
isWhiteSpace,
isValidEscape,
} from './char-code-definitions.js';
function getCharCode(source, offset) {
return offset < source.length ? source.charCodeAt(offset) : 0;
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;
}
if (code === 13 /* \r */ && getCharCode(source, offset + 1) === 10 /* \n */) {
return 2;
}
return 1;
return 1;
}
export function cmpChar(testStr, offset, referenceCode) {
let code = testStr.charCodeAt(offset);
let code = testStr.charCodeAt(offset);
// code.toLowerCase() for A..Z
if (isUppercaseLetter(code)) {
code = code | 32;
}
// code.toLowerCase() for A..Z
if (isUppercaseLetter(code)) {
code = code | 32;
}
return code === referenceCode;
return code === referenceCode;
}
export function cmpStr(testStr, start, end, referenceStr) {
if (end - start !== referenceStr.length) {
return false;
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 (start < 0 || end > testStr.length) {
return false;
if (testCode !== referenceCode) {
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;
return true;
}
export function findWhiteSpaceStart(source, offset) {
for (; offset >= 0; offset--) {
if (!isWhiteSpace(source.charCodeAt(offset))) {
break;
}
for (; offset >= 0; offset--) {
if (!isWhiteSpace(source.charCodeAt(offset))) {
break;
}
}
return offset + 1;
return offset + 1;
}
export function findWhiteSpaceEnd(source, offset) {
for (; offset < source.length; offset++) {
if (!isWhiteSpace(source.charCodeAt(offset))) {
break;
}
for (; offset < source.length; offset++) {
if (!isWhiteSpace(source.charCodeAt(offset))) {
break;
}
}
return offset;
return offset;
}
export function findDecimalNumberEnd(source, offset) {
for (; offset < source.length; offset++) {
if (!isDigit(source.charCodeAt(offset))) {
break;
}
for (; offset < source.length; offset++) {
if (!isDigit(source.charCodeAt(offset))) {
break;
}
}
return offset;
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;
// 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);
}
// 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;
}
}
return offset;
// 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
@ -117,138 +121,138 @@ export function consumeEscaped(source, offset) {
// 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);
// 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;
// name code point
if (isName(code)) {
// Append the code point to result.
continue;
}
return offset;
// 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);
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);
// 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);
}
// 3. While the next input code point is a digit, consume it and append it to repr.
// ... followed by a digit
if (isDigit(code)) {
offset = findDecimalNumberEnd(source, offset + 1);
code = source.charCodeAt(offset);
// 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);
}
}
// 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;
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);
// 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);
}
// U+0029 RIGHT PARENTHESIS ())
// EOF
if (code === 0x0029) {
// Return.
offset++;
break;
}
return offset;
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];
}
// 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);
// 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;
}
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);
// Otherwise, return the code point with that value.
return String.fromCodePoint(code);
}

View File

@ -17,453 +17,453 @@
let releasedCursors = null;
export class List {
static createItem(data) {
return {
prev: null,
next: null,
data
};
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,
};
}
constructor() {
this.head = null;
this.tail = null;
this.cursor = null;
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;
}
createItem(data) {
return List.createItem(data);
}
*[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++;
}
// cursor helpers
allocateCursor(prev, next) {
let cursor;
return size;
}
get isEmpty() {
return this.head === null;
}
get first() {
return this.head && this.head.data;
}
get last() {
return this.tail && this.tail.data;
}
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
};
}
// convertors
fromArray(array) {
let cursor = null;
this.head = null;
this.cursor = cursor;
for (let data of array) {
const item = List.createItem(data);
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
if (cursor !== null) {
cursor.next = item;
} else {
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);
item.prev = cursor;
cursor = 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;
}
this.tail = cursor;
return this;
}
toArray() {
return [...this];
}
toJSON() {
return [...this];
}
// tail always points to new item
this.tail = item;
// 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);
return this;
// 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;
}
insertData(data, before) {
return this.insert(List.createItem(data), before);
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;
}
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');
}
if (item.next !== null) {
item.next.prev = item.prev;
} else {
if (this.tail !== 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;
this.tail = item.prev;
}
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);
item.prev = null;
item.next = null;
// 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;
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;
}
replace(oldItem, newItemOrList) {
if ('head' in newItemOrList) {
this.insertList(newItemOrList, oldItem);
} else {
this.insert(newItemOrList, oldItem);
}
this.remove(oldItem);
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);
}
}

View File

@ -1,21 +1,21 @@
import { List } from './List.js';
export function clone(node) {
const result = {};
const result = {};
for (const key in node) {
let value = node[key];
for (const key in 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;
if (value) {
if (Array.isArray(value) || value instanceof List) {
value = value.map(clone);
} else if (value.constructor === Object) {
value = clone(value);
}
}
return result;
result[key] = value;
}
return result;
}

View File

@ -1,14 +1,17 @@
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();
// 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`);
}
});
};
return Object.assign(error, {
name,
message,
get stack() {
return (errorStack.stack || '').replace(
/^(.+\n){1,3}/,
`${name}: ${message}\n`
);
},
});
}

View File

@ -1,101 +1,102 @@
import {
isName,
isValidEscape,
consumeEscaped,
decodeEscaped
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 = '';
const end = str.length - 1;
let decoded = '';
for (let i = 0; i < str.length; i++) {
let code = str.charCodeAt(i);
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;
}
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);
code = str.charCodeAt(++i);
// consume escaped
if (isValidEscape(REVERSE_SOLIDUS, code)) {
const escapeStart = i - 1;
const escapeEnd = consumeEscaped(str, escapeStart);
// 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];
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;
return decoded;
}
// https://drafts.csswg.org/cssom/#serialize-an-identifier
// § 2.1. Common Serializing Idioms
export function encode(str) {
let encoded = '';
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 '\\-';
// 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;
}
// 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);
}
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;
}
return encoded;
// 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;
}

View File

@ -6,101 +6,107 @@ export const keyword = getKeywordDescriptor;
export const property = getPropertyDescriptor;
export const vendorPrefix = getVendorPrefix;
export function isCustomProperty(str, offset) {
offset = offset || 0;
offset = offset || 0;
return str.length - offset >= 2 &&
str.charCodeAt(offset) === HYPHENMINUS &&
str.charCodeAt(offset + 1) === HYPHENMINUS;
return (
str.length - offset >= 2 &&
str.charCodeAt(offset) === HYPHENMINUS &&
str.charCodeAt(offset + 1) === HYPHENMINUS
);
}
function getVendorPrefix(str, offset) {
offset = offset || 0;
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);
// 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);
}
}
if (secondDashIndex !== -1) {
return str.substring(offset, secondDashIndex + 1);
}
}
}
return '';
return '';
}
function getKeywordDescriptor(keyword) {
if (keywords.has(keyword)) {
return keywords.get(keyword);
}
if (keywords.has(keyword)) {
return keywords.get(keyword);
}
const name = keyword.toLowerCase();
let descriptor = keywords.get(name);
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
});
}
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);
keywords.set(keyword, descriptor);
return descriptor;
return descriptor;
}
function getPropertyDescriptor(property) {
if (properties.has(property)) {
return properties.get(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;
}
}
let name = property;
let hack = property[0];
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,
});
if (hack === '/') {
hack = property[1] === '/' ? '//' : '/';
} else if (hack !== '_' &&
hack !== '*' &&
hack !== '$' &&
hack !== '#' &&
hack !== '+' &&
hack !== '&') {
hack = '';
}
properties.set(property, descriptor);
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;
return descriptor;
}

Some files were not shown because too many files have changed in this diff Show More