diff --git a/packages/happy-dom/src/css/declaration/css-parser/CSSStyleDeclarationCSSParser.ts b/packages/happy-dom/src/css/declaration/css-parser/CSSStyleDeclarationCSSParser.ts index 34cce2a30..7298b785a 100644 --- a/packages/happy-dom/src/css/declaration/css-parser/CSSStyleDeclarationCSSParser.ts +++ b/packages/happy-dom/src/css/declaration/css-parser/CSSStyleDeclarationCSSParser.ts @@ -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; @@ -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 }; } } diff --git a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts index 9bd3080f3..c77fb160a 100644 --- a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts +++ b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts @@ -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 }); } @@ -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 }); } @@ -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; @@ -239,24 +247,27 @@ export default class CSSStyleDeclarationElementStyle { const elementStyleAttribute = (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({ @@ -274,7 +285,7 @@ export default class CSSStyleDeclarationElementStyle { } } } - }); + } } for (const name of CSSStyleDeclarationElementMeasurementProperties) { @@ -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; + elements: IStyleAndElement[]; + rootElement?: IStyleAndElement; hostElement?: IStyleAndElement; }): void { if (!options.elements.length) { @@ -326,6 +339,13 @@ export default class CSSStyleDeclarationElementStyle { priorityWeight: 0 }); } + } else if (selectorText.startsWith(':root')) { + if (options.rootElement) { + options.rootElement.cssTexts.push({ + cssText: (rule)[PropertySymbol.cssText], + priorityWeight: 0 + }); + } } else { for (const element of options.elements) { const match = QuerySelector.matches(element.element, selectorText, { diff --git a/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertyManager.ts b/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertyManager.ts index de060af2e..ce1a969c8 100644 --- a/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertyManager.ts +++ b/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertyManager.ts @@ -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); } - }); + } } } diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementStyleSheetLoader.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementStyleSheetLoader.ts index 69f9de8ac..d74bb7043 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementStyleSheetLoader.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementStyleSheetLoader.ts @@ -38,6 +38,7 @@ export default class HTMLLinkElementStyleSheetLoader { public async loadStyleSheet(url: string | null, rel: string | null): Promise { const element = this.#element; const browserSettings = this.#browserFrame.page.context.browser.settings; + const window = element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]; if ( !url || @@ -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; } @@ -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 }>( - (element[PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]) + (window) ))[PropertySymbol.readyStateManager]; this.#loadedStyleSheetURL = absoluteURL; diff --git a/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts b/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts index 9bb370282..6cdbe9584 100644 --- a/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts +++ b/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts @@ -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. @@ -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; } /** @@ -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; + } + } } diff --git a/packages/happy-dom/test/window/BrowserWindow.test.ts b/packages/happy-dom/test/window/BrowserWindow.test.ts index 2fe889f05..00a41b0fe 100644 --- a/packages/happy-dom/test/window/BrowserWindow.test.ts +++ b/packages/happy-dom/test/window/BrowserWindow.test.ts @@ -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');