428 lines
10 KiB
JavaScript
428 lines
10 KiB
JavaScript
|
// @ts-check
|
||
|
/** @typedef {import('./index').Visitor} Visitor */
|
||
|
|
||
|
/**
|
||
|
* Composes multiple visitor objects into a single one.
|
||
|
* @param {Visitor[]} visitors
|
||
|
* @return {Visitor}
|
||
|
*/
|
||
|
function composeVisitors(visitors) {
|
||
|
if (visitors.length === 1) {
|
||
|
return visitors[0];
|
||
|
}
|
||
|
|
||
|
/** @type Visitor */
|
||
|
let res = {};
|
||
|
composeObjectVisitors(res, visitors, 'Rule', ruleVisitor, wrapUnknownAtRule);
|
||
|
composeObjectVisitors(res, visitors, 'RuleExit', ruleVisitor, wrapUnknownAtRule);
|
||
|
composeObjectVisitors(res, visitors, 'Declaration', declarationVisitor, wrapCustomProperty);
|
||
|
composeObjectVisitors(res, visitors, 'DeclarationExit', declarationVisitor, wrapCustomProperty);
|
||
|
composeSimpleVisitors(res, visitors, 'Url');
|
||
|
composeSimpleVisitors(res, visitors, 'Color');
|
||
|
composeSimpleVisitors(res, visitors, 'Image');
|
||
|
composeSimpleVisitors(res, visitors, 'ImageExit');
|
||
|
composeSimpleVisitors(res, visitors, 'Length');
|
||
|
composeSimpleVisitors(res, visitors, 'Angle');
|
||
|
composeSimpleVisitors(res, visitors, 'Ratio');
|
||
|
composeSimpleVisitors(res, visitors, 'Resolution');
|
||
|
composeSimpleVisitors(res, visitors, 'Time');
|
||
|
composeSimpleVisitors(res, visitors, 'CustomIdent');
|
||
|
composeSimpleVisitors(res, visitors, 'DashedIdent');
|
||
|
composeArrayFunctions(res, visitors, 'MediaQuery');
|
||
|
composeArrayFunctions(res, visitors, 'MediaQueryExit');
|
||
|
composeSimpleVisitors(res, visitors, 'SupportsCondition');
|
||
|
composeSimpleVisitors(res, visitors, 'SupportsConditionExit');
|
||
|
composeArrayFunctions(res, visitors, 'Selector');
|
||
|
composeTokenVisitors(res, visitors, 'Token', 'token', false);
|
||
|
composeTokenVisitors(res, visitors, 'Function', 'function', false);
|
||
|
composeTokenVisitors(res, visitors, 'FunctionExit', 'function', true);
|
||
|
composeTokenVisitors(res, visitors, 'Variable', 'var', false);
|
||
|
composeTokenVisitors(res, visitors, 'VariableExit', 'var', true);
|
||
|
composeTokenVisitors(res, visitors, 'EnvironmentVariable', 'env', false);
|
||
|
composeTokenVisitors(res, visitors, 'EnvironmentVariableExit', 'env', true);
|
||
|
return res;
|
||
|
}
|
||
|
|
||
|
module.exports = composeVisitors;
|
||
|
|
||
|
function wrapUnknownAtRule(k, f) {
|
||
|
return k === 'unknown' ? (value => f({ type: 'unknown', value })) : f;
|
||
|
}
|
||
|
|
||
|
function wrapCustomProperty(k, f) {
|
||
|
return k === 'custom' ? (value => f({ property: 'custom', value })) : f;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {import('./index').Visitor['Rule']} f
|
||
|
* @param {import('./ast').Rule} item
|
||
|
*/
|
||
|
function ruleVisitor(f, item) {
|
||
|
if (typeof f === 'object') {
|
||
|
if (item.type === 'unknown') {
|
||
|
let v = f.unknown;
|
||
|
if (typeof v === 'object') {
|
||
|
v = v[item.value.name];
|
||
|
}
|
||
|
return v?.(item.value);
|
||
|
}
|
||
|
return f[item.type]?.(item);
|
||
|
}
|
||
|
return f?.(item);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {import('./index').Visitor['Declaration']} f
|
||
|
* @param {import('./ast').Declaration} item
|
||
|
*/
|
||
|
function declarationVisitor(f, item) {
|
||
|
if (typeof f === 'object') {
|
||
|
/** @type {string} */
|
||
|
let name = item.property;
|
||
|
if (item.property === 'unparsed') {
|
||
|
name = item.value.propertyId.property;
|
||
|
} else if (item.property === 'custom') {
|
||
|
let v = f.custom;
|
||
|
if (typeof v === 'object') {
|
||
|
v = v[item.value.name];
|
||
|
}
|
||
|
return v?.(item.value);
|
||
|
}
|
||
|
return f[name]?.(item);
|
||
|
}
|
||
|
return f?.(item);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* @param {Visitor[]} visitors
|
||
|
* @param {string} key
|
||
|
* @returns {[any[], boolean, Set<string>]}
|
||
|
*/
|
||
|
function extractObjectsOrFunctions(visitors, key) {
|
||
|
let values = [];
|
||
|
let hasFunction = false;
|
||
|
let allKeys = new Set();
|
||
|
for (let visitor of visitors) {
|
||
|
let v = visitor[key];
|
||
|
if (v) {
|
||
|
if (typeof v === 'function') {
|
||
|
hasFunction = true;
|
||
|
} else {
|
||
|
for (let key in v) {
|
||
|
allKeys.add(key);
|
||
|
}
|
||
|
}
|
||
|
values.push(v);
|
||
|
}
|
||
|
}
|
||
|
return [values, hasFunction, allKeys];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @template {keyof Visitor} K
|
||
|
* @param {Visitor} res
|
||
|
* @param {Visitor[]} visitors
|
||
|
* @param {K} key
|
||
|
* @param {(visitor: Visitor[K], item: any) => any | any[] | void} apply
|
||
|
* @param {(k: string, f: any) => any} wrapKey
|
||
|
*/
|
||
|
function composeObjectVisitors(res, visitors, key, apply, wrapKey) {
|
||
|
let [values, hasFunction, allKeys] = extractObjectsOrFunctions(visitors, key);
|
||
|
if (values.length === 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (values.length === 1) {
|
||
|
res[key] = values[0];
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let f = createArrayVisitor(visitors, (visitor, item) => apply(visitor[key], item));
|
||
|
if (hasFunction) {
|
||
|
res[key] = f;
|
||
|
} else {
|
||
|
/** @type {any} */
|
||
|
let v = {};
|
||
|
for (let k of allKeys) {
|
||
|
v[k] = wrapKey(k, f);
|
||
|
}
|
||
|
res[key] = v;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {Visitor} res
|
||
|
* @param {Visitor[]} visitors
|
||
|
* @param {string} key
|
||
|
* @param {import('./ast').TokenOrValue['type']} type
|
||
|
* @param {boolean} isExit
|
||
|
*/
|
||
|
function composeTokenVisitors(res, visitors, key, type, isExit) {
|
||
|
let [values, hasFunction, allKeys] = extractObjectsOrFunctions(visitors, key);
|
||
|
if (values.length === 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (values.length === 1) {
|
||
|
res[key] = values[0];
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let f = createTokenVisitor(visitors, type, isExit);
|
||
|
if (hasFunction) {
|
||
|
res[key] = f;
|
||
|
} else {
|
||
|
let v = {};
|
||
|
for (let key of allKeys) {
|
||
|
v[key] = f;
|
||
|
}
|
||
|
res[key] = v;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {Visitor[]} visitors
|
||
|
* @param {import('./ast').TokenOrValue['type']} type
|
||
|
*/
|
||
|
function createTokenVisitor(visitors, type, isExit) {
|
||
|
let v = createArrayVisitor(visitors, (visitor, /** @type {import('./ast').TokenOrValue} */ item) => {
|
||
|
let f;
|
||
|
switch (item.type) {
|
||
|
case 'token':
|
||
|
f = visitor.Token;
|
||
|
if (typeof f === 'object') {
|
||
|
f = f[item.value.type];
|
||
|
}
|
||
|
break;
|
||
|
case 'function':
|
||
|
f = isExit ? visitor.FunctionExit : visitor.Function;
|
||
|
if (typeof f === 'object') {
|
||
|
f = f[item.value.name];
|
||
|
}
|
||
|
break;
|
||
|
case 'var':
|
||
|
f = isExit ? visitor.VariableExit : visitor.Variable;
|
||
|
break;
|
||
|
case 'env':
|
||
|
f = isExit ? visitor.EnvironmentVariableExit : visitor.EnvironmentVariable;
|
||
|
if (typeof f === 'object') {
|
||
|
let name;
|
||
|
switch (item.value.name.type) {
|
||
|
case 'ua':
|
||
|
case 'unknown':
|
||
|
name = item.value.name.value;
|
||
|
break;
|
||
|
case 'custom':
|
||
|
name = item.value.name.ident;
|
||
|
break;
|
||
|
}
|
||
|
f = f[name];
|
||
|
}
|
||
|
break;
|
||
|
case 'color':
|
||
|
f = visitor.Color;
|
||
|
break;
|
||
|
case 'url':
|
||
|
f = visitor.Url;
|
||
|
break;
|
||
|
case 'length':
|
||
|
f = visitor.Length;
|
||
|
break;
|
||
|
case 'angle':
|
||
|
f = visitor.Angle;
|
||
|
break;
|
||
|
case 'time':
|
||
|
f = visitor.Time;
|
||
|
break;
|
||
|
case 'resolution':
|
||
|
f = visitor.Resolution;
|
||
|
break;
|
||
|
case 'dashed-ident':
|
||
|
f = visitor.DashedIdent;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if (!f) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let res = f(item.value);
|
||
|
switch (item.type) {
|
||
|
case 'color':
|
||
|
case 'url':
|
||
|
case 'length':
|
||
|
case 'angle':
|
||
|
case 'time':
|
||
|
case 'resolution':
|
||
|
case 'dashed-ident':
|
||
|
if (Array.isArray(res)) {
|
||
|
res = res.map(value => ({ type: item.type, value }))
|
||
|
} else if (res) {
|
||
|
res = { type: item.type, value: res };
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return res;
|
||
|
});
|
||
|
|
||
|
return value => v({ type, value });
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {Visitor[]} visitors
|
||
|
* @param {string} key
|
||
|
*/
|
||
|
function extractFunctions(visitors, key) {
|
||
|
let functions = [];
|
||
|
for (let visitor of visitors) {
|
||
|
let f = visitor[key];
|
||
|
if (f) {
|
||
|
functions.push(f);
|
||
|
}
|
||
|
}
|
||
|
return functions;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {Visitor} res
|
||
|
* @param {Visitor[]} visitors
|
||
|
* @param {string} key
|
||
|
*/
|
||
|
function composeSimpleVisitors(res, visitors, key) {
|
||
|
let functions = extractFunctions(visitors, key);
|
||
|
if (functions.length === 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (functions.length === 1) {
|
||
|
res[key] = functions[0];
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
res[key] = arg => {
|
||
|
let mutated = false;
|
||
|
for (let f of functions) {
|
||
|
let res = f(arg);
|
||
|
if (res) {
|
||
|
arg = res;
|
||
|
mutated = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return mutated ? arg : undefined;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {Visitor} res
|
||
|
* @param {Visitor[]} visitors
|
||
|
* @param {string} key
|
||
|
*/
|
||
|
function composeArrayFunctions(res, visitors, key) {
|
||
|
let functions = extractFunctions(visitors, key);
|
||
|
if (functions.length === 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (functions.length === 1) {
|
||
|
res[key] = functions[0];
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
res[key] = createArrayVisitor(functions, (f, item) => f(item));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @template T
|
||
|
* @template V
|
||
|
* @param {T[]} visitors
|
||
|
* @param {(visitor: T, item: V) => V | V[] | void} apply
|
||
|
* @returns {(item: V) => V | V[] | void}
|
||
|
*/
|
||
|
function createArrayVisitor(visitors, apply) {
|
||
|
let seen = new Bitset(visitors.length);
|
||
|
return arg => {
|
||
|
let arr = [arg];
|
||
|
let mutated = false;
|
||
|
seen.clear();
|
||
|
for (let i = 0; i < arr.length; i++) {
|
||
|
// For each value, call all visitors. If a visitor returns a new value,
|
||
|
// we start over, but skip the visitor that generated the value or saw
|
||
|
// it before (to avoid cycles). This way, visitors can be composed in any order.
|
||
|
for (let v = 0; v < visitors.length;) {
|
||
|
if (seen.get(v)) {
|
||
|
v++;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
let item = arr[i];
|
||
|
let visitor = visitors[v];
|
||
|
let res = apply(visitor, item);
|
||
|
if (Array.isArray(res)) {
|
||
|
if (res.length === 0) {
|
||
|
arr.splice(i, 1);
|
||
|
} else if (res.length === 1) {
|
||
|
arr[i] = res[0];
|
||
|
} else {
|
||
|
arr.splice(i, 1, ...res);
|
||
|
}
|
||
|
mutated = true;
|
||
|
seen.set(v);
|
||
|
v = 0;
|
||
|
} else if (res) {
|
||
|
arr[i] = res;
|
||
|
mutated = true;
|
||
|
seen.set(v);
|
||
|
v = 0;
|
||
|
} else {
|
||
|
v++;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!mutated) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
return arr.length === 1 ? arr[0] : arr;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
class Bitset {
|
||
|
constructor(maxBits = 32) {
|
||
|
this.bits = 0;
|
||
|
this.more = maxBits > 32 ? new Uint32Array(Math.ceil((maxBits - 32) / 32)) : null;
|
||
|
}
|
||
|
|
||
|
/** @param {number} bit */
|
||
|
get(bit) {
|
||
|
if (bit >= 32 && this.more) {
|
||
|
let i = Math.floor((bit - 32) / 32);
|
||
|
let b = bit % 32;
|
||
|
return Boolean(this.more[i] & (1 << b));
|
||
|
} else {
|
||
|
return Boolean(this.bits & (1 << bit));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** @param {number} bit */
|
||
|
set(bit) {
|
||
|
if (bit >= 32 && this.more) {
|
||
|
let i = Math.floor((bit - 32) / 32);
|
||
|
let b = bit % 32;
|
||
|
this.more[i] |= 1 << b;
|
||
|
} else {
|
||
|
this.bits |= 1 << bit;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
clear() {
|
||
|
this.bits = 0;
|
||
|
if (this.more) {
|
||
|
this.more.fill(0);
|
||
|
}
|
||
|
}
|
||
|
}
|