UNPKG

@tbela99/css-parser

Version:

CSS parser, minifier and validator for node and the browser

1,356 lines (1,354 loc) 50.4 kB
import { eq } from '../parser/utils/eq.js'; import { renderValue, doRender } from '../renderer/render.js'; import * as index from './features/index.js'; import { walkValues } from './walk.js'; import { EnumToken } from './types.js'; import { isWhiteSpace, isIdent, isFunction, isIdentStart } from '../syntax/syntax.js'; import { FeatureWalkMode } from './features/type.js'; import { trimArray } from '../validation/match.js'; import { TOKENS, PARENT, OPTIMIZED, RAW, combinators, LOC } from '../syntax/constants.js'; import { replaceNodeOrValue } from '../parser/utils/token.js'; import { parseString } from '../parser/parse.js'; import { tokenize } from '../parser/tokenize.js'; import { replaceCompound } from './expand.js'; const notEndingWith = ["(", "["].concat(combinators); const rules = [ EnumToken.AtRuleNodeType, EnumToken.RuleNodeType, EnumToken.AtRuleTokenType, EnumToken.KeyFramesRuleNodeType, ]; // @ts-ignore const features = Object.values(index).sort((a, b) => a.ordering - b.ordering); /** * Apply minification rules to the ast tree * @param ast * @param options * @param recursive * @param errors * @param nestingContent * * @private */ function minify(ast, options = {}, recursive = false, errors, nestingContent, context = {}) { let preprocess = false; let postprocess = false; let parents; let replacement; if (!("features" in options)) { // @ts-ignore options = { removeDuplicateDeclarations: true, computeShorthand: true, computeCalcExpression: true, removePrefix: false, features: [], ...options, }; for (const feature of features) { feature.register(options); } options.features.sort((a, b) => a.ordering - b.ordering); } for (const feature of options.features) { if (feature.processMode & FeatureWalkMode.Pre) { preprocess = true; } if (feature.processMode & FeatureWalkMode.Post) { postprocess = true; } } if (preprocess) { parents = new Set([ast]); for (const parent of parents) { if (parent.typ == EnumToken.CommentTokenType || parent.typ == EnumToken.CDOCOMMTokenType) { continue; } replacement = parent; for (const feature of options.features) { if ((feature.processMode & FeatureWalkMode.Pre) === 0 || (feature.accept != null && !feature.accept.has(parent.typ))) { continue; } if (rules.includes(replacement.typ) && !Array.isArray(replacement[TOKENS])) { replacement[TOKENS] = parseString(replacement.typ == EnumToken.RuleNodeType || replacement.typ === EnumToken.KeyFramesRuleNodeType ? replacement.sel : replacement.nam); } const result = feature.run(replacement, options, parent[PARENT] ?? ast, context, FeatureWalkMode.Pre); if (result != null) { replacement = result; } } if (replacement != parent && parent[PARENT] != null) { replaceNodeOrValue(parent[PARENT], parent, replacement); } if ("chi" in replacement) { // @ts-ignore for (const node of replacement.chi) { node[PARENT] = replacement; parents.add(node); } } } for (const feature of options.features) { if (feature.processMode & FeatureWalkMode.Pre && "cleanup" in feature) { // @ts-ignore feature.cleanup(ast, options, context, FeatureWalkMode.Pre); } } } doMinify(ast, options, recursive, errors, nestingContent, context); parents = new Set([ast]); for (const parent of parents) { if (parent.typ == EnumToken.CommentTokenType || parent.typ == EnumToken.CDOCOMMTokenType) { continue; } replacement = parent; if (postprocess) { for (const feature of options.features) { if ((feature.processMode & FeatureWalkMode.Post) === 0 || (feature.accept != null && !feature.accept.has(parent.typ))) { continue; } const result = feature.run(replacement, options, parent[PARENT] ?? ast, context, FeatureWalkMode.Post); if (result != null) { replacement = result; } } } if (replacement != null && replacement != parent && parent[PARENT] != null) { // @ts-ignore replaceNodeOrValue(parent[PARENT], parent, replacement); } if ("chi" in replacement) { for (const node of replacement.chi) { node[PARENT] = replacement; parents.add(node); } } } if (postprocess) { for (const feature of options.features) { if (feature.processMode & FeatureWalkMode.Post && "cleanup" in feature) { // @ts-ignore feature.cleanup(ast, options, context, FeatureWalkMode.Post); } } } return ast; } function transformAtRuleMediaPrelude(values) { let hasUpdates = false; for (let { value, parent, parents } of walkValues(values)) { if (value.typ === EnumToken.MediaQueryConditionTokenType) { if (value.op.typ == EnumToken.AndTokenType && // @ts-ignore value.l.typ === EnumToken.IdenTokenType && // @ts-ignore value.l.val.toLowerCase() === "all") { if (parent === null) { // @ts-ignore values[values.indexOf(value)] = value.l; } else { replaceNodeOrValue(parent, value, value.l); // @ts-ignore value = value.l; } hasUpdates = true; } } // range operator if (parent != null && parent.typ === EnumToken.MediaQueryConditionTokenType && parent.op.typ == EnumToken.AndTokenType && // @ts-ignore parent.l.typ == EnumToken.ParensTokenType) { let token = parent.r.find((t) => t.typ !== EnumToken.WhitespaceTokenType && t.typ !== EnumToken.CommentTokenType); if (token?.typ === EnumToken.ParensTokenType) { // @ts-ignore const node1 = parent.l.chi.find((t) => t.typ !== EnumToken.WhitespaceTokenType && t.typ !== EnumToken.CommentTokenType); const node2 = token.chi.find((t) => t.typ !== EnumToken.WhitespaceTokenType && t.typ !== EnumToken.CommentTokenType); if (node1?.typ === EnumToken.MediaQueryConditionTokenType && node2?.typ === EnumToken.MediaQueryConditionTokenType && node1.op.typ == EnumToken.ColonTokenType && node2.op.typ == EnumToken.ColonTokenType && // @ts-ignore node1.l.typ == EnumToken.IdenTokenType && // @ts-ignore node2.l.typ == EnumToken.IdenTokenType && // @ts-ignore node1.l.val.startsWith("min-") && // @ts-ignore node2.l.val.startsWith("max-") && // @ts-ignore node1.l.val.slice(4) == // @ts-ignore node2.l.val.slice(4)) { const val1 = node1.r.find((t) => t.typ !== EnumToken.WhitespaceTokenType && t.typ !== EnumToken.CommentTokenType); const val2 = node2.r.find((t) => t.typ !== EnumToken.WhitespaceTokenType && t.typ !== EnumToken.CommentTokenType); const replacement = { typ: EnumToken.ParensTokenType, chi: [ // @ts-ignore { typ: EnumToken.MediaRangeQueryTokenType, op: { typ: EnumToken.IdenTokenType, // @ts-ignore val: node1.l.val.slice(4), }, l: val1, r: val2, [LOC]: value[LOC], }, ], }; // @ts-expect-error const p = parents?.[parents?.indexOf?.(parent) + 1]; if (p != null) { replaceNodeOrValue(p, parent, replacement); } else { values.splice(values.indexOf(parent), 1, replacement); } hasUpdates = true; value = replacement; } } } } return { hasUpdates, values: trimArray(values) }; } /** * Minify at-rule media * - remove redundant tokens * - generate range queries * @param ast * * @private */ function minifyAtRuleMedia(tokens) { let hasUpdates = false; const sections = tokens .reduce((acc, t) => { if (t.typ === EnumToken.CommaTokenType) { acc.push([]); } else { acc[acc.length - 1].push(t); } return acc; }, [[]]) .reduce((acc, values) => { if (acc.has("all")) { return acc; } const result = transformAtRuleMediaPrelude(values); if (result.values.length === 0) { return acc; } if (result.hasUpdates) { hasUpdates = true; } acc.set(values.reduce((acc, t) => acc + renderValue(t), ""), result.values); return acc; }, new Map()); if (sections.has("all")) { tokens.length = 0; } else if (hasUpdates) { tokens.length = 0; tokens.push(...[...sections.values()].reduce((acc, t) => { if (acc.length > 0) { acc.push({ typ: EnumToken.CommaTokenType, }); } acc.push(...t); return acc; }, [])); } // return ast; return tokens; } /** * Reduce selectors * @param acc * @param curr * * @private */ function reduce(acc, curr) { // trim :is() if (curr[0] == "&") { if (curr[1] == " " && !isIdent(curr[2]) && !isFunction(curr[2])) { curr.splice(0, 2); } } acc.push(curr.join("")); return acc; } /** * Apply minification rules to the ast tree * @param ast * @param options * @param recursive * @param errors * @param nestingContent * @param context * * @private */ function doMinify(ast, options = {}, recursive = false, errors, nestingContent, context = {}) { if (!("nodes" in context)) { context.nodes = new Set(); } if (context.nodes.has(ast)) { return ast; } context.nodes.add(ast); // @ts-ignore if ("chi" in ast && ast.chi.length > 0) { const reducer = reduce.bind(ast); if (!nestingContent) { nestingContent = options.nestingRules && ast.typ == EnumToken.RuleNodeType; } let i = 0; let previous = null; let node = null; let nodeIndex = -1; for (; i < ast.chi.length; i++) { if (ast.chi[i].typ === EnumToken.CommentNodeType) { continue; } while (previous?.typ === EnumToken.CommentNodeType) { previous = ast.chi[--nodeIndex]; continue; } node = ast.chi[i]; if (node.typ === EnumToken.AtRuleNodeType && node.nam === "font-face") { continue; } if (node.typ === EnumToken.KeyframesAtRuleNodeType) { if (previous?.typ === EnumToken.KeyframesAtRuleNodeType && node.nam === previous.nam && node.val === previous.val) { ast.chi?.splice(nodeIndex--, 1); previous = ast?.chi?.[nodeIndex] ?? null; i = nodeIndex; continue; } } else if (node.typ === EnumToken.KeyFramesRuleNodeType) { if (previous?.typ === EnumToken.KeyFramesRuleNodeType && node.sel === previous.sel) { // do not merge keyframes // https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@keyframes#resolving_duplicates previous.chi.push(...node.chi); ast.chi.splice(i, 1); previous = ast?.chi?.[nodeIndex] ?? null; i = nodeIndex; continue; } let k; for (k = 0; k < node.chi.length; k++) { if (node.chi[k].typ == EnumToken.DeclarationNodeType) { let l = node.chi[k].val.length; while (l--) { if (node.chi[k].val[l].typ == EnumToken.ImportantTokenType) { node.chi.splice(k--, 1); break; } if ([EnumToken.WhitespaceTokenType, EnumToken.CommentTokenType].includes(node.chi[k].val[l].typ)) { continue; } break; } } } } else if (node.typ == EnumToken.AtRuleNodeType) { if (node.nam == "media") { if (Array.isArray(node[TOKENS])) { const slice = node[TOKENS].slice(); minifyAtRuleMedia(slice); if (slice.length !== node[TOKENS].length) { node[TOKENS].length = 0; node[TOKENS].push(...slice); node.val = slice.reduce((acc, curr, index, arr) => acc + (curr.typ === EnumToken.CommentTokenType || (curr.typ === EnumToken.WhitespaceTokenType && arr[index + 1]?.typ === EnumToken.CommentTokenType && (index + 3 < arr.length || arr[index + 2]?.typ === EnumToken.WhitespaceTokenType)) ? "" : renderValue(curr)), ""); } } if (["all", "", null].includes(node.val)) { ast.chi?.splice(i--, 1, ...node.chi); continue; } } else if (node.nam === "import" && Array.isArray(node[TOKENS])) { let l = 0; let token; for (; l < node[TOKENS].length; l++) { token = node[TOKENS][l]; if (token.typ === EnumToken.ParensTokenType || token.typ === EnumToken.MediaQueryConditionTokenType || (token.typ === EnumToken.IdenTokenType && "layer" !== token.val)) { break; } } if (l < node[TOKENS].length) { const slice = node[TOKENS]?.slice(l); node[TOKENS].splice(l, slice.length, ...minifyAtRuleMedia(slice)); node.val = trimArray(node[TOKENS]).reduce((acc, curr, index, arr) => acc + (curr.typ === EnumToken.CommentTokenType || (curr.typ === EnumToken.WhitespaceTokenType && arr[index + 1]?.typ === EnumToken.CommentTokenType && (index + 3 < arr.length || arr[index + 2].typ === EnumToken.WhitespaceTokenType)) ? "" : renderValue(curr)), ""); } } if (previous?.typ == EnumToken.AtRuleNodeType && node.nam != "font-face" && previous.nam === node.nam && previous.val === node.val) { if ("chi" in node) { // @ts-ignore previous.chi.push(...node.chi); if (!hasDeclaration(previous)) { context.nodes.delete(previous); doMinify(previous, options, recursive, errors, nestingContent, context); } } ast?.chi?.splice(i--, 1); continue; } if (!hasDeclaration(node)) { doMinify(node, options, recursive, errors, nestingContent, context); } previous = node; nodeIndex = i; continue; } // @ts-ignore else if (node.typ === EnumToken.RuleNodeType) { reduceRuleSelector(node); let wrapper = null; let match; if (options.nestingRules) { if (previous?.typ == EnumToken.RuleNodeType) { reduceRuleSelector(previous); // @ts-ignore match = matchSelectors(previous[RAW], node[RAW]); if (match != null) { wrapper = wrapNodes(previous, node, match, ast, reducer, i, nodeIndex); nodeIndex = i - 1; previous = ast.chi[nodeIndex]; } } if (wrapper != null) { while (i < ast.chi.length) { const nextNode = ast.chi[i]; if (nextNode.typ != EnumToken.RuleNodeType) { break; } reduceRuleSelector(nextNode); match = matchSelectors(wrapper[RAW], nextNode[RAW]); if (match == null) { break; } wrapper = wrapNodes(wrapper, nextNode, match, ast, reducer, i, nodeIndex); } nodeIndex = --i; previous = ast.chi[nodeIndex]; doMinify(wrapper, options, recursive, errors, nestingContent, context); continue; } // @ts-ignore else if (node[OPTIMIZED] != null && // @ts-ignore node[OPTIMIZED].match && // @ts-ignore node[OPTIMIZED].selector.length > 1) { // @ts-ignore wrapper = { ...node, chi: [], sel: node[OPTIMIZED].optimized[0], [RAW]: [[node[OPTIMIZED].optimized[0]]], }; // @ts-ignore node.sel = node[OPTIMIZED].selector.reduce(reducer, []).join(","); // @ts-ignore node[RAW] = node[OPTIMIZED].selector.slice(); // @ts-ignore wrapper.chi.push(node); // @ts-ignore ast.chi.splice(i, 1, wrapper); node = wrapper; } else if (node[OPTIMIZED]?.reducible) { if (node[OPTIMIZED].optimized.length === 1) { const sel1 = node[OPTIMIZED].optimized[0] + ":is(" + node[OPTIMIZED].selector.reduce(reducer, []).join(",") + ")"; const sel2 = node[OPTIMIZED].selector.reduce((acc, curr) => (acc.length > 0 ? acc + "," : "") + node[OPTIMIZED].optimized[0] + curr.join(""), ""); node.sel = sel1.length < sel2.length ? sel1 : sel2; } else if (node[OPTIMIZED].optimized.length === 0) { const testIdent = /^[a-zA-Z]/; node.sel = node[OPTIMIZED].selector.reduce((acc, curr) => (acc.length > 0 ? acc + "," : "") + (nestingContent && testIdent.test(curr[0]) ? "& " : "") + curr.join(""), ""); } } } // @ts-ignore else if (node[OPTIMIZED]?.match) { let wrap = true; // @ts-ignore const selector = node[OPTIMIZED].selector.reduce((acc, curr) => { if (curr[0] == "&" && curr.length > 1) { if (curr[1] == " ") { curr.splice(0, 2); } else { curr.splice(0, 1); } } else if (combinators.includes(curr[0])) { curr.unshift("&"); wrap = false; } acc.push(curr); return acc; }, []); if (!wrap) { wrap = selector.some((s) => s[0] != "&"); } let rule = null; const optimized = node[OPTIMIZED].optimized.slice(); if (optimized.length > 1) { const check = optimized.at(-2); if (!combinators.includes(check)) { let last = optimized.pop(); wrap = false; rule = optimized.join("") + `:is(${selector .map((s) => { if (s[0] == "&") { s.splice(0, 1, last); } else { s.unshift(last); } return s.join(""); }) .join(",")})`; } } if (rule == null) { rule = selector .map((s) => { if (s[0] == "&") { s.splice(0, 1, ...node[OPTIMIZED].optimized); } return s.join(""); }) .join(","); } let sel = wrap ? node[OPTIMIZED].optimized.join("") + `:is(${rule})` : rule; if (sel.length < node.sel.length) { node.sel = sel; } } else if (node[OPTIMIZED]?.reducible) { if (node[OPTIMIZED].optimized.length === 1) { const sel1 = node[OPTIMIZED].optimized[0] + ":is(" + node[OPTIMIZED].selector.reduce(reducer, []).join(",") + ")"; const sel2 = node[OPTIMIZED].selector.reduce((acc, curr) => (acc.length > 0 ? acc + "," : "") + node[OPTIMIZED].optimized[0] + curr.join(""), ""); node.sel = sel1.length < sel2.length ? sel1 : sel2; } else if (node[OPTIMIZED].optimized.length === 0) { const testIdent = /^[a-zA-Z]/; node.sel = node[OPTIMIZED].selector.reduce((acc, curr) => (acc.length > 0 ? acc + "," : "") + (nestingContent && testIdent.test(curr[0]) ? "& " : "") + curr.join(""), ""); } } else if (node[OPTIMIZED]?.optimized.length > 0) { const sel = node[OPTIMIZED].optimized.join(""); if (sel.length < node.sel.length) { node.sel = sel; node[RAW] = [node[OPTIMIZED].optimized.slice()]; } } doMinify(node, options, recursive, errors, nestingContent, context); } if (previous != null) { if ("chi" in previous && "chi" in node) { if (previous.typ === node.typ) { let shouldMerge = true; let k = previous.chi.length; while (k-- > 0) { if (previous.chi[k].typ === EnumToken.CommentNodeType || previous.chi[k].typ === EnumToken.InvalidRuleNodeType || previous.chi[k].typ === EnumToken.InvalidRuleNodeType) { continue; } shouldMerge = previous.chi[k].typ === EnumToken.DeclarationNodeType; break; } if (shouldMerge) { if (((node.typ === EnumToken.RuleNodeType || node.typ === EnumToken.KeyFramesRuleNodeType) && node.sel === previous.sel) || (node.typ == EnumToken.AtRuleNodeType && node.nam !== "font-face" && node.nam === previous.nam)) { node.chi.unshift(...previous.chi); doMinify(node, options, recursive, errors, nestingContent, context); ast.chi.splice(nodeIndex, 1); previous = ast.chi[--i]; nodeIndex = i; continue; } else if (node.typ == previous?.typ && [EnumToken.KeyFramesRuleNodeType, EnumToken.RuleNodeType].includes(node.typ)) { const intersect = diff(previous, node, options); if (intersect != null) { if (intersect.node1.chi.length == 0) { ast.chi.splice(i--, 1); } else { ast.chi.splice(i--, 1, intersect.node1); } if (intersect.node2.chi.length == 0) { if (intersect.result != null) { ast.chi.splice(nodeIndex, 1, intersect.result); } else { ast.chi.splice(nodeIndex, 1); } i--; if (nodeIndex == i) { nodeIndex = i; } } else { if (intersect.result != null) { ast.chi.splice(nodeIndex, 1, intersect.result, intersect.node2); } else { ast.chi.splice(nodeIndex, 1, intersect.node2); } i = (nodeIndex ?? 0) + 1; } if (node != ast.chi[i]) { node = ast.chi[i]; } previous = intersect.result; nodeIndex = i; } } } } if (recursive && previous != null && previous != node) { if (!hasDeclaration(previous)) { doMinify(previous, options, recursive, errors, nestingContent, context); } } } } if (!nestingContent && previous != null && previous.typ == EnumToken.RuleNodeType && previous.sel.includes("&")) { fixSelector(previous); } previous = node; nodeIndex = i; } if (recursive && node != null && "chi" in node) { if (node.typ == EnumToken.KeyframesAtRuleNodeType || !node.chi.some((n) => n.typ == EnumToken.DeclarationNodeType)) { if (!(node.typ == EnumToken.AtRuleNodeType && node.nam != "font-face")) { doMinify(node, options, recursive, errors, nestingContent, context); } } } if (!nestingContent && node != null && node.typ == EnumToken.RuleNodeType && node.sel.includes("&")) { fixSelector(node); } } return ast; } /** * Check if a rule has a declaration * @param node * * @private */ function hasDeclaration(node) { // @ts-ignore for (let i = 0; i < node.chi?.length; i++) { // @ts-ignore if (node.chi[i].typ == EnumToken.CommentNodeType) { continue; } // @ts-ignore return node.chi[i].typ == EnumToken.DeclarationNodeType; } return true; } /** * Optimize selector * @param selector * * @private */ function optimizeSelector(selector) { const map = new Set(); selector = selector .reduce((acc, curr) => { // @ts-ignore if (curr.length > 0 && curr.at(-1).startsWith(":is(")) { // @ts-ignore const rules = splitRule(curr.at(-1).slice(4, -1)).map((x) => { if (x[0] == "&" && x.length > 1) { return x.slice(x[1] == " " ? 2 : 1); } return x; }); const part = curr.slice(0, -1); for (const rule of rules) { acc.push(part.concat(rule)); } return acc; } acc.push(curr); return acc; }, []) .filter((x) => { const str = x.join(""); if (map.has(str)) { return false; } map.add(str); return true; }); const optimized = []; const k = selector.reduce((acc, curr) => acc == 0 ? curr.length : curr.length == 0 ? acc : Math.min(acc, curr.length), 0); let i = 0; let j; let match; for (; i < k; i++) { const item = selector[0][i]; match = true; for (j = 1; j < selector.length; j++) { if (item != selector[j][i]) { match = false; break; } } if (!match) { break; } optimized.push(item); } while (optimized.length > 0) { const last = optimized.at(-1); if (last == " " || combinators.includes(last)) { optimized.pop(); continue; } break; } selector.forEach((selector) => selector.splice(0, optimized.length)); let reducible = optimized.length == 1; if (optimized[0] == "&") { if (optimized[1] == " ") { optimized.splice(0, 2); } } if (optimized.length == 0 || optimized[0].charAt(0) == "&" || selector.length == 1) { return { match: false, optimized, selector: selector.map((selector) => selector[0] == "&" && selector[1] == " " ? selector.slice(2) : selector), reducible: selector.length > 1 && selector.every((selector) => !combinators.includes(selector[0])), }; } return { match: true, optimized, selector: selector.reduce((acc, curr) => { let hasCompound = true; if (hasCompound && curr.length > 0) { hasCompound = !["&"].concat(combinators).includes(curr[0].charAt(0)); } // @ts-ignore if (hasCompound && curr[0] == " ") { hasCompound = false; curr.unshift("&"); } if (curr.length == 0) { curr.push("&"); hasCompound = false; } if (reducible) { const chr = curr[0].charAt(0); // @ts-ignore reducible = chr == "." || chr == ":" || isIdentStart(chr.charCodeAt(0)); } acc.push(hasCompound ? ["&"].concat(curr) : curr); return acc; }, []), reducible: selector.every((selector) => ![">", "+", "~", "&"].includes(selector[0])), }; } /** * Split selector string * @param buffer * * @internal */ function splitRule(buffer) { const result = [[]]; let str = ""; for (let i = 0; i < buffer.length; i++) { let chr = buffer.charAt(i); if (isWhiteSpace(chr.charCodeAt(0))) { if (str !== "") { // @ts-ignore result.at(-1).push(str); str = ""; } // @ts-ignore if (result.at(-1).length > 0) { // @ts-ignore result.at(-1).push(" "); } // i = k; continue; } if (chr == ",") { if (str !== "") { result.at(-1).push(str); str = ""; } result.push([]); continue; } if (chr == ".") { if (str !== "") { result.at(-1).push(str); str = ""; } str += chr; continue; } if (combinators.includes(chr)) { if (str !== "") { result.at(-1).push(str); str = ""; } if (chr == "|" && buffer.charAt(i + 1) == "|") { chr += buffer.charAt(++i); } result.at(-1).push(chr); continue; } if (chr == ":") { if (str !== "") { result.at(-1).push(str); str = ""; } if (buffer.charAt(i + 1) == ":") { chr += buffer.charAt(++i); } str += chr; continue; } str += chr; if (chr == "\\") { str += buffer.charAt(++i); continue; } if (chr == "(" || chr == "[") { const open = chr; const close = chr == "(" ? ")" : "]"; let inParens = 1; let k = i; while (++k < buffer.length) { chr = buffer.charAt(k); if (chr == "\\") { str += buffer.slice(k, k + 2); k++; continue; } str += chr; if (chr == open) { inParens++; } else if (chr == close) { inParens--; } if (inParens == 0) { break; } } i = k; } } if (str !== "") { result.at(-1).push(str); } return result; } /** * Reduce selector * @param acc * @param curr * * @private */ function reduceSelector(acc, curr) { let hasCompoundSelector = true; // @ts-ignore curr = curr.slice(this.match[0].length); while (curr.length > 0) { if (curr[0] == " ") { hasCompoundSelector = false; curr.unshift("&"); continue; } break; } if (hasCompoundSelector && curr.length > 0) { hasCompoundSelector = !["&"].concat(combinators).includes(curr[0].charAt(0)); } if (curr[0] == ":is(") { let canReduce = true; const isCompound = curr.reduce((acc, token, index) => { if (index == 0) { canReduce = curr[1] == "&"; } else if (token == ")") ; else if (token == ",") { if (!canReduce) { canReduce = curr[index + 1] == "&"; } acc.push([]); } else acc.at(-1)?.push(token); return acc; }, [[]]); if (canReduce) { curr = isCompound.reduce((acc, curr) => { if (acc.length > 0) { acc.push(","); } acc.push(...curr); return acc; }, []); } } acc.push( // @ts-ignore this.match.length == 0 ? ["&"] : hasCompoundSelector && curr[0] != "&" && (curr.length == 0 || !combinators.includes(curr[0].charAt(0))) ? ["&"].concat(curr) : curr); return acc; } /** * Match selectors * @param selector1 * @param selector2 * * @private */ function matchSelectors(selector1, selector2) { let match = [[]]; const j = Math.min(selector1.reduce((acc, curr) => Math.min(acc, curr.length), selector1.length > 0 ? selector1[0].length : 0), selector2.reduce((acc, curr) => Math.min(acc, curr.length), selector2.length > 0 ? selector2[0].length : 0)); let i = 0; let k; let l; let token; let matching = true; let matchFunction = 0; let inAttr = 0; const regEx = /^:is\(([:.][^\s,]+)\)$/; for (const _1 of selector1) { if (_1[0] !== "&") { continue; } for (let i = 1; i < _1.length; i++) { const token = _1[i]; if (token.startsWith(":is(")) { const match = regEx.exec(token); if (match != null) { _1[i] = match[1]; } } } } for (const _1 of selector2) { if (_1[0] !== "&") { continue; } for (let i = 1; i < _1.length; i++) { const token = _1[i]; if (token.startsWith(":is(")) { const match = regEx.exec(token); if (match != null) { _1[i] = match[1]; } } } } for (; i < j; i++) { k = 0; token = selector1[0][i]; for (; k < selector1.length; k++) { if (selector1[k][i] != token) { matching = false; break; } } if (matching) { l = 0; for (; l < selector2.length; l++) { if (selector2[l][i] != token) { matching = false; break; } } } if (!matching) { break; } if (token.endsWith("(")) { matchFunction++; } match.at(-1).push(token); } // invalid function if (matchFunction != 0 || inAttr != 0) { return null; } for (const part of match) { while (part.length > 0) { const token = part.at(-1); if (token == " " || combinators.includes(token) || notEndingWith.includes(token.at(-1))) { part.pop(); continue; } break; } } if (match.every((t) => t.length == 0)) { return null; } if (eq([["&"]], match)) { return null; } const reducer = reduceSelector.bind({ match }); // @ts-ignore selector1 = selector1.reduce(reducer, []); // @ts-ignore selector2 = selector2.reduce(reducer, []); return selector1 == null || selector2 == null ? null : { eq: eq(selector1, selector2), match, selector1, selector2, }; } /** * Fix selector * @param node * * @private */ function fixSelector(node) { if (node.sel.includes("&")) { const attributes = [...tokenize(node.sel)].map((t) => t.token); // parseString(node.sel); for (const attr of walkValues(attributes)) { if (attr.value.typ == EnumToken.PseudoClassFuncTokenType && attr.value.val == ":is") { let i = attr.value.chi.length; while (i--) { if (attr.value.chi[i].typ == EnumToken.NestingSelectorTokenType) { attr.value.chi.splice(i, 1); } } } } node.sel = attributes.reduce((acc, curr) => acc + renderValue(curr), ""); } } /** * Wrap nodes * @param previous * @param node * @param match * @param ast * @param reducer * @param i * @param nodeIndex * * @private */ function wrapNodes(previous, node, match, ast, reducer, i, nodeIndex) { // @ts-ignore let pSel = match.selector1.reduce(reducer, []).join(","); // @ts-ignore let nSel = match.selector2.reduce(reducer, []).join(","); const wrapper = { ...previous, chi: [], // @ts-ignore sel: match.match.reduce(reducer, []).join(","), [RAW]: match.match.map((t) => t.slice()), }; if (pSel == "&" || pSel === "") { wrapper.chi.push(...previous.chi); if (nSel == "&" || nSel === "") { wrapper.chi.push(...node.chi); } else { wrapper.chi.push(node); } } else { wrapper.chi.push(previous, node); } ast.chi.splice(i, 1, wrapper); ast.chi.splice(nodeIndex, 1); previous.sel = pSel; previous[RAW] = match.selector1; node.sel = nSel; node[RAW] = match.selector2; reduceRuleSelector(wrapper); return wrapper; } /** * Diff nodes * @param n1 * @param n2 * @param reducer * @param options * * @private */ function diff(n1, n2, options = {}) { if (!("cache" in options)) { options.cache = new WeakMap(); } let node1 = n1; let node2 = n2; let exchanged = false; if (node1.chi.length > node2.chi.length) { const t = node1; node1 = node2; node2 = t; exchanged = true; } let i = node1.chi.length; let j = node2.chi.length; const raw1 = node1[RAW]; const raw2 = node2[RAW]; if (raw1 != null && raw2 != null) { const prefixes1 = new Set(); const prefixes2 = new Set(); for (const token1 of raw1) { for (const t of token1) { if (t.includes(":")) { const matches = t.match(/::?-([a-z]+)-/); if (matches == null) { continue; } prefixes1.add(matches[1]); if (prefixes1.size > 1) { break; } } } if (prefixes1.size > 1) { break; } } for (const token2 of raw2) { for (const t of token2) { if (t.includes(":")) { const matches = t.match(/::?-([a-z]+)-/); if (matches == null) { continue; } prefixes2.add(matches[1]); if (prefixes2.size > 1) { break; } } } if (prefixes2.size > 1) { break; } } if (prefixes1.size != prefixes2.size) { return null; } for (const prefix of prefixes1) { if (!prefixes2.has(prefix)) { return null; } } } const css1 = options.cache.get(node1); const css2 = options.cache.get(node2); node1 = { ...node1, chi: node1.chi.slice() }; node2 = { ...node2, chi: node2.chi.slice() }; if (css1 != null) { options.cache.set(node1, css1); } if (css2 != null) { options.cache.set(node2, css2); } if (raw1 != null) { node1[RAW] = raw1; } if (raw2 != null) { node2[RAW] = raw2; } const intersect = []; while (i--) { if (node1.chi[i].typ == EnumToken.CommentNodeType) { continue; } j = node2.chi.length; while (j--) { if (node2.chi[j].typ == EnumToken.CommentNodeType) { continue; } if (node1.chi[i].nam == node2.chi[j].nam) { if (node1.chi[i].typ == node2.chi[j].typ && eq(node1.chi[i], node2.chi[j])) { intersect.push(node1.chi[i]); node1.chi.splice(i, 1); node2.chi.splice(j, 1); options.cache.delete(node1); options.cache.delete(node2); break; } } } } const result = intersect.length === 0 && (node1.chi.length > 0 || node2.chi.length > 0) ? null : { ...node1, // @ts-ignore sel: [ ...new Set(splitRule(node1.sel) .concat(splitRule(node2.sel)) .map((s) => s.join(""))), ].join(","), // @ts-ignore chi: intersect.reverse(), }; if (result == null || [n1, n2].reduce((acc, curr) => { let css = options.cache.get(curr); if (css == null) { css = doRender(curr, options).code; options.cache.set(curr, css); } return curr.chi.length == 0 ? acc : acc + css.length; }, 0) <= [node1, node2, result].reduce((acc, curr) => { const css = doRender(curr, options).code; return curr.chi.length == 0 ? acc : acc + css.length; }, 0)) { if (node1.chi.length != 0 && node2.chi.length != 0) { return null; } } if (result != null) { result[TOKENS] = null; result[RAW] = null; const optimized = optimizeSelector(splitRule(result.sel)); if (optimized?.match) { const rule = optimized.selector.reduce((acc, curr) => { if (acc.length > 0) { acc += ","; } if (curr.length > 2 && curr[0] === "&" && curr[1] === " ") { return acc + curr.slice(2).join(""); } else if (curr.length > 1 && curr[0] === "&") { return acc + curr.slice(1).join(""); } return acc + curr.join(""); }, ""); const match = optimized.optimized.join(""); const sel = match + ":is(" + replaceCompound(rule, match) + ")"; if (sel.length < result.sel.length) { result.sel = sel; } } } return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node1 : node2 }; } /** * Reduce rule selector * @param node * * @private */ function reduceRuleSelector(node) { if (node[RAW] == null) { node[RAW] = splitRule(node.sel); } let optimized = optimizeSelector(node[RAW].reduce((acc, curr) => { acc.push(curr.slice()); return acc; }, [])); if (optimized != null) { node[OPTIMIZED] = optimized; } if (optimized != null && optimized.match && optimized.reducible && optimized.selector.length > 1) { for (const selector of optimized.selector) { if (selector.length > 1 && selector[0] == "&" && (combinators.includes(selector[1]) || !/^[a-zA-Z:]/.test(selector[1]))) { selector.shift(); } } const unique = new Set(); const reduced = optimized.selector.reduce((acc, curr) => { const sig = curr.join(""); if (!unique.has(sig)) { if (acc.length > 0) { acc.push(","); } unique.add(sig);