UNPKG

@tbela99/css-parser

Version:

CSS parser for node and the browser

1,189 lines (1,188 loc) 64.9 kB
import { isIdentStart, isIdent, isIdentColor, mathFuncs, isColor, parseColor, isPseudo, pseudoElements, isAtKeyword, isFunction, isNumber, isPercentage, isFlex, isDimension, parseDimension, isHexColor, isHash, mediaTypes } from '../syntax/syntax.js'; import { EnumToken, ColorType, ValidationLevel, SyntaxValidationResult } from '../ast/types.js'; import { minify, definedPropertySettings, combinators } from '../ast/minify.js'; import { walkValues, walk, WalkerOptionEnum } from '../ast/walk.js'; import { expand } from '../ast/expand.js'; import './utils/config.js'; import { parseDeclarationNode } from './utils/declaration.js'; import { renderToken } from '../renderer/render.js'; import { funcLike, timingFunc, timelineFunc, COLORS_NAMES, systemColors, deprecatedSystemColors, colorsFunc } from '../syntax/color/utils/constants.js'; import { buildExpression } from '../ast/math/expression.js'; import { tokenize } from './tokenize.js'; import '../validation/config.js'; import '../validation/parser/types.js'; import '../validation/parser/parse.js'; import { validateSelector } from '../validation/selector.js'; import { validateAtRule } from '../validation/atrule.js'; import { splitTokenList } from '../validation/utils/list.js'; import '../validation/syntaxes/complex-selector.js'; import { validateKeyframeSelector } from '../validation/syntaxes/keyframe-selector.js'; import { evaluateSyntax } from '../validation/syntax.js'; import { validateAtRuleKeyframes } from '../validation/at-rules/keyframes.js'; const urlTokenMatcher = /^(["']?)[a-zA-Z0-9_/.-][a-zA-Z0-9_/:.#?-]+(\1)$/; const trimWhiteSpace = [EnumToken.CommentTokenType, EnumToken.GtTokenType, EnumToken.GteTokenType, EnumToken.LtTokenType, EnumToken.LteTokenType, EnumToken.ColumnCombinatorTokenType]; const BadTokensTypes = [ EnumToken.BadCommentTokenType, EnumToken.BadCdoTokenType, EnumToken.BadUrlTokenType, EnumToken.BadStringTokenType ]; const enumTokenHints = new Set([ EnumToken.WhitespaceTokenType, EnumToken.SemiColonTokenType, EnumToken.ColonTokenType, EnumToken.BlockStartTokenType, EnumToken.BlockStartTokenType, EnumToken.AttrStartTokenType, EnumToken.AttrEndTokenType, EnumToken.StartParensTokenType, EnumToken.EndParensTokenType, EnumToken.CommaTokenType, EnumToken.GtTokenType, EnumToken.LtTokenType, EnumToken.GteTokenType, EnumToken.LteTokenType, EnumToken.CommaTokenType, EnumToken.StartMatchTokenType, EnumToken.EndMatchTokenType, EnumToken.IncludeMatchTokenType, EnumToken.DashMatchTokenType, EnumToken.ContainMatchTokenType, EnumToken.EOFTokenType ]); function reject(reason) { throw new Error(reason ?? 'Parsing aborted'); } /** * parse css string * @param iterator * @param options */ async function doParse(iterator, options = {}) { if (options.signal != null) { options.signal.addEventListener('abort', reject); } options = { src: '', sourcemap: false, minify: true, pass: 1, parseColor: true, nestingRules: true, resolveImport: false, resolveUrls: false, removeCharset: true, removeEmpty: true, removeDuplicateDeclarations: true, computeTransform: true, computeShorthand: true, computeCalcExpression: true, inlineCssVariables: false, setParent: true, removePrefix: false, validation: ValidationLevel.Default, lenient: true, ...options }; if (typeof options.validation == 'boolean') { options.validation = options.validation ? ValidationLevel.All : ValidationLevel.None; } if (options.expandNestingRules) { options.nestingRules = false; } if (options.resolveImport) { options.resolveUrls = true; } const startTime = performance.now(); const errors = []; const src = options.src; const stack = []; const stats = { src: options.src ?? '', bytesIn: 0, importedBytesIn: 0, parse: `0ms`, minify: `0ms`, total: `0ms`, imports: [] }; let ast = { typ: EnumToken.StyleSheetNodeType, chi: [] }; let tokens = []; let map = new Map; let context = ast; if (options.sourcemap) { ast.loc = { sta: { ind: 0, lin: 1, col: 1 }, end: { ind: 0, lin: 1, col: 1 }, src: '' }; } const iter = tokenize(iterator); let item; let node; const rawTokens = []; const imports = []; while (item = iter.next().value) { stats.bytesIn = item.bytesIn; rawTokens.push(item); if (item.hint != null && BadTokensTypes.includes(item.hint)) { // bad token continue; } if (item.hint != EnumToken.EOFTokenType) { tokens.push(item); } else if (ast.loc != null) { for (let i = stack.length - 1; i >= 0; i--) { stack[i].loc.end = { ...item.end }; } ast.loc.end = item.end; } if (item.token == ';' || item.token == '{') { node = parseNode(tokens, context, options, errors, src, map, rawTokens); rawTokens.length = 0; if (node != null) { if ('chi' in node) { stack.push(node); context = node; } else if (node.typ == EnumToken.AtRuleNodeType && node.nam == 'import') { imports.push(node); } } else if (item.token == '{') { let inBlock = 1; tokens = [item]; do { item = iter.next().value; if (item == null) { break; } tokens.push(item); if (item.token == '{') { inBlock++; } else if (item.token == '}') { inBlock--; } } while (inBlock != 0); if (tokens.length > 0) { errors.push({ action: 'drop', message: 'invalid block', rawTokens: tokens.slice() }); } } tokens = []; map = new Map; } else if (item.token == '}') { parseNode(tokens, context, options, errors, src, map, rawTokens); rawTokens.length = 0; if (context.loc != null) { context.loc.end = item.end; } const previousNode = stack.pop(); context = (stack[stack.length - 1] ?? ast); if (previousNode != null && previousNode.typ == EnumToken.InvalidRuleTokenType) { const index = context.chi.findIndex(node => node == previousNode); if (index > -1) { context.chi.splice(index, 1); } } if (options.removeEmpty && previousNode != null && previousNode.chi.length == 0 && context.chi[context.chi.length - 1] == previousNode) { context.chi.pop(); } tokens = []; map = new Map; } } if (tokens.length > 0) { node = parseNode(tokens, context, options, errors, src, map, rawTokens); rawTokens.length = 0; if (node != null) { if (node.typ == EnumToken.AtRuleNodeType && node.nam == 'import') { imports.push(node); } else if ('chi' in node && node.typ != EnumToken.InvalidRuleTokenType) { stack.push(node); context = node; } } if (context != null && context.typ == EnumToken.InvalidRuleTokenType) { // @ts-ignore const index = context.chi.findIndex((node) => node == context); if (index > -1) { context.chi.splice(index, 1); } } } if (imports.length > 0 && options.resolveImport) { await Promise.all(imports.map(async (node) => { const token = node.tokens[0]; const url = token.typ == EnumToken.StringTokenType ? token.val.slice(1, -1) : token.val; try { const root = await options.load(url, options.src).then((src) => { return doParse(src, Object.assign({}, options, { minify: false, setParent: false, src: options.resolve(url, options.src).absolute })); }); stats.importedBytesIn += root.stats.bytesIn; stats.imports.push(root.stats); node.parent.chi.splice(node.parent.chi.indexOf(node), 1, ...root.ast.chi); if (root.errors.length > 0) { errors.push(...root.errors); } } catch (error) { // @ts-ignore errors.push({ action: 'ignore', message: 'doParse: ' + error.message, error }); } })); } while (stack.length > 0 && context != ast) { const previousNode = stack.pop(); context = (stack[stack.length - 1] ?? ast); // remove empty nodes if (options.removeEmpty && previousNode != null && previousNode.chi.length == 0 && context.chi[context.chi.length - 1] == previousNode) { context.chi.pop(); continue; } break; } const endParseTime = performance.now(); if (options.expandNestingRules) { ast = expand(ast); } if (options.visitor != null) { for (const result of walk(ast)) { if (result.node.typ == EnumToken.DeclarationNodeType && (typeof options.visitor.Declaration == 'function' || options.visitor.Declaration?.[result.node.nam] != null)) { const callable = typeof options.visitor.Declaration == 'function' ? options.visitor.Declaration : options.visitor.Declaration[result.node.nam]; const results = await callable(result.node); if (results == null || (Array.isArray(results) && results.length == 0)) { continue; } result.parent.chi.splice(result.parent.chi.indexOf(result.node), 1, ...(Array.isArray(results) ? results : [results])); } else if (options.visitor.Rule != null && result.node.typ == EnumToken.RuleNodeType) { const results = await options.visitor.Rule(result.node); if (results == null || (Array.isArray(results) && results.length == 0)) { continue; } result.parent.chi.splice(result.parent.chi.indexOf(result.node), 1, ...(Array.isArray(results) ? results : [results])); } else if (options.visitor.AtRule != null && result.node.typ == EnumToken.AtRuleNodeType && // @ts-ignore (typeof options.visitor.AtRule == 'function' || options.visitor.AtRule?.[result.node.nam] != null)) { const callable = typeof options.visitor.AtRule == 'function' ? options.visitor.AtRule : options.visitor.AtRule[result.node.nam]; const results = await callable(result.node); if (results == null || (Array.isArray(results) && results.length == 0)) { continue; } result.parent.chi.splice(result.parent.chi.indexOf(result.node), 1, ...(Array.isArray(results) ? results : [results])); } } } if (options.minify) { if (ast.chi.length > 0) { let passes = options.pass ?? 1; while (passes--) { minify(ast, options, true, errors, false); } } } const endTime = performance.now(); if (options.signal != null) { options.signal.removeEventListener('abort', reject); } stats.bytesIn += stats.importedBytesIn; return { ast, errors, stats: { ...stats, parse: `${(endParseTime - startTime).toFixed(2)}ms`, minify: `${(endTime - endParseTime).toFixed(2)}ms`, total: `${(endTime - startTime).toFixed(2)}ms` } }; } function getLastNode(context) { let i = context.chi.length; while (i--) { if ([EnumToken.CommentTokenType, EnumToken.CDOCOMMTokenType, EnumToken.WhitespaceTokenType].includes(context.chi[i].typ)) { continue; } return context.chi[i]; } return null; } function parseNode(results, context, options, errors, src, map, rawTokens) { let tokens = []; for (const t of results) { const node = getTokenType(t.token, t.hint); map.set(node, { sta: t.sta, end: t.end, src }); tokens.push(node); } let i; let loc; for (i = 0; i < tokens.length; i++) { if (tokens[i].typ == EnumToken.CommentTokenType || tokens[i].typ == EnumToken.CDOCOMMTokenType) { const location = map.get(tokens[i]); if (tokens[i].typ == EnumToken.CDOCOMMTokenType && context.typ != EnumToken.StyleSheetNodeType) { errors.push({ action: 'drop', message: `CDOCOMM not allowed here ${JSON.stringify(tokens[i], null, 1)}`, location }); continue; } loc = location; // @ts-ignore context.chi.push(tokens[i]); if (options.sourcemap) { tokens[i].loc = loc; } } else if (tokens[i].typ != EnumToken.WhitespaceTokenType) { break; } } tokens = tokens.slice(i); if (tokens.length == 0) { return null; } let delim = tokens.at(-1); if (delim.typ == EnumToken.SemiColonTokenType || delim.typ == EnumToken.BlockStartTokenType || delim.typ == EnumToken.BlockEndTokenType) { tokens.pop(); } while ([EnumToken.WhitespaceTokenType, EnumToken.BadStringTokenType, EnumToken.BadCommentTokenType].includes(tokens.at(-1)?.typ)) { tokens.pop(); } if (tokens.length == 0) { return null; } if (tokens[0]?.typ == EnumToken.AtRuleTokenType) { const atRule = tokens.shift(); const location = map.get(atRule); // @ts-ignore while ([EnumToken.WhitespaceTokenType].includes(tokens[0]?.typ)) { tokens.shift(); } rawTokens.shift(); if (atRule.val == 'import') { // only @charset and @layer are accepted before @import // @ts-ignore if (context.chi.length > 0) { // @ts-ignore let i = context.chi.length; while (i--) { // @ts-ignore const type = context.chi[i].typ; if (type == EnumToken.CommentNodeType) { continue; } if (type != EnumToken.AtRuleNodeType) { // @ts-ignore if (!(type == EnumToken.InvalidAtRuleTokenType && // @ts-ignore ['charset', 'layer', 'import'].includes(context.chi[i].nam))) { errors.push({ action: 'drop', message: 'invalid @import', location }); return null; } } // @ts-ignore const name = context.chi[i].nam; if (name != 'charset' && name != 'import' && name != 'layer') { errors.push({ action: 'drop', message: 'invalid @import', location }); return null; } break; } } // @ts-ignore if (tokens[0]?.typ != EnumToken.StringTokenType && tokens[0]?.typ != EnumToken.UrlFunctionTokenType) { errors.push({ action: 'drop', message: 'doParse: invalid @import', location }); return null; } // @ts-ignore if (tokens[0].typ == EnumToken.UrlFunctionTokenType && tokens[1]?.typ != EnumToken.UrlTokenTokenType && tokens[1]?.typ != EnumToken.StringTokenType) { errors.push({ action: 'drop', message: 'doParse: invalid @import', location }); return null; } } if (atRule.val == 'import') { // @ts-ignore if (tokens[0].typ == EnumToken.UrlFunctionTokenType) { if (tokens[1].typ == EnumToken.UrlTokenTokenType || tokens[1].typ == EnumToken.StringTokenType) { tokens.shift(); if (tokens[0]?.typ == EnumToken.UrlTokenTokenType) { // @ts-ignore tokens[0].typ = EnumToken.StringTokenType; // @ts-ignore tokens[0].val = `"${tokens[0].val}"`; } // @ts-ignore while (tokens[1]?.typ == EnumToken.WhitespaceTokenType || tokens[1]?.typ == EnumToken.CommentTokenType) { tokens.splice(1, 1); } // @ts-ignore if (tokens[1]?.typ == EnumToken.EndParensTokenType) { tokens.splice(1, 1); } } } } // https://www.w3.org/TR/css-nesting-1/#conditionals // allowed nesting at-rules // there must be a top level rule in the stack if (atRule.val == 'charset') { let spaces = 0; // https://developer.mozilla.org/en-US/docs/Web/CSS/@charset for (let k = 0; k < rawTokens.length; k++) { if (rawTokens[k].hint == EnumToken.WhitespaceTokenType) { spaces += rawTokens[k].len; continue; } if (rawTokens[k].hint == EnumToken.CommentTokenType) { continue; } if (rawTokens[k].hint == EnumToken.CDOCOMMTokenType) { continue; } if (spaces > 1) { errors.push({ action: 'drop', message: '@charset must have only one space', // @ts-ignore location }); return null; } if (rawTokens[k].hint != EnumToken.StringTokenType || rawTokens[k].token[0] != '"') { errors.push({ action: 'drop', message: '@charset expects a "<charset>"', location }); return null; } break; } if (options.removeCharset) { return null; } } const t = parseAtRulePrelude(parseTokens(tokens, { minify: options.minify }), atRule); const raw = t.reduce((acc, curr) => { acc.push(renderToken(curr, { removeComments: true, convertColor: false })); return acc; }, []); const nam = renderToken(atRule, { removeComments: true }); // @ts-ignore const node = { typ: /^(-[a-z]+-)?keyframes$/.test(nam) ? EnumToken.KeyframeAtRuleNodeType : EnumToken.AtRuleNodeType, nam, val: raw.join('') }; Object.defineProperties(node, { tokens: { ...definedPropertySettings, enumerable: false, value: t.slice() }, raw: { ...definedPropertySettings, value: raw } }); if (delim.typ == EnumToken.BlockStartTokenType) { node.chi = []; } loc = map.get(atRule); if (options.sourcemap) { node.loc = loc; node.loc.end = { ...map.get(delim).end }; } let isValid = true; if (node.nam == 'else') { const prev = getLastNode(context); if (prev != null && prev.typ == EnumToken.AtRuleNodeType && ['when', 'else'].includes(prev.nam)) { if (prev.nam == 'else') { isValid = Array.isArray(prev.tokens) && prev.tokens.length > 0; } } else { isValid = false; } } // @ts-ignore const valid = options.validation == ValidationLevel.None ? { valid: SyntaxValidationResult.Valid, error: '', node, syntax: '@' + node.nam } : isValid ? (node.typ == EnumToken.KeyframeAtRuleNodeType ? validateAtRuleKeyframes(node) : validateAtRule(node, options, context)) : { valid: SyntaxValidationResult.Drop, node, syntax: '@' + node.nam, error: '@' + node.nam + ' not allowed here'}; if (valid.valid == SyntaxValidationResult.Drop) { errors.push({ action: 'drop', message: valid.error + ' - "' + tokens.reduce((acc, curr) => acc + renderToken(curr, { minify: false }), '') + '"', // @ts-ignore location: { src, ...(map.get(valid.node) ?? location) } }); // @ts-ignore node.typ = EnumToken.InvalidAtRuleTokenType; } else { node.val = node.tokens.reduce((acc, curr) => acc + renderToken(curr, { minify: false, convertColor: false, removeComments: true }), ''); } context.chi.push(node); Object.defineProperties(node, { parent: { ...definedPropertySettings, value: context }, validSyntax: { ...definedPropertySettings, value: valid.valid == SyntaxValidationResult.Valid } }); return node; } else { // rule if (delim.typ == EnumToken.BlockStartTokenType) { const location = map.get(tokens[0]); const uniq = new Map; parseTokens(tokens, { minify: true }); const ruleType = context.typ == EnumToken.KeyframeAtRuleNodeType ? EnumToken.KeyFrameRuleNodeType : EnumToken.RuleNodeType; if (ruleType == EnumToken.RuleNodeType) { parseSelector(tokens); } const node = { typ: ruleType, sel: [...tokens.reduce((acc, curr, index, array) => { if (curr.typ == EnumToken.CommentTokenType) { return acc; } if (curr.typ == EnumToken.WhitespaceTokenType) { if (trimWhiteSpace.includes(array[index - 1]?.typ) || trimWhiteSpace.includes(array[index + 1]?.typ) || combinators.includes(array[index - 1]?.val) || combinators.includes(array[index + 1]?.val)) { return acc; } } if (ruleType == EnumToken.KeyFrameRuleNodeType) { if (curr.typ == EnumToken.IdenTokenType && curr.val == 'from') { Object.assign(curr, { typ: EnumToken.PercentageTokenType, val: '0' }); } else if (curr.typ == EnumToken.PercentageTokenType && curr.val == '100') { Object.assign(curr, { typ: EnumToken.IdenTokenType, val: 'to' }); } } let t = renderToken(curr, { minify: false }); if (t == ',') { acc.push([]); } else { acc[acc.length - 1].push(t); } return acc; }, [[]]).reduce((acc, curr) => { let i = 0; for (; i < curr.length; i++) { if (i + 1 < curr.length && curr[i] == '*') { if (curr[i] == '*') { let index = curr[i + 1] == ' ' ? 2 : 1; if (!['>', '~', '+'].includes(curr[index])) { curr.splice(i, index); } } } } acc.set(curr.join(''), curr); return acc; }, uniq).keys()].join(','), chi: [] }; Object.defineProperty(node, 'tokens', { ...definedPropertySettings, enumerable: false, value: tokens.slice() }); loc = location; if (options.sourcemap) { node.loc = loc; } // @ts-ignore context.chi.push(node); Object.defineProperty(node, 'parent', { ...definedPropertySettings, value: context }); // @ts-ignore const valid = options.validation == ValidationLevel.None ? { valid: SyntaxValidationResult.Valid, error: null } : ruleType == EnumToken.KeyFrameRuleNodeType ? validateKeyframeSelector(tokens) : validateSelector(tokens, options, context); if (valid.valid != SyntaxValidationResult.Valid) { // @ts-ignore node.typ = EnumToken.InvalidRuleTokenType; node.sel = tokens.reduce((acc, curr) => acc + renderToken(curr, { minify: false }), ''); errors.push({ action: 'drop', message: valid.error + ' - "' + tokens.reduce((acc, curr) => acc + renderToken(curr, { minify: false }), '') + '"', // @ts-ignore location }); } Object.defineProperty(node, 'validSyntax', { ...definedPropertySettings, value: valid.valid == SyntaxValidationResult.Valid }); return node; } else { let name = null; let value = null; let i = 0; for (; i < tokens.length; i++) { if (tokens[i].typ == EnumToken.LiteralTokenType && tokens[i].val.length > 1) { const start = tokens[i].val.charAt(0); const val = tokens[i].val.slice(1); if (['/', '*'].includes(start) && isNumber(val)) { tokens.splice(i, 1, { typ: EnumToken.LiteralTokenType, val: tokens[i].val.charAt(0) }, { typ: EnumToken.NumberTokenType, val: tokens[i].val.slice(1) }); } else if (start == '/' && isFunction(val)) { tokens.splice(i, 1, { typ: EnumToken.LiteralTokenType, val: '/' }, getTokenType(val)); } } } parseTokens(tokens, { ...options, parseColor: true }); for (i = 0; i < tokens.length; i++) { if (tokens[i].typ == EnumToken.CommentTokenType) { continue; } if (name == null && [EnumToken.IdenTokenType, EnumToken.DashedIdenTokenType].includes(tokens[i].typ)) { name = tokens.slice(0, i + 1); } else if (name == null && tokens[i].typ == EnumToken.ColorTokenType && [ColorType.SYS, ColorType.DPSYS].includes(tokens[i].kin)) { name = tokens.slice(0, i + 1); tokens[i].typ = EnumToken.IdenTokenType; } else if (name != null && funcLike.concat([ EnumToken.LiteralTokenType, EnumToken.IdenTokenType, EnumToken.DashedIdenTokenType, EnumToken.PseudoClassTokenType, EnumToken.PseudoClassFuncTokenType ]).includes(tokens[i].typ)) { if (tokens[i].val?.charAt?.(0) == ':') { Object.assign(tokens[i], getTokenType(tokens[i].val.slice(1))); if ('chi' in tokens[i]) { tokens[i].typ = EnumToken.FunctionTokenType; if (colorsFunc.includes(tokens[i].val) && isColor(tokens[i])) { parseColor(tokens[i]); } } tokens.splice(i--, 0, { typ: EnumToken.ColonTokenType }); continue; } if ('chi' in tokens[i]) { tokens[i].typ = EnumToken.FunctionTokenType; } value = tokens.slice(i); } if (tokens[i].typ == EnumToken.ColonTokenType) { name = tokens.slice(0, i); value = tokens.slice(i + 1); break; } } if (name == null) { name = tokens; } const location = map.get(name[0]); if (name.length > 0) { for (let i = 1; i < name.length; i++) { if (name[i].typ != EnumToken.WhitespaceTokenType && name[i].typ != EnumToken.CommentTokenType) { errors.push({ action: 'drop', message: 'doParse: invalid declaration', location }); return null; } } } const nam = renderToken(name.shift(), { removeComments: true }); if (value == null || (!nam.startsWith('--') && value.length == 0)) { errors.push({ action: 'drop', message: 'doParse: invalid declaration', location }); if (options.lenient) { const node = { typ: EnumToken.InvalidDeclarationNodeType, nam, val: [] }; if (options.sourcemap) { node.loc = location; node.loc.end = { ...map.get(delim).end }; } context.chi.push(node); } return null; } for (const { value: token } of walkValues(value, null, { fn: (node) => node.typ == EnumToken.FunctionTokenType && node.val == 'calc' ? WalkerOptionEnum.IgnoreChildren : null, type: EnumToken.FunctionTokenType })) { if (token.typ == EnumToken.FunctionTokenType && token.val == 'calc') { for (const { value: node, parent } of walkValues(token.chi, token)) { // fix expressions starting with '/' or '*' such as '/4' in (1 + 1)/4 if (node.typ == EnumToken.LiteralTokenType && node.val.length > 0) { if (node.val[0] == '/' || node.val[0] == '*') { parent.chi.splice(parent.chi.indexOf(node), 1, { typ: node.val[0] == '/' ? EnumToken.Div : EnumToken.Mul }, ...parseString(node.val.slice(1))); } } } } } const node = { typ: EnumToken.DeclarationNodeType, nam, val: value }; if (options.sourcemap) { node.loc = location; node.loc.end = { ...map.get(delim).end }; } // do not allow declarations in style sheets if (context.typ == EnumToken.StyleSheetNodeType && options.lenient) { // @ts-ignore node.typ = EnumToken.InvalidDeclarationNodeType; context.chi.push(node); return null; } const result = parseDeclarationNode(node, errors, location); Object.defineProperty(result, 'parent', { ...definedPropertySettings, value: context }); if (result != null) { if (options.validation == ValidationLevel.All) { const valid = evaluateSyntax(result, options); Object.defineProperty(result, 'validSyntax', { ...definedPropertySettings, value: valid.valid == SyntaxValidationResult.Valid }); if (valid.valid == SyntaxValidationResult.Drop) { errors.push({ action: 'drop', message: valid.error, syntax: valid.syntax, node: valid.node, location: map.get(valid.node) ?? valid.node?.loc ?? result.loc ?? location }); if (!options.lenient) { return null; } // @ts-ignore node.typ = EnumToken.InvalidDeclarationNodeType; } } context.chi.push(result); } return null; } } } /** * parse at-rule prelude * @param tokens * @param atRule */ function parseAtRulePrelude(tokens, atRule) { // @ts-ignore for (const { value, parent } of walkValues(tokens, null, null, true)) { if (value.typ == EnumToken.CommentTokenType || value.typ == EnumToken.WhitespaceTokenType || value.typ == EnumToken.CommaTokenType) { continue; } if (value.typ == EnumToken.PseudoClassFuncTokenType || value.typ == EnumToken.PseudoClassTokenType) { if (parent?.typ == EnumToken.ParensTokenType) { const index = parent.chi.indexOf(value); let i = index; while (i--) { if (parent.chi[i].typ == EnumToken.IdenTokenType || parent.chi[i].typ == EnumToken.DashedIdenTokenType) { break; } } if (i >= 0) { const token = getTokenType(parent.chi[index].val.slice(1) + (funcLike.includes(parent.chi[index].typ) ? '(' : '')); parent.chi[index].val = token.val; parent.chi[index].typ = token.typ; if (parent.chi[index].typ == EnumToken.FunctionTokenType && isColor(parent.chi[index])) { parseColor(parent.chi[index]); } parent.chi.splice(i, index - i + 1, { typ: EnumToken.MediaQueryConditionTokenType, l: parent.chi[i], r: parent.chi.slice(index), op: { typ: EnumToken.ColonTokenType } }); } } } if (atRule.val == 'page' && value.typ == EnumToken.PseudoClassTokenType) { if ([':left', ':right', ':first', ':blank'].includes(value.val)) { // @ts-ignore value.typ = EnumToken.PseudoPageTokenType; } } if (atRule.val == 'layer') { if (parent == null && value.typ == EnumToken.LiteralTokenType) { if (value.val.charAt(0) == '.') { if (isIdent(value.val.slice(1))) { // @ts-ignore value.typ = EnumToken.ClassSelectorTokenType; } } } } if (value.typ == EnumToken.IdenTokenType) { if (parent == null && mediaTypes.some((t) => { if (value.val.localeCompare(t, 'en', { sensitivity: 'base' }) == 0) { // @ts-ignore value.typ = EnumToken.MediaFeatureTokenType; return true; } return false; })) { continue; } if (value.typ == EnumToken.IdenTokenType && 'and'.localeCompare(value.val, 'en', { sensitivity: 'base' }) == 0) { // @ts-ignore value.typ = EnumToken.MediaFeatureAndTokenType; continue; } if (value.typ == EnumToken.IdenTokenType && 'or'.localeCompare(value.val, 'en', { sensitivity: 'base' }) == 0) { // @ts-ignore value.typ = EnumToken.MediaFeatureOrTokenType; continue; } if (value.typ == EnumToken.IdenTokenType && ['not', 'only'].some((t) => t.localeCompare(value.val, 'en', { sensitivity: 'base' }) == 0)) { // @ts-ignore const array = parent?.chi ?? tokens; const startIndex = array.indexOf(value); let index = startIndex + 1; if (index == 0) { continue; } while (index < array.length && [EnumToken.CommentTokenType, EnumToken.WhitespaceTokenType].includes(array[index].typ)) { index++; } if (array[index] == null || array[index].typ == EnumToken.CommaTokenType) { continue; } Object.assign(array[startIndex], { typ: value.val.toLowerCase() == 'not' ? EnumToken.MediaFeatureNotTokenType : EnumToken.MediaFeatureOnlyTokenType, val: array[index] }); array.splice(startIndex + 1, index - startIndex); continue; } } if (value.typ == EnumToken.FunctionTokenType && value.val == 'selector') { parseSelector(value.chi); } if (value.typ == EnumToken.ParensTokenType || (value.typ == EnumToken.FunctionTokenType && ['media', 'supports', 'style', 'scroll-state'].includes(value.val))) { let i; let nameIndex = -1; let valueIndex = -1; const dashedIdent = value.typ == EnumToken.FunctionTokenType && value.val == 'style'; for (let i = 0; i < value.chi.length; i++) { if (value.chi[i].typ == EnumToken.CommentTokenType || value.chi[i].typ == EnumToken.WhitespaceTokenType) { continue; } if ((dashedIdent && value.chi[i].typ == EnumToken.DashedIdenTokenType) || value.chi[i].typ == EnumToken.IdenTokenType || value.chi[i].typ == EnumToken.FunctionTokenType || value.chi[i].typ == EnumToken.ColorTokenType) { nameIndex = i; } break; } if (nameIndex == -1) { continue; } for (let i = nameIndex + 1; i < value.chi.length; i++) { if (value.chi[i].typ == EnumToken.CommentTokenType || value.chi[i].typ == EnumToken.WhitespaceTokenType) { continue; } if (value.chi[i].typ == EnumToken.LiteralTokenType && value.chi[i].val.startsWith(':') && isDimension(value.chi[i].val.slice(1))) { value.chi.splice(i, 1, { typ: EnumToken.ColonTokenType, }, Object.assign(value.chi[i], parseDimension(value.chi[i].val.slice(1)))); i--; continue; } if (nameIndex != -1 && value.chi[i].typ == EnumToken.PseudoClassTokenType) { value.chi.splice(i, 1, { typ: EnumToken.ColonTokenType, }, Object.assign(value.chi[i], { typ: EnumToken.IdenTokenType, val: value.chi[i].val.slice(1) })); i--; continue; } valueIndex = i; break; } if (valueIndex == -1) { continue; } for (i = nameIndex + 1; i < value.chi.length; i++) { if ([ EnumToken.GtTokenType, EnumToken.LtTokenType, EnumToken.GteTokenType, EnumToken.LteTokenType, EnumToken.ColonTokenType ].includes(value.chi[valueIndex].typ)) { const val = value.chi.splice(valueIndex, 1)[0]; const node = value.chi.splice(nameIndex, 1)[0]; // 'background' // @ts-ignore if (node.typ == EnumToken.ColorTokenType && node.kin == ColorType.DPSYS) { // @ts-ignore delete node.kin; node.typ = EnumToken.IdenTokenType; } while (value.chi[0]?.typ == EnumToken.WhitespaceTokenType) { value.chi.shift(); } const t = [{ typ: EnumToken.MediaQueryConditionTokenType, l: node, op: { typ: val.typ }, r: value.chi.slice() }]; value.chi.length = 0; value.chi.push(...t); } } } } return tokens; } /** * parse selector * @param tokens */ function parseSelector(tokens) { for (const { value, parent } of walkValues(tokens)) { if (value.typ == EnumToken.CommentTokenType || value.typ == EnumToken.WhitespaceTokenType || value.typ == EnumToken.CommaTokenType || value.typ == EnumToken.IdenTokenType || value.typ == EnumToken.HashTokenType) { continue; } if (parent == null) { if (value.typ == EnumToken.GtTokenType) { // @ts-ignore value.typ = EnumToken.ChildCombinatorTokenType; } else if (value.typ == EnumToken.LiteralTokenType) { if (value.val.charAt(0) == '&') { // @ts-ignore value.typ = EnumToken.NestingSelectorTokenType; // @ts-ignore delete value.val; } else if (value.val.charAt(0) == '.') { if (!isIdent(value.val.slice(1))) { // @ts-ignore value.typ = EnumToken.InvalidClassSelectorTokenType; } else { // @ts-ignore value.typ = EnumToken.ClassSelectorTokenType; } } if (['*', '>', '+', '~'].includes(value.val)) { switch (value.val) { case '*': // @ts-ignore value.typ = EnumToken.UniversalSelectorTokenType; break; case '>': // @ts-ignore value.typ = EnumToken.ChildCombinatorTokenType; break; case '+': // @ts-ignore value.typ = EnumToken.NextSiblingCombinatorTokenType; break; case '~': // @ts-ignore value.typ = EnumToken.SubsequentSiblingCombinatorTokenType; break; } // @ts-ignore delete value.val; } } else if (value.typ == EnumToken.ColorTokenType) { if (value.kin == ColorType.LIT || value.kin == ColorType.HEX || value.kin == ColorType.SYS || value.kin == ColorType.DPSYS) { if (value.kin == ColorType.HEX) { if (!isIdent(value.val.slice(1))) { continue; } // @ts-ignore value.typ = EnumToken.HashTokenType; } else { // @ts-ignore value.typ = EnumToken.IdenTokenType; } // @ts-ignore delete value.kin; } } } } let i = 0; const combinators = [ EnumToken.ChildCombinatorTokenType, EnumToken.NextSiblingCombinatorTokenType, EnumToken.SubsequentSiblingCombinatorTokenType ]; for (; i < tokens.length; i++) { if (combinators.includes(tokens[i].typ)) { if (i + 1 < tokens.length && [EnumToken.WhitespaceTokenType, EnumToken.DescendantCombinatorTokenType].includes(tokens[i + 1].typ)) { tokens.splice(i + 1, 1); } if (i > 0 && [EnumToken.WhitespaceTokenType, EnumToken.DescendantCombinatorTokenType].includes(tokens[i - 1].typ)) { tokens.splice(i - 1, 1); i--; continue; } } if (tokens[i].typ == EnumToken.WhitespaceTokenType) { tokens[i].typ = EnumToken.DescendantCombinatorTokenType; } } return tokens; } /** * parse css string * @param src * @param options */ function parseString(src, options = { location: false }) { return parseTokens([...tokenize(src)].reduce((acc, t) => { if (t.hint == EnumToken.EOFTokenType) { return acc; } const token = getTokenType(t.token, t.hint); if (options.location) { Object.assign(token, { loc: t.sta }); } acc.push(token); return acc; }, [])); } function getTokenType(val, hint) { if (hint != null) { return enumTokenHints.has(hint) ? { typ: hint } : { typ: hint, val }; } switch (val) { case ' ': return { typ: EnumToken.WhitespaceTokenType }; case ';': return { typ: EnumToken.SemiColonTokenType }; case '{': return { typ: EnumToken.BlockStartTokenType }; case '}': return { typ: EnumToken.BlockEndTokenType }; case '[': return { typ: EnumToken.AttrStartTokenType }; case ']': return { typ: EnumToken.AttrEndTokenType }; case ':': return { typ: EnumToken.ColonTokenType }; case ')': return { typ: EnumToken.EndParensTokenType }; case '(': return { typ: EnumToken.StartParensTokenType }; case '=': return { typ: EnumToken.DelimTokenType }; case ',': return { typ: EnumToken.CommaTokenType }; case '<': return { typ: EnumToken.LtTokenType }; case '>': return { typ: EnumToken.GtTokenType }; } if (isPseudo(val)) { return val.endsWith('(') ? { typ: EnumToken.PseudoClassFuncTokenType, val: val.slice(0, -1), chi: [] } : ( // https://www.w3.org/TR/selectors-4/#single-colon-pseudos val.startsWith('::') || pseudoElements.includes(val) ? { typ: EnumToken.PseudoElementTokenType, val } : { typ: EnumToken.PseudoClassTokenType, val }); } if (isAtKeyword(val)) { return { typ: EnumToken.AtRuleTokenType, val: val.slice(1) }; } if (isFunction(val)) { val = val.slice(0, -1); if (val == 'url') { return { typ: EnumToken.UrlFunctionTokenType, val, chi: [] }; } if (['linear-gradient', 'radial-gradient', 'repeating-linear-gradient', 'repeating-radial-gradient', 'conic-gradient', 'image', 'image-set', 'element', 'cross-fade', 'paint'].includes(val)) { return { typ: EnumToken.ImageFunctionTokenType, val, chi: [] }; } if (timingFunc.includes(val.toLowerCase())) { return { typ: EnumToken.TimingFunctionTokenType, val, chi: [] }; } if (timelineFunc.includes(val)) { return { typ: EnumToken.TimelineFunctionTokenType, val, chi: [] }; } return { typ: E