jsdom
Version:
A JavaScript implementation of many web standards
308 lines (273 loc) • 10.5 kB
JavaScript
"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;