Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates to resolve CSS var() #3299

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
43 changes: 31 additions & 12 deletions lib/jsdom/browser/Window.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ const reportException = require("../living/helpers/runtime-script-errors");
const { getCurrentEventHandlerValue } = require("../living/helpers/create-event-accessor.js");
const { fireAnEvent } = require("../living/helpers/events");
const SessionHistory = require("../living/window/SessionHistory");
const { forEachMatchingSheetRuleOfElement, getResolvedValue, propertiesWithResolvedValueImplemented,
const { forEachMatchingSheetRuleOfElement,
ResolvedProperty,
SHADOW_DOM_PSEUDO_REGEXP } = require("../living/helpers/style-rules.js");
const CustomElementRegistry = require("../living/generated/CustomElementRegistry");
const jsGlobals = require("./js-globals.json");

const GlobalEventHandlersImpl = require("../living/nodes/GlobalEventHandlers-impl").implementation;
const WindowEventHandlersImpl = require("../living/nodes/WindowEventHandlers-impl").implementation;
const cssProperties = require("../living/helpers/css-properties");

const events = new Set([
// GlobalEventHandlers
Expand Down Expand Up @@ -797,24 +799,41 @@ function Window(options) {
const { forEach } = Array.prototype;
const { style } = elt;

// https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle
//
// TODO: Does not yet meet spec. Still needs correct handling of
// shorthand properties, "resolved" properties, and any property
// that's not yet defined in css-properties.js.
const declarations = Object.keys(cssProperties);
forEachMatchingSheetRuleOfElement(elt, rule => {
forEach.call(rule.style, property => {
declaration.setProperty(
property,
rule.style.getPropertyValue(property),
rule.style.getPropertyPriority(property)
);
if (!declarations.includes(property)) {
declarations.push(property);
}
});
});
forEach.call(style, property => {
if (!declarations.includes(property)) {
declarations.push(property);
}
});
declarations.sort();

// https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle
const declarations = Object.keys(propertiesWithResolvedValueImplemented);
forEach.call(declarations, property => {
declaration.setProperty(property, getResolvedValue(elt, property));
});
let styledProperty = new ResolvedProperty(
declaration.getPropertyValue(property),
declaration.getPropertyPriority(property)
);

forEach.call(style, property => {
declaration.setProperty(property, style.getPropertyValue(property), style.getPropertyPriority(property));
if (!styledProperty.value) {
styledProperty = cssProperties[property](elt);
}

declaration.setProperty(
property,
styledProperty.value,
styledProperty.priority
);
});

return declaration;
Expand Down
104 changes: 104 additions & 0 deletions lib/jsdom/living/helpers/css-properties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"use strict";

const { getComputedValue, PropertyRequest } = require("./style-rules");

const initialColor = "rgb(0, 0, 0)";

const properties = {
__proto__: null,

createProperty(name, initial = "", func = null, inherited = "") {
const resolvedFunc = func ? func : getComputedValue;
const doesInherit = Boolean(inherited);

this[name] = element => {
const request = new PropertyRequest(element, name, initial, doesInherit);
return resolvedFunc(request);
};
}
};

// https://drafts.csswg.org/cssom/#resolved-value
//
// Specifically, to meet this spec:
// https://drafts.csswg.org/css-color-4/#resolving-color-values
function resolveColor(element) {
// TODO: not implemented
return getComputedValue(element);
}

// https://drafts.csswg.org/cssom/#resolved-values
//
// Specfically, to meet the specifications here:
// https://drafts.csswg.org/css-sizing-3/#propdef-height
function resolveSizeProperty(element) {
// TODO: not implemented
return getComputedValue(element);
}

// Properties where default and resolved values are implemented. This is less
// than every supported property. Those that don"t appear in this object may
// not have correct behavior.
//
// https://drafts.csswg.org/indexes/#properties
properties.createProperty("background-attachment", "scroll");
properties.createProperty("background-clip", "border-box");
properties.createProperty("background-color", "transparent", resolveColor);
properties.createProperty("background-image", "none");
properties.createProperty("background-origin", "padding-box");
properties.createProperty("background-position", "0% 0%");
properties.createProperty("background-repeat", "repeat");
properties.createProperty("background-size", "auto auto");
properties.createProperty("border", `medium none ${initialColor}`);
properties.createProperty("border-bottom", "medium none currentcolor");
properties.createProperty("border-bottom-color", "currentcolor", resolveColor);
properties.createProperty("border-bottom-style", "none");
properties.createProperty("border-bottom-width", "medium");
properties.createProperty("border-left", "medium none currentcolor");
properties.createProperty("border-left-color", "currentcolor", resolveColor);
properties.createProperty("border-left-style", "none");
properties.createProperty("border-left-width", "medium");
properties.createProperty("border-right", "medium none currentcolor");
properties.createProperty("border-right-color", "currentcolor", resolveColor);
properties.createProperty("border-right-style", "none");
properties.createProperty("border-right-width", "medium");
properties.createProperty("border-spacing", "0px");
properties.createProperty("border-style", "none");
properties.createProperty("border-top", "medium none currentcolor");
properties.createProperty("border-top-color", "currentcolor", resolveColor);
properties.createProperty("border-top-style", "none");
properties.createProperty("border-top-width", "medium");
properties.createProperty("box-shadow", "none", resolveColor);
properties.createProperty("color", initialColor, resolveColor, "inherited");
properties.createProperty("filter", "none");
properties.createProperty("height", "auto", resolveSizeProperty);
properties.createProperty("margin", "0px 0px 0px 0px");
properties.createProperty("margin-bottom", "0px", resolveSizeProperty);
properties.createProperty("margin-left", "0px", resolveSizeProperty);
properties.createProperty("margin-right", "0px", resolveSizeProperty);
properties.createProperty("margin-top", "0px", resolveSizeProperty);
properties.createProperty("text-indent", "0px", getComputedValue, "inherited");
properties.createProperty("text-shadow", "none", getComputedValue, "inherited");
properties.createProperty("visibility", "visible", getComputedValue, "inherited");
properties.createProperty("width", "auto", resolveSizeProperty);

delete properties.createProperty;

// Not yet implemented and custom properties are proxied through this default
// handler.
module.exports = new Proxy(
properties,
{
get: (target, prop) => {
const property = Reflect.get(target, prop);
if (property) {
return property;
}
return element => {
const request = new PropertyRequest(element, prop);
return getComputedValue(request);
};
}
}
);

114 changes: 114 additions & 0 deletions lib/jsdom/living/helpers/custom-property-dependency-graph.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"use strict";
// This class is constructed to implement
// https://www.w3.org/TR/css-variables-1/#cycles
class CustomPropertyDependencyGraph {
// Takes one argument:
// The function to lookup the property in the list of stylesheets
constructor(propertyLookup) {
this._nodes = [];
this._propertyLookup = propertyLookup;
}

// Creates the graph starting at the given custom property
createNodesFrom(propertyName) {
this._nodes = [];
this._attachNodes(undefined, propertyName);
}

// Returns true if the graph is cyclic around the custom property
isCyclicAround(propertyName) {
const node = this._getNode(propertyName);
const isCyclic = node.find(propertyName);
this._unvisitNodes();
return isCyclic;
}

// Initializes a node with no outgoing edges
_createNode(nodeName) {
this._nodes.push({
name: nodeName,
outgoing: [],
visited: false,

find(node) {
this.visited = true;
for (const outgoingNode of this.outgoing) {
if (outgoingNode.name === node) {
return true;
}
if (!outgoingNode.visited) {
const wasFound = outgoingNode.find(node);
if (wasFound) {
return wasFound;
}
}
}
return false;
}
});
}

// Attaches the two nodes with an edge from "fromNode" to "newNode".
// If newNode doesn't exist, one is created. Upon creation, all
// outgoing edges/nodes are also created.
_attachNodes(fromNode, newNode) {
if (!exports.isCustomProperty(newNode)) {
return;
}

if (!this._contains(newNode)) {
this._createNode(newNode);
this._drawEdgeConnecting(fromNode, newNode);
const varSubstituteVal = this._propertyLookup(newNode);
const dependencies = varSubstituteVal.value.matchAll(/var\((--[-\w]+)/g);

let outgoingEdges = [];
if (dependencies) {
outgoingEdges = [...dependencies].map(match => match[1]);
}

for (const nodeAtEndOfEdge of outgoingEdges) {
this._attachNodes(newNode, nodeAtEndOfEdge);
}
} else {
this._drawEdgeConnecting(fromNode, newNode);
}
}

// Attaches the two nodes with an edge from "fromNode" to "newNode".
// No new nodes are created.
_drawEdgeConnecting(fromNodeName, toNodeName) {
if (!fromNodeName || !toNodeName) {
return;
}

const fromNodeRef = this._getNode(fromNodeName);
const toNodeRef = this._getNode(toNodeName);
if (!fromNodeRef) {
return;
}

if (!fromNodeRef.outgoing.some(e => e === toNodeRef)) {
fromNodeRef.outgoing.push(toNodeRef);
}
}

// Helper functions
_contains(nodeName) {
return this._nodes.some(e => e.name === nodeName);
}
_getNode(nodeName) {
return this._nodes.find(e => e.name === nodeName);
}
_unvisitNodes() {
this._nodes.forEach(e => {
e.visited = false;
});
}
}

exports.CustomPropertyDependencyGraph = CustomPropertyDependencyGraph;
exports.isCustomProperty = propertyName => {
return typeof propertyName === "string" &&
propertyName.indexOf("--") === 0;
};