Skip to content

Commit

Permalink
fix: [#1364] Fixes problem related to CSS properties not being used w…
Browse files Browse the repository at this point in the history
…hen defined after the CSS value
  • Loading branch information
capricorn86 committed Apr 1, 2024
1 parent c0e1b48 commit afbf758
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 65 deletions.
1 change: 1 addition & 0 deletions packages/happy-dom/src/PropertySymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,4 @@ export const navigator = Symbol('navigator');
export const screen = Symbol('screen');
export const sessionStorage = Symbol('sessionStorage');
export const localStorage = Symbol('localStorage');
export const cssProperties = Symbol('cssProperties');
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
@@ -1,6 +1,6 @@
import ShadowRoot from '../../../nodes/shadow-root/ShadowRoot.js';
import * as PropertySymbol from '../../../PropertySymbol.js';
import Element from '../../../nodes/element/Element.js';
import HTMLElement from '../../../nodes/html-element/HTMLElement.js';
import Document from '../../../nodes/document/Document.js';
import HTMLStyleElement from '../../../nodes/html-style-element/HTMLStyleElement.js';
import NodeList from '../../../nodes/node/NodeList.js';
Expand All @@ -19,11 +19,19 @@ import CSSMeasurementConverter from '../measurement-converter/CSSMeasurementConv
import MediaQueryList from '../../../match-media/MediaQueryList.js';
import WindowBrowserSettingsReader from '../../../window/WindowBrowserSettingsReader.js';

// 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;

const CSS_VARIABLE_REGEXP = /var\( *(--[^), ]+)\)|var\( *(--[^), ]+), *([^), ]+)\)/;
const CSS_MEASUREMENT_REGEXP = /[0-9.]+(px|rem|em|vw|vh|%|vmin|vmax|cm|mm|in|pt|pc|Q)/g;

type IStyleAndElement = {
element: Element | ShadowRoot | Document;
element: HTMLElement | ShadowRoot | Document;
cssTexts: Array<{ cssText: string; priorityWeight: number }>;
};

Expand All @@ -41,7 +49,7 @@ export default class CSSStyleDeclarationElementStyle {
documentCacheID: null
};

private element: Element;
private element: HTMLElement;
private computed: boolean;

/**
Expand All @@ -50,7 +58,7 @@ export default class CSSStyleDeclarationElementStyle {
* @param element Element.
* @param [computed] Computed.
*/
constructor(element: Element, computed = false) {
constructor(element: HTMLElement, computed = false) {
this.element = element;
this.computed = computed;
}
Expand Down Expand Up @@ -89,7 +97,7 @@ export default class CSSStyleDeclarationElementStyle {
const documentElements: Array<IStyleAndElement> = [];
const parentElements: Array<IStyleAndElement> = [];
let styleAndElement: IStyleAndElement = {
element: <Element | ShadowRoot | Document>this.element,
element: <HTMLElement | ShadowRoot | Document>this.element,
cssTexts: []
};
let shadowRootElements: Array<IStyleAndElement> = [];
Expand Down Expand Up @@ -132,15 +140,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 All @@ -155,7 +171,7 @@ export default class CSSStyleDeclarationElementStyle {
);

styleAndElement = {
element: <Element>shadowRoot.host,
element: <HTMLElement>shadowRoot.host,
cssTexts: []
};

Expand All @@ -170,18 +186,18 @@ 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
});
}

shadowRootElements = [];
} else {
styleAndElement = {
element: <Element>styleAndElement.element[PropertySymbol.parentNode],
element: <HTMLElement>styleAndElement.element[PropertySymbol.parentNode],
cssTexts: []
};
}
Expand All @@ -190,7 +206,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 All @@ -200,63 +216,66 @@ export default class CSSStyleDeclarationElementStyle {
let elementCSSText = '';
if (
CSSStyleDeclarationElementDefaultCSS[
(<Element>parentElement.element)[PropertySymbol.tagName]
(<HTMLElement>parentElement.element)[PropertySymbol.tagName]
]
) {
if (
typeof CSSStyleDeclarationElementDefaultCSS[
(<Element>parentElement.element)[PropertySymbol.tagName]
(<HTMLElement>parentElement.element)[PropertySymbol.tagName]
] === 'string'
) {
elementCSSText +=
CSSStyleDeclarationElementDefaultCSS[
(<Element>parentElement.element)[PropertySymbol.tagName]
(<HTMLElement>parentElement.element)[PropertySymbol.tagName]
];
} else {
for (const key of Object.keys(
CSSStyleDeclarationElementDefaultCSS[
(<Element>parentElement.element)[PropertySymbol.tagName]
(<HTMLElement>parentElement.element)[PropertySymbol.tagName]
]
)) {
if (key === 'default' || !!parentElement.element[key]) {
elementCSSText +=
CSSStyleDeclarationElementDefaultCSS[
(<Element>parentElement.element)[PropertySymbol.tagName]
(<HTMLElement>parentElement.element)[PropertySymbol.tagName]
][key];
}
}
}
elementCSSText +=
CSSStyleDeclarationElementDefaultCSS[
(<Element>parentElement.element)[PropertySymbol.tagName]
(<HTMLElement>parentElement.element)[PropertySymbol.tagName]
];
}

for (const cssText of parentElement.cssTexts) {
elementCSSText += cssText.cssText;
}

const elementStyleAttribute = (<Element>parentElement.element)[PropertySymbol.attributes][
const elementStyleAttribute = (<HTMLElement>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 (parsedValue && (!propertyManager.get(name)?.important || important)) {
propertyManager.set(name, parsedValue, important);

if (isCSSVariable) {
cssVariables[name] = cssValue;
} else if (name === 'font' || name === 'font-size') {
if (name === 'font' || name === 'font-size') {
const fontSize = propertyManager.properties['font-size'];
if (fontSize !== null) {
const parsedValue = this.parseMeasurementsInValue({
Expand All @@ -265,7 +284,7 @@ export default class CSSStyleDeclarationElementStyle {
parentFontSize,
parentSize: parentFontSize
});
if ((<Element>parentElement.element)[PropertySymbol.tagName] === 'HTML') {
if ((<HTMLElement>parentElement.element)[PropertySymbol.tagName] === 'HTML') {
rootFontSize = parsedValue;
} else if (parentElement !== targetElement) {
parentFontSize = parsedValue;
Expand All @@ -274,7 +293,7 @@ export default class CSSStyleDeclarationElementStyle {
}
}
}
});
}
}

for (const name of CSSStyleDeclarationElementMeasurementProperties) {
Expand Down Expand Up @@ -302,11 +321,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,9 +347,16 @@ 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, {
const match = QuerySelector.matches(<HTMLElement>element.element, selectorText, {
ignoreErrors: true
});
if (match) {
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
Loading

0 comments on commit afbf758

Please sign in to comment.