UNPKG

@tbela99/css-parser

Version:

CSS parser for node and the browser

598 lines (595 loc) 26.6 kB
import { getAngle, convertColor } from '../syntax/color/color.js'; import { colorsFunc, funcLike } from '../syntax/color/utils/constants.js'; import { EnumToken, ColorType } from '../ast/types.js'; import '../ast/minify.js'; import '../ast/walk.js'; import { expand } from '../ast/expand.js'; import { isColor, pseudoElements, mathFuncs, isNewLine } from '../syntax/syntax.js'; import { minifyNumber } from '../syntax/utils.js'; import { SourceMap } from './sourcemap/sourcemap.js'; 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, minify: 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; if (options.sourcemap === 'inline') { result.code += `\n/*# sourceMappingURL=data:application/json,${encodeURIComponent(JSON.stringify(result.map))} */`; } } return result; } function updateSourceMap(node, options, cache, sourcemap, position, str) { if ([ EnumToken.RuleNodeType, EnumToken.AtRuleNodeType, EnumToken.KeyFrameRuleNodeType, EnumToken.KeyframeAtRuleNodeType ].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}${(options.minify ? filterValues(data.val) : 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: case EnumToken.KeyframeAtRuleNodeType: if ([EnumToken.AtRuleNodeType, EnumToken.KeyframeAtRuleNodeType].includes(data.typ) && !('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.nam.startsWith('--') && 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}${(options.minify ? filterValues(node.val) : 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 ([EnumToken.AtRuleNodeType, EnumToken.KeyframeAtRuleNodeType].includes(data.typ)) { 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.InvalidDeclarationNodeType: case EnumToken.InvalidRuleTokenType: case EnumToken.InvalidAtRuleTokenType: default: return ''; } } /** * 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; // @ts-ignore if (token.chi[0].typ == EnumToken.IdenTokenType && token.chi[0].val == 'from') { // @ts-ignore token.cal = 'rel'; } else { // @ts-ignore if (token.val == 'color-mix' && token.chi[0].typ == EnumToken.IdenTokenType && token.chi[0].val == 'in') { // @ts-ignore token.cal = 'mix'; } else { // @ts-ignore if (token.val == 'color') { // @ts-ignore token.cal = 'col'; } // @ts-ignore 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 = minifyNumber(+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 == ColorType.LIGHT_DARK || ('chi' in token && options.convertColor === false)) { return token.val + '(' + token.chi.reduce((acc, curr) => acc + renderToken(curr, options, cache), '') + ')'; } if (options.convertColor !== false) { const value = convertColor(token, typeof options.convertColor == 'boolean' ? ColorType.HEX : ColorType[ColorType[options.convertColor ?? 'HEX']?.toUpperCase?.().replaceAll?.('-', '_')] ?? ColorType.HEX); // if (value != null) { token = value; } } if ([ColorType.HEX, ColorType.LIT, ColorType.SYS, ColorType.DPSYS].includes(token.kin)) { return token.val; } if (Array.isArray(token.chi)) { const isLegacy = ['rgb', 'rgba', 'hsl', 'hsla'].includes(token.val.toLowerCase()); const useAlpha = (['rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'oklab', 'oklch', 'lab', 'lch'].includes(token.val.toLowerCase()) && token.chi.length == 4) || ('color'.localeCompare(token.val, undefined, { sensitivity: 'base' }) == 0 && token.chi.length == 5); return (token.val.endsWith('a') ? token.val.slice(0, -1) : token.val) + '(' + token.chi.reduce((acc, curr, index, array) => { if (/[,/]\s*$/.test(acc)) { if (curr.typ == EnumToken.WhitespaceTokenType) { return acc.trimEnd(); } return acc.trimStart() + renderToken(curr, options, cache); } if (isLegacy && curr.typ == EnumToken.CommaTokenType) { return acc.trimEnd() + ' '; } if (curr.typ == EnumToken.WhitespaceTokenType) { return acc.trimEnd() + ' '; } if (curr.typ == EnumToken.CommaTokenType || (curr.typ == EnumToken.LiteralTokenType && curr.val == '/')) { return acc.trimEnd() + (curr.typ == EnumToken.CommaTokenType ? ',' : '/'); } return acc.trimEnd() + (useAlpha && index == array.length - 1 ? '/' : ' ') + renderToken(curr, options, cache); }, '').trimStart() + ')'; } 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.val + '(' + token.chi.reduce((acc, curr) => acc + renderToken(curr, options, cache, reducer), '') + ')'; } 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) : minifyNumber(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 = minifyNumber(angle); if (v.length + 4 < value.length) { val = v; unit = u; value = v + u; } break; case 'deg': v = minifyNumber(angle * 360); if (v.length + 3 < value.length) { val = v; unit = u; value = v + u; } break; case 'rad': v = minifyNumber(angle * (2 * Math.PI)); if (v.length + 3 < value.length) { val = v; unit = u; value = v + u; } break; case 'grad': v = minifyNumber(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 = minifyNumber(val / 1000); if (v.length + 1 <= val.length) { return v + 's'; } return val + 'ms'; } return val + 's'; } if (token.typ == EnumToken.ResolutionTokenType && unit == 'dppx') { unit = 'x'; } 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) : minifyNumber(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) : minifyNumber(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 + ':' + (options.minify ? filterValues(token.val) : 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'; } errors?.push({ action: 'ignore', message: `render: unexpected token ${JSON.stringify(token, null, 1)}` }); return ''; } function filterValues(values) { let i = 0; for (; i < values.length; i++) { if (values[i].typ == EnumToken.ImportantTokenType && values[i - 1]?.typ == EnumToken.WhitespaceTokenType) { values.splice(i - 1, 1); } else if (funcLike.includes(values[i].typ) && !['var', 'calc'].includes(values[i].val) && values[i + 1]?.typ == EnumToken.WhitespaceTokenType) { values.splice(i + 1, 1); } } return values; } export { doRender, filterValues, renderToken };