Skip to content
Merged
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
209 changes: 125 additions & 84 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 { LRUCache } = require("lru-cache");
const { asciiLowercase } = require("./utils/strings");

// CSS global keywords
Expand Down Expand Up @@ -86,6 +87,11 @@ const varContainedRegEx = /(?<=[*/\s(])var\(/;
// Patched css-tree
const cssTree = csstree.fork(syntaxes);

// Instance of the LRU Cache. Stores up to 4096 items.
const lruCache = new LRUCache({
max: 4096
});

/**
* Prepares a stringified value.
*
Expand Down Expand Up @@ -192,16 +198,23 @@ const isValidPropertyValue = (prop, val) => {
}
return false;
}
let ast;
const cacheKey = `isValidPropertyValue_${prop}_${val}`;
const cachedValue = lruCache.get(cacheKey);
if (typeof cachedValue === "boolean") {
return cachedValue;
}
let result;
try {
ast = parseCSS(val, {
const ast = parseCSS(val, {
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;
lruCache.set(cacheKey, result);
return result;
};

/**
Expand All @@ -218,6 +231,11 @@ const resolveCalc = (val, opt = { format: "specifiedValue" }) => {
if (val === "" || hasVarFunc(val) || !hasCalcFunc(val)) {
return val;
}
const cacheKey = `resolveCalc_${val}`;
const cachedValue = lruCache.get(cacheKey);
if (typeof cachedValue === "string") {
return cachedValue;
}
const obj = parseCSS(val, { context: "value" }, true);
if (!obj?.children) {
return;
Expand All @@ -243,7 +261,9 @@ const resolveCalc = (val, opt = { format: "specifiedValue" }) => {
values.push(itemName ?? itemValue);
}
}
return values.join(" ");
const resolvedValue = values.join(" ");
lruCache.set(cacheKey, resolvedValue);
return resolvedValue;
};

/**
Expand All @@ -269,123 +289,144 @@ const parsePropertyValue = (prop, val, opt = {}) => {
}
val = calculatedValue;
}
const cacheKey = `parsePropertyValue_${prop}_${val}_${caseSensitive}`;
const cachedValue = lruCache.get(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"
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I just noticed. Why does this use { context: "value" } but the other one use { options: { context: "value" } }? Is there a hidden bug here?

(I was wondering if we might get some performance by caching parseCSS() results, i.e., sharing the cache between parsePropertyValue and isValidPropertyValue. But, that could be a followup anyway, so it is not blocking.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1b2c0b8

Thanks for the catch, was a typo...
{ context: "value" } is the correct option for csstree.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry a bit that our test coverage didn't catch this, but oh well...

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 {
}
lruCache.set(cacheKey, parsedValue);
if (parsedValue === false) {
return;
}
return val;
return parsedValue;
};

/**
Expand Down
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