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

fix: [#1364] Fixes problem with CSS variable declaration #1371

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 };
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Loading