Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 136 additions & 98 deletions lib/parsers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
};

/**
Expand All @@ -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;
Expand All @@ -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;
};

/**
Expand All @@ -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;
};

/**
Expand Down Expand Up @@ -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
};
40 changes: 40 additions & 0 deletions lib/utils/cache.js
Original file line number Diff line number Diff line change
@@ -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
};
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down