UNPKG

jsdom

Version:

A JavaScript implementation of many web standards

388 lines (353 loc) 13.2 kB
"use strict"; const fs = require("node:fs"); const path = require("node:path"); const { parseStyleSheet } = require("./css-parser"); const CSSStyleRule = require("../../../../generated/idl/CSSStyleRule.js"); const CSSImportRule = require("../../../../generated/idl/CSSImportRule.js"); const CSSMediaRule = require("../../../../generated/idl/CSSMediaRule.js"); const Specificity = require("@bramus/specificity").default; const CSSStyleProperties = require("../../../../generated/idl/CSSStyleProperties.js"); const { getSpecifiedColor, getComputedOrUsedColor } = require("./colors"); const { asciiLowercase } = require("../../helpers/strings"); const { evaluateMediaList } = require("../MediaList-impl.js"); const { deprecatedAliases, systemColors } = require("./system-colors"); const defaultStyleSheet = fs.readFileSync( path.resolve(__dirname, "../../../browser/default-stylesheet.css"), { encoding: "utf-8" } ); let parsedDefaultStyleSheet; // Properties for which getResolvedValue is implemented. This is less than // every supported property. // https://drafts.csswg.org/indexes/#properties const propertiesWithResolvedValueImplemented = { "__proto__": null, // https://drafts.csswg.org/css2/visufx.html#visibility "visibility": { inherited: true, initial: "visible", computedValue: "as-specified" }, // https://svgwg.org/svg2-draft/interact.html#PointerEventsProperty "pointer-events": { inherited: true, initial: "auto", computedValue: "as-specified" }, // https://drafts.csswg.org/css-backgrounds-3/#propdef-background-color "background-color": { inherited: false, initial: "transparent", computedValue: "computed-color" }, // https://drafts.csswg.org/css-logical-1/#propdef-border-block-end-color "border-block-start-color": { inherited: false, initial: "currentcolor", computedValue: "computed-color" }, "border-block-end-color": { inherited: false, initial: "currentcolor", computedValue: "computed-color" }, "border-inline-start-color": { inherited: false, initial: "currentcolor", computedValue: "computed-color" }, "border-inline-end-color": { inherited: false, initial: "currentcolor", computedValue: "computed-color" }, // https://drafts.csswg.org/css-backgrounds-3/#propdef-border-bottom-color "border-top-color": { inherited: false, initial: "currentcolor", computedValue: "computed-color" }, "border-right-color": { inherited: false, initial: "currentcolor", computedValue: "computed-color" }, "border-bottom-color": { inherited: false, initial: "currentcolor", computedValue: "computed-color" }, "border-left-color": { inherited: false, initial: "currentcolor", computedValue: "computed-color" }, // https://drafts.csswg.org/css-ui-4/#propdef-caret-color "caret-color": { inherited: true, initial: "auto", computedValue: "computed-color" }, // https://drafts.csswg.org/css-color-4/#propdef-color "color": { inherited: true, initial: "canvastext", computedValue: "computed-color" }, // https://drafts.csswg.org/css-ui-4/#propdef-outline-color "outline-color": { inherited: false, initial: "invert", computedValue: "computed-color" }, // https://drafts.csswg.org/css-display/#the-display-properties // Currently only "as-specified" is supported as a computed value "display": { inherited: false, initial: "inline", computedValue: "as-specified" } }; const implementedProperties = Object.keys(propertiesWithResolvedValueImplemented); function getComputedStyleDeclaration(elementImpl) { const styleCache = elementImpl._ownerDocument._styleCache; const cachedDeclaration = styleCache.get(elementImpl); if (cachedDeclaration) { const clonedDeclaration = CSSStyleProperties.createImpl(elementImpl._globalObject, [], { computed: true, ownerNode: elementImpl }); for (let i = 0; i < cachedDeclaration.length; i++) { const property = cachedDeclaration.item(i); const value = cachedDeclaration.getPropertyValue(property); const priority = cachedDeclaration.getPropertyPriority(property); clonedDeclaration.setProperty(property, value, priority); } return clonedDeclaration; } const declaration = prepareComputedStyleDeclaration(elementImpl, { styleCache }); // TODO: Remove later. for (const property of implementedProperties) { declaration.setProperty(property, getResolvedValue(elementImpl, property)); } declaration._readonly = true; return declaration; } function prepareComputedStyleDeclaration(elementImpl, { styleCache }) { const { style } = elementImpl; const declaration = CSSStyleProperties.createImpl(elementImpl._globalObject, [], { computed: true, ownerNode: elementImpl }); applyStyleSheetRules(elementImpl, declaration); for (let i = 0; i < style.length; i++) { handlePropertyForInlineStyle(style.item(i), declaration, style); } styleCache.set(elementImpl, declaration); return declaration; } function applyStyleSheetRules(elementImpl, declaration) { if (!parsedDefaultStyleSheet) { // The parsed default stylesheet will be composed of CSSOM objects from the first global object accessed. This is a // bit strange, but since we only ever access the internals of `parsedDefaultStyleSheet`, and don't expose it to // callers, it shouldn't cause any issues. parsedDefaultStyleSheet = parseStyleSheet(defaultStyleSheet, elementImpl._globalObject); } const specificities = new Map(); handleSheet(parsedDefaultStyleSheet, elementImpl, declaration, specificities); for (const sheetImpl of elementImpl._ownerDocument.styleSheets._list) { handleSheet(sheetImpl, elementImpl, declaration, specificities); } } function handleSheet(sheetImpl, elementImpl, declaration, specificities) { for (const ruleImpl of sheetImpl.cssRules._list) { if (CSSImportRule.isImpl(ruleImpl)) { if (ruleImpl.styleSheet !== null && evaluateMediaList(ruleImpl.media._list)) { for (const innerRule of ruleImpl.styleSheet.cssRules._list) { handleRule(innerRule, elementImpl, declaration, specificities); } } } else if (CSSMediaRule.isImpl(ruleImpl)) { if (evaluateMediaList(ruleImpl.media._list)) { for (const innerRule of ruleImpl.cssRules._list) { handleRule(innerRule, elementImpl, declaration, specificities); } } } else if (CSSStyleRule.isImpl(ruleImpl)) { handleRule(ruleImpl, elementImpl, declaration, specificities); } } } function handleRule(ruleImpl, elementImpl, declaration, specificities) { const { ast, match } = matches(ruleImpl.selectorText, elementImpl); if (match) { handleStyle(ruleImpl.style, declaration, specificities, ast); } } function handleStyle(style, declaration, specificities, ast) { for (let i = 0; i < style.length; i++) { const property = style.item(i); handleProperty(property, declaration, style, specificities, ast); } } function handleProperty(property, declaration, style, specificities, ast) { const value = style.getPropertyValue(property); const priority = style.getPropertyPriority(property); if (priority) { declaration.setProperty(property, value, priority); } else if (!declaration.getPropertyPriority(property)) { const { value: specificity } = Specificity.max(...Specificity.calculate(ast)); if (specificities.has(property)) { if (Specificity.compare(specificity, specificities.get(property)) >= 0) { specificities.set(property, specificity); declaration.setProperty(property, value); } } else { specificities.set(property, specificity); declaration.setProperty(property, value); } } } function handlePropertyForInlineStyle(property, declaration, style) { const value = style.getPropertyValue(property); const priority = style.getPropertyPriority(property); if (!declaration.getPropertyPriority(property) || priority) { declaration.setProperty(property, value, priority); } } function matches(selectorText, elementImpl) { try { const domSelector = elementImpl._ownerDocument._getDOMSelector(); const { ast, match, pseudoElement } = domSelector.check(selectorText, elementImpl); // `pseudoElement` is a pseudo-element selector (e.g. `::before`). // However, we do not support getComputedStyle(element, pseudoElement), so `match` is set to `false`. if (pseudoElement) { return { match: false }; } return { ast, match, pseudoElement }; } catch { // fall through } return { match: false }; } // Naive implementation of https://drafts.csswg.org/css-cascade-4/#cascading // based on the previous jsdom implementation of getComputedStyle. // Does not implement https://drafts.csswg.org/css-cascade-4/#cascade-specificity, // or rather specificity is only implemented by the order in which the matching // rules appear. The last rule is the most specific while the first rule is // the least specific. function getCascadedPropertyValue(element, property) { const cached = element._ownerDocument._styleCache.get(element); if (cached) { return cached.getPropertyValue(property); } return getComputedStyleDeclaration(element).getPropertyValue(property); } // https://drafts.csswg.org/css-cascade-4/#specified-value function getSpecifiedValue(element, property) { const { initial, inherited, computedValue } = propertiesWithResolvedValueImplemented[property]; const cascade = getCascadedPropertyValue(element, property); if (cascade !== "") { if (computedValue === "computed-color") { return getSpecifiedColor(cascade); } return cascade; } // Defaulting if (inherited && element.parentElement !== null) { return getComputedValue(element.parentElement, property); } // root element without parent element or inherited property return initial; } // https://drafts.csswg.org/css-cascade-4/#computed-value function getComputedValue(element, property) { const { computedValue, inherited, initial } = propertiesWithResolvedValueImplemented[property]; let specifiedValue = getSpecifiedValue(element, property); // https://drafts.csswg.org/css-cascade/#defaulting-keywords switch (specifiedValue) { case "initial": { specifiedValue = initial; break; } case "inherit": { if (element.parentElement !== null) { specifiedValue = getComputedValue(element.parentElement, property); } else { specifiedValue = initial; } break; } case "unset": { if (inherited && element.parentElement !== null) { specifiedValue = getComputedValue(element.parentElement, property); } else { specifiedValue = initial; } break; } // TODO: https://drafts.csswg.org/css-cascade-5/#revert-layer case "revert-layer": { break; } // TODO: https://drafts.csswg.org/css-cascade-5/#default case "revert": { break; } default: { // fall through; specifiedValue is not a CSS-wide keyword. } } if (computedValue === "as-specified") { return specifiedValue; } else if (computedValue === "computed-color") { let value = asciiLowercase(specifiedValue); // https://drafts.csswg.org/css-color-4/#resolving-other-colors if (specifiedValue === "currentcolor") { if (property === "color") { if (element.parentElement !== null) { return getComputedValue(element.parentElement, "color"); } value = initial; } else { return getComputedValue(element, "color"); } } if (systemColors.has(value) || deprecatedAliases.has(value)) { let key = value; if (deprecatedAliases.has(value)) { key = deprecatedAliases.get(value); } const { light, dark } = systemColors.get(key); const colorScheme = getCascadedPropertyValue(element, "color-scheme"); if (colorScheme === "dark") { return dark; } return light; } return getComputedOrUsedColor(specifiedValue); } throw new TypeError(`Internal error: unrecognized computed value instruction '${computedValue}'`); } // https://drafts.csswg.org/cssom/#resolved-value // Only implements the properties that are defined in propertiesWithResolvedValueImplemented. function getResolvedValue(element, property) { // We can always use the computed value with the current set of propertiesWithResolvedValueImplemented: // * Color properties end up with the used value, but we don't implement any actual differences between used and // computed that https://drafts.csswg.org/css-cascade-5/#used-value gestures at. // * The other properties fall back to the "any other property: The resolved value is the computed value." case. return getComputedValue(element, property); } function invalidateStyleCache(elementImpl) { if (elementImpl._attached) { elementImpl._ownerDocument._styleCache = new WeakMap(); } } module.exports = { SHADOW_DOM_PSEUDO_REGEXP: /^::(?:part|slotted)\(/i, getComputedStyleDeclaration, invalidateStyleCache };