UNPKG

@tbela99/css-parser

Version:

CSS parser for node and the browser

277 lines (274 loc) 12.7 kB
import { splitRule, combinators } from './minify.js'; import { parseString } from '../parser/parse.js'; import { EnumToken } from './types.js'; import { walkValues } from './walk.js'; import { renderToken } from '../renderer/render.js'; import '../renderer/color/utils/constants.js'; import '../parser/utils/config.js'; /** * expand nested css ast * @param ast */ function expand(ast) { // if (![EnumToken.RuleNodeType, EnumToken.StyleSheetNodeType, EnumToken.AtRuleNodeType].includes(ast.typ)) { return ast; } if (EnumToken.RuleNodeType == ast.typ) { return { typ: EnumToken.StyleSheetNodeType, chi: expandRule(ast) }; } if (!('chi' in ast)) { return ast; } const result = { ...ast, chi: [] }; // @ts-ignore for (let i = 0; i < ast.chi.length; i++) { // @ts-ignore const node = ast.chi[i]; if (node.typ == EnumToken.RuleNodeType) { // @ts-ignore result.chi.push(...expandRule(node)); // i += expanded.length - 1; } else if (node.typ == EnumToken.AtRuleNodeType && 'chi' in node) { let hasRule = false; let j = node.chi.length; while (j--) { // @ts-ignore if (node.chi[j].typ == EnumToken.RuleNodeType || node.chi[j].typ == EnumToken.AtRuleNodeType) { hasRule = true; break; } } // @ts-ignore result.chi.push({ ...(hasRule ? expand(node) : node) }); } else { // @ts-ignore result.chi.push(node); } } return result; } function expandRule(node) { const ast = { ...node, chi: node.chi.slice() }; const result = []; if (ast.typ == EnumToken.RuleNodeType) { let i = 0; for (; i < ast.chi.length; i++) { if (ast.chi[i].typ == EnumToken.RuleNodeType) { const rule = ast.chi[i]; if (!rule.sel.includes('&')) { const selRule = splitRule(rule.sel); if (selRule.length > 1) { const r = ':is(' + selRule.map(a => a.join('')).join(',') + ')'; rule.sel = splitRule(ast.sel).reduce((a, b) => a.concat([b.join('') + r]), []).join(','); } else { // selRule = splitRule(selRule.reduce((acc, curr) => acc + (acc.length > 0 ? ',' : '') + curr.join(''), '')); const arSelf = splitRule(ast.sel).filter((r) => r.every((t) => t != ':before' && t != ':after' && !t.startsWith('::'))).reduce((acc, curr) => acc.concat([curr.join('')]), []).join(','); if (arSelf.length == 0) { ast.chi.splice(i--, 1); continue; } // selRule.forEach(arr => combinators.includes(arr[0].charAt(0)) ? arr.unshift(arSelf) : arr.unshift(arSelf, ' ')); rule.sel = selRule.reduce((acc, curr) => { acc.push(curr.join('')); return acc; }, []).join(','); } } else { let childSelectorCompound = []; let withCompound = []; let withoutCompound = []; // pseudo elements cannot be used with '&' // https://www.w3.org/TR/css-nesting-1/#example-7145ff1e const rules = splitRule(ast.sel).filter((r) => r.every((t) => t != ':before' && t != ':after' && !t.startsWith('::'))); const parentSelector = !node.sel.includes('&'); if (rules.length == 0) { ast.chi.splice(i--, 1); continue; } for (const sel of (rule.raw ?? splitRule(rule.sel))) { const s = sel.join(''); if (s.includes('&') || parentSelector) { if (s.indexOf('&', 1) == -1) { if (s.at(0) == '&') { if (s.at(1) == ' ') { childSelectorCompound.push(s.slice(2)); } else { if (s == '&' || parentSelector) { withCompound.push(s); } else { withoutCompound.push(s.slice(1)); } } } else { withoutCompound.push(s); } } else { withCompound.push(s); } } else { withoutCompound.push(s); } } const selectors = []; const selector = rules.length > 1 ? ':is(' + rules.map(a => a.join('')).join(',') + ')' : rules[0].join(''); if (childSelectorCompound.length > 0) { if (childSelectorCompound.length == 1) { selectors.push(replaceCompound('& ' + childSelectorCompound[0].trim(), selector)); } else { selectors.push(replaceCompound('& :is(' + childSelectorCompound.reduce((acc, curr) => acc + (acc.length > 0 ? ',' : '') + curr.trim(), '') + ')', selector)); } } if (withCompound.length > 0) { if (withCompound.every((t) => t[0] == '&' && t.indexOf('&', 1) == -1)) { withoutCompound.push(...withCompound.map(t => t.slice(1))); withCompound.length = 0; } } if (withoutCompound.length > 0) { if (withoutCompound.length == 1) { const useIs = rules.length == 1 && selector.match(/^[a-zA-Z.:]/) != null && selector.includes(' ') && withoutCompound.length == 1 && withoutCompound[0].match(/^[a-zA-Z]+$/) != null; const compound = useIs ? ':is(&)' : '&'; selectors.push(replaceCompound(rules.length == 1 ? (useIs ? withoutCompound[0] + ':is(&)' : (selector.match(/^[.:]/) && withoutCompound[0].match(/^[a-zA-Z]+$/) ? withoutCompound[0] + compound : compound + withoutCompound[0])) : (withoutCompound[0].match(/^[a-zA-Z:]+$/) ? withoutCompound[0].trim() + compound : '&' + (withoutCompound[0].match(/^\S+$/) ? withoutCompound[0].trim() : ':is(' + withoutCompound[0].trim() + ')')), selector)); } else { selectors.push(replaceCompound('&:is(' + withoutCompound.reduce((acc, curr) => acc + (acc.length > 0 ? ',' : '') + curr.trim(), '') + ')', selector)); } } if (withCompound.length > 0) { if (withCompound.length == 1) { selectors.push(replaceCompound(withCompound[0], selector)); } else { for (const w of withCompound) { selectors.push(replaceCompound(w, selector)); } } } rule.sel = selectors.reduce((acc, curr) => curr.length == 0 ? acc : acc + (acc.length > 0 ? ',' : '') + curr, ''); } ast.chi.splice(i--, 1); result.push(...expandRule(rule)); } else if (ast.chi[i].typ == EnumToken.AtRuleNodeType) { let astAtRule = ast.chi[i]; const values = []; if (astAtRule.nam == 'scope') { if (astAtRule.val.includes('&')) { astAtRule.val = replaceCompound(astAtRule.val, ast.sel); } const slice = astAtRule.chi.slice().filter(t => t.typ == EnumToken.RuleNodeType && t.sel.includes('&')); if (slice.length > 0) { expandRule({ ...node, chi: astAtRule.chi.slice() }); } } else { // @ts-ignore const clone = { ...ast, chi: astAtRule.chi.slice() }; // @ts-ignore astAtRule.chi.length = 0; for (const r of expandRule(clone)) { if (r.typ == EnumToken.AtRuleNodeType && 'chi' in r) { if (astAtRule.val !== '' && r.val !== '') { if (astAtRule.nam == 'media' && r.nam == 'media') { r.val = astAtRule.val + ' and ' + r.val; } else if (astAtRule.nam == 'layer' && r.nam == 'layer') { r.val = astAtRule.val + '.' + r.val; } } // @ts-ignore values.push(r); } else if (r.typ == EnumToken.RuleNodeType) { // @ts-ignore astAtRule.chi.push(...expandRule(r)); } else { // @ts-ignore astAtRule.chi.push(r); } } } // @ts-ignore result.push(...(astAtRule.chi.length > 0 ? [astAtRule].concat(values) : values)); ast.chi.splice(i--, 1); } } } // @ts-ignore return ast.chi.length > 0 ? [ast].concat(result) : result; } /** * replace compound selector * @param input * @param replace */ function replaceCompound(input, replace) { const tokens = parseString(input); let replacement = null; for (const t of walkValues(tokens)) { if (t.value.typ == EnumToken.LiteralTokenType) { if (t.value.val == '&') { if (tokens.length == 2) { if (replacement == null) { replacement = parseString(replace); } if (tokens[1].typ == EnumToken.IdenTokenType) { t.value.val = replacement.length == 1 || (!replace.includes(' ') && replace.charAt(0).match(/[:.]/)) ? tokens[1].val + replace : replaceCompoundLiteral(tokens[1].val + '&', replace); tokens.splice(1, 1); } else { t.value.val = replaceCompoundLiteral(t.value.val, replace); } continue; } const rule = splitRule(replace); t.value.val = rule.length > 1 ? ':is(' + replace + ')' : replace; } else if (t.value.val.length > 1 && t.value.val.charAt(0) == '&') { t.value.val = replaceCompoundLiteral(t.value.val, replace); } } } return tokens.reduce((acc, curr) => acc + renderToken(curr), ''); } function replaceCompoundLiteral(selector, replace) { const tokens = ['']; let i = 0; for (; i < selector.length; i++) { if (selector.charAt(i) == '&') { tokens.push('&'); tokens.push(''); } else { tokens[tokens.length - 1] += selector.charAt(i); } } return tokens.sort((a, b) => { if (a == '&') { return 1; } return b == '&' ? -1 : 0; }).reduce((acc, curr) => { if (acc.length > 0 && curr == '&' && (replace.charAt(0) != '.' || replace.includes(' '))) { return acc + ':is(' + replace + ')'; } return acc + (curr == '&' ? replace : curr); }, ''); } export { expand, replaceCompound };