// @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]} */ 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); } } }