UNPKG

@tbela99/css-parser

Version:

CSS parser for node and the browser

1,013 lines (1,010 loc) 36.6 kB
import { parseString } from '../parser/parse.js'; import { EnumToken } from './types.js'; import { walkValues } from './walk.js'; import { replaceCompound } from './expand.js'; import { isIdent, isFunction, isWhiteSpace, isIdentStart } from '../syntax/syntax.js'; import '../parser/utils/config.js'; import { eq } from '../parser/utils/eq.js'; import { doRender, renderToken } from '../renderer/render.js'; import * as index from './features/index.js'; const combinators = ['+', '>', '~', '||', '|']; const definedPropertySettings = { configurable: true, enumerable: false, writable: true }; const notEndingWith = ['(', '['].concat(combinators); // @ts-ignore const features = Object.values(index).sort((a, b) => a.ordering - b.ordering); /** * minify ast * @param ast * @param options * @param recursive * @param errors * @param nestingContent * @param context */ function minify(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); if (!('features' in options)) { // @ts-ignore options = { removeDuplicateDeclarations: true, computeShorthand: true, computeCalcExpression: true, removePrefix: false, features: [], ...options }; // @ts-ignore for (const feature of features) { feature.register(options); } } function reducer(acc, curr, index, array) { // trim :is() if (array.length == 1 && array[0][0] == ':is(' && array[0].at(-1) == ')') { curr = curr.slice(1, -1); } if (curr[0] == '&') { if (curr[1] == ' ' && !isIdent(curr[2]) && !isFunction(curr[2])) { curr.splice(0, 2); } else if (combinators.includes(curr[1])) { curr.shift(); } } else if (ast.typ == EnumToken.RuleNodeType && (isIdent(curr[0]) || isFunction(curr[0]))) { curr.unshift('&', ' '); } acc.push(curr.join('')); return acc; } // @ts-ignore if ('chi' in ast && ast.chi.length > 0) { if (!nestingContent) { nestingContent = options.nestingRules && ast.typ == EnumToken.RuleNodeType; } let i = 0; let previous; let node; let nodeIndex; // @ts-ignore for (; i < ast.chi.length; i++) { // @ts-ignore if (ast.chi[i].typ == EnumToken.CommentNodeType) { continue; } // @ts-ignore node = ast.chi[i]; // @ts-ignore if (previous == node) { // @ts-ignore ast.chi.splice(i, 1); i--; continue; } if (node.typ == EnumToken.AtRuleNodeType && node.nam == 'font-face') { continue; } if (node.typ == EnumToken.AtRuleNodeType) { // @ts-ignore if (node.nam == 'media' && ['all', '', null].includes(node.val)) { // @ts-ignore ast.chi?.splice(i, 1, ...node.chi); i--; continue; } // @ts-ignore if (previous?.typ == EnumToken.AtRuleNodeType && previous.nam == node.nam && previous.val == node.val) { if ('chi' in node) { // @ts-ignore previous.chi.push(...node.chi); } ast?.chi?.splice(i--, 1); continue; } // @ts-ignore if (!hasDeclaration(node)) { minify(node, options, recursive, errors, nestingContent, context); } previous = node; nodeIndex = i; continue; } // @ts-ignore if (node.typ == EnumToken.RuleNodeType) { reduceRuleSelector(node); let wrapper; let match; // @ts-ignore if (options.nestingRules) { // @ts-ignore if (previous?.typ == EnumToken.RuleNodeType) { // @ts-ignore reduceRuleSelector(previous); // @ts-ignore match = matchSelectors(previous.raw, node.raw, ast.typ, errors); // @ts-ignore if (match != null) { // @ts-ignore wrapper = wrapNodes(previous, node, match, ast, reducer, i, nodeIndex); nodeIndex = i - 1; // @ts-ignore previous = ast.chi[nodeIndex]; } } // @ts-ignore if (wrapper != null) { // @ts-ignore while (i < ast.chi.length) { // @ts-ignore const nextNode = ast.chi[i]; // @ts-ignore if (nextNode.typ != EnumToken.RuleNodeType) { break; } reduceRuleSelector(nextNode); // @ts-ignore match = matchSelectors(wrapper.raw, nextNode.raw, ast.typ, errors); // @ts-ignore if (match == null) { break; } // @ts-ignore wrapper = wrapNodes(wrapper, nextNode, match, ast, reducer, i, nodeIndex); } nodeIndex = --i; // @ts-ignore previous = ast.chi[nodeIndex]; minify(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] }; // @ts-ignore Object.defineProperty(wrapper, 'raw', { ...definedPropertySettings, // @ts-ignore value: [[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; } } // @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 { if (ast.typ != EnumToken.RuleNodeType && combinators.includes(curr[1])) { wrap = false; } else { curr.splice(0, 1); } } } else if (combinators.includes(curr[0])) { curr.unshift('&'); wrap = false; } // @ts-ignore acc.push(curr); return acc; }, []); if (!wrap) { wrap = selector.some((s) => s[0] != '&'); } let rule = selector.map(s => { if (s[0] == '&') { // @ts-ignore s[0] = node.optimized.optimized[0]; } return s.join(''); }).join(','); // @ts-ignore let sel = wrap ? node.optimized.optimized.join('') + `:is(${rule})` : rule; if (rule.includes('&')) { // @ts-ignore rule = replaceCompound(rule, node.optimized.optimized[0]); } if (sel.length < node.sel.length) { node.sel = sel; } } } // @ts-ignore if (previous != null) { // @ts-ignore if ('chi' in previous && ('chi' in node)) { // @ts-ignore if (previous.typ == node.typ) { let shouldMerge = true; // @ts-ignore let k = previous.chi.length; while (k-- > 0) { // @ts-ignore if (previous.chi[k].typ == EnumToken.CommentNodeType) { continue; } // @ts-ignore shouldMerge = previous.chi[k].typ == EnumToken.DeclarationNodeType; break; } if (shouldMerge) { // @ts-ignore if (((node.typ == EnumToken.RuleNodeType || node.typ == EnumToken.KeyFrameRuleNodeType) && node.sel == previous.sel) || // @ts-ignore (node.typ == EnumToken.AtRuleNodeType) && node.val != 'font-face' && node.val == previous.val) { // @ts-ignore node.chi.unshift(...previous.chi); // @ts-ignore ast.chi.splice(nodeIndex, 1); // @ts-ignore if (!hasDeclaration(node)) { // @ts-ignore // minifyRule(node, <MinifyOptions>options, ast, context); // } else { minify(node, options, recursive, errors, nestingContent, context); } i--; previous = node; nodeIndex = i; continue; } else if (node.typ == previous?.typ && [EnumToken.KeyFrameRuleNodeType, EnumToken.RuleNodeType].includes(node.typ)) { const intersect = diff(previous, node, reducer, options); if (intersect != null) { if (intersect.node1.chi.length == 0) { // @ts-ignore ast.chi.splice(i--, 1); // @ts-ignore // node = ast.chi[i]; } else { // @ts-ignore ast.chi.splice(i--, 1, intersect.node1); // node = ast.chi intersect.node1; } if (intersect.node2.chi.length == 0) { // @ts-ignore ast.chi.splice(nodeIndex, 1, intersect.result); i--; // @ts-ignore if (nodeIndex == i) { nodeIndex = i; } } else { // @ts-ignore ast.chi.splice(nodeIndex, 1, intersect.result, intersect.node2); // @ts-ignore i = (nodeIndex ?? 0) + 1; } reduceRuleSelector(intersect.result); // @ts-ignore if (node != ast.chi[i]) { // @ts-ignore node = ast.chi[i]; } previous = intersect.result; nodeIndex = i; } } } } // @ts-ignore if (recursive && previous != node) { // @ts-ignore if (!hasDeclaration(previous)) { // @ts-ignore // minifyRule(previous, <MinifyOptions>options, ast, context); // } else { minify(previous, options, recursive, errors, nestingContent, context); } } } else { if ('chi' in previous) { // @ts-ignore if (!hasDeclaration(previous)) { // @ts-ignore // minifyRule(previous, <MinifyOptions>options, ast, context); // } else { minify(previous, options, recursive, errors, nestingContent, context); } } } } if (!nestingContent && // @ts-ignore previous != null && // previous.optimized != null && previous.typ == EnumToken.RuleNodeType && previous.sel.includes('&')) { fixSelector(previous); } previous = node; nodeIndex = i; } // @ts-ignore if (recursive && node != null && ('chi' in node)) { // @ts-ignore if (!node.chi.some(n => n.typ == EnumToken.DeclarationNodeType)) { // @ts-ignore if (!(node.typ == EnumToken.AtRuleNodeType && node.nam != 'font-face')) { minify(node, options, recursive, errors, nestingContent, context); } } } if (!nestingContent && // @ts-ignore node != null && // previous.optimized != null && node.typ == EnumToken.RuleNodeType && node.sel.includes('&')) { fixSelector(node); } } if (ast.typ == EnumToken.StyleSheetNodeType) { let parent; let parents = [ast]; while (parents.length > 0) { parent = parents.shift(); // @ts-ignore for (let k = 0; k < parent.chi.length; k++) { // @ts-ignore const node = parent.chi[k]; if (!('chi' in node) || node.typ == EnumToken.StyleSheetNodeType || (node.typ == EnumToken.AtRuleNodeType && node.nam == 'font-face')) { continue; } // @ts-ignore if (node.chi.length > 0) { parents.push(node); Object.defineProperty(node, 'parent', { ...definedPropertySettings, value: parent }); for (const feature of options.features) { feature.run(node, options, parent, context); } } // @ts-ignore if (options.removeEmpty && node.chi.length == 0) { // @ts-ignore parent.chi.splice(k, 1); k--; } } } for (const feature of options.features) { if ('cleanup' in feature) { // @ts-ignore feature.cleanup(ast, options, context); } } } return ast; } 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; } function reduceSelector(selector) { if (selector.length == 0) { return null; } 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; }, []); 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)); // combinator if (combinators.includes(optimized.at(-1))) { const combinator = optimized.pop(); selector.forEach((selector) => selector.unshift(combinator)); } 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.codePointAt(0)); } acc.push(hasCompound ? ['&'].concat(curr) : curr); return acc; }, []), reducible: selector.every((selector) => !['>', '+', '~', '&'].includes(selector[0])) }; } /** * split selector string * @param buffer */ 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))) { let k = i; while (k + 1 < buffer.length) { if (isWhiteSpace(buffer[k + 1].charCodeAt(0))) { k++; continue; } break; } 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 !== '') { // @ts-ignore result.at(-1).push(str); str = ''; } result.push([]); continue; } if (chr == ':') { if (str !== '') { // @ts-ignore 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 == "'") { let k = i; while (++k < buffer.length) { chr = buffer.charAt(k); str += chr; if (chr == '//') { str += buffer.charAt(++k); continue; } if (chr == buffer.charAt(i)) { break; } } 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 !== '') { // @ts-ignore result.at(-1).push(str); } return result; } function matchSelectors(selector1, selector2, parentType, errors) { 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; 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 == ',') { match.push([]); } else { if (token.endsWith('(')) { matchFunction++; } if (token.endsWith('[')) { inAttr++; } else if (token == ')') { matchFunction--; } else if (token == ']') { inAttr--; } match.at(-1).push(token); } } // invalid function if (matchFunction != 0 || inAttr != 0) { return null; } if (parentType != EnumToken.RuleNodeType) { for (const part of match) { if (part.length > 0 && combinators.includes(part[0].charAt(0))) { return null; } } } if (match.length > 1) { errors?.push({ action: 'ignore', message: `minify: unsupported multilevel matching\n${JSON.stringify({ match, selector1, selector2 }, null, 1)}` }); 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; } function reduce(acc, curr) { if (acc === null) { return null; } let hasCompoundSelector = true; curr = curr.slice(match[0].length); while (curr.length > 0) { if (curr[0] == ' ') { hasCompoundSelector = false; curr.unshift('&'); continue; } break; } // invalid function match if (curr.length > 0 && curr[0].endsWith('(') && curr.at(-1) != ')') { return null; } if (curr.length == 1 && combinators.includes(curr[0].charAt(0))) { return null; } if (hasCompoundSelector && curr.length > 0) { hasCompoundSelector = !['&'].concat(combinators).includes(curr[0].charAt(0)); } if (curr[0] == ':is(') { let inFunction = 0; let canReduce = true; const isCompound = curr.reduce((acc, token, index) => { if (index == 0) { inFunction++; canReduce = curr[1] == '&'; } else if (token.endsWith('(')) { if (inFunction == 0) { canReduce = false; } inFunction++; } else if (token == ')') { inFunction--; } else if (token == ',') { if (!canReduce) { canReduce = curr[index + 1] == '&'; } acc.push([]); } else acc.at(-1)?.push(token); return acc; }, [[]]); if (inFunction > 0) { canReduce = false; } if (canReduce) { curr = isCompound.reduce((acc, curr) => { if (acc.length > 0) { acc.push(','); } acc.push(...curr); return acc; }, []); } } // @todo: check hasCompoundSelector && curr[0] == '&' && curr[1] == ' ' acc.push(match.length == 0 ? ['&'] : (hasCompoundSelector && curr[0] != '&' && (curr.length == 0 || !combinators.includes(curr[0].charAt(0))) ? ['&'].concat(curr) : curr)); return acc; } // @ts-ignore selector1 = selector1.reduce(reduce, []); // @ts-ignore selector2 = selector2.reduce(reduce, []); return selector1 == null || selector2 == null ? null : { eq: eq(selector1, selector2), match, selector1, selector2 }; } function fixSelector(node) { // @ts-ignore if (node.sel.includes('&')) { const attributes = 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.LiteralTokenType && attr.value.chi[i].val == '&') { attr.value.chi.splice(i, 1); } } } } node.sel = attributes.reduce((acc, curr) => acc + renderToken(curr), ''); } } 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(','); // @ts-ignore const wrapper = { ...previous, chi: [], sel: match.match.reduce(reducer, []).join(',') }; // @ts-ignore Object.defineProperty(wrapper, 'raw', { ...definedPropertySettings, // @ts-ignore value: match.match.map(t => t.slice()) }); if (pSel == '&' || pSel === '') { // @ts-ignore wrapper.chi.push(...previous.chi); // @ts-ignore if ((nSel == '&' || nSel === '')) { // @ts-ignore wrapper.chi.push(...node.chi); } else { // @ts-ignore wrapper.chi.push(node); } } else { // @ts-ignore wrapper.chi.push(previous, node); } // @ts-ignore ast.chi.splice(i, 1, wrapper); // @ts-ignore ast.chi.splice(nodeIndex, 1); // @ts-ignore previous.sel = pSel; // @ts-ignore previous.raw = match.selector1; // @ts-ignore node.sel = nSel; // @ts-ignore node.raw = match.selector2; reduceRuleSelector(wrapper); return wrapper; } function diff(n1, n2, reducer, options = {}) { 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; if (i == 0 || j == 0) { // @ts-ignore return null; } // @ts-ignore const raw1 = node1.raw; // @ts-ignore 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[0] == ':') { 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[0] == ':') { 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; } } } // @ts-ignore node1 = { ...node1, chi: node1.chi.slice() }; node2 = { ...node2, chi: node2.chi.slice() }; if (raw1 != null) { Object.defineProperty(node1, 'raw', { ...definedPropertySettings, value: raw1 }); } if (raw2 != null) { Object.defineProperty(node2, 'raw', { ...definedPropertySettings, value: raw2 }); } const intersect = []; while (i--) { if (node1.chi[i].typ == EnumToken.CommentNodeType) { continue; } j = node2.chi.length; if (j == 0) { break; } while (j--) { if (node2.chi[j].typ == EnumToken.CommentNodeType) { continue; } if (node1.chi[i].nam == node2.chi[j].nam) { if (eq(node1.chi[i], node2.chi[j])) { intersect.push(node1.chi[i]); node1.chi.splice(i, 1); node2.chi.splice(j, 1); break; } } } } // @ts-ignore const result = (intersect.length == 0 ? null : { ...node1, // @ts-ignore sel: [...new Set([...(n1?.raw?.reduce(reducer, []) ?? splitRule(n1.sel)).concat(n2?.raw?.reduce(reducer, []) ?? splitRule(n2.sel))])].join(','), chi: intersect.reverse() }); if (result == null || [n1, n2].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + doRender(curr, options).code.length, 0) <= [node1, node2, result].reduce((acc, curr) => curr.chi.length == 0 ? acc : acc + doRender(curr, options).code.length, 0)) { // @ts-ignore return null; } return { result, node1: exchanged ? node2 : node1, node2: exchanged ? node1 : node2 }; } function reduceRuleSelector(node) { if (node.raw == null) { Object.defineProperty(node, 'raw', { ...definedPropertySettings, value: splitRule(node.sel) }); } // @ts-ignore // if (node.raw != null) { // @ts-ignore let optimized = reduceSelector(node.raw.reduce((acc, curr) => { acc.push(curr.slice()); return acc; }, [])); if (optimized != null) { Object.defineProperty(node, 'optimized', { ...definedPropertySettings, value: 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 raw = [ [ optimized.optimized[0], ':is(' ].concat(optimized.selector.reduce((acc, curr) => { const sig = curr.join(''); if (!unique.has(sig)) { if (acc.length > 0) { acc.push(','); } unique.add(sig); acc.push(...curr); } return acc; }, [])).concat(')') ]; const sel = raw[0].join(''); if (sel.length < node.sel.length) { node.sel = sel; // node.raw = raw; Object.defineProperty(node, 'raw', { ...definedPropertySettings, value: raw }); } } } export { combinators, definedPropertySettings, minify, splitRule };