| // @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 = {}; |
| composeSimpleVisitors(res, visitors, 'StyleSheet'); |
| composeSimpleVisitors(res, visitors, 'StyleSheetExit'); |
| composeObjectVisitors(res, visitors, 'Rule', ruleVisitor, wrapCustomAndUnknownAtRule); |
| composeObjectVisitors(res, visitors, 'RuleExit', ruleVisitor, wrapCustomAndUnknownAtRule); |
| 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 wrapCustomAndUnknownAtRule(k, f) { |
| if (k === 'unknown') { |
| return (value => f({ type: 'unknown', value })); |
| } |
| if (k === 'custom') { |
| return (value => f({ type: 'custom', value })); |
| } |
| return 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); |
| } |
| if (item.type === 'custom') { |
| let v = f.custom; |
| 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); |
| } |
| } |
| } |