Skip to content

Commit

Permalink
Merge pull request #1371 from capricorn86/1364-the-value-of-getproper…
Browse files Browse the repository at this point in the history
…tyvalue-is-wrong-when-using-style-attributes-and-custom-properties

fix: [#1364] Fixes problem with CSS variable declaration
  • Loading branch information
capricorn86 committed Apr 1, 2024
2 parents 1ad442b + 9155ec1 commit f9d4343
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 46 deletions.
@@ -1,7 +1,8 @@
// PropName => \s*([^:;]+?)\s*:
// PropValue => \s*((?:[^(;]*?(?:\([^)]*\))?)*?) <- will match any non ';' char except inside (), nested parentheses are not supported
// !important => \s*(!important)?
// EndOfRule => \s*(?:$|;)
// Groups:
// Property name => \s*([^:;]+?)\s*:
// Property value => \s*((?:[^(;]*?(?:\([^)]*\))?)*?) <- will match any non ';' char except inside (), nested parentheses are not supported
// Important ("!important") => \s*(!important)?
// End of rule => \s*(?:$|;)
const SPLIT_RULES_REGEXP =
/\s*([^:;]+?)\s*:\s*((?:[^(;]*?(?:\([^)]*\))?)*?)\s*(!important)?\s*(?:$|;)/g;

Expand All @@ -15,15 +16,29 @@ export default class CSSStyleDeclarationCSSParser {
* @param cssText CSS string.
* @param callback Callback.
*/
public static parse(
cssText: string,
callback: (name: string, value: string, important: boolean) => void
): void {
const rules = Array.from(cssText.matchAll(SPLIT_RULES_REGEXP));
for (const [, key, value, important] of rules) {
if (key && value) {
callback(key.trim(), value.trim(), !!important);
public static parse(cssText: string): {
rules: Array<{ name: string; value: string; important: boolean }>;
properties: { [name: string]: string };
} {
const properties: { [name: string]: string } = {};
const rules: Array<{ name: string; value: string; important: boolean }> = [];
const regexp = new RegExp(SPLIT_RULES_REGEXP);
let match;

while ((match = regexp.exec(cssText))) {
const name = (match[1] ?? '').trim();
const value = (match[2] ?? '').trim();
const important = match[3] ? true : false;

if (name && value) {
if (name.startsWith('--')) {
properties[name] = value;
}

rules.push({ name, value, important });
}
}

return { rules, properties };
}
}
Expand Up @@ -132,15 +132,23 @@ export default class CSSStyleDeclarationElementStyle {
if (sheet) {
this.parseCSSRules({
elements: documentElements,
rootElement:
documentElements[0].element[PropertySymbol.tagName] === 'HTML'
? documentElements[0]
: null,
cssRules: sheet.cssRules
});
}
}

for (const styleSheet of this.element[PropertySymbol.ownerDocument].adoptedStyleSheets) {
for (const sheet of this.element[PropertySymbol.ownerDocument].adoptedStyleSheets) {
this.parseCSSRules({
elements: documentElements,
cssRules: styleSheet.cssRules
rootElement:
documentElements[0].element[PropertySymbol.tagName] === 'HTML'
? documentElements[0]
: null,
cssRules: sheet.cssRules
});
}

Expand Down Expand Up @@ -170,10 +178,10 @@ export default class CSSStyleDeclarationElementStyle {
}
}

for (const styleSheet of shadowRoot.adoptedStyleSheets) {
for (const sheet of shadowRoot.adoptedStyleSheets) {
this.parseCSSRules({
elements: shadowRootElements,
cssRules: styleSheet.cssRules,
cssRules: sheet.cssRules,
hostElement: styleAndElement
});
}
Expand All @@ -190,7 +198,7 @@ export default class CSSStyleDeclarationElementStyle {
// Concatenates all parent element CSS to one string.
const targetElement = parentElements[parentElements.length - 1];
const propertyManager = new CSSStyleDeclarationPropertyManager();
const cssVariables: { [k: string]: string } = {};
const cssProperties: { [k: string]: string } = {};
let rootFontSize: string | number = 16;
let parentFontSize: string | number = 16;

Expand Down Expand Up @@ -239,24 +247,27 @@ export default class CSSStyleDeclarationElementStyle {
const elementStyleAttribute = (<Element>parentElement.element)[PropertySymbol.attributes][
'style'
];

if (elementStyleAttribute) {
elementCSSText += elementStyleAttribute[PropertySymbol.value];
}

CSSStyleDeclarationCSSParser.parse(elementCSSText, (name, value, important) => {
const isCSSVariable = name.startsWith('--');
const rulesAndProperties = CSSStyleDeclarationCSSParser.parse(elementCSSText);
const rules = rulesAndProperties.rules;

Object.assign(cssProperties, rulesAndProperties.properties);

for (const { name, value, important } of rules) {
if (
isCSSVariable ||
CSSStyleDeclarationElementInheritedProperties[name] ||
parentElement === targetElement
) {
const cssValue = this.parseCSSVariablesInValue(value, cssVariables);
if (cssValue && (!propertyManager.get(name)?.important || important)) {
propertyManager.set(name, cssValue, important);
const parsedValue = this.parseCSSVariablesInValue(value.trim(), cssProperties);

if (isCSSVariable) {
cssVariables[name] = cssValue;
} else if (name === 'font' || name === 'font-size') {
if (parsedValue && (!propertyManager.get(name)?.important || important)) {
propertyManager.set(name, parsedValue, important);

if (name === 'font' || name === 'font-size') {
const fontSize = propertyManager.properties['font-size'];
if (fontSize !== null) {
const parsedValue = this.parseMeasurementsInValue({
Expand All @@ -274,7 +285,7 @@ export default class CSSStyleDeclarationElementStyle {
}
}
}
});
}
}

for (const name of CSSStyleDeclarationElementMeasurementProperties) {
Expand Down Expand Up @@ -302,11 +313,13 @@ export default class CSSStyleDeclarationElementStyle {
* @param options Options.
* @param options.elements Elements.
* @param options.cssRules CSS rules.
* @param options.rootElement Root element.
* @param [options.hostElement] Host element.
*/
private parseCSSRules(options: {
cssRules: CSSRule[];
elements: Array<IStyleAndElement>;
elements: IStyleAndElement[];
rootElement?: IStyleAndElement;
hostElement?: IStyleAndElement;
}): void {
if (!options.elements.length) {
Expand All @@ -326,6 +339,13 @@ export default class CSSStyleDeclarationElementStyle {
priorityWeight: 0
});
}
} else if (selectorText.startsWith(':root')) {
if (options.rootElement) {
options.rootElement.cssTexts.push({
cssText: (<CSSStyleRule>rule)[PropertySymbol.cssText],
priorityWeight: 0
});
}
} else {
for (const element of options.elements) {
const match = QuerySelector.matches(<Element>element.element, selectorText, {
Expand Down
Expand Up @@ -30,11 +30,12 @@ export default class CSSStyleDeclarationPropertyManager {
*/
constructor(options?: { cssText?: string }) {
if (options?.cssText) {
CSSStyleDeclarationCSSParser.parse(options.cssText, (name, value, important) => {
if (important || !this.get(name)?.important) {
this.set(name, value, important);
const { rules } = CSSStyleDeclarationCSSParser.parse(options.cssText);
for (const rule of rules) {
if (rule.important || !this.get(rule.name)?.important) {
this.set(rule.name, rule.value, rule.important);
}
});
}
}
}

Expand Down
Expand Up @@ -38,6 +38,7 @@ export default class HTMLLinkElementStyleSheetLoader {
public async loadStyleSheet(url: string | null, rel: string | null): Promise<void> {
const element = this.#element;
const browserSettings = this.#browserFrame.page.context.browser.settings;
const window = element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow];

if (
!url ||
Expand All @@ -50,10 +51,7 @@ export default class HTMLLinkElementStyleSheetLoader {

let absoluteURL: string;
try {
absoluteURL = new URL(
url,
element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow].location.href
).href;
absoluteURL = new URL(url, window.location.href).href;
} catch (error) {
return;
}
Expand All @@ -79,10 +77,10 @@ export default class HTMLLinkElementStyleSheetLoader {

const resourceFetch = new ResourceFetch({
browserFrame: this.#browserFrame,
window: element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]
window: window
});
const readyStateManager = (<{ [PropertySymbol.readyStateManager]: DocumentReadyStateManager }>(
(<unknown>element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow])
(<unknown>window)
))[PropertySymbol.readyStateManager];

this.#loadedStyleSheetURL = absoluteURL;
Expand Down
@@ -1,6 +1,7 @@
import CSSStyleSheet from '../../css/CSSStyleSheet.js';
import * as PropertySymbol from '../../PropertySymbol.js';
import HTMLElement from '../html-element/HTMLElement.js';
import Node from '../node/Node.js';

/**
* HTML Style Element.
Expand All @@ -17,14 +18,7 @@ export default class HTMLStyleElement extends HTMLElement {
* @returns CSS style sheet.
*/
public get sheet(): CSSStyleSheet {
if (!this[PropertySymbol.isConnected]) {
return null;
}
if (!this[PropertySymbol.sheet]) {
this[PropertySymbol.sheet] = new CSSStyleSheet();
}
this[PropertySymbol.sheet].replaceSync(this.textContent);
return this[PropertySymbol.sheet];
return this[PropertySymbol.sheet] ? this[PropertySymbol.sheet] : null;
}

/**
Expand Down Expand Up @@ -84,4 +78,51 @@ export default class HTMLStyleElement extends HTMLElement {
this.setAttribute('disabled', '');
}
}

/**
* @override
*/
public override appendChild(node: Node): Node {
const returnValue = super.appendChild(node);
if (this[PropertySymbol.sheet]) {
this[PropertySymbol.sheet].replaceSync(this.textContent);
}
return returnValue;
}

/**
* @override
*/
public override removeChild(node: Node): Node {
const returnValue = super.removeChild(node);
if (this[PropertySymbol.sheet]) {
this[PropertySymbol.sheet].replaceSync(this.textContent);
}
return returnValue;
}

/**
* @override
*/
public override insertBefore(newNode: Node, referenceNode: Node | null): Node {
const returnValue = super.insertBefore(newNode, referenceNode);
if (this[PropertySymbol.sheet]) {
this[PropertySymbol.sheet].replaceSync(this.textContent);
}
return returnValue;
}

/**
* @override
*/
public override [PropertySymbol.connectToNode](parentNode: Node = null): void {
super[PropertySymbol.connectToNode](parentNode);

if (parentNode) {
this[PropertySymbol.sheet] = new CSSStyleSheet();
this[PropertySymbol.sheet].replaceSync(this.textContent);
} else {
this[PropertySymbol.sheet] = null;
}
}
}
38 changes: 38 additions & 0 deletions packages/happy-dom/test/window/BrowserWindow.test.ts
Expand Up @@ -748,6 +748,44 @@ describe('BrowserWindow', () => {
expect(computedStyle.color).toBe('green');
});

it('Handles variables in style attributes.', () => {
const div = document.createElement('div');

div.setAttribute('style', '--my-color1: pink;');

const style = document.createElement('style');

style.textContent = `
div {
border-color: var(--my-color1);
}
`;

document.head.appendChild(style);
document.body.appendChild(div);

expect(window.getComputedStyle(div).getPropertyValue('border-color')).toBe('pink');
});

it('Handles variables in root pseudo element (:root).', () => {
const div = document.createElement('div');
const style = document.createElement('style');

style.textContent = `
:root {
--my-color1: pink;
}
div {
border-color: var(--my-color1);
}
`;

document.head.appendChild(style);
document.body.appendChild(div);

expect(window.getComputedStyle(div).getPropertyValue('border-color')).toBe('pink');
});

it('Ingores invalid selectors in parsed CSS.', () => {
const parent = document.createElement('div');
const element = document.createElement('span');
Expand Down

0 comments on commit f9d4343

Please sign in to comment.