@tbela99/css-parser
Version:
CSS parser, minifier and validator for node and the browser
1,108 lines (1,106 loc) • 121 kB
JavaScript
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);