diff --git a/lib/parsers.js b/lib/parsers.js index 9f25fca5..a50ebf94 100644 --- a/lib/parsers.js +++ b/lib/parsers.js @@ -6,6 +6,7 @@ const { } = require("@asamuzakjp/css-color"); const { next: syntaxes } = require("@csstools/css-syntax-patches-for-csstree"); const csstree = require("css-tree"); +const { getCache, setCache } = require("./utils/cache"); const { asciiLowercase } = require("./utils/strings"); // CSS global keywords @@ -193,16 +194,25 @@ const isValidPropertyValue = (prop, val) => { } return false; } - let ast; + const cacheKey = `isValidPropertyValue_${prop}_${val}`; + const cachedValue = getCache(cacheKey); + if (typeof cachedValue === "boolean") { + return cachedValue; + } + let result; try { - ast = parseCSS(val, { - context: "value" + const ast = parseCSS(val, { + options: { + context: "value" + } }); + const { error, matched } = cssTree.lexer.matchProperty(prop, ast); + result = error === null && matched !== null; } catch { - return false; + result = false; } - const { error, matched } = cssTree.lexer.matchProperty(prop, ast); - return error === null && matched !== null; + setCache(cacheKey, result); + return result; }; /** @@ -219,6 +229,11 @@ const resolveCalc = (val, opt = { format: "specifiedValue" }) => { if (val === "" || hasVarFunc(val) || !hasCalcFunc(val)) { return val; } + const cacheKey = `resolveCalc_${val}`; + const cachedValue = getCache(cacheKey); + if (typeof cachedValue === "string") { + return cachedValue; + } const obj = parseCSS(val, { context: "value" }, true); if (!obj?.children) { return; @@ -244,7 +259,9 @@ const resolveCalc = (val, opt = { format: "specifiedValue" }) => { values.push(itemName ?? itemValue); } } - return values.join(" "); + const resolvedValue = values.join(" "); + setCache(cacheKey, resolvedValue); + return resolvedValue; }; /** @@ -270,123 +287,144 @@ const parsePropertyValue = (prop, val, opt = {}) => { } val = calculatedValue; } + const cacheKey = `parsePropertyValue_${prop}_${val}_${caseSensitive}`; + const cachedValue = getCache(cacheKey); + if (cachedValue === false) { + return; + } else if (inArray) { + if (Array.isArray(cachedValue)) { + return cachedValue; + } + } else if (typeof cachedValue === "string") { + return cachedValue; + } + let parsedValue; const lowerCasedValue = asciiLowercase(val); if (GLOBAL_KEYS.has(lowerCasedValue)) { if (inArray) { - return [ + parsedValue = [ { type: AST_TYPES.GLOBAL_KEYWORD, name: lowerCasedValue } ]; + } else { + parsedValue = lowerCasedValue; } - return lowerCasedValue; } else if (SYS_COLORS.has(lowerCasedValue)) { if (/^(?:(?:-webkit-)?(?:[a-z][a-z\d]*-)*color|border)$/i.test(prop)) { if (inArray) { - return [ + parsedValue = [ { type: AST_TYPES.IDENTIFIER, name: lowerCasedValue } ]; + } else { + parsedValue = lowerCasedValue; } - return lowerCasedValue; - } - return; - } - try { - const ast = parseCSS(val, { - context: "value" - }); - const { error, matched } = cssTree.lexer.matchProperty(prop, ast); - if (error || !matched) { - return; + } else { + parsedValue = false; } - if (inArray) { - const obj = cssTree.toPlainObject(ast); - const items = obj.children; - const parsedValues = []; - for (const item of items) { - const { children, name, type, value, unit } = item; - switch (type) { - case AST_TYPES.DIMENSION: { - parsedValues.push({ - type, - value, - unit: asciiLowercase(unit) - }); - break; - } - case AST_TYPES.FUNCTION: { - const css = cssTree - .generate(item) - .replace(/\)(?!\)|\s|,)/g, ") ") - .trim(); - const raw = items.length === 1 ? val : css; - // Remove "${name}(" from the start and ")" from the end - const itemValue = raw.slice(name.length + 1, -1).trim(); - if (name === "calc") { - if (children.length === 1) { - const [child] = children; - if (child.type === AST_TYPES.NUMBER) { - parsedValues.push({ - type: AST_TYPES.CALC, - isNumber: true, - value: `${parseFloat(child.value)}`, - name, - raw - }); + } else { + try { + const ast = parseCSS(val, { + context: "value" + }); + const { error, matched } = cssTree.lexer.matchProperty(prop, ast); + if (error || !matched) { + parsedValue = false; + } else if (inArray) { + const obj = cssTree.toPlainObject(ast); + const items = obj.children; + const values = []; + for (const item of items) { + const { children, name, type, value, unit } = item; + switch (type) { + case AST_TYPES.DIMENSION: { + values.push({ + type, + value, + unit: asciiLowercase(unit) + }); + break; + } + case AST_TYPES.FUNCTION: { + const css = cssTree + .generate(item) + .replace(/\)(?!\)|\s|,)/g, ") ") + .trim(); + const raw = items.length === 1 ? val : css; + // Remove "${name}(" from the start and ")" from the end + const itemValue = raw.slice(name.length + 1, -1).trim(); + if (name === "calc") { + if (children.length === 1) { + const [child] = children; + if (child.type === AST_TYPES.NUMBER) { + values.push({ + type: AST_TYPES.CALC, + isNumber: true, + value: `${parseFloat(child.value)}`, + name, + raw + }); + } else { + values.push({ + type: AST_TYPES.CALC, + isNumber: false, + value: `${asciiLowercase(itemValue)}`, + name, + raw + }); + } } else { - parsedValues.push({ + values.push({ type: AST_TYPES.CALC, isNumber: false, - value: `${asciiLowercase(itemValue)}`, + value: asciiLowercase(itemValue), name, raw }); } } else { - parsedValues.push({ - type: AST_TYPES.CALC, - isNumber: false, - value: asciiLowercase(itemValue), + values.push({ + type, name, + value: asciiLowercase(itemValue), raw }); } - } else { - parsedValues.push({ - type, - name, - value: asciiLowercase(itemValue), - raw - }); + break; } - break; - } - case AST_TYPES.IDENTIFIER: { - if (caseSensitive) { - parsedValues.push(item); - } else { - parsedValues.push({ - type, - name: asciiLowercase(name) - }); + case AST_TYPES.IDENTIFIER: { + if (caseSensitive) { + values.push(item); + } else { + values.push({ + type, + name: asciiLowercase(name) + }); + } + break; + } + default: { + values.push(item); } - break; - } - default: { - parsedValues.push(item); } } + parsedValue = values; + } else { + parsedValue = val; } - return parsedValues; + } catch { + parsedValue = false; } - } catch { + } + setCache(cacheKey, parsedValue); + if (parsedValue === false) { return; } - return val; + return parsedValue; }; /** @@ -639,22 +677,22 @@ const parseGradient = (val) => { }; module.exports = { - prepareValue, - isGlobalKeyword, - hasVarFunc, hasCalcFunc, - splitValue, - parseCSS, + hasVarFunc, + isGlobalKeyword, isValidPropertyValue, - resolveCalc, - parsePropertyValue, - parseNumber, + parseAngle, + parseCSS, + parseColor, + parseGradient, parseLength, - parsePercentage, parseLengthPercentage, - parseAngle, - parseUrl, + parseNumber, + parsePercentage, + parsePropertyValue, parseString, - parseColor, - parseGradient + parseUrl, + prepareValue, + resolveCalc, + splitValue }; diff --git a/lib/utils/cache.js b/lib/utils/cache.js new file mode 100644 index 00000000..6a614ee0 --- /dev/null +++ b/lib/utils/cache.js @@ -0,0 +1,40 @@ +"use strict"; + +const { LRUCache } = require("lru-cache"); + +// Instance of the LRU Cache. Stores up to 4096 items. +const lruCache = new LRUCache({ + max: 4096 +}); + +// A sentinel symbol used to store null values, as lru-cache does not support nulls. +const nullSentinel = Symbol("null"); + +/** + * Sets a value in the cache for the given key. + * If the value is null, it is internally stored as `nullSentinel`. + * + * @param {string|number} key - The cache key. + * @param {any} value - The value to be cached. + * @returns {void} + */ +const setCache = (key, value) => { + lruCache.set(key, value === null ? nullSentinel : value); +}; + +/** + * Retrieves the cached value associated with the given key. + * If the stored value is `nullSentinel`, it returns null. + * + * @param {string|number} key - The cache key. + * @returns {any|null|undefined} The cached item, undefined if the key does not exist. + */ +const getCache = (key) => { + const value = lruCache.get(key); + return value === nullSentinel ? null : value; +}; + +module.exports = { + getCache, + setCache +}; diff --git a/package-lock.json b/package-lock.json index 068408b1..3c7a278d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", - "css-tree": "^3.1.0" + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" }, "devDependencies": { "@babel/generator": "^7.28.5", diff --git a/package.json b/package.json index e4d4684b..7ef05321 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", - "css-tree": "^3.1.0" + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" }, "devDependencies": { "@babel/generator": "^7.28.5",