UNPKG

@tbela99/css-parser

Version:

CSS parser for node and the browser

657 lines (654 loc) 29.5 kB
import { getAngle, color2srgbvalues, clamp } from './color/color.js'; import { colorFuncColorSpace, COLORS_NAMES } from './color/utils/constants.js'; import { getComponents } from './color/utils/components.js'; import { reduceHexValue, srgb2hexvalues, rgb2hex, hsl2hex, hwb2hex, cmyk2hex, oklab2hex, oklch2hex, lab2hex, lch2hex } from './color/hex.js'; import { EnumToken } from '../ast/types.js'; import '../ast/minify.js'; import '../ast/walk.js'; import { expand } from '../ast/expand.js'; import { colorMix } from './color/colormix.js'; import { parseRelativeColor } from './color/relativecolor.js'; import { SourceMap } from './sourcemap/sourcemap.js'; import { isColor, pseudoElements, mathFuncs, isNewLine } from '../syntax/syntax.js'; const colorsFunc = ['rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'device-cmyk', 'color-mix', 'color', 'oklab', 'lab', 'oklch', 'lch', 'light-dark']; function reduceNumber(val) { val = String(+val); if (val === '0') { return '0'; } const chr = val.charAt(0); if (chr == '-') { const slice = val.slice(0, 2); if (slice == '-0') { return val.length == 2 ? '0' : '-' + val.slice(2); } } if (chr == '0') { return val.slice(1); } return val; } function update(position, str) { let i = 0; for (; i < str.length; i++) { if (isNewLine(str[i].charCodeAt(0))) { position.lin++; position.col = 0; } else { position.col++; } } } /** * render ast * @param data * @param options */ function doRender(data, options = {}) { const minify = options.minify ?? true; const beautify = options.beautify ?? !minify; options = { ...(beautify ? { indent: ' ', newLine: '\n', } : { indent: '', newLine: '', }), ...(minify ? { removeEmpty: true, removeComments: true } : { removeEmpty: false, removeComments: false, }), sourcemap: false, convertColor: true, expandNestingRules: false, preserveLicense: false, ...options }; if (options.withParents) { // @ts-ignore let parent = data.parent; // @ts-ignore while (data.parent != null) { // @ts-ignore parent = { ...data.parent, chi: [{ ...data }] }; // @ts-ignore parent.parent = data.parent.parent; // @ts-ignore data = parent; } } const startTime = performance.now(); const errors = []; const sourcemap = options.sourcemap ? new SourceMap : null; const cache = Object.create(null); const result = { code: renderAstNode(options.expandNestingRules ? expand(data) : data, options, sourcemap, { ind: 0, lin: 1, col: 1 }, errors, function reducer(acc, curr) { if (curr.typ == EnumToken.CommentTokenType && options.removeComments) { if (!options.preserveLicense || !curr.val.startsWith('/*!')) { return acc; } return acc + curr.val; } return acc + renderToken(curr, options, cache, reducer, errors); }, cache), errors, stats: { total: `${(performance.now() - startTime).toFixed(2)}ms` } }; if (options.output != null) { // @ts-ignore options.output = options.resolve(options.output, options.cwd).absolute; } if (sourcemap != null) { result.map = sourcemap; } return result; } function updateSourceMap(node, options, cache, sourcemap, position, str) { if ([EnumToken.RuleNodeType, EnumToken.AtRuleNodeType, EnumToken.KeyFrameRuleNodeType].includes(node.typ)) { let src = node.loc?.src ?? ''; let output = options.output ?? ''; if (!(src in cache)) { // @ts-ignore cache[src] = options.resolve(src, options.cwd ?? '').relative; } if (!(output in cache)) { // @ts-ignore cache[output] = options.resolve(output, options.cwd).relative; } // @ts-ignore sourcemap.add({ src: cache[output], sta: { ...position } }, { ...node.loc, // @ts-ignore src: options.resolve(cache[src], options.cwd).relative }); } update(position, str); } /** * render ast node * @param data * @param options * @param sourcemap * @param position * @param errors * @param reducer * @param cache * @param level * @param indents */ function renderAstNode(data, options, sourcemap, position, errors, reducer, cache, level = 0, indents = []) { if (indents.length < level + 1) { indents.push(options.indent.repeat(level)); } if (indents.length < level + 2) { indents.push(options.indent.repeat(level + 1)); } const indent = indents[level]; const indentSub = indents[level + 1]; switch (data.typ) { case EnumToken.DeclarationNodeType: return `${data.nam}:${options.indent}${data.val.reduce(reducer, '')}`; case EnumToken.CommentNodeType: case EnumToken.CDOCOMMNodeType: if (data.val.startsWith('/*# sourceMappingURL=')) { // ignore sourcemap return ''; } return !options.removeComments || (options.preserveLicense && data.val.startsWith('/*!')) ? data.val : ''; case EnumToken.StyleSheetNodeType: return data.chi.reduce((css, node) => { const str = renderAstNode(node, options, sourcemap, { ...position }, errors, reducer, cache, level, indents); if (str === '') { return css; } if (css === '') { if (sourcemap != null && node.loc != null) { updateSourceMap(node, options, cache, sourcemap, position, str); } return str; } if (sourcemap != null && node.loc != null) { update(position, options.newLine); updateSourceMap(node, options, cache, sourcemap, position, str); } return `${css}${options.newLine}${str}`; }, ''); case EnumToken.AtRuleNodeType: case EnumToken.RuleNodeType: case EnumToken.KeyFrameRuleNodeType: if (data.typ == EnumToken.AtRuleNodeType && !('chi' in data)) { return `${indent}@${data.nam}${data.val === '' ? '' : options.indent || ' '}${data.val};`; } // @ts-ignore let children = data.chi.reduce((css, node) => { let str; if (node.typ == EnumToken.CommentNodeType) { str = options.removeComments && (!options.preserveLicense || !node.val.startsWith('/*!')) ? '' : node.val; } else if (node.typ == EnumToken.DeclarationNodeType) { if (node.val.length == 0) { // @ts-ignore errors.push({ action: 'ignore', message: `render: invalid declaration ${JSON.stringify(node)}`, location: node.loc }); return ''; } str = `${node.nam}:${options.indent}${node.val.reduce(reducer, '').trimEnd()};`; } else if (node.typ == EnumToken.AtRuleNodeType && !('chi' in node)) { str = `${data.val === '' ? '' : options.indent || ' '}${data.val};`; } else { str = renderAstNode(node, options, sourcemap, { ...position }, errors, reducer, cache, level + 1, indents); } if (css === '') { return str; } if (str === '') { return css; } return `${css}${options.newLine}${indentSub}${str}`; }, ''); if (options.removeEmpty && children === '') { return ''; } if (children.endsWith(';')) { children = children.slice(0, -1); } if (data.typ == EnumToken.AtRuleNodeType) { return `@${data.nam}${data.val === '' ? '' : options.indent || ' '}${data.val}${options.indent}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}`; } return data.sel + `${options.indent}{${options.newLine}` + (children === '' ? '' : indentSub + children + options.newLine) + indent + `}`; case EnumToken.InvalidRuleTokenType: case EnumToken.InvalidAtRuleTokenType: return ''; default: // return renderToken(data as Token, options, cache, reducer, errors); throw new Error(`render: unexpected token ${JSON.stringify(data, null, 1)}`); } } /** * render ast token * @param token * @param options * @param cache * @param reducer * @param errors */ function renderToken(token, options = {}, cache = Object.create(null), reducer, errors) { if (reducer == null) { reducer = function (acc, curr) { if (curr.typ == EnumToken.CommentTokenType && options.removeComments) { if (!options.preserveLicense || !curr.val.startsWith('/*!')) { return acc; } return acc + curr.val; } return acc + renderToken(curr, options, cache, reducer, errors); }; } if (token.typ == EnumToken.FunctionTokenType && colorsFunc.includes(token.val)) { if (isColor(token)) { // @ts-ignore token.typ = EnumToken.ColorTokenType; if (token.chi[0].typ == EnumToken.IdenTokenType && token.chi[0].val == 'from') { // @ts-ignore token.cal = 'rel'; } else if (token.val == 'color-mix' && token.chi[0].typ == EnumToken.IdenTokenType && token.chi[0].val == 'in') { // @ts-ignore token.cal = 'mix'; } else { if (token.val == 'color') { // @ts-ignore token.cal = 'col'; } token.chi = token.chi.filter((t) => ![EnumToken.WhitespaceTokenType, EnumToken.CommaTokenType, EnumToken.CommentTokenType].includes(t.typ)); } } } switch (token.typ) { case EnumToken.ListToken: return token.chi.reduce((acc, curr) => acc + renderToken(curr, options, cache), ''); case EnumToken.BinaryExpressionTokenType: if ([EnumToken.Mul, EnumToken.Div].includes(token.op)) { let result = ''; if (token.l.typ == EnumToken.BinaryExpressionTokenType && [EnumToken.Add, EnumToken.Sub].includes(token.l.op)) { result = '(' + renderToken(token.l, options, cache) + ')'; } else { result = renderToken(token.l, options, cache); } result += token.op == EnumToken.Mul ? '*' : '/'; if (token.r.typ == EnumToken.BinaryExpressionTokenType && [EnumToken.Add, EnumToken.Sub].includes(token.r.op)) { result += '(' + renderToken(token.r, options, cache) + ')'; } else { result += renderToken(token.r, options, cache); } return result; } return renderToken(token.l, options, cache) + (token.op == EnumToken.Add ? ' + ' : (token.op == EnumToken.Sub ? ' - ' : (token.op == EnumToken.Mul ? '*' : '/'))) + renderToken(token.r, options, cache); case EnumToken.FractionTokenType: const fraction = renderToken(token.l) + '/' + renderToken(token.r); if (+token.r.val != 0) { const value = reduceNumber(+token.l.val / +token.r.val); if (value.length <= fraction.length) { return value; } } return fraction; case EnumToken.Add: return ' + '; case EnumToken.Sub: return ' - '; case EnumToken.UniversalSelectorTokenType: case EnumToken.Mul: return '*'; case EnumToken.Div: return '/'; case EnumToken.ColorTokenType: if (token.kin == 'light-dark') { return token.val + '(' + token.chi.reduce((acc, curr) => acc + renderToken(curr, options, cache), '') + ')'; } if (options.convertColor) { if (token.cal == 'mix' && token.val == 'color-mix') { const children = token.chi.reduce((acc, t) => { if (t.typ == EnumToken.ColorTokenType) { acc.push([t]); } else { if (![EnumToken.WhitespaceTokenType, EnumToken.CommentTokenType].includes(t.typ)) { acc[acc.length - 1].push(t); } } return acc; }, [[]]); const value = colorMix(children[0][1], children[0][2], children[1][0], children[1][1], children[2][0], children[2][1]); if (value != null) { token = value; } } if (token.cal == 'rel' && ['rgb', 'hsl', 'hwb', 'lab', 'lch', 'oklab', 'oklch', 'color'].includes(token.val)) { const chi = getComponents(token); const offset = token.val == 'color' ? 2 : 1; // @ts-ignore const color = chi[1]; const components = parseRelativeColor(token.val == 'color' ? chi[offset].val : token.val, color, chi[offset + 1], chi[offset + 2], chi[offset + 3], chi[offset + 4]); if (components != null) { token.chi = [...(token.val == 'color' ? [chi[offset]] : []), ...Object.values(components)]; delete token.cal; } } if (token.val == 'color') { if (token.chi[0].typ == EnumToken.IdenTokenType && colorFuncColorSpace.includes(token.chi[0].val.toLowerCase())) { // @ts-ignore return reduceHexValue(srgb2hexvalues(...color2srgbvalues(token))); } } if (token.cal != null) { let slice = false; if (token.cal == 'rel') { const last = token.chi.at(-1); if ((last.typ == EnumToken.NumberTokenType && last.val == '1') || (last.typ == EnumToken.IdenTokenType && last.val == 'none')) { const prev = token.chi.at(-2); if (prev.typ == EnumToken.LiteralTokenType && prev.val == '/') { slice = true; } } } return clamp(token).val + '(' + (slice ? token.chi.slice(0, -2) : token.chi).reduce((acc, curr) => { const val = renderToken(curr, options, cache); if ([EnumToken.LiteralTokenType, EnumToken.CommaTokenType].includes(curr.typ)) { return acc + val; } if (acc.length > 0) { return acc + (['/', ','].includes(acc.at(-1)) ? '' : ' ') + val; } return val; }, '') + ')'; } if (token.kin == 'lit' && token.val.localeCompare('currentcolor', undefined, { sensitivity: 'base' }) == 0) { return 'currentcolor'; } clamp(token); if (Array.isArray(token.chi) && token.chi.some((t) => t.typ == EnumToken.FunctionTokenType || (t.typ == EnumToken.ColorTokenType && Array.isArray(t.chi)))) { return (token.val.endsWith('a') ? token.val.slice(0, -1) : token.val) + '(' + token.chi.reduce((acc, curr) => acc + (acc.length > 0 && !(acc.endsWith('/') || curr.typ == EnumToken.LiteralTokenType) ? ' ' : '') + renderToken(curr, options, cache), '') + ')'; } let value = token.kin == 'hex' ? token.val.toLowerCase() : (token.kin == 'lit' ? COLORS_NAMES[token.val.toLowerCase()] : ''); if (token.val == 'rgb' || token.val == 'rgba') { value = rgb2hex(token); } else if (token.val == 'hsl' || token.val == 'hsla') { value = hsl2hex(token); } else if (token.val == 'hwb') { value = hwb2hex(token); } else if (token.val == 'device-cmyk') { value = cmyk2hex(token); } else if (token.val == 'oklab') { value = oklab2hex(token); } else if (token.val == 'oklch') { value = oklch2hex(token); } else if (token.val == 'lab') { value = lab2hex(token); } else if (token.val == 'lch') { value = lch2hex(token); } if (value !== '') { return reduceHexValue(value); } } if (['hex', 'lit', 'sys', 'dpsys'].includes(token.kin)) { return token.val; } if (Array.isArray(token.chi)) { return (token.val.endsWith('a') ? token.val.slice(0, -1) : token.val) + '(' + token.chi.reduce((acc, curr) => acc + (acc.length > 0 && !(acc.endsWith('/') || curr.typ == EnumToken.LiteralTokenType) ? ' ' : '') + renderToken(curr, options, cache), '') + ')'; } case EnumToken.ParensTokenType: case EnumToken.FunctionTokenType: case EnumToken.UrlFunctionTokenType: case EnumToken.ImageFunctionTokenType: case EnumToken.TimingFunctionTokenType: case EnumToken.PseudoClassFuncTokenType: case EnumToken.TimelineFunctionTokenType: case EnumToken.GridTemplateFuncTokenType: if (token.typ == EnumToken.FunctionTokenType && mathFuncs.includes(token.val) && token.chi.length == 1 && ![EnumToken.BinaryExpressionTokenType, EnumToken.FractionTokenType, EnumToken.IdenTokenType].includes(token.chi[0].typ) && // @ts-ignore token.chi[0].val?.typ != EnumToken.FractionTokenType) { return token.chi.reduce((acc, curr) => acc + renderToken(curr, options, cache, reducer), ''); } // @ts-ignore return ( /* options.minify && 'Pseudo-class-func' == token.typ && token.val.slice(0, 2) == '::' ? token.val.slice(1) :*/token.val ?? '') + '(' + token.chi.reduce(reducer, '') + ')'; case EnumToken.MatchExpressionTokenType: return renderToken(token.l, options, cache, reducer, errors) + renderToken(token.op, options, cache, reducer, errors) + renderToken(token.r, options, cache, reducer, errors) + (token.attr ? ' ' + token.attr : ''); case EnumToken.NameSpaceAttributeTokenType: return (token.l == null ? '' : renderToken(token.l, options, cache, reducer, errors)) + '|' + renderToken(token.r, options, cache, reducer, errors); case EnumToken.BlockStartTokenType: return '{'; case EnumToken.BlockEndTokenType: return '}'; case EnumToken.StartParensTokenType: return '('; case EnumToken.DelimTokenType: case EnumToken.EqualMatchTokenType: return '='; case EnumToken.IncludeMatchTokenType: return '~='; case EnumToken.DashMatchTokenType: return '|='; case EnumToken.StartMatchTokenType: return '^='; case EnumToken.EndMatchTokenType: return '$='; case EnumToken.ContainMatchTokenType: return '*='; case EnumToken.LtTokenType: return '<'; case EnumToken.LteTokenType: return '<='; case EnumToken.SubsequentSiblingCombinatorTokenType: return '~'; case EnumToken.NextSiblingCombinatorTokenType: return '+'; case EnumToken.GtTokenType: case EnumToken.ChildCombinatorTokenType: return '>'; case EnumToken.GteTokenType: return '>='; case EnumToken.ColumnCombinatorTokenType: return '||'; case EnumToken.EndParensTokenType: return ')'; case EnumToken.AttrStartTokenType: return '['; case EnumToken.AttrEndTokenType: return ']'; case EnumToken.DescendantCombinatorTokenType: case EnumToken.WhitespaceTokenType: return ' '; case EnumToken.ColonTokenType: return ':'; case EnumToken.SemiColonTokenType: return ';'; case EnumToken.CommaTokenType: return ','; case EnumToken.ImportantTokenType: return '!important'; case EnumToken.AttrTokenType: case EnumToken.IdenListTokenType: return '[' + token.chi.reduce(reducer, '') + ']'; case EnumToken.TimeTokenType: case EnumToken.AngleTokenType: case EnumToken.LengthTokenType: case EnumToken.DimensionTokenType: case EnumToken.FrequencyTokenType: case EnumToken.ResolutionTokenType: let val = token.val.typ == EnumToken.FractionTokenType ? renderToken(token.val, options, cache) : reduceNumber(token.val); let unit = token.unit; if (token.typ == EnumToken.AngleTokenType && !val.includes('/')) { const angle = getAngle(token); let v; let value = val + unit; for (const u of ['turn', 'deg', 'rad', 'grad']) { if (token.unit == u) { continue; } switch (u) { case 'turn': v = reduceNumber(angle); if (v.length + 4 < value.length) { val = v; unit = u; value = v + u; } break; case 'deg': v = reduceNumber(angle * 360); if (v.length + 3 < value.length) { val = v; unit = u; value = v + u; } break; case 'rad': v = reduceNumber(angle * (2 * Math.PI)); if (v.length + 3 < value.length) { val = v; unit = u; value = v + u; } break; case 'grad': v = reduceNumber(angle * 400); if (v.length + 4 < value.length) { val = v; unit = u; value = v + u; } break; } } } if (val === '0') { if (token.typ == EnumToken.TimeTokenType) { return '0s'; } if (token.typ == EnumToken.FrequencyTokenType) { return '0Hz'; } // @ts-ignore if (token.typ == EnumToken.ResolutionTokenType) { return '0x'; } return '0'; } if (token.typ == EnumToken.TimeTokenType) { if (unit == 'ms') { // @ts-ignore const v = reduceNumber(val / 1000); if (v.length + 1 <= val.length) { return v + 's'; } return val + 'ms'; } return val + 's'; } return val.includes('/') ? val.replace('/', unit + '/') : val + unit; case EnumToken.FlexTokenType: case EnumToken.PercentageTokenType: const uni = token.typ == EnumToken.PercentageTokenType ? '%' : 'fr'; const perc = token.val.typ == EnumToken.FractionTokenType ? renderToken(token.val, options, cache) : reduceNumber(token.val); return options.minify && perc == '0' ? '0' : (perc.includes('/') ? perc.replace('/', uni + '/') : perc + uni); case EnumToken.NumberTokenType: return token.val.typ == EnumToken.FractionTokenType ? renderToken(token.val, options, cache) : reduceNumber(token.val); case EnumToken.CommentTokenType: if (options.removeComments && (!options.preserveLicense || !token.val.startsWith('/*!'))) { return ''; } case EnumToken.PseudoClassTokenType: case EnumToken.PseudoElementTokenType: // https://www.w3.org/TR/selectors-4/#single-colon-pseudos if (token.typ == EnumToken.PseudoElementTokenType && pseudoElements.includes(token.val.slice(1))) { return token.val.slice(1); } case EnumToken.UrlTokenTokenType: if (token.typ == EnumToken.UrlTokenTokenType) { if (options.output != null) { if (!('original' in token)) { // do not modify original token token = { ...token }; Object.defineProperty(token, 'original', { enumerable: false, writable: false, value: token.val }); } // @ts-ignore if (!(token.original in cache)) { let output = options.output ?? ''; const key = output + 'abs'; if (!(key in cache)) { // @ts-ignore cache[key] = options.dirname(options.resolve(output, options.cwd).absolute); } // @ts-ignore cache[token.original] = options.resolve(token.original, cache[key]).relative; } // @ts-ignore token.val = cache[token.original]; } } case EnumToken.HashTokenType: case EnumToken.IdenTokenType: case EnumToken.AtRuleTokenType: case EnumToken.StringTokenType: case EnumToken.LiteralTokenType: case EnumToken.DashedIdenTokenType: case EnumToken.PseudoPageTokenType: case EnumToken.ClassSelectorTokenType: return /* options.minify && 'Pseudo-class' == token.typ && '::' == token.val.slice(0, 2) ? token.val.slice(1) : */ token.val; case EnumToken.NestingSelectorTokenType: return '&'; case EnumToken.InvalidAttrTokenType: return '[' + token.chi.reduce((acc, curr) => acc + renderToken(curr, options, cache), ''); case EnumToken.InvalidClassSelectorTokenType: return token.val; case EnumToken.DeclarationNodeType: return token.nam + ':' + token.val.reduce((acc, curr) => acc + renderToken(curr, options, cache), ''); case EnumToken.MediaQueryConditionTokenType: return renderToken(token.l, options, cache, reducer, errors) + renderToken(token.op, options, cache, reducer, errors) + token.r.reduce((acc, curr) => acc + renderToken(curr, options, cache), ''); case EnumToken.MediaFeatureTokenType: return token.val; case EnumToken.MediaFeatureNotTokenType: return 'not ' + renderToken(token.val, options, cache, reducer, errors); case EnumToken.MediaFeatureOnlyTokenType: return 'only ' + renderToken(token.val, options, cache, reducer, errors); case EnumToken.MediaFeatureAndTokenType: return 'and'; case EnumToken.MediaFeatureOrTokenType: return 'or'; default: throw new Error(`render: unexpected token ${JSON.stringify(token, null, 1)}`); } errors?.push({ action: 'ignore', message: `render: unexpected token ${JSON.stringify(token, null, 1)}` }); return ''; } export { colorsFunc, doRender, reduceNumber, renderToken };