Fix async lifecycle ordering, add _spa_init boot phase, update to jqhtml _load_only/_load_render_only flags

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2026-03-06 22:33:38 +00:00
parent 11c95a2886
commit d1ac456279
2718 changed files with 70593 additions and 6320 deletions

View File

@@ -162,6 +162,16 @@ function maybeMultiplied(scanner, node) {
return maybeMultiplied(scanner, multiplier);
}
// https://www.w3.org/TR/css-values-4/#component-multipliers
// > the # and ? multipliers, {A} and ? multipliers, and {A,B} and ? multipliers
// > may be stacked as #?, {A}?, and {A,B}?, respectively
// Represent "{}?" as nested multipliers as well as "+#".
// The "#?" case is already handled above, in maybeMultiplied()
if (scanner.charCode() === QUESTIONMARK &&
scanner.charCodeAt(scanner.pos - 1) === RIGHTCURLYBRACKET) {
return maybeMultiplied(scanner, multiplier);
}
return multiplier;
}
@@ -378,35 +388,65 @@ function regroupTerms(terms, combinators) {
return combinator;
}
function readImplicitGroup(scanner, stopCharCode) {
function readImplicitGroup(scanner, stopCharCode = -1) {
const combinators = Object.create(null);
const terms = [];
let token;
let prevToken = null;
let prevTokenPos = scanner.pos;
let prevTokenIsFunction = false;
while (scanner.charCode() !== stopCharCode && (token = peek(scanner, stopCharCode))) {
if (token.type !== 'Spaces') {
if (token.type === 'Combinator') {
// check for combinator in group beginning and double combinator sequence
if (prevToken === null || prevToken.type === 'Combinator') {
scanner.pos = prevTokenPos;
scanner.error('Unexpected combinator');
}
while (scanner.charCode() !== stopCharCode) {
let token = prevTokenIsFunction
? readImplicitGroup(scanner, RIGHTPARENTHESIS)
: peek(scanner);
combinators[token.value] = true;
} else if (prevToken !== null && prevToken.type !== 'Combinator') {
combinators[' '] = true; // a b
terms.push({
type: 'Combinator',
value: ' '
});
if (!token) {
break;
}
if (token.type === 'Spaces') {
continue;
}
if (prevTokenIsFunction) {
if (token.terms.length === 0) {
prevTokenIsFunction = false;
continue;
}
terms.push(token);
prevToken = token;
prevTokenPos = scanner.pos;
if (token.combinator === ' ') {
while (token.terms.length > 1) {
combinators[' '] = true; // a b
terms.push({
type: 'Combinator',
value: ' '
}, token.terms.shift());
}
token = token.terms[0];
}
}
if (token.type === 'Combinator') {
// check for combinator in group beginning and double combinator sequence
if (prevToken === null || prevToken.type === 'Combinator') {
scanner.pos = prevTokenPos;
scanner.error('Unexpected combinator');
}
combinators[token.value] = true;
} else if (prevToken !== null && prevToken.type !== 'Combinator') {
combinators[' '] = true; // a b
terms.push({
type: 'Combinator',
value: ' '
});
}
terms.push(token);
prevToken = token;
prevTokenPos = scanner.pos;
prevTokenIsFunction = token.type === 'Function';
}
// check for combinator in group ending
@@ -424,11 +464,11 @@ function readImplicitGroup(scanner, stopCharCode) {
};
}
function readGroup(scanner, stopCharCode) {
function readGroup(scanner) {
let result;
scanner.eat(LEFTSQUAREBRACKET);
result = readImplicitGroup(scanner, stopCharCode);
result = readImplicitGroup(scanner, RIGHTSQUAREBRACKET);
scanner.eat(RIGHTSQUAREBRACKET);
result.explicit = true;
@@ -441,7 +481,7 @@ function readGroup(scanner, stopCharCode) {
return result;
}
function peek(scanner, stopCharCode) {
function peek(scanner) {
let code = scanner.charCode();
switch (code) {
@@ -450,7 +490,7 @@ function peek(scanner, stopCharCode) {
break;
case LEFTSQUAREBRACKET:
return maybeMultiplied(scanner, readGroup(scanner, stopCharCode));
return maybeMultiplied(scanner, readGroup(scanner));
case LESSTHANSIGN:
return scanner.nextCharCode() === APOSTROPHE

View File

@@ -1,59 +0,0 @@
'use strict';
const SyntaxError = require('./SyntaxError.cjs');
const TAB = 9;
const N = 10;
const F = 12;
const R = 13;
const SPACE = 32;
class Tokenizer {
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));
}
skipWs() {
this.pos = this.findWsEnd(this.pos);
}
findWsEnd(pos) {
for (; pos < this.str.length; pos++) {
const code = this.str.charCodeAt(pos);
if (code !== R && code !== N && code !== F && code !== SPACE && code !== TAB) {
break;
}
}
return pos;
}
substringToPos(end) {
return this.str.substring(this.pos, this.pos = end);
}
eat(code) {
if (this.charCode() !== code) {
this.error('Expect `' + String.fromCharCode(code) + '`');
}
this.pos++;
}
peek() {
return this.pos < this.str.length ? this.str.charAt(this.pos++) : '';
}
error(message) {
throw new SyntaxError.SyntaxError(message, this.str, this.pos);
}
}
exports.Tokenizer = Tokenizer;

View File

@@ -26,12 +26,6 @@ function processChildren(node, delimeter) {
node.children.forEach(this.node, this);
}
function processChunk(chunk) {
index.tokenize(chunk, (type, start, end) => {
this.token(type, chunk.slice(start, end));
});
}
function createGenerator(config) {
const types$1 = new Map();
@@ -55,9 +49,13 @@ function createGenerator(config) {
}
},
tokenBefore: tokenBefore.safe,
token(type, value) {
token(type, value, suppressAutoWhiteSpace) {
prevCode = this.tokenBefore(prevCode, type, value);
if (!suppressAutoWhiteSpace && prevCode & 1) {
this.emit(' ', types.WhiteSpace, true);
}
this.emit(value, type, false);
if (type === types.Delim && value.charCodeAt(0) === REVERSESOLIDUS) {
@@ -90,7 +88,14 @@ function createGenerator(config) {
node: (node) => handlers.node(node),
children: processChildren,
token: (type, value) => handlers.token(type, value),
tokenize: processChunk
tokenize: (raw) =>
index.tokenize(raw, (type, start, end) => {
handlers.token(
type,
raw.slice(start, end),
start !== 0 // suppress auto whitespace for internal value tokens
);
})
};
handlers.node(node);

View File

@@ -5,17 +5,20 @@ const types = require('../tokenizer/types.cjs');
const PLUSSIGN = 0x002B; // U+002B PLUS SIGN (+)
const HYPHENMINUS = 0x002D; // U+002D HYPHEN-MINUS (-)
// code:
// 0xxxxxxx x0000000 - char code (0x80 for non-ASCII) or delim value
// 00000000 0xxxxxx0 - token type (0 for delim, 1 for token)
// 00000000 0000000x - reserved for carriage emit flag (0 for no space, 1 for space)
const code = (type, value) => {
if (type === types.Delim) {
type = value;
}
if (typeof type === 'string') {
const charCode = type.charCodeAt(0);
return charCode > 0x7F ? 0x8000 : charCode << 8;
type = Math.min(type.charCodeAt(0), 0x80) << 6; // replace non-ASCII with 0x80
}
return type;
return type << 1;
};
// https://www.w3.org/TR/css-syntax-3/#serialization
@@ -152,14 +155,10 @@ function createMap(pairs) {
type !== types.Function &&
type !== types.CDC) ||
(nextCharCode === PLUSSIGN)
? isWhiteSpaceRequired.has(prevCode << 16 | nextCharCode << 8)
: isWhiteSpaceRequired.has(prevCode << 16 | nextCode);
? isWhiteSpaceRequired.has((prevCode & 0xFFFE) << 16 | nextCharCode << 7)
: isWhiteSpaceRequired.has((prevCode & 0xFFFE) << 16 | nextCode);
if (emitWs) {
this.emit(' ', types.WhiteSpace, true);
}
return nextCode;
return nextCode | emitWs;
};
}

View File

@@ -7,7 +7,91 @@ const charCodeDefinitions = require('../tokenizer/char-code-definitions.cjs');
const types = require('../tokenizer/types.cjs');
const utils = require('../tokenizer/utils.cjs');
const calcFunctionNames = ['calc(', '-moz-calc(', '-webkit-calc('];
// CSS mathematical functions categorized by return type behavior
// See: https://www.w3.org/TR/css-values-4/#math
// Calculation functions that return different types depending on input
const calcFunctionNames = [
'calc(',
'-moz-calc(',
'-webkit-calc('
];
// Comparison functions that return different types depending on input
const comparisonFunctionNames = [
'min(',
'max(',
'clamp('
];
// Functions that return a stepped value, i.e. a value that is rounded to the nearest step
const steppedValueFunctionNames = [
'round(',
'mod(',
'rem('
];
// Trigonometrical functions that return a <number>
const trigNumberFunctionNames = [
'sin(',
'cos(',
'tan('
];
// Trigonometrical functions that return a <angle>
const trigAngleFunctionNames = [
'asin(',
'acos(',
'atan(',
'atan2('
];
// Other functions that return a <number>
const otherNumberFunctionNames = [
'pow(',
'sqrt(',
'log(',
'exp(',
'sign('
];
// Exponential functions that return a <number> or <dimension> or <percentage>
const expNumberDimensionPercentageFunctionNames = [
'hypot('
];
// Return the same type as the input
const signFunctionNames = [
'abs('
];
const numberFunctionNames = [
...calcFunctionNames,
...comparisonFunctionNames,
...steppedValueFunctionNames,
...trigNumberFunctionNames,
...otherNumberFunctionNames,
...expNumberDimensionPercentageFunctionNames,
...signFunctionNames
];
const percentageFunctionNames = [
...calcFunctionNames,
...comparisonFunctionNames,
...steppedValueFunctionNames,
...expNumberDimensionPercentageFunctionNames,
...signFunctionNames
];
const dimensionFunctionNames = [
...calcFunctionNames,
...comparisonFunctionNames,
...steppedValueFunctionNames,
...trigAngleFunctionNames,
...expNumberDimensionPercentageFunctionNames,
...signFunctionNames
];
const balancePair = new Map([
[types.Function, types.RightParenthesis],
[types.LeftParenthesis, types.RightParenthesis],
@@ -114,16 +198,17 @@ function consumeFunction(token, getNextToken) {
return length;
}
// TODO: implement
// can be used wherever <length>, <frequency>, <angle>, <time>, <percentage>, <number>, or <integer> values are allowed
// https://drafts.csswg.org/css-values/#calc-notation
function calc(next) {
function math(next, functionNames) {
return function(token, getNextToken, opts) {
if (token === null) {
return 0;
}
if (token.type === types.Function && eqStrAny(token.value, calcFunctionNames)) {
if (token.type === types.Function && eqStrAny(token.value, functionNames)) {
return consumeFunction(token, getNextToken);
}
@@ -530,12 +615,12 @@ const productionTypes = {
'ident': tokenType(types.Ident),
// percentage
'percentage': calc(percentage),
'percentage': math(percentage, percentageFunctionNames),
// numeric
'zero': zero(),
'number': calc(number),
'integer': calc(integer),
'number': math(number, numberFunctionNames),
'integer': math(integer, numberFunctionNames),
// complex types
'custom-ident': customIdent,
@@ -549,6 +634,17 @@ const productionTypes = {
'any-value': anyValue
};
const unitGroups = [
'length',
'angle',
'time',
'frequency',
'resolution',
'flex',
'decibel',
'semitones'
];
// dimensions types depend on units set
function createDemensionTypes(units) {
const {
@@ -563,15 +659,45 @@ function createDemensionTypes(units) {
} = units || {};
return {
'dimension': calc(dimension(null)),
'angle': calc(dimension(angle)),
'decibel': calc(dimension(decibel)),
'frequency': calc(dimension(frequency)),
'flex': calc(dimension(flex)),
'length': calc(zero(dimension(length))),
'resolution': calc(dimension(resolution)),
'semitones': calc(dimension(semitones)),
'time': calc(dimension(time))
'dimension': math(dimension(null), dimensionFunctionNames),
'angle': math(dimension(angle), dimensionFunctionNames),
'decibel': math(dimension(decibel), dimensionFunctionNames),
'frequency': math(dimension(frequency), dimensionFunctionNames),
'flex': math(dimension(flex), dimensionFunctionNames),
'length': math(zero(dimension(length)), dimensionFunctionNames),
'resolution': math(dimension(resolution), dimensionFunctionNames),
'semitones': math(dimension(semitones), dimensionFunctionNames),
'time': math(dimension(time), dimensionFunctionNames)
};
}
// The <attr-unit> production matches any identifier that is an ASCII case-insensitive
// match for the name of a CSS dimension unit, such as px, or the <delim-token> %.
function createAttrUnit(units) {
const unitSet = new Set();
for (const group of unitGroups) {
if (Array.isArray(units[group])) {
for (const unit of units[group]) {
unitSet.add(unit.toLowerCase());
}
}
}
return function attrUnit(token) {
if (token === null) {
return 0;
}
if (token.type === types.Delim && token.value === '%') {
return 1;
}
if (token.type === types.Ident && unitSet.has(token.value.toLowerCase())) {
return 1;
}
return 0;
};
}
@@ -579,7 +705,8 @@ function createGenericTypes(units) {
return {
...tokenTypes,
...productionTypes,
...createDemensionTypes(units)
...createDemensionTypes(units),
'attr-unit': createAttrUnit(units)
};
}
@@ -587,3 +714,4 @@ exports.createDemensionTypes = createDemensionTypes;
exports.createGenericTypes = createGenericTypes;
exports.productionTypes = productionTypes;
exports.tokenTypes = tokenTypes;
exports.unitGroups = unitGroups;

View File

@@ -17,6 +17,36 @@ const SEMICOLON = 0x003B; // U+003B SEMICOLON (;)
const LEFTCURLYBRACKET = 0x007B; // U+007B LEFT CURLY BRACKET ({)
const NULL = 0;
const arrayMethods = {
createList() {
return [];
},
createSingleNodeList(node) {
return [node];
},
getFirstListNode(list) {
return list && list[0] || null;
},
getLastListNode(list) {
return list && list.length > 0 ? list[list.length - 1] : null;
}
};
const listMethods = {
createList() {
return new List.List();
},
createSingleNodeList(node) {
return new List.List().appendData(node);
},
getFirstListNode(list) {
return list && list.first;
},
getLastListNode(list) {
return list && list.last;
}
};
function createParseContext(name) {
return function() {
return this[name]();
@@ -97,18 +127,10 @@ function createParser(config) {
return code === SEMICOLON ? 2 : 0;
},
createList() {
return new List.List();
},
createSingleNodeList(node) {
return new List.List().appendData(node);
},
getFirstListNode(list) {
return list && list.first;
},
getLastListNode(list) {
return list && list.last;
},
createList: NOOP,
createSingleNodeList: NOOP,
getFirstListNode: NOOP,
getLastListNode: NOOP,
parseWithFallback(consumer, fallback) {
const startIndex = this.tokenIndex;
@@ -280,6 +302,36 @@ function createParser(config) {
);
}
});
const createTokenIterateAPI = () => ({
filename,
source,
tokenCount: parser.tokenCount,
getTokenType: (index) =>
parser.getTokenType(index),
getTokenTypeName: (index) =>
names[parser.getTokenType(index)],
getTokenStart: (index) =>
parser.getTokenStart(index),
getTokenEnd: (index) =>
parser.getTokenEnd(index),
getTokenValue: (index) =>
parser.source.substring(parser.getTokenStart(index), parser.getTokenEnd(index)),
substring: (start, end) =>
parser.source.substring(start, end),
balance: parser.balance.subarray(0, parser.tokenCount + 1),
isBlockOpenerTokenType: parser.isBlockOpenerTokenType,
isBlockCloserTokenType: parser.isBlockCloserTokenType,
getBlockTokenPairIndex: (index) =>
parser.getBlockTokenPairIndex(index),
getLocation: (offset) =>
locationMap.getLocation(offset, filename),
getRangeLocation: (start, end) =>
locationMap.getLocationRange(start, end, filename)
});
const parse = function(source_, options) {
source = source_;
@@ -303,12 +355,22 @@ function createParser(config) {
parser.parseValue = 'parseValue' in options ? Boolean(options.parseValue) : true;
parser.parseCustomProperty = 'parseCustomProperty' in options ? Boolean(options.parseCustomProperty) : false;
const { context = 'default', onComment } = options;
const { context = 'default', list = true, onComment, onToken } = options;
if (context in parser.context === false) {
throw new Error('Unknown context `' + context + '`');
}
Object.assign(parser, list ? listMethods : arrayMethods);
if (Array.isArray(onToken)) {
parser.forEachToken((type, start, end) => {
onToken.push({ type, start, end });
});
} else if (typeof onToken === 'function') {
parser.forEachToken(onToken.bind(createTokenIterateAPI()));
}
if (typeof onComment === 'function') {
parser.forEachToken((type, start, end) => {
if (type === types.Comment) {

View File

@@ -10,23 +10,31 @@ function appendOrSet(a, b) {
return b || null;
}
function sliceProps(obj, props) {
function extractProps(obj, props) {
const result = Object.create(null);
for (const [key, value] of Object.entries(obj)) {
if (value) {
result[key] = {};
for (const prop of Object.keys(value)) {
if (props.includes(prop)) {
result[key][prop] = value[prop];
}
}
for (const prop of Object.keys(obj)) {
if (props.includes(prop)) {
result[prop] = obj[prop];
}
}
return result;
}
function mergeDicts(base, ext, fields) {
const result = { ...base };
for (const [key, props] of Object.entries(ext)) {
result[key] = {
...result[key],
...fields ? extractProps(props, fields) : props
};
}
return result;
}
function mix(dest, src) {
const result = { ...dest };
@@ -89,14 +97,6 @@ function mix(dest, src) {
}
break;
case 'scope':
case 'features':
result[prop] = { ...dest[prop] };
for (const [name, props] of Object.entries(value)) {
result[prop][name] = { ...result[prop][name], ...props };
}
break;
case 'parseContext':
result[prop] = {
...dest[prop],
@@ -104,19 +104,18 @@ function mix(dest, src) {
};
break;
case 'scope':
case 'features':
result[prop] = mergeDicts(dest[prop], value);
break;
case 'atrule':
case 'pseudo':
result[prop] = {
...dest[prop],
...sliceProps(value, ['parse'])
};
result[prop] = mergeDicts(dest[prop], value, ['parse']);
break;
case 'node':
result[prop] = {
...dest[prop],
...sliceProps(value, ['name', 'structure', 'parse', 'generate', 'walkContext'])
};
result[prop] = mergeDicts(dest[prop], value, ['name', 'structure', 'parse', 'generate', 'walkContext']);
break;
}
}

View File

@@ -1,34 +0,0 @@
'use strict';
const types = require('../../tokenizer/types.cjs');
const FULLSTOP = 0x002E; // U+002E FULL STOP (.)
const name = 'LayerName';
const structure = {
name: String
};
function parse() {
let name = this.consume(types.Ident);
while (this.isDelim(FULLSTOP)) {
this.eat(types.Delim);
name += '.' + this.consume(types.Ident);
}
return {
type: 'LayerName',
loc: this.getLocation(this.tokenStart, this.tokenEnd),
name
};
}
function generate(node) {
this.tokenize(node.name);
}
exports.generate = generate;
exports.name = name;
exports.parse = parse;
exports.structure = structure;

View File

@@ -1,42 +0,0 @@
'use strict';
const types = require('../../tokenizer/types.cjs');
const name = 'LayerNameList';
const structure = {
children: [[
'MediaQuery'
]]
};
function parse() {
const children = this.createList();
this.skipSC();
while (!this.eof) {
children.push(this.LayerName());
if (this.tokenType !== types.Comma) {
break;
}
this.next();
this.skipSC();
}
return {
type: 'LayerNameList',
loc: this.getLocationFromList(children),
children
};
}
function generate(node) {
this.children(node, () => this.token(types.Comma, ','));
}
exports.generate = generate;
exports.name = name;
exports.parse = parse;
exports.structure = structure;

View File

@@ -1,70 +0,0 @@
'use strict';
const types = require('../../tokenizer/types.cjs');
const MediaFeatureToken = new Set([types.Colon, types.RightParenthesis, types.EOF]);
const name = 'MediaCondition';
const structure = {
children: [[
'Identifier',
'MediaFeature',
'MediaFeatureRange'
]]
};
function parse() {
const children = this.createList();
scan: while (!this.eof) {
switch (this.tokenType) {
case types.Comment:
case types.WhiteSpace:
this.next();
continue;
case types.Ident:
children.push(this.Identifier());
break;
case types.LeftParenthesis:
if (this.lookupTypeNonSC(1) === types.Ident && MediaFeatureToken.has(this.lookupTypeNonSC(2))) {
children.push(this.MediaFeature());
} else if (this.lookupTypeNonSC(1) === types.LeftParenthesis || this.lookupTypeNonSC(2) === types.LeftParenthesis) {
this.next();
children.push(this.MediaCondition());
this.eat(types.RightParenthesis);
} else {
children.push(this.MediaFeatureRange());
}
break;
default:
break scan;
}
}
return {
type: 'MediaCondition',
loc: this.getLocationFromList(children),
children
};
}
function generate(node) {
node.children.forEach(child => {
if (child.type === 'MediaCondition') {
this.token(types.LeftParenthesis, '(');
this.node(child);
this.token(types.RightParenthesis, ')');
} else {
this.node(child);
}
});
}
exports.generate = generate;
exports.name = name;
exports.parse = parse;
exports.structure = structure;

View File

@@ -1,76 +0,0 @@
'use strict';
const types = require('../../tokenizer/types.cjs');
const name = 'MediaFeature';
const structure = {
name: String,
value: ['Identifier', 'Number', 'Dimension', 'Ratio', null]
};
function parse() {
const start = this.tokenStart;
let name;
let value = null;
this.eat(types.LeftParenthesis);
this.skipSC();
name = this.consume(types.Ident);
this.skipSC();
if (this.tokenType !== types.RightParenthesis) {
this.eat(types.Colon);
this.skipSC();
switch (this.tokenType) {
case types.Number:
if (this.lookupNonWSType(1) === types.Delim) {
value = this.Ratio();
} else {
value = this.Number();
}
break;
case types.Dimension:
value = this.Dimension();
break;
case types.Ident:
value = this.Identifier();
break;
default:
this.error('Number, dimension, ratio or identifier is expected');
}
this.skipSC();
}
this.eat(types.RightParenthesis);
return {
type: 'MediaFeature',
loc: this.getLocation(start, this.tokenStart),
name,
value
};
}
function generate(node) {
this.token(types.LeftParenthesis, '(');
this.token(types.Ident, node.name);
if (node.value !== null) {
this.token(types.Colon, ':');
this.node(node.value);
}
this.token(types.RightParenthesis, ')');
}
exports.generate = generate;
exports.name = name;
exports.parse = parse;
exports.structure = structure;

View File

@@ -1,11 +0,0 @@
'use strict';
const featureRange = require('./common/feature-range.cjs');
const name = 'MediaFeatureRange';
const parse = featureRange.createParse(name);
exports.generate = featureRange.generate;
exports.structure = featureRange.structure;
exports.name = name;
exports.parse = parse;

View File

@@ -1,69 +0,0 @@
'use strict';
const types = require('../../tokenizer/types.cjs');
const name = 'SupportsDeclaration';
const structure = {
feature: String,
value: 'Declaration'
};
function parse() {
const start = this.tokenStart;
let featureName = 'declaration';
let valueParser = this.Declaration;
if (this.tokenType === types.Function) {
featureName = this.consumeFunctionName();
valueParser = this.supportsFeature[featureName.toLowerCase()];
if (!valueParser) {
this.error(`Unknown supports feature ${featureName.toLowerCase()}()`);
}
} else {
this.eat(types.LeftParenthesis);
}
this.skipSC();
const value = this.parseWithFallback(
() => {
const startValueToken = this.tokenIndex;
const value = valueParser.call(this);
if (this.eof === false &&
this.isBalanceEdge(startValueToken) === false) {
this.error();
}
return value;
},
(startToken) => this.Raw(startToken, null, false)
);
if (!this.eof) {
this.eat(types.RightParenthesis);
}
return {
type: 'SupportsDeclaration',
loc: this.getLocation(start, this.tokenStart),
feature: featureName,
value
};
}
function generate(node) {
if (node.feature !== 'declaration') {
this.token(types.Function, node.feature + '(');
} else {
this.token(types.LeftParenthesis, '(');
}
this.node(node.value);
this.token(types.RightParenthesis, ')');
}
exports.generate = generate;
exports.name = name;
exports.parse = parse;
exports.structure = structure;

View File

@@ -1,112 +0,0 @@
'use strict';
const types = require('../../../tokenizer/types.cjs');
const LESSTHANSIGN = 60; // <
const EQUALSIGN = 61; // =
const GREATERTHANSIGN = 62; // >
const structure = {
left: ['Identifier', 'Number', 'Dimension', 'Ratio'],
leftComparison: String,
middle: ['Identifier', 'Number', 'Dimension', 'Ratio'],
rightComparison: [String, null],
right: ['Identifier', 'Number', 'Dimension', 'Ratio', null]
};
function readTerm() {
this.skipSC();
switch (this.tokenType) {
case types.Number:
if (this.lookupNonWSType(1) === types.Delim) {
return this.Ratio();
} else {
return this.Number();
}
case types.Dimension:
return this.Dimension();
case types.Ident:
return this.Identifier();
default:
this.error('Number, dimension, ratio or identifier is expected');
}
}
function readComparison(expectColon) {
this.skipSC();
if (this.isDelim(LESSTHANSIGN) ||
this.isDelim(GREATERTHANSIGN)) {
const value = this.source[this.tokenStart];
this.next();
if (this.isDelim(EQUALSIGN)) {
this.next();
return value + '=';
}
return value;
}
if (this.isDelim(EQUALSIGN)) {
return '=';
}
this.error(`Expected ${expectColon ? '":", ' : ''}"<", ">", "=" or ")"`);
}
function createParse(type) {
return function parse() {
const start = this.tokenStart;
this.skipSC();
this.eat(types.LeftParenthesis);
const left = readTerm.call(this);
const leftComparison = readComparison.call(this, left.type === 'Identifier');
const middle = readTerm.call(this);
let rightComparison = null;
let right = null;
if (this.lookupNonWSType(0) !== types.RightParenthesis) {
rightComparison = readComparison.call(this);
right = readTerm.call(this);
}
this.skipSC();
this.eat(types.RightParenthesis);
return {
type,
loc: this.getLocation(start, this.tokenStart),
left,
leftComparison,
middle,
rightComparison,
right
};
};
}
function generate(node) {
this.token(types.LeftParenthesis, '(');
this.node(node.left);
this.tokenize(node.leftComparison);
this.node(node.middle);
if (node.right) {
this.tokenize(node.rightComparison);
this.node(node.right);
}
this.token(types.RightParenthesis, ')');
}
exports.createParse = createParse;
exports.generate = generate;
exports.structure = structure;

View File

@@ -1,76 +0,0 @@
'use strict';
const types = require('../../../tokenizer/types.cjs');
const structure = {
name: String,
value: ['Identifier', 'Number', 'Dimension', 'Ratio', null]
};
function createParse(type) {
return function parse() {
const start = this.tokenStart;
let name;
let value = null;
this.eat(types.LeftParenthesis);
this.skipSC();
name = this.consume(types.Ident);
this.skipSC();
if (this.tokenType !== types.RightParenthesis) {
this.eat(types.Colon);
this.skipSC();
switch (this.tokenType) {
case types.Number:
if (this.lookupNonWSType(1) === types.Delim) {
value = this.Ratio();
} else {
value = this.Number();
}
break;
case types.Dimension:
value = this.Dimension();
break;
case types.Ident:
value = this.Identifier();
break;
default:
this.error('Number, dimension, ratio or identifier is expected');
}
this.skipSC();
}
this.eat(types.RightParenthesis);
return {
type,
loc: this.getLocation(start, this.tokenStart),
name,
value
};
};
}
function generate(node) {
this.token(types.LeftParenthesis, '(');
this.token(types.Ident, node.name);
if (node.value !== null) {
this.token(types.Colon, ':');
this.node(node.value);
}
this.token(types.RightParenthesis, ')');
}
exports.createParse = createParse;
exports.generate = generate;
exports.structure = structure;

View File

@@ -7,14 +7,25 @@ const types = require('./types.cjs');
const OFFSET_MASK = 0x00FFFFFF;
const TYPE_SHIFT = 24;
const BLOCK_OPEN_TOKEN = 1;
const BLOCK_CLOSE_TOKEN = 2;
const balancePair = new Uint8Array(32); // 32b of memory ought to be enough for anyone (any number of tokens)
balancePair[types.Function] = types.RightParenthesis;
balancePair[types.LeftParenthesis] = types.RightParenthesis;
balancePair[types.LeftSquareBracket] = types.RightSquareBracket;
balancePair[types.LeftCurlyBracket] = types.RightCurlyBracket;
function isBlockOpenerToken(tokenType) {
return balancePair[tokenType] !== 0;
const blockTokens = new Uint8Array(32);
blockTokens[types.Function] = BLOCK_OPEN_TOKEN;
blockTokens[types.LeftParenthesis] = BLOCK_OPEN_TOKEN;
blockTokens[types.LeftSquareBracket] = BLOCK_OPEN_TOKEN;
blockTokens[types.LeftCurlyBracket] = BLOCK_OPEN_TOKEN;
blockTokens[types.RightParenthesis] = BLOCK_CLOSE_TOKEN;
blockTokens[types.RightSquareBracket] = BLOCK_CLOSE_TOKEN;
blockTokens[types.RightCurlyBracket] = BLOCK_CLOSE_TOKEN;
function boundIndex(index, min, max) {
return index < min ? min : index > max ? max : index;
}
class TokenStream {
@@ -66,7 +77,7 @@ class TokenStream {
// pop state
balanceStart = prevBalanceStart;
balanceCloseType = balancePair[offsetAndType[prevBalanceStart] >> TYPE_SHIFT];
} else if (isBlockOpenerToken(type)) { // check for FunctionToken, <(-token>, <[-token> and <{-token>
} else if (this.isBlockOpenerTokenType(type)) { // check for FunctionToken, <(-token>, <[-token> and <{-token>
// push state
balanceStart = index;
balanceCloseType = balancePair[type];
@@ -184,14 +195,53 @@ class TokenStream {
return this.firstCharOffset;
}
getTokenEnd(tokenIndex) {
if (tokenIndex === this.tokenIndex) {
return this.tokenEnd;
}
return this.offsetAndType[boundIndex(tokenIndex, 0, this.tokenCount)] & OFFSET_MASK;
}
getTokenType(tokenIndex) {
if (tokenIndex === this.tokenIndex) {
return this.tokenType;
}
return this.offsetAndType[boundIndex(tokenIndex, 0, this.tokenCount)] >> TYPE_SHIFT;
}
substrToCursor(start) {
return this.source.substring(start, this.tokenStart);
}
isBalanceEdge(pos) {
return this.balance[this.tokenIndex] < pos;
// return this.balance[this.balance[pos]] !== this.tokenIndex;
isBlockOpenerTokenType(tokenType) {
return blockTokens[tokenType] === BLOCK_OPEN_TOKEN;
}
isBlockCloserTokenType(tokenType) {
return blockTokens[tokenType] === BLOCK_CLOSE_TOKEN;
}
getBlockTokenPairIndex(tokenIndex) {
const type = this.getTokenType(tokenIndex);
if (blockTokens[type] === 1) {
// block open token
const pairIndex = this.balance[tokenIndex];
const closeType = this.getTokenType(pairIndex);
return balancePair[type] === closeType ? pairIndex : -1;
} else if (blockTokens[type] === 2) {
// block close token
const pairIndex = this.balance[tokenIndex];
const openType = this.getTokenType(pairIndex);
return balancePair[openType] === type ? pairIndex : -1;
}
return -1;
}
isBalanceEdge(tokenIndex) {
return this.balance[this.tokenIndex] < tokenIndex;
}
isDelim(code, offset) {
if (offset) {
return (
@@ -268,7 +318,7 @@ class TokenStream {
default:
// fast forward to the end of balanced block for an open block tokens
if (isBlockOpenerToken(this.offsetAndType[cursor] >> TYPE_SHIFT)) {
if (this.isBlockOpenerTokenType(this.offsetAndType[cursor] >> TYPE_SHIFT)) {
cursor = balanceEnd;
}
}