UNPKG

jsdom

Version:

A JavaScript implementation of many web standards

308 lines (273 loc) 10.5 kB
"use strict"; const fs = require("node:fs"); const path = require("node:path"); const Specificity = require("@bramus/specificity").default; const CSSImportRule = require("../../../../generated/idl/CSSImportRule.js"); const CSSMediaRule = require("../../../../generated/idl/CSSMediaRule.js"); const CSSStyleProperties = require("../../../../generated/idl/CSSStyleProperties.js"); const CSSStyleRule = require("../../../../generated/idl/CSSStyleRule.js"); const { asciiLowercase } = require("../../helpers/strings"); const { evaluateMediaList } = require("../MediaList-impl.js"); const { parseStyleSheet } = require("./css-parser"); const { isGlobalKeyword } = require("./css-values"); const { systemColors } = require("./system-colors"); const defaultStyleSheet = fs.readFileSync( path.resolve(__dirname, "../../../browser/default-stylesheet.css"), { encoding: "utf-8" } ); let parsedDefaultStyleSheet; 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); } clonedDeclaration._readonly = true; return clonedDeclaration; } const declaration = prepareComputedStyleDeclaration(elementImpl, styleCache); 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) { if (ruleMightMatchElement(ruleImpl, elementImpl)) { 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 ruleMightMatchElement(ruleImpl, elementImpl) { if (!ruleImpl._selectorSubjects) { const domSelector = elementImpl._ownerDocument._getDOMSelector(); ruleImpl._selectorSubjects = domSelector.extractSubjects( ruleImpl.selectorText, elementImpl._ownerDocument.contentType !== "text/html" ); } const subjects = ruleImpl._selectorSubjects; if (subjects.length === 0) { return true; } const id = elementImpl.getAttributeNS(null, "id"); const { classList } = elementImpl; // TODO: Remove toLowerCase() after updating the dom-selector. const tag = elementImpl._localName.toLowerCase(); for (const keys of subjects) { let mightMatch = true; if (keys.id && keys.id !== id) { mightMatch = false; } else if (keys.className && !classList.contains(keys.className)) { mightMatch = false; // TODO: Remove toLowerCase() after updating the dom-selector. } else if (keys.tag && keys.tag.toLowerCase() !== tag) { mightMatch = false; } if (mightMatch) { return true; } } return false; } function matches(selectorText, elementImpl) { 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 }; } function replaceEmptyValueAndKeywords(property, value, elementImpl, { inherit, initial, isColor, longhands }) { if (value === "") { if (longhands) { return ""; } else if (!inherit || !elementImpl.parentElement) { return initial; } value = getInheritedPropertyValue(property, elementImpl, { inherit, initial, isColor }); } if (isGlobalKeyword(value)) { value = replaceGlobalKeywords(property, value, elementImpl, { inherit, initial, isColor }); } return value; } function getInheritedPropertyValue(property, elementImpl, { inherit, initial, isColor }) { const styleCache = elementImpl._ownerDocument._styleCache; const { parentElement } = elementImpl; if (!parentElement) { return initial; } let parent = parentElement; while (parent) { let declaration; if (styleCache.has(parent)) { declaration = styleCache.get(parent); } else { declaration = prepareComputedStyleDeclaration(parent, styleCache); } // For color-related properties, unset the _computed flag to retrieve the specified value. // @asamuzakjp/css-color handles the resolution of the specified value. if (isColor) { declaration._computed = false; } let value = declaration.getPropertyValue(property); if (isColor) { // Restore the _computed flag. declaration._computed = true; // If the value is a system color value, retrieve it again as a computed value. if (value && systemColors.has(asciiLowercase(value))) { value = declaration.getPropertyValue(property); } } if (value) { if (isColor && isGlobalKeyword(value)) { return replaceGlobalKeywords(property, value, parent, { inherit, initial, isColor }); } return value; } else if (!parent.parentElement || !inherit) { break; } parent = parent.parentElement; } return initial; } function replaceGlobalKeywords(property, value, elementImpl, { inherit, initial, isColor }) { let element = elementImpl; while (element) { switch (value) { case "initial": { return initial; } case "inherit": { if (!element.parentElement) { return initial; } value = getInheritedPropertyValue(property, element, { inherit, initial, isColor }); break; } case "unset": { if (!inherit || !element.parentElement) { return initial; } value = getInheritedPropertyValue(property, element, { inherit, initial, isColor }); break; } case "revert-layer": { // TODO: https://drafts.csswg.org/css-cascade-5/#revert-layer return value; } case "revert": { // TODO: https://drafts.csswg.org/css-cascade-5/#default return value; } default: { // fall through; value is not a CSS-wide keyword. } } if (element.parentElement) { if (!value) { element = element.parentElement; } else if (isGlobalKeyword(value)) { return replaceGlobalKeywords(property, value, element, { inherit, initial, isColor }); } else { return value; } } else { return initial; } } return value; } exports.SHADOW_DOM_PSEUDO_REGEXP = /^::(?:part|slotted)\(/i; exports.getComputedStyleDeclaration = getComputedStyleDeclaration; exports.getInheritedPropertyValue = getInheritedPropertyValue; exports.replaceEmptyValueAndKeywords = replaceEmptyValueAndKeywords;