Skip to content

Commit 4097d66

Browse files
authored
Resolve computed CSS values lazily in CSSStyleDeclaration
Move computed-value resolution into CSSStyleDeclaration so getComputedStyle() resolves properties on access instead of relying on a small hardcoded set of pre-resolved properties. This improves correctness by applying computed-value rules across a broader set of properties, including inheritance/defaulting keywords and color-related values such as currentcolor and system colors. It also adds generated property metadata and caches per-declaration inputs needed for computed-value resolution. This fixes several getComputedStyle() gaps around inherited values, custom properties, opacity, font-family, and border-color behavior. Fixes #3614. Fixes #3339. Fixes #3563. Fixes #3971. Fixes #3972. Fixes #3984. Closes #4085 by superseding it. Closes #3615 by superseding it.
1 parent cf5523f commit 4097d66

11 files changed

Lines changed: 639 additions & 320 deletions

File tree

lib/jsdom/living/css/CSSStyleDeclaration-impl.js

Lines changed: 253 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
const DOMException = require("../../../generated/idl/DOMException.js");
44
const idlUtils = require("../../../generated/idl/utils.js");
5+
const propertyDefinitions = require("../../../generated/css-property-definitions");
56
const propertyDescriptors = require("../../../generated/css-property-descriptors");
7+
const propertyMetadata = require("../../../generated/css-property-metadata");
8+
const { asciiLowercase } = require("../helpers/strings");
9+
const computedStyle = require("./helpers/computed-style");
10+
const cssValues = require("./helpers/css-values");
11+
const csstree = require("./helpers/patched-csstree");
612
const {
713
borderProperties,
814
getPositionValue,
@@ -11,27 +17,26 @@ const {
1117
prepareProperties,
1218
shorthandProperties
1319
} = require("./helpers/shorthand-properties");
14-
const {
15-
hasVarFunc, isGlobalKeyword, parsePropertyValue
16-
} = require("./helpers/css-values");
17-
const csstree = require("./helpers/patched-csstree");
18-
const { asciiLowercase } = require("../helpers/strings");
20+
const { systemColors } = require("./helpers/system-colors");
1921

2022
class CSSStyleDeclarationImpl {
2123
// https://drafts.csswg.org/cssom/#css-declaration-blocks
2224
// `_priorities` and `#values` together represent the spec's "declarations".
23-
#computed;
25+
_computed;
2426
_readonly = false;
2527
_priorities = new Map();
2628
#values = new Map();
2729
parentRule;
2830
#ownerNode;
2931
#updating = false;
3032

33+
// Internal private fields.
34+
#computedValueOpts = new Map();
35+
#cachedPropertyValues = new Map();
36+
3137
constructor(globalObject, args, { computed, ownerNode, parentRule } = {}) {
3238
this._globalObject = globalObject;
33-
34-
this.#computed = Boolean(computed);
39+
this._computed = Boolean(computed);
3540
this.parentRule = parentRule || null;
3641
this.#ownerNode = ownerNode || null;
3742
}
@@ -42,7 +47,7 @@ class CSSStyleDeclarationImpl {
4247
* @returns {string} The serialized CSS text.
4348
*/
4449
get cssText() {
45-
if (this.#computed) {
50+
if (this._computed) {
4651
return "";
4752
}
4853
const properties = new Map();
@@ -111,7 +116,7 @@ class CSSStyleDeclarationImpl {
111116
if (typeof property === "string" && typeof value === "string") {
112117
const priority = important ? "important" : "";
113118
const isCustomProperty = property.startsWith("--");
114-
if (isCustomProperty || hasVarFunc(value)) {
119+
if (isCustomProperty || cssValues.hasVarFunc(value)) {
115120
if (properties.has(property)) {
116121
const { priority: itemPriority } = properties.get(property);
117122
if (!itemPriority) {
@@ -121,7 +126,7 @@ class CSSStyleDeclarationImpl {
121126
properties.set(property, { property, value, priority });
122127
}
123128
} else {
124-
const parsedValue = parsePropertyValue(property, value);
129+
const parsedValue = cssValues.parsePropertyValue(property, value);
125130
if (parsedValue) {
126131
if (properties.has(property)) {
127132
const { priority: itemPriority } = properties.get(property);
@@ -174,10 +179,25 @@ class CSSStyleDeclarationImpl {
174179
* @returns {string} The property value, or empty string if not set.
175180
*/
176181
getPropertyValue(property) {
177-
if (this.#values.has(property)) {
178-
return this.#values.get(property).toString();
182+
const value = this.#values.get(property) ?? "";
183+
if (this._computed) {
184+
if (this.#cachedPropertyValues.has(property)) {
185+
const cachedValue = this.#cachedPropertyValues.get(property);
186+
// Return the cached resolved value if the specified value haven't changed.
187+
if (value === cachedValue.value) {
188+
return cachedValue.resolvedValue;
189+
}
190+
}
191+
const resolvedValue = this.#getComputedValue(property, value);
192+
if (propertyDefinitions.has(property)) {
193+
const { longhands } = propertyDefinitions.get(property);
194+
if (!longhands) {
195+
this.#cachedPropertyValues.set(property, { resolvedValue, value });
196+
}
197+
}
198+
return resolvedValue;
179199
}
180-
return "";
200+
return value;
181201
}
182202

183203
/**
@@ -273,7 +293,7 @@ class CSSStyleDeclarationImpl {
273293

274294
// https://drafts.csswg.org/cssom/#update-style-attribute-for
275295
#updateStyleAttribute() {
276-
if (this.#computed || !this.#ownerNode || this.#ownerNode._settingCssText) {
296+
if (this._computed || !this.#ownerNode || this.#ownerNode._settingCssText) {
277297
return;
278298
}
279299
this.#ownerNode._settingCssText = true;
@@ -308,6 +328,219 @@ class CSSStyleDeclarationImpl {
308328
}
309329
}
310330

331+
#getComputedValue(property, value) {
332+
// Invalid or unsupported property.
333+
if (!propertyDefinitions.has(property) && !property.startsWith("--")) {
334+
return "";
335+
}
336+
337+
const { inherited, initial = "", longhands } = cssValues.getPropertyDefinition(property);
338+
const { caseSensitive, functionTypes = {} } = this.#getPropertyMetadata(property);
339+
const isColor = Boolean(functionTypes.color || functionTypes.paint);
340+
341+
if (!value || cssValues.isGlobalKeyword(value)) {
342+
value = computedStyle.replaceEmptyValueAndKeywords(
343+
property,
344+
value,
345+
this.#ownerNode,
346+
{ inherit: inherited === "yes", initial, isColor, longhands }
347+
);
348+
}
349+
350+
if (property === "color" && /currentcolor/i.test(value)) {
351+
value = computedStyle.getInheritedPropertyValue(
352+
property,
353+
this.#ownerNode,
354+
{ inherit: true, initial, isColor }
355+
);
356+
}
357+
358+
if (cssValues.hasVarFunc(value)) {
359+
// TODO: Resolve css var().
360+
}
361+
362+
if (longhands) {
363+
if (isColor) {
364+
value = asciiLowercase(value);
365+
if (systemColors.has(value)) {
366+
return value;
367+
}
368+
}
369+
return this.#resolveShorthand(property, value);
370+
}
371+
372+
return this.#resolveLonghand(property, value, { caseSensitive, isColor });
373+
}
374+
375+
#getPropertyMetadata(property) {
376+
if (propertyMetadata.has(property)) {
377+
return propertyMetadata.get(property);
378+
}
379+
380+
const value = this.#values.get(property) ?? "";
381+
// TODO: Also check if all or part of the value is quoted.
382+
const caseSensitive = (cssValues.hasVarFunc(value) || value.startsWith("--")) ? true : undefined;
383+
384+
return { caseSensitive };
385+
}
386+
387+
#resolveShorthand(property, value) {
388+
// TODO: resolve other shorthands e.g. background, flex etc.
389+
switch (property) {
390+
case "margin":
391+
case "padding": {
392+
return this.#resolvePositionShorthand(property);
393+
}
394+
default: {
395+
if (property.startsWith("border")) {
396+
return this.#resolveBorderShorthands(property);
397+
}
398+
return value;
399+
}
400+
}
401+
}
402+
403+
#resolvePositionShorthand(property) {
404+
const shorthandItem = shorthandProperties.get(property);
405+
if (!shorthandItem || !shorthandItem.shorthandFor) {
406+
return "";
407+
}
408+
const longhandValues = [];
409+
for (const [longhandProperty] of shorthandItem.shorthandFor) {
410+
longhandValues.push(this.getPropertyValue(longhandProperty));
411+
}
412+
return getPositionValue(longhandValues);
413+
}
414+
415+
#resolveLonghand(property, value, { caseSensitive, isColor }) {
416+
const options = this.#prepareComputedValueOpts();
417+
const parsedValue = cssValues.parsePropertyValue(property, value, {
418+
caseSensitive,
419+
...options
420+
});
421+
422+
if (isColor) {
423+
const resolvedValue = cssValues.serializeColor(parsedValue, options);
424+
if (resolvedValue) {
425+
return resolvedValue;
426+
}
427+
}
428+
429+
// TODO: Resolve special cases other than color.
430+
431+
return value;
432+
}
433+
434+
#resolveBorderShorthands(property) {
435+
switch (property) {
436+
case "border": {
437+
const values = [];
438+
for (const item of ["top", "right", "bottom", "left"]) {
439+
const value = this.getPropertyValue(`border-${item}`);
440+
if (!value) {
441+
return "";
442+
}
443+
values.push(value);
444+
}
445+
const [top, right, bottom, left] = values;
446+
if (top === right && top === bottom && top === left) {
447+
return top;
448+
}
449+
return "";
450+
}
451+
case "border-top":
452+
case "border-right":
453+
case "border-bottom":
454+
case "border-left": {
455+
const values = [];
456+
for (const item of ["width", "style", "color"]) {
457+
const value = this.getPropertyValue(`${property}-${item}`);
458+
if (!value) {
459+
return "";
460+
}
461+
values.push(value);
462+
}
463+
return values.join(" ");
464+
}
465+
// border-width, border-style, border-color
466+
default: {
467+
return this.#resolvePositionShorthand(property);
468+
}
469+
}
470+
}
471+
472+
// Options are used when resolving relative values or specified values.
473+
#prepareComputedValueOpts() {
474+
if (!this.#computedValueOpts.has("options")) {
475+
this.#computedValueOpts.set("options", { format: "computedValue" });
476+
}
477+
const options = this.#computedValueOpts.get("options");
478+
479+
// Return the cached options if the specified raw values haven't changed.
480+
const rawColorScheme = this.#values.get("color-scheme") ?? "";
481+
const rawColor = this.#values.get("color") ?? "";
482+
if (
483+
this.#computedValueOpts.get("rawColorScheme") === rawColorScheme &&
484+
this.#computedValueOpts.get("rawColor") === rawColor
485+
) {
486+
return options;
487+
}
488+
// Store current raw values for future cache validation.
489+
this.#computedValueOpts.set("rawColorScheme", rawColorScheme);
490+
this.#computedValueOpts.set("rawColor", rawColor);
491+
492+
// Prepare color-scheme.
493+
const colorScheme = computedStyle.replaceEmptyValueAndKeywords(
494+
"color-scheme",
495+
rawColorScheme,
496+
this.#ownerNode,
497+
{ inherit: true, initial: "normal" }
498+
);
499+
this.#cachedPropertyValues.set("color-scheme", {
500+
resolvedValue: colorScheme,
501+
value: rawColorScheme
502+
});
503+
options.colorScheme = colorScheme;
504+
505+
// Prepare current color.
506+
let currentColor = computedStyle.replaceEmptyValueAndKeywords(
507+
"color",
508+
rawColor,
509+
this.#ownerNode,
510+
{ inherit: true, initial: "canvastext" }
511+
);
512+
currentColor = asciiLowercase(currentColor);
513+
// Replace currentcolor keyword.
514+
if (currentColor === "currentcolor") {
515+
currentColor = computedStyle.getInheritedPropertyValue(
516+
"color",
517+
this.#ownerNode,
518+
{ inherit: true, initial: "canvastext", isColor: true }
519+
);
520+
}
521+
// Resolve system colors.
522+
if (systemColors.has(currentColor)) {
523+
currentColor = cssValues.resolveSystemColorValue(currentColor, colorScheme);
524+
} else {
525+
// Resolve named colors.
526+
if (/^[a-z]+$/.test(currentColor)) {
527+
currentColor = cssValues.resolveColor(currentColor, { format: "computedValue" });
528+
}
529+
this.#cachedPropertyValues.set("color", {
530+
resolvedValue: currentColor,
531+
value: rawColor
532+
});
533+
}
534+
options.currentColor = currentColor;
535+
536+
// TODO: Add customProperty, dimension etc.
537+
538+
// Store options.
539+
this.#computedValueOpts.set("options", options);
540+
541+
return options;
542+
}
543+
311544
/**
312545
* Helper to handle border property expansion.
313546
*
@@ -322,7 +555,7 @@ class CSSStyleDeclarationImpl {
322555
priority = this._priorities.get(property) ?? "";
323556
}
324557
if (property === "border") {
325-
properties.set(property, { propery: property, value, priority });
558+
properties.set(property, { property, value, priority });
326559
} else {
327560
for (const itemProperty of this.#values.keys()) {
328561
if (borderProperties.has(itemProperty)) {
@@ -371,13 +604,13 @@ class CSSStyleDeclarationImpl {
371604
} else {
372605
this._setProperty(property, value, priority);
373606
}
374-
if (value && !hasVarFunc(value)) {
607+
if (value && !cssValues.hasVarFunc(value)) {
375608
const longhandValues = [];
376609
const shorthandItem = shorthandProperties.get(shorthandProperty);
377610
let hasGlobalKeyword = false;
378611
for (const [longhandProperty] of shorthandItem.shorthandFor) {
379612
if (longhandProperty === property) {
380-
if (isGlobalKeyword(value)) {
613+
if (cssValues.isGlobalKeyword(value)) {
381614
hasGlobalKeyword = true;
382615
}
383616
longhandValues.push(value);
@@ -387,7 +620,7 @@ class CSSStyleDeclarationImpl {
387620
if (!longhandValue || longhandPriority !== priority) {
388621
break;
389622
}
390-
if (isGlobalKeyword(longhandValue)) {
623+
if (cssValues.isGlobalKeyword(longhandValue)) {
391624
hasGlobalKeyword = true;
392625
}
393626
longhandValues.push(longhandValue);
@@ -479,7 +712,7 @@ class CSSStyleDeclarationImpl {
479712
} else {
480713
this._setProperty(property, value, priority);
481714
}
482-
if (value && !hasVarFunc(value)) {
715+
if (value && !cssValues.hasVarFunc(value)) {
483716
const longhandValues = [];
484717
const { shorthandFor, position: shorthandPosition } = shorthandProperties.get(shorthandProperty);
485718
for (const [longhandProperty] of shorthandFor) {

0 commit comments

Comments
 (0)