From 06e23b3ca4ea9d22ff185b2e3c7bb967f828a384 Mon Sep 17 00:00:00 2001 From: malko Date: Thu, 13 Jul 2023 18:34:40 +0200 Subject: [PATCH 1/4] #976@minor: Add background image test cases. --- packages/happy-dom/test/css/CSSParser.test.ts | 10 +++++++--- packages/happy-dom/test/css/data/CSSParserInput.ts | 8 ++++++-- .../test/css/declaration/CSSStyleDeclaration.test.ts | 9 +++++++++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/happy-dom/test/css/CSSParser.test.ts b/packages/happy-dom/test/css/CSSParser.test.ts index 57ec19409..a02392e51 100644 --- a/packages/happy-dom/test/css/CSSParser.test.ts +++ b/packages/happy-dom/test/css/CSSParser.test.ts @@ -41,22 +41,26 @@ describe('CSSParser', () => { expect((cssRules[1]).parentStyleSheet).toBe(cssStyleSheet); expect((cssRules[1]).selectorText).toBe('.container'); expect((cssRules[1]).cssText).toBe( - '.container { flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; --css-variable: 1px; }' + '.container { flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; --css-variable: 1px; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="), url("test.jpg"); }' ); - expect((cssRules[1]).style.length).toBe(5); + expect((cssRules[1]).style.length).toBe(6); expect((cssRules[1]).style.parentRule).toBe(cssRules[1]); expect((cssRules[1]).style[0]).toBe('flex-grow'); expect((cssRules[1]).style[1]).toBe('display'); expect((cssRules[1]).style[2]).toBe('flex-direction'); expect((cssRules[1]).style[3]).toBe('overflow'); expect((cssRules[1]).style[4]).toBe('--css-variable'); + expect((cssRules[1]).style[5]).toBe('background-image'); expect((cssRules[1]).style.flexGrow).toBe('1'); expect((cssRules[1]).style.display).toBe('flex'); expect((cssRules[1]).style.flexDirection).toBe('column'); expect((cssRules[1]).style.overflow).toBe('hidden'); expect((cssRules[1]).style.getPropertyValue('--css-variable')).toBe('1px'); + expect((cssRules[1]).style.backgroundImage).toBe( + 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="), url("test.jpg")' + ); expect((cssRules[1]).style.cssText).toBe( - 'flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; --css-variable: 1px;' + 'flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; --css-variable: 1px; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="), url("test.jpg");' ); // CSSMediaRule diff --git a/packages/happy-dom/test/css/data/CSSParserInput.ts b/packages/happy-dom/test/css/data/CSSParserInput.ts index 797e6764d..b2d5688cd 100644 --- a/packages/happy-dom/test/css/data/CSSParserInput.ts +++ b/packages/happy-dom/test/css/data/CSSParserInput.ts @@ -12,6 +12,10 @@ export default ` flex-direction: column; overflow: hidden; --css-variable: 1px; + background-image: + url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="), + url(test.jpg) + ; } @media screen and (max-width: 36rem) { @@ -58,7 +62,7 @@ export default ` color: red; } } - + @supports (display: flex) { .container { color: green; @@ -74,5 +78,5 @@ export default ` /* Single-line comment */ .foo { color: red; } - + `.trim(); diff --git a/packages/happy-dom/test/css/declaration/CSSStyleDeclaration.test.ts b/packages/happy-dom/test/css/declaration/CSSStyleDeclaration.test.ts index dedb9e558..8f13e7216 100644 --- a/packages/happy-dom/test/css/declaration/CSSStyleDeclaration.test.ts +++ b/packages/happy-dom/test/css/declaration/CSSStyleDeclaration.test.ts @@ -1885,6 +1885,15 @@ describe('CSSStyleDeclaration', () => { element.setAttribute('style', 'background-image: url(test.jpg), url(test2.jpg)'); expect(declaration.backgroundImage).toBe('url("test.jpg"), url("test2.jpg")'); + + element.setAttribute( + 'style', + 'background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=)' + ); + + expect(declaration.backgroundImage).toBe( + 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=")' + ); }); }); From c3c9d803fa9671a62a99390caef7c72e290ff8b8 Mon Sep 17 00:00:00 2001 From: malko Date: Thu, 13 Jul 2023 18:36:53 +0200 Subject: [PATCH 2/4] #976@minor: Fix CSS value incorrect comma split. --- .../CSSStyleDeclarationCSSParser.ts | 31 ++++++++----------- .../CSSStyleDeclarationPropertySetParser.ts | 11 ++++--- 2 files changed, 19 insertions(+), 23 deletions(-) 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 f62282808..856933774 100644 --- a/packages/happy-dom/src/css/declaration/css-parser/CSSStyleDeclarationCSSParser.ts +++ b/packages/happy-dom/src/css/declaration/css-parser/CSSStyleDeclarationCSSParser.ts @@ -12,24 +12,19 @@ export default class CSSStyleDeclarationCSSParser { cssText: string, callback: (name: string, value: string, important: boolean) => void ): void { - const parts = cssText.split(';'); - - for (const part of parts) { - if (part) { - const [name, value]: string[] = part.trim().split(':'); - if (value) { - const trimmedName = name.trim(); - const trimmedValue = value.trim(); - if (trimmedName && trimmedValue) { - const important = trimmedValue.endsWith(' !important'); - const valueWithoutImportant = trimmedValue.replace(' !important', ''); - - if (valueWithoutImportant) { - callback(trimmedName, valueWithoutImportant, important); - } - } - } + const rules = [ + ...cssText.matchAll( + // PropName => \s*([^:;]+?)\s*: + // PropValue => \s*((?:[^(;]*?(?:\([^)]*\))?)*?) <- will match any non ';' char except inside (), nested parentheses are not supported + // !important => \s*(!important)? + // EndOfRule => \s*(?:$|;) + /\s*([^:;]+?)\s*:\s*((?:[^(;]*?(?:\([^)]*\))?)*?)\s*(!important)?\s*(?:$|;)/g + ) + ]; + rules.forEach(([, key, value, important]) => { + if (key && value) { + callback(key.trim(), value.trim(), !!important); } - } + }); } } diff --git a/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts b/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts index 541a0c21a..146d11da2 100644 --- a/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts +++ b/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts @@ -2,6 +2,7 @@ import CSSStyleDeclarationValueParser from './CSSStyleDeclarationValueParser.js' import ICSSStyleDeclarationPropertyValue from './ICSSStyleDeclarationPropertyValue.js'; const RECT_REGEXP = /^rect\((.*)\)$/i; +const SPLIT_PARTS_REGEXP = /,(?=(?:(?:(?!\))[\s\S])*\()|[^\(\)]*$)/; // Split on commas that are outside of parentheses const BORDER_STYLE = [ 'none', 'hidden', @@ -2383,7 +2384,7 @@ export default class CSSStyleDeclarationPropertySetParser { return { 'background-size': { value: lowerValue, important } }; } - const imageParts = lowerValue.split(','); + const imageParts = lowerValue.split(SPLIT_PARTS_REGEXP); const parsed = []; for (const imagePart of imageParts) { @@ -2554,7 +2555,7 @@ export default class CSSStyleDeclarationPropertySetParser { }; } - const imageParts = value.replace(/ *, */g, ',').split(','); + const imageParts = value.split(SPLIT_PARTS_REGEXP); let x = ''; let y = ''; @@ -2667,7 +2668,7 @@ export default class CSSStyleDeclarationPropertySetParser { return { 'background-position-x': { value: lowerValue, important } }; } - const imageParts = lowerValue.replace(/ *, */g, ',').split(','); + const imageParts = lowerValue.split(SPLIT_PARTS_REGEXP); let parsedValue = ''; for (const imagePart of imageParts) { @@ -2718,7 +2719,7 @@ export default class CSSStyleDeclarationPropertySetParser { return { 'background-position-y': { value: lowerValue, important } }; } - const imageParts = lowerValue.replace(/ *, */g, ',').split(','); + const imageParts = lowerValue.split(SPLIT_PARTS_REGEXP); let parsedValue = ''; for (const imagePart of imageParts) { @@ -2794,7 +2795,7 @@ export default class CSSStyleDeclarationPropertySetParser { return { 'background-image': { value: lowerValue, important } }; } - const parts = value.replace(/ *, */g, ',').split(','); + const parts = value.split(SPLIT_PARTS_REGEXP); const parsed = []; for (const part of parts) { From 4054058d16081f6ab2d169a6b576cce80813b999 Mon Sep 17 00:00:00 2001 From: malko Date: Mon, 17 Jul 2023 10:46:57 +0200 Subject: [PATCH 3/4] #976@minor: Optimizations. --- .../CSSStyleDeclarationCSSParser.ts | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) 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 856933774..34cce2a30 100644 --- a/packages/happy-dom/src/css/declaration/css-parser/CSSStyleDeclarationCSSParser.ts +++ b/packages/happy-dom/src/css/declaration/css-parser/CSSStyleDeclarationCSSParser.ts @@ -1,3 +1,10 @@ +// PropName => \s*([^:;]+?)\s*: +// PropValue => \s*((?:[^(;]*?(?:\([^)]*\))?)*?) <- will match any non ';' char except inside (), nested parentheses are not supported +// !important => \s*(!important)? +// EndOfRule => \s*(?:$|;) +const SPLIT_RULES_REGEXP = + /\s*([^:;]+?)\s*:\s*((?:[^(;]*?(?:\([^)]*\))?)*?)\s*(!important)?\s*(?:$|;)/g; + /** * CSS parser. */ @@ -12,19 +19,11 @@ export default class CSSStyleDeclarationCSSParser { cssText: string, callback: (name: string, value: string, important: boolean) => void ): void { - const rules = [ - ...cssText.matchAll( - // PropName => \s*([^:;]+?)\s*: - // PropValue => \s*((?:[^(;]*?(?:\([^)]*\))?)*?) <- will match any non ';' char except inside (), nested parentheses are not supported - // !important => \s*(!important)? - // EndOfRule => \s*(?:$|;) - /\s*([^:;]+?)\s*:\s*((?:[^(;]*?(?:\([^)]*\))?)*?)\s*(!important)?\s*(?:$|;)/g - ) - ]; - rules.forEach(([, key, value, important]) => { + 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); } - }); + } } } From 16814c875ee85782e88c85ea23f6d5aed5b612fa Mon Sep 17 00:00:00 2001 From: malko Date: Mon, 17 Jul 2023 12:15:31 +0200 Subject: [PATCH 4/4] #976@minor: Fixes #670 ensuring last ";" in cssText. --- packages/happy-dom/src/css/CSSParser.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/happy-dom/src/css/CSSParser.ts b/packages/happy-dom/src/css/CSSParser.ts index f5bfa33fe..58c929a17 100644 --- a/packages/happy-dom/src/css/CSSParser.ts +++ b/packages/happy-dom/src/css/CSSParser.ts @@ -118,7 +118,10 @@ export default class CSSParser { stack.push(parentRule); } else { if (parentRule) { - const cssText = css.substring(lastIndex, match.index).trim(); + const cssText = css + .substring(lastIndex, match.index) + .trim() + .replace(/([^;])$/, '$1;'); // Ensure last semicolon switch (parentRule.type) { case CSSRule.FONT_FACE_RULE: case CSSRule.KEYFRAME_RULE: