UNPKG

@tbela99/css-parser

Version:

CSS parser, minifier and validator for node and the browser

1,108 lines (1,106 loc) 121 kB
import { isIdentColor, parseColor, isColor } from '../syntax/syntax.js'; import { camelize, equalsIgnoreCase, dasherize } from './utils/text.js'; import { renderValue } from '../renderer/render.js'; import { EnumToken, EnumAstNodeStatus, ModuleCaseTransformEnum, ModuleScopeEnumOptions } from '../ast/types.js'; import { minify } from '../ast/minify.js'; import { expand } from '../ast/expand.js'; import { WalkerEvent, walk, walkValues } from '../ast/walk.js'; import { tokenizeStream, tokenize } from './tokenize.js'; import { ROOT, LOC, tokensfuncDefMap, STATE, PARENT, TOKENS, ERRORS, pageMarginBoxType } from '../syntax/constants.js'; import { hashAlgorithms, hash } from './utils/hash.js'; import { parseSelector } from './utils/selector.js'; import { parseDeclaration } from './utils/declaration.js'; import { getSyntaxRule } from '../validation/config.js'; import { matchSelectorSyntax, trimArray, matchAllSyntaxes, createValidationContext } from '../validation/match.js'; import { ValidationSyntaxGroupEnum } from '../validation/parser/typedef.js'; import { matchAtRuleImportSyntax } from './utils/at-rule-import.js'; import { matchAtRuleWhenElseSyntax } from './utils/at-rule-when-else.js'; import { parseAtRuleSupportSyntax } from './utils/at-rule-support.js'; import { replaceNodeOrValue, trimWhiteSpaceTokens } from './utils/token.js'; import { parseAtRuleContainerQueryList } from './utils/at-rule-container.js'; import { parseMediaqueryList } from './utils/at-rule-media.js'; import { matchAtRuleSyntax } from './utils/at-rule.js'; import { parseAtRuleFontFeatureValues } from './utils/at-rule-font-feature-values.js'; import { matchGenericSyntax } from './utils/at-rule-generic.js'; import { memoize } from './utils/cache.js'; function renderTokens(tokens, options) { if (tokens == null || tokens.length === 0) return ""; if (options != null) return tokens.map((t) => renderValue(t, options)).join(""); return tokens.map((t) => renderValue(t)).join(""); } const trimWhiteSpace = [ EnumToken.CommentTokenType, EnumToken.GtTokenType, EnumToken.GteTokenType, EnumToken.LtTokenType, EnumToken.LteTokenType, EnumToken.ColumnCombinatorTokenType, ]; const BadTokensTypes = [ EnumToken.BadCommentTokenType, EnumToken.BadCdoTokenType, EnumToken.BadUrlTokenType, EnumToken.BadStringTokenType, ]; let keyNameCounter = 0; const forbiddenStartCharacters = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"].map((c) => c.charCodeAt(0)); /** * Short-scoped name generator. * * @param localName * @param filePath * @param pattern * @param hashLength * * @returns string */ const getShortNameGenerator = memoize((localName, filePath, pattern, hashLength = 5) => { let value = keyNameCounter.toString(36); keyNameCounter++; while (forbiddenStartCharacters.includes(value.charCodeAt(0))) { value = keyNameCounter.toString(36); keyNameCounter++; } return value; }); function reject(reason) { throw new Error(reason ?? "Parsing aborted"); } /** * Transform case of key name * @param key * @param how * * @throws Error * @private */ const getKeyName = memoize((key, how) => { switch (how) { case ModuleCaseTransformEnum.CamelCase: case ModuleCaseTransformEnum.CamelCaseOnly: return camelize(key); case ModuleCaseTransformEnum.DashCase: case ModuleCaseTransformEnum.DashCaseOnly: return dasherize(key); } return key; }); /** * Generate scoped name * @param localName * @param filePath * @param pattern * @param hashLength * * @throws Error * @private */ const generateScopedName = memoize(async (localName, filePath, pattern, hashLength = 5) => { if (localName.startsWith("--")) { localName = localName.slice(2); } const matches = /.*?(([^/]+)\/)?([^/\\]*?)(\.([^?/]+))?([?].*)?$/.exec(filePath); const folder = matches?.[2]?.replace?.(/[^A-Za-z0-9_-]/g, "_") ?? ""; const fileBase = matches?.[3] ?? ""; const ext = matches?.[5] ?? ""; const path = filePath.replace(/[^A-Za-z0-9_-]/g, "_"); // sanitize localName for safe char set (replace spaces/illegal chars) const safeLocal = localName.replace(/[^A-Za-z0-9_-]/g, "_"); const hashString = `${localName}::${filePath}`; let result = ""; let inParens = 0; let key = ""; let position = 0; // Compose final scoped name. Ensure the entire class doesn't start with a digit: for (const char of pattern) { position += char.length; if (char == "[") { inParens++; if (inParens != 1) { throw new Error(`Unexpected character: '${char} at position ${position - 1}' in pattern '${pattern}'`); } continue; } if (char == "]") { inParens--; if (inParens != 0) { throw new Error(`Unexpected character: '${char}:${position - 1}'`); } let hashAlgo = null; let length = null; if (key.includes(":")) { const parts = key.split(":"); if (parts.length == 2) { // @ts-ignore [key, length] = parts; // @ts-ignore if (key == "hash" && hashAlgorithms.includes(length)) { // @ts-ignore hashAlgo = length; length = null; } } if (parts.length == 3) { // @ts-ignore [key, hashAlgo, length] = parts; } if (length != null && !Number.isInteger(+length)) { throw new Error(`Unsupported hash length: '${length}'. expecting format [hash:length] or [hash:hash-algo:length]`); } } const slice = length != null && length != fileBase.length; switch (key) { case "hash": result += await hash(hashString, length ?? hashLength, hashAlgo); break; case "name": // @ts-expect-error result += slice ? fileBase.slice(0, +length) : fileBase; break; case "local": // @ts-expect-error result += slice ? safeLocal.slice(0, +length) : localName; break; case "ext": // @ts-expect-error result += slice ? ext.slice(0, +length) : ext; break; case "path": // @ts-expect-error result += slice ? path.slice(0, +length) : path; break; case "folder": // @ts-expect-error result += slice ? folder.slice(0, +length) : folder; break; default: throw new Error(`Unsupported key: '${key}'`); } key = ""; continue; } if (inParens > 0) { key += char; } else { result += char; } } // if leading char is digit, prefix underscore (very rare) return (/^[0-9]/.test(result) ? "_" : "") + result; }); /** * Parse css string * @param iter * @param options * * @throws Error * @private */ async function doParse(iter, options = {}) { if (options.signal != null) { options.signal.addEventListener("abort", reject); } options = { src: "", sourcemap: false, minify: true, pass: 1, expandIfSyntax: false, 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: false, lenient: true, ...options, }; if (typeof options.validation !== "boolean") { options.validation = !!options.validation; } if (options.module) { options.expandNestingRules = true; } 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, nodesCount: 0, tokensCount: 0, importedBytesIn: 0, tokenize: `0ms`, parse: `0ms`, minify: `0ms`, total: `0ms`, imports: [], }; const invalidNodes = []; let ast = { typ: EnumToken.StyleSheetNodeType, chi: [], }; let tokens = []; let context = ast; ast[ROOT] = ast; if (options.sourcemap) { ast[LOC] = { sta: { ind: 0, lin: 1, col: 1, }, end: { ind: 0, lin: 1, col: 1, }, src: "", }; } let valuesHandlers; let preValuesHandlers; let postValuesHandlers; let preVisitorsHandlersMap; let visitorsHandlersMap; let postVisitorsHandlersMap; const imports = []; let item; let node; // @ts-ignore ignore error let isAsync = typeof iter[Symbol.asyncIterator] === "function"; let parensMatch = 0; let curlyBracketMatch = 0; if (options.visitor != null) { valuesHandlers = new Map(); preValuesHandlers = new Map(); postValuesHandlers = new Map(); preVisitorsHandlersMap = new Map(); visitorsHandlersMap = new Map(); postVisitorsHandlersMap = new Map(); const visitors = Object.entries(options.visitor); let key; let value; let i; for (i = 0; i < visitors.length; i++) { key = visitors[i][0]; value = visitors[i][1]; if (Number.isInteger(+key)) { visitors.splice(i + 1, 0, ...Object.entries(value)); continue; } if (Array.isArray(value)) { // @ts-ignore visitors.splice(i + 1, 0, ...value.map((item) => [key, item])); continue; } if (key in EnumToken) { if (typeof value == "function") { if (!valuesHandlers.has(EnumToken[key])) { valuesHandlers.set(EnumToken[key], []); } valuesHandlers.get(EnumToken[key]).push(value); } else if (typeof value == "object" && "type" in value && "handler" in value && value.type in WalkerEvent) { if (value.type == WalkerEvent.Enter) { if (!preValuesHandlers.has(EnumToken[key])) { preValuesHandlers.set(EnumToken[key], []); } preValuesHandlers .get(EnumToken[key]) .push(value.handler); } else if (value.type == WalkerEvent.Leave) { if (!postValuesHandlers.has(EnumToken[key])) { postValuesHandlers.set(EnumToken[key], []); } postValuesHandlers .get(EnumToken[key]) .push(value.handler); } } else { errors.push({ action: "ignore", message: `doParse: visitor.${key} is not a valid key name` }); } } else if (["Declaration", "Rule", "AtRule", "KeyframesRule", "KeyframesAtRule"].includes(key)) { if (typeof value == "function") { if (!visitorsHandlersMap.has(key)) { visitorsHandlersMap.set(key, []); } visitorsHandlersMap .get(key) .push(value); } else if (typeof value == "object") { if ("type" in value && "handler" in value && value.type in WalkerEvent) { if (value.type == WalkerEvent.Enter) { if (!preVisitorsHandlersMap.has(key)) { preVisitorsHandlersMap.set(key, []); } preVisitorsHandlersMap .get(key) .push(value.handler); } else if (value.type == WalkerEvent.Leave) { if (!postVisitorsHandlersMap.has(key)) { postVisitorsHandlersMap.set(key, []); } postVisitorsHandlersMap .get(key) .push(value.handler); } } else { if (!visitorsHandlersMap.has(key)) { visitorsHandlersMap.set(key, []); } visitorsHandlersMap .get(key) .push(value); } } else { errors.push({ action: "ignore", message: `doParse: visitor.${key} is not a valid key name` }); } } else { errors.push({ action: "ignore", message: `doParse: visitor.${key} is not a valid key name` }); } } } if (Array.isArray(iter)) { // @ts-expect-error iter = iter[Symbol.iterator](); } while ((item = isAsync ? // @ts-expect-error (await iter.next()).value : // @ts-expect-error iter.next().value)) { stats.bytesIn = item.bytesIn; stats.tokensCount++; if (BadTokensTypes.includes(item.token.typ)) { tokens.push(item.token); errors.push({ action: "drop", message: "Bad token", syntax: null, node: item.token, location: item.token[LOC], }); // bad token continue; } if (item.token.typ === EnumToken.StartParensTokenType || tokensfuncDefMap.has(item.token.typ)) { parensMatch++; } else if (item.token.typ === EnumToken.EndParensTokenType && parensMatch > 0) { parensMatch--; } if (item.token.typ === EnumToken.BlockStartTokenType) { curlyBracketMatch++; } else if (item.token.typ === EnumToken.BlockEndTokenType && curlyBracketMatch > 0) { curlyBracketMatch--; } tokens.push(item.token); // console.debug([item.token, {parensMatch, curlyBracketMatch}]); // if (parensMatch === 0) { if (parensMatch === 0 && (item.token.typ === EnumToken.SemiColonTokenType || item.token.typ === EnumToken.BlockStartTokenType || item.token.typ === EnumToken.EOFTokenType)) { node = parseNode(tokens, context, options, errors, stats, invalidNodes); 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.typ == EnumToken.BlockStartTokenType) { let inBlock = 1; tokens = [item.token]; do { item = isAsync ? // @ts-expect-error (await iter.next()).value : // @ts-expect-error iter.next().value; if (item == null) { break; } tokens.push(item.token); if (item.token.typ === EnumToken.BlockStartTokenType) { inBlock++; } else if (item.token.typ === EnumToken.BlockEndTokenType) { inBlock--; } } while (inBlock != 0); if (tokens.length > 0) { errors.push({ action: "drop", message: "invalid block", location: { src, sta: tokens[0][LOC].sta, end: tokens[tokens.length - 1][LOC].end, }, }); } } tokens = []; } else if ((parensMatch === 0 || curlyBracketMatch === 0) && item.token.typ === EnumToken.BlockEndTokenType) { parseNode(tokens, context, options, errors, stats, invalidNodes); if (context[LOC] != null) { context[LOC].end = item.token[LOC].end; } const previousNode = stack.pop(); context = (stack[stack.length - 1] ?? ast); if (options.removeEmpty && previousNode != null && previousNode.chi.length == 0 && context.chi[context.chi.length - 1] == previousNode) { context.chi.pop(); } tokens = []; parensMatch = 0; curlyBracketMatch = 0; } // } } if (tokens.length > 0) { node = parseNode(tokens, context, options, errors, stats, invalidNodes); if (node != null) { if (node.typ == EnumToken.AtRuleNodeType && "import" === node.val) { imports.push(node); } if ("chi" in node /* && node.typ != EnumToken.InvalidRuleNodeType */) { stack.push(node); context = node; } } } if (imports.length > 0 && options.resolveImport) { await Promise.all(imports.map(async (node) => { if (node[STATE] !== EnumAstNodeStatus.Validated) { return; } const token = node[TOKENS][0]; const url = token.typ == EnumToken.StringTokenType ? token.val.slice(1, -1) : token.val; try { const result = options.load(url, options.src || options.cwd); const stream = result instanceof Promise || Object.getPrototypeOf(result).constructor.name == "AsyncFunction" ? await result : result; const root = await doParse(stream instanceof ReadableStream ? tokenizeStream(stream) : tokenize({ stream, src: options.resolve(url, options.src || options.cwd).relative, buffer: "", offset: 0, position: { ind: 0, lin: 1, col: 1 }, currentPosition: { ind: -1, lin: 1, col: 0 }, }), Object.assign({}, options, { minify: false, setParent: false, src: options.resolve(url, options.src || options.cwd).relative, })); 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 ignore error errors.push({ action: "ignore", message: ("doParse: " + error.message), error }); } })); } const endParseTime = performance.now(); if (options.expandNestingRules) { ast = expand(ast); } let replacement; let callable; if (options.visitor != null) { let parens; for (const result of walk(ast)) { parens = null; if (valuesHandlers.size > 0 || preVisitorsHandlersMap.size > 0 || visitorsHandlersMap.size > 0 || postVisitorsHandlersMap.size > 0) { if ((result.node.typ == EnumToken.DeclarationNodeType && (preVisitorsHandlersMap.has("Declaration") || visitorsHandlersMap.has("Declaration") || postVisitorsHandlersMap.has("Declaration"))) || (result.node.typ == EnumToken.AtRuleNodeType && (preVisitorsHandlersMap.has("AtRule") || visitorsHandlersMap.has("AtRule") || postVisitorsHandlersMap.has("AtRule"))) || (result.node.typ == EnumToken.KeyframesAtRuleNodeType && (preVisitorsHandlersMap.has("KeyframesAtRule") || visitorsHandlersMap.has("KeyframesAtRule") || postVisitorsHandlersMap.has("KeyframesAtRule")))) { const handlers = []; const key = result.node.typ == EnumToken.DeclarationNodeType ? "Declaration" : result.node.typ == EnumToken.AtRuleNodeType ? "AtRule" : "KeyframesAtRule"; if (preVisitorsHandlersMap.has(key)) { handlers.push( // @ts-expect-error ...preVisitorsHandlersMap.get(key)); } if (visitorsHandlersMap.has(key)) { // @ts-ignore handlers.push(...visitorsHandlersMap.get(key)); } if (postVisitorsHandlersMap.has(key)) { // @ts-ignore handlers.push(...postVisitorsHandlersMap.get(key)); } let node = result.node; for (const handler of handlers) { callable = typeof handler == "function" ? handler : handler[camelize(node.typ === EnumToken.DeclarationNodeType || node.typ === EnumToken.AtRuleNodeType ? node.nam : node.val)]; if (callable == null) { continue; } // @ts-expect-error replacement = callable(node, result[PARENT], ast, function* () { if (parens == null) { // @ts-expect-error parens = [...result.parents()]; } yield* parens[Symbol.iterator](); }); if (replacement == null) { continue; } isAsync = replacement instanceof Promise || Object.getPrototypeOf(replacement).constructor.name == "AsyncFunction"; if (replacement) { replacement = await replacement; } if (replacement == null || replacement == node) { continue; } // @ts-ignore node = replacement; if (Array.isArray(node)) { break; } } if (node != result.node) { replaceNodeOrValue(result.parent, result.node, node); } } else if ((result.node.typ == EnumToken.RuleNodeType && (preVisitorsHandlersMap.has("Rule") || visitorsHandlersMap.has("Rule") || postVisitorsHandlersMap.has("Rule"))) || (result.node.typ == EnumToken.KeyFramesRuleNodeType && (preVisitorsHandlersMap.has("KeyframesRule") || visitorsHandlersMap.has("KeyframesRule") || postVisitorsHandlersMap.has("KeyframesRule")))) { const handlers = []; const key = result.node.typ == EnumToken.RuleNodeType ? "Rule" : "KeyframesRule"; if (preVisitorsHandlersMap.has(key)) { handlers.push(...preVisitorsHandlersMap.get(key)); } if (visitorsHandlersMap.has(key)) { handlers.push(...visitorsHandlersMap.get(key)); } if (postVisitorsHandlersMap.has(key)) { handlers.push(...postVisitorsHandlersMap.get(key)); } let node = result.node; for (const callable of handlers) { replacement = callable(node, result.parent, result.root, // @ts-expect-error function* () { if (parens == null) { // @ts-expect-error parens = [...result.parents()]; } yield* parens[Symbol.iterator](); }); if (replacement == null) { continue; } isAsync = replacement instanceof Promise || Object.getPrototypeOf(replacement).constructor.name == "AsyncFunction"; if (replacement) { replacement = await replacement; } if (replacement == null || replacement == node) { continue; } // @ts-ignore node = replacement; // if (Array.isArray(node)) { break; } } // @ts-ignore if (node != result.node) { // @ts-ignore replaceNodeOrValue(result.parent, result.node, node); } } else if (valuesHandlers.size > 0) { let node = null; node = result.node; if (valuesHandlers.has(node.typ)) { for (const valueHandler of valuesHandlers.get(node.typ)) { callable = valueHandler; replacement = callable(node, result.parent, ast, // @ts-expect-error function* () { if (parens == null) { // @ts-expect-error parens = [...result.parents()]; } yield* parens[Symbol.iterator](); }); if (replacement == null) { continue; } isAsync = replacement instanceof Promise || Object.getPrototypeOf(replacement).constructor.name == "AsyncFunction"; if (isAsync) { replacement = await replacement; } if (replacement != null && replacement != node) { node = replacement; } } } if (node != result.node) { // @ts-ignore replaceNodeOrValue(result[PARENT], value, node); } const tokens = Array.isArray(result.node[TOKENS]) ? result.node[TOKENS] : []; if (Array.isArray(result.node.val)) { tokens.push(...result.node.val); } if (tokens.length == 0) { continue; } for (const { value, parent, root, parents } of walkValues(tokens, result.node)) { node = value; if (valuesHandlers.has(node.typ)) { let parens = null; for (const valueHandler of valuesHandlers.get(node.typ)) { callable = valueHandler; // @ts-expect-error let result = callable(node, parent, root, function* () { if (parens == null) { // @ts-expect-error parens = [...parents()]; } yield* parens[Symbol.iterator](); }); if (result == null) { continue; } isAsync = result instanceof Promise || Object.getPrototypeOf(result).constructor.name == "AsyncFunction"; if (isAsync) { result = await result; } if (result != null && result != node) { node = result; } if (Array.isArray(node)) { break; } } } if (node != value) { // @ts-ignore replaceNodeOrValue(parent, value, node); } } } } } } // console.debug("invalid nodes", invalidNodes); if (invalidNodes.length > 0) { let k = invalidNodes.length; while (k-- > 0) { if (invalidNodes[k][STATE] == EnumAstNodeStatus.Validated || invalidNodes[k][STATE] == EnumAstNodeStatus.Unvalidated || invalidNodes[k][STATE] == EnumAstNodeStatus.ValidationFailed) { continue; } if (options.lenient && invalidNodes[k][STATE] == EnumAstNodeStatus.Unknown) { continue; } invalidNodes[k][PARENT].chi.splice(invalidNodes[k][PARENT].chi.indexOf(invalidNodes[k]), 1); } } 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; } if (options.minify) { if (ast.chi.length > 0) { let passes = options.pass ?? 1; while (passes--) { minify(ast, options, true, errors, false); } } } stats.bytesIn += stats.importedBytesIn; let endTime = performance.now(); const result = { ast, errors, stats: { ...stats, parse: `${(endParseTime - startTime).toFixed(2)}ms`, minify: `${(endTime - endParseTime).toFixed(2)}ms`, tokenize: `${(options?.parseInfo?.time ?? 0).toFixed(2)}ms`, total: `${(endTime - startTime).toFixed(2)}ms`, }, }; if (options.module) { const moduleSettings = { hashLength: 5, filePath: "", scoped: ModuleScopeEnumOptions.Local, naming: ModuleCaseTransformEnum.IgnoreCase, pattern: "", generateScopedName, ...(typeof options.module != "object" ? {} : options.module), }; const parseModuleTime = performance.now(); const namesMapping = {}; const global = new Set(); const processed = new Set(); const pattern = typeof options.module == "boolean" ? null : moduleSettings.pattern; const importMapping = {}; const cssVariablesMap = {}; const importedCssVariables = {}; let mapping = {}; let revMapping = {}; let filePath = typeof options.module == "boolean" ? options.src : (moduleSettings.filePath ?? options.src); filePath = filePath === "" ? options.src : options.resolve(filePath, options.dirname(options.src), options.cwd).relative; if (typeof options.module == "number") { if (options.module & ModuleCaseTransformEnum.CamelCase) { moduleSettings.naming = ModuleCaseTransformEnum.CamelCase; } else if (options.module & ModuleCaseTransformEnum.CamelCaseOnly) { moduleSettings.naming = ModuleCaseTransformEnum.CamelCaseOnly; } else if (options.module & ModuleCaseTransformEnum.DashCase) { moduleSettings.naming = ModuleCaseTransformEnum.DashCase; } else if (options.module & ModuleCaseTransformEnum.DashCaseOnly) { moduleSettings.naming = ModuleCaseTransformEnum.DashCaseOnly; } if (options.module & ModuleScopeEnumOptions.Global) { moduleSettings.scoped = ModuleScopeEnumOptions.Global; } if (options.module & ModuleScopeEnumOptions.Pure) { // @ts-ignore moduleSettings.scoped |= ModuleScopeEnumOptions.Pure; } if (options.module & ModuleScopeEnumOptions.Shortest) { // @ts-ignore moduleSettings.scoped |= ModuleScopeEnumOptions.Shortest; } if (options.module & ModuleScopeEnumOptions.ICSS) { // @ts-ignore moduleSettings.scoped |= ModuleScopeEnumOptions.ICSS; } } if (typeof moduleSettings.scoped == "boolean") { moduleSettings.scoped = moduleSettings.scoped ? ModuleScopeEnumOptions.Local : ModuleScopeEnumOptions.Global; } if (moduleSettings.scoped & ModuleScopeEnumOptions.Shortest) { moduleSettings.generateScopedName = getShortNameGenerator; } moduleSettings.filePath = filePath; moduleSettings.pattern = pattern != null && pattern !== "" ? pattern : filePath === "" ? `[local]_[hash]` : `[local]_[hash]_[name]`; for (const { node, parent } of walk(ast)) { if (node.typ == EnumToken.CssVariableImportTokenType) { const url = node.val.find((t) => t.typ == EnumToken.StringTokenType).val.slice(1, -1); const src = options.resolve(url, options.dirname(options.src), options.cwd); const result = options.load(src, ""); const stream = result instanceof Promise || Object.getPrototypeOf(result).constructor.name == "AsyncFunction" ? await result : result; const parseInfo = { stream, buffer: "", offset: 0, time: 0, position: { ind: 0, lin: 1, col: 1 }, currentPosition: { ind: -1, lin: 1, col: 0 }, }; const root = await doParse(stream instanceof ReadableStream ? tokenizeStream(stream, parseInfo) : tokenize(parseInfo), Object.assign({}, options, { minify: false, setParent: false, src: src.relative, })); options.parseInfo.time += parseInfo.time; cssVariablesMap[node.nam] = root.cssModuleVariables; parent.chi.splice(parent.chi.indexOf(node), 1); continue; } if (node.typ == EnumToken.CssVariableDeclarationMapTokenType) { const from = node.from.find((t) => t.typ == EnumToken.IdenTokenType || isIdentColor(t)); if (!(from.val in cssVariablesMap)) { errors.push({ node, message: `could not resolve @value import from '${from.val}'`, action: "drop", }); } else { for (const token of node.vars) { if (token.typ == EnumToken.IdenTokenType || isIdentColor(token)) { if (!(token.val in cssVariablesMap[from.val])) { errors.push({ node, message: `value '${token.val}' is not exported from '${from.val}'`, action: "drop", }); continue; } result.cssModuleVariables ??= {}; result.cssModuleVariables[token.val] = importedCssVariables[token.val] = cssVariablesMap[from.val][token.val]; } } } parent.chi.splice(parent.chi.indexOf(node), 1); continue; } if (node.typ == EnumToken.CssVariableTokenType) { if (parent?.typ == EnumToken.StyleSheetNodeType) { if (result.cssModuleVariables == null) { result.cssModuleVariables = {}; } result.cssModuleVariables[node.nam] = node; } parent.chi.splice(parent.chi.indexOf(node), 1); continue; } if (node.typ == EnumToken.DeclarationNodeType) { if (node.nam.startsWith("--")) { if (!(node.nam in namesMapping)) { let result = moduleSettings.scoped & ModuleScopeEnumOptions.Global ? node.nam : moduleSettings.generateScopedName(node.nam, moduleSettings.filePath, moduleSettings.pattern, moduleSettings.hashLength); let value = result instanceof Promise ? await result : result; mapping[node.nam] = "--" + (moduleSettings.naming & ModuleCaseTransformEnum.DashCaseOnly || moduleSettings.naming & ModuleCaseTransformEnum.CamelCaseOnly ? getKeyName(value, moduleSettings.naming) : value); revMapping[node.nam] = node.nam; } node.nam = mapping[node.nam]; } if (equalsIgnoreCase("composes", node.nam)) { const tokens = []; // let isValid: boolean = true; for (const token of node.val) { if (token.typ == EnumToken.ComposesSelectorNodeType) { tokens.push(token); } } // find parent rule let parentRule = node[PARENT]; while (parentRule != null && parentRule.typ != EnumToken.RuleNodeType) { parentRule = parentRule[PARENT]; } if ( /* !isValid || */tokens.length == 0) { errors.push({ action: "drop", message: `composes is empty`, node, }); parentRule.chi.splice(parentRule.chi.indexOf(node), 1); continue; } for (const token of tokens) { // composes: a b c; if (token.r == null) { for (const rule of token.l) { if (rule.typ == EnumToken.WhitespaceTokenType || rule.typ == EnumToken.CommentTokenType) { continue; } if (!(rule.val in mapping)) { let result = moduleSettings.scoped & ModuleScopeEnumOptions.Global ? rule.val : moduleSettings.generateScopedName(rule.val, moduleSettings.filePath, moduleSettings.pattern, moduleSettings.hashLength); let value = result instanceof Promise ? await result : result; mapping[rule.val] = (rule.typ == EnumToken.DashedIdenTokenType ? "--" : "") + (moduleSettings.naming & ModuleCaseTransformEnum.DashCaseOnly || moduleSettings.naming & ModuleCaseTransformEnum.CamelCaseOnly ? getKeyName(value, moduleSettings.naming) : value); revMapping[mapping[rule.val]] = rule.val; } if (parentRule != null) { for (const tk of parentRule[TOKENS]) { if (tk.typ == EnumToken.ClassSelectorTokenType) { const val = tk.val.slice(1); if (val in revMapping) { const key = revMapping[val]; mapping[key] = [ ...new Set([ ...mapping[key].split(" "), mapping[rule.val], ]), ].join(" "); } } } } } } // composes: a b c from 'file.css'; else if (token.r.typ == EnumToken.String) { const url = token.r.val.slice(1, -1); const src = options.resolve(url, options.dirname(options.src), options.cwd); const result = options.load(src, ""); const stream = result instanceof Promise || Object.getPrototypeOf(result).constructor.name == "AsyncFunction" ? await result : result; const root = await doParse(stream instanceof ReadableStream ? tokenizeStream(stream) : tokenize({ stream, buffer: "", offset: 0, position: { ind: 0, lin: 1, col: 1 }, currentPosition: { ind: -1, lin: 1, col: 0 }, }), Object.assign({}, options, { minify: false, setParent: false, src: src.relative, })); const srcIndex = (src.relative.startsWith("/") || src.relative.startsWith("../") ? "" : "./") + src.relative; if (Object.keys(root.mapping).length > 0) { importMapping[srcIndex] = {}; } if (parentRule != null) { for (const tk of parentRule[TOKENS]) { if (tk.typ == EnumToken.ClassSelectorTokenType) { const val = tk.val.slice(1); if (val in revMapping) { const key = revMapping[val]; const values = []; for (const iden of token.l) { if (iden.typ != EnumToken.IdenTokenType && iden.typ != EnumToken.DashedIdenTokenType) { continue; } if (!(iden.val in root.mapping)) { const result = moduleSettings.scoped & ModuleScopeEnumOptions.Global ? iden.val : moduleSettings.generateScopedName(iden.val, srcIndex, moduleSettings.pattern, moduleSettings.hashLength); let value = result instanceof Promise ? await result : result; root.mapping[iden.val] = moduleSettings.naming & ModuleCaseTransformEnum.DashCaseOnly || moduleSettings.naming & ModuleCaseTransformEnum.CamelCaseOnly ? getKeyName(value, moduleSettings.naming) : value; root.revMapping[root.mapping[iden.val]] = iden.val; } importMapping[srcIndex][iden.val] = root.mapping[iden.val]; values.push(root.mapping[iden.val]); } mapping[key] = [...new Set([...mapping[key].split(" "), ...values])].join(" "); } } } } } // composes: a b c from global; else if (token.r.typ == EnumToken.IdenTokenType) { // global if (parentRule != null) { if (equalsIgnoreCase("global", token.r.val)) { for (const tk of parentRule[TOKENS]) { if (tk.typ == EnumToken.ClassSelectorTokenType) { const val = tk.val.slice(1); if (val in revMapping) { const key = revMapping[val]; mapping[key] = [ ...new Set([ ...mapping[key].split(" "), ...token.l.reduce((acc, curr) => { if (curr.typ == EnumToken.IdenTokenType) { acc.push(curr.val);