Skip to content

Commit 2818a50

Browse files
authored
Add LRU cache for CSS parsing and validation
Should help with jsdom/jsdom#3985.
1 parent dcfbbaf commit 2818a50

3 files changed

Lines changed: 129 additions & 86 deletions

File tree

lib/parsers.js

Lines changed: 125 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const {
66
} = require("@asamuzakjp/css-color");
77
const { next: syntaxes } = require("@csstools/css-syntax-patches-for-csstree");
88
const csstree = require("css-tree");
9+
const { LRUCache } = require("lru-cache");
910
const { asciiLowercase } = require("./utils/strings");
1011

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

90+
// Instance of the LRU Cache. Stores up to 4096 items.
91+
const lruCache = new LRUCache({
92+
max: 4096
93+
});
94+
8995
/**
9096
* Prepares a stringified value.
9197
*
@@ -192,16 +198,23 @@ const isValidPropertyValue = (prop, val) => {
192198
}
193199
return false;
194200
}
195-
let ast;
201+
const cacheKey = `isValidPropertyValue_${prop}_${val}`;
202+
const cachedValue = lruCache.get(cacheKey);
203+
if (typeof cachedValue === "boolean") {
204+
return cachedValue;
205+
}
206+
let result;
196207
try {
197-
ast = parseCSS(val, {
208+
const ast = parseCSS(val, {
198209
context: "value"
199210
});
211+
const { error, matched } = cssTree.lexer.matchProperty(prop, ast);
212+
result = error === null && matched !== null;
200213
} catch {
201-
return false;
214+
result = false;
202215
}
203-
const { error, matched } = cssTree.lexer.matchProperty(prop, ast);
204-
return error === null && matched !== null;
216+
lruCache.set(cacheKey, result);
217+
return result;
205218
};
206219

207220
/**
@@ -218,6 +231,11 @@ const resolveCalc = (val, opt = { format: "specifiedValue" }) => {
218231
if (val === "" || hasVarFunc(val) || !hasCalcFunc(val)) {
219232
return val;
220233
}
234+
const cacheKey = `resolveCalc_${val}`;
235+
const cachedValue = lruCache.get(cacheKey);
236+
if (typeof cachedValue === "string") {
237+
return cachedValue;
238+
}
221239
const obj = parseCSS(val, { context: "value" }, true);
222240
if (!obj?.children) {
223241
return;
@@ -243,7 +261,9 @@ const resolveCalc = (val, opt = { format: "specifiedValue" }) => {
243261
values.push(itemName ?? itemValue);
244262
}
245263
}
246-
return values.join(" ");
264+
const resolvedValue = values.join(" ");
265+
lruCache.set(cacheKey, resolvedValue);
266+
return resolvedValue;
247267
};
248268

249269
/**
@@ -269,123 +289,144 @@ const parsePropertyValue = (prop, val, opt = {}) => {
269289
}
270290
val = calculatedValue;
271291
}
292+
const cacheKey = `parsePropertyValue_${prop}_${val}_${caseSensitive}`;
293+
const cachedValue = lruCache.get(cacheKey);
294+
if (cachedValue === false) {
295+
return;
296+
} else if (inArray) {
297+
if (Array.isArray(cachedValue)) {
298+
return cachedValue;
299+
}
300+
} else if (typeof cachedValue === "string") {
301+
return cachedValue;
302+
}
303+
let parsedValue;
272304
const lowerCasedValue = asciiLowercase(val);
273305
if (GLOBAL_KEYS.has(lowerCasedValue)) {
274306
if (inArray) {
275-
return [
307+
parsedValue = [
276308
{
277309
type: AST_TYPES.GLOBAL_KEYWORD,
278310
name: lowerCasedValue
279311
}
280312
];
313+
} else {
314+
parsedValue = lowerCasedValue;
281315
}
282-
return lowerCasedValue;
283316
} else if (SYS_COLORS.has(lowerCasedValue)) {
284317
if (/^(?:(?:-webkit-)?(?:[a-z][a-z\d]*-)*color|border)$/i.test(prop)) {
285318
if (inArray) {
286-
return [
319+
parsedValue = [
287320
{
288321
type: AST_TYPES.IDENTIFIER,
289322
name: lowerCasedValue
290323
}
291324
];
325+
} else {
326+
parsedValue = lowerCasedValue;
292327
}
293-
return lowerCasedValue;
294-
}
295-
return;
296-
}
297-
try {
298-
const ast = parseCSS(val, {
299-
context: "value"
300-
});
301-
const { error, matched } = cssTree.lexer.matchProperty(prop, ast);
302-
if (error || !matched) {
303-
return;
328+
} else {
329+
parsedValue = false;
304330
}
305-
if (inArray) {
306-
const obj = cssTree.toPlainObject(ast);
307-
const items = obj.children;
308-
const parsedValues = [];
309-
for (const item of items) {
310-
const { children, name, type, value, unit } = item;
311-
switch (type) {
312-
case AST_TYPES.DIMENSION: {
313-
parsedValues.push({
314-
type,
315-
value,
316-
unit: asciiLowercase(unit)
317-
});
318-
break;
319-
}
320-
case AST_TYPES.FUNCTION: {
321-
const css = cssTree
322-
.generate(item)
323-
.replace(/\)(?!\)|\s|,)/g, ") ")
324-
.trim();
325-
const raw = items.length === 1 ? val : css;
326-
// Remove "${name}(" from the start and ")" from the end
327-
const itemValue = raw.slice(name.length + 1, -1).trim();
328-
if (name === "calc") {
329-
if (children.length === 1) {
330-
const [child] = children;
331-
if (child.type === AST_TYPES.NUMBER) {
332-
parsedValues.push({
333-
type: AST_TYPES.CALC,
334-
isNumber: true,
335-
value: `${parseFloat(child.value)}`,
336-
name,
337-
raw
338-
});
331+
} else {
332+
try {
333+
const ast = parseCSS(val, {
334+
context: "value"
335+
});
336+
const { error, matched } = cssTree.lexer.matchProperty(prop, ast);
337+
if (error || !matched) {
338+
parsedValue = false;
339+
} else if (inArray) {
340+
const obj = cssTree.toPlainObject(ast);
341+
const items = obj.children;
342+
const values = [];
343+
for (const item of items) {
344+
const { children, name, type, value, unit } = item;
345+
switch (type) {
346+
case AST_TYPES.DIMENSION: {
347+
values.push({
348+
type,
349+
value,
350+
unit: asciiLowercase(unit)
351+
});
352+
break;
353+
}
354+
case AST_TYPES.FUNCTION: {
355+
const css = cssTree
356+
.generate(item)
357+
.replace(/\)(?!\)|\s|,)/g, ") ")
358+
.trim();
359+
const raw = items.length === 1 ? val : css;
360+
// Remove "${name}(" from the start and ")" from the end
361+
const itemValue = raw.slice(name.length + 1, -1).trim();
362+
if (name === "calc") {
363+
if (children.length === 1) {
364+
const [child] = children;
365+
if (child.type === AST_TYPES.NUMBER) {
366+
values.push({
367+
type: AST_TYPES.CALC,
368+
isNumber: true,
369+
value: `${parseFloat(child.value)}`,
370+
name,
371+
raw
372+
});
373+
} else {
374+
values.push({
375+
type: AST_TYPES.CALC,
376+
isNumber: false,
377+
value: `${asciiLowercase(itemValue)}`,
378+
name,
379+
raw
380+
});
381+
}
339382
} else {
340-
parsedValues.push({
383+
values.push({
341384
type: AST_TYPES.CALC,
342385
isNumber: false,
343-
value: `${asciiLowercase(itemValue)}`,
386+
value: asciiLowercase(itemValue),
344387
name,
345388
raw
346389
});
347390
}
348391
} else {
349-
parsedValues.push({
350-
type: AST_TYPES.CALC,
351-
isNumber: false,
352-
value: asciiLowercase(itemValue),
392+
values.push({
393+
type,
353394
name,
395+
value: asciiLowercase(itemValue),
354396
raw
355397
});
356398
}
357-
} else {
358-
parsedValues.push({
359-
type,
360-
name,
361-
value: asciiLowercase(itemValue),
362-
raw
363-
});
399+
break;
364400
}
365-
break;
366-
}
367-
case AST_TYPES.IDENTIFIER: {
368-
if (caseSensitive) {
369-
parsedValues.push(item);
370-
} else {
371-
parsedValues.push({
372-
type,
373-
name: asciiLowercase(name)
374-
});
401+
case AST_TYPES.IDENTIFIER: {
402+
if (caseSensitive) {
403+
values.push(item);
404+
} else {
405+
values.push({
406+
type,
407+
name: asciiLowercase(name)
408+
});
409+
}
410+
break;
411+
}
412+
default: {
413+
values.push(item);
375414
}
376-
break;
377-
}
378-
default: {
379-
parsedValues.push(item);
380415
}
381416
}
417+
parsedValue = values;
418+
} else {
419+
parsedValue = val;
382420
}
383-
return parsedValues;
421+
} catch {
422+
parsedValue = false;
384423
}
385-
} catch {
424+
}
425+
lruCache.set(cacheKey, parsedValue);
426+
if (parsedValue === false) {
386427
return;
387428
}
388-
return val;
429+
return parsedValue;
389430
};
390431

391432
/**

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"dependencies": {
1919
"@asamuzakjp/css-color": "^4.1.1",
2020
"@csstools/css-syntax-patches-for-csstree": "^1.0.21",
21-
"css-tree": "^3.1.0"
21+
"css-tree": "^3.1.0",
22+
"lru-cache": "^11.2.4"
2223
},
2324
"devDependencies": {
2425
"@babel/generator": "^7.28.5",

0 commit comments

Comments
 (0)