From a78cd8f28d8b83cff087aab826b2ed97920b2813 Mon Sep 17 00:00:00 2001 From: Yusuke Iinuma Date: Tue, 5 Nov 2024 09:57:41 +0900 Subject: [PATCH] feat: [#1147] Adds support for aspect-ratio to CSSStyleDeclaration (#1537) * feat: [#1147] Adds support for the aspect-ratio property * chore: [#1147] Adds support for aspect-ratio to CSSStyleDeclaration * chore: [#1147] Adds support for aspect-ratio to CSSStyleDeclaration * chore: [#1147] Adds support for aspect-ratio to CSSStyleDeclaration --------- Co-authored-by: David Ortner --- .../css/declaration/CSSStyleDeclaration.ts | 8 ++ .../CSSStyleDeclarationPropertyManager.ts | 3 + .../CSSStyleDeclarationPropertySetParser.ts | 123 +++++++++++++----- .../declaration/CSSStyleDeclaration.test.ts | 40 ++++++ 4 files changed, 144 insertions(+), 30 deletions(-) diff --git a/packages/happy-dom/src/css/declaration/CSSStyleDeclaration.ts b/packages/happy-dom/src/css/declaration/CSSStyleDeclaration.ts index ec4b81318..578a6d7e0 100644 --- a/packages/happy-dom/src/css/declaration/CSSStyleDeclaration.ts +++ b/packages/happy-dom/src/css/declaration/CSSStyleDeclaration.ts @@ -4791,6 +4791,14 @@ export default class CSSStyleDeclaration { this.setProperty('container-name', value); } + public get aspectRatio(): string { + return this.getPropertyValue('aspect-ratio'); + } + + public set aspectRatio(value: string) { + this.setProperty('aspect-ratio', value); + } + /* eslint-enable jsdoc/require-jsdoc */ /** 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 ce1a969c8..bab47d96e 100644 --- a/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertyManager.ts +++ b/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertyManager.ts @@ -509,6 +509,9 @@ export default class CSSStyleDeclarationPropertyManager { case 'visibility': properties = CSSStyleDeclarationPropertySetParser.getVisibility(value, important); break; + case 'aspect-ratio': + properties = CSSStyleDeclarationPropertySetParser.getAspectRatio(value, important); + break; default: const trimmedValue = value.trim(); 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 39d9f7b04..8c2a5c61a 100644 --- a/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts +++ b/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts @@ -2,8 +2,9 @@ import CSSStyleDeclarationValueParser from './CSSStyleDeclarationValueParser.js' import ICSSStyleDeclarationPropertyValue from './ICSSStyleDeclarationPropertyValue.js'; const RECT_REGEXP = /^rect\((.*)\)$/i; -const SPLIT_COMMA_SEPARATED_REGEXP = /,(?=(?:(?:(?!\))[\s\S])*\()|[^\(\)]*$)/; // Split on commas that are outside of parentheses -const SPLIT_SPACE_SEPARATED_REGEXP = /\s+(?=(?:(?:(?!\))[\s\S])*\()|[^\(\)]*$)/; // Split on spaces that are outside of parentheses +const SPLIT_COMMA_SEPARATED_WITH_PARANTHESES_REGEXP = /,(?=(?:(?:(?!\))[\s\S])*\()|[^\(\)]*$)/; // Split on commas that are outside of parentheses +const SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP = /\s+(?=(?:(?:(?!\))[\s\S])*\()|[^\(\)]*$)/; // Split on spaces that are outside of parentheses +const WHITE_SPACE_GLOBAL_REGEXP = /\s+/gm; const BORDER_STYLE = [ 'none', 'hidden', @@ -497,7 +498,7 @@ export default class CSSStyleDeclarationPropertySetParser { ...this.getOutlineWidth('initial', important) }; - const parts = value.split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = value.split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); for (const part of parts) { const width = this.getOutlineWidth(part, important); @@ -649,7 +650,9 @@ export default class CSSStyleDeclarationPropertySetParser { ...this.getBorderImage('initial', important) }; - const parts = value.replace(/\s*,\s*/g, ',').split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = value + .replace(/\s*,\s*/g, ',') + .split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); for (const part of parts) { const width = this.getBorderWidth(part, important); @@ -695,7 +698,7 @@ export default class CSSStyleDeclarationPropertySetParser { }; } - const parts = value.split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = value.split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); const top = this.getBorderTopWidth(parts[0], important); const right = this.getBorderRightWidth(parts[1] || parts[0], important); const bottom = this.getBorderBottomWidth(parts[2] || parts[0], important); @@ -741,7 +744,7 @@ export default class CSSStyleDeclarationPropertySetParser { }; } - const parts = value.split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = value.split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); const top = this.getBorderTopStyle(parts[0], important); const right = this.getBorderRightStyle(parts[1] || parts[0], important); const bottom = this.getBorderBottomStyle(parts[2] || parts[0], important); @@ -788,7 +791,7 @@ export default class CSSStyleDeclarationPropertySetParser { }; } - const parts = value.split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = value.split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); const top = this.getBorderTopColor(parts[0], important); const right = this.getBorderRightColor(parts[1] || parts[0], important); const bottom = this.getBorderBottomColor(parts[2] || parts[0], important); @@ -843,7 +846,7 @@ export default class CSSStyleDeclarationPropertySetParser { parsedValue = parsedValue.replace(sourceMatch[0], ''); } - const parts = parsedValue.split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = parsedValue.split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); if (sourceMatch) { parts.push(sourceMatch[1]); @@ -1038,7 +1041,7 @@ export default class CSSStyleDeclarationPropertySetParser { }; } - const parts = lowerValue.split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = lowerValue.split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); if (parts.length > 4) { return null; @@ -1099,7 +1102,7 @@ export default class CSSStyleDeclarationPropertySetParser { }; } - const parts = value.split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = value.split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); if (parts.length > 4) { return null; @@ -1154,7 +1157,7 @@ export default class CSSStyleDeclarationPropertySetParser { }; } - const parts = lowerValue.split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = lowerValue.split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); if (parts.length > 2) { return null; @@ -1545,7 +1548,7 @@ export default class CSSStyleDeclarationPropertySetParser { }; } - const parts = value.split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = value.split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); const topLeft = this.getBorderTopLeftRadius(parts[0], important); const topRight = this.getBorderTopRightRadius(parts[1] || parts[0], important); const bottomRight = this.getBorderBottomRightRadius(parts[2] || parts[0], important); @@ -1683,7 +1686,7 @@ export default class CSSStyleDeclarationPropertySetParser { ...this.getBorderTopColor('initial', important) }; - const parts = value.split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = value.split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); for (const part of parts) { const width = this.getBorderTopWidth(part, important); @@ -1732,7 +1735,7 @@ export default class CSSStyleDeclarationPropertySetParser { ...this.getBorderRightColor('initial', important) }; - const parts = value.split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = value.split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); for (const part of parts) { const width = this.getBorderRightWidth(part, important); @@ -1781,7 +1784,7 @@ export default class CSSStyleDeclarationPropertySetParser { ...this.getBorderBottomColor('initial', important) }; - const parts = value.split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = value.split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); for (const part of parts) { const width = this.getBorderBottomWidth(part, important); @@ -1830,7 +1833,7 @@ export default class CSSStyleDeclarationPropertySetParser { ...this.getBorderLeftColor('initial', important) }; - const parts = value.split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = value.split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); for (const part of parts) { const width = this.getBorderLeftWidth(part, important); @@ -1873,7 +1876,7 @@ export default class CSSStyleDeclarationPropertySetParser { }; } - const parts = value.split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = value.split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); const top = this.getPaddingTop(parts[0], important); const right = this.getPaddingRight(parts[1] || parts[0], important); const bottom = this.getPaddingBottom(parts[2] || parts[0], important); @@ -2006,7 +2009,7 @@ export default class CSSStyleDeclarationPropertySetParser { }; } - const parts = value.split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = value.split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); const top = this.getMarginTop(parts[0], important); const right = this.getMarginRight(parts[1] || parts[0], important); const bottom = this.getMarginBottom(parts[2] || parts[0], important); @@ -2164,7 +2167,7 @@ export default class CSSStyleDeclarationPropertySetParser { }; } - const parts = value.split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = value.split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); const flexGrow = this.getFlexGrow(parts[0], important); const flexShrink = this.getFlexShrink(parts[1] || '1', important); const flexBasis = this.getFlexBasis(parts[2] || '0%', important); @@ -2300,7 +2303,7 @@ export default class CSSStyleDeclarationPropertySetParser { const parts = value .replace(/\s,\s/g, ',') .replace(/\s\/\s/g, '/') - .split(SPLIT_SPACE_SEPARATED_REGEXP); + .split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); const backgroundPositions = []; @@ -2397,7 +2400,7 @@ export default class CSSStyleDeclarationPropertySetParser { return { 'background-size': { value: lowerValue, important } }; } - const imageParts = lowerValue.split(SPLIT_COMMA_SEPARATED_REGEXP); + const imageParts = lowerValue.split(SPLIT_COMMA_SEPARATED_WITH_PARANTHESES_REGEXP); const parsed = []; for (const imagePart of imageParts) { @@ -2568,12 +2571,12 @@ export default class CSSStyleDeclarationPropertySetParser { }; } - const imageParts = value.split(SPLIT_COMMA_SEPARATED_REGEXP); + const imageParts = value.split(SPLIT_COMMA_SEPARATED_WITH_PARANTHESES_REGEXP); let x = ''; let y = ''; for (const imagePart of imageParts) { - const parts = imagePart.trim().split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = imagePart.trim().split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); if (x) { x += ','; @@ -2681,11 +2684,11 @@ export default class CSSStyleDeclarationPropertySetParser { return { 'background-position-x': { value: lowerValue, important } }; } - const imageParts = lowerValue.split(SPLIT_COMMA_SEPARATED_REGEXP); + const imageParts = lowerValue.split(SPLIT_COMMA_SEPARATED_WITH_PARANTHESES_REGEXP); let parsedValue = ''; for (const imagePart of imageParts) { - const parts = imagePart.trim().split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = imagePart.trim().split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); if (parsedValue) { parsedValue += ','; @@ -2732,11 +2735,11 @@ export default class CSSStyleDeclarationPropertySetParser { return { 'background-position-y': { value: lowerValue, important } }; } - const imageParts = lowerValue.split(SPLIT_COMMA_SEPARATED_REGEXP); + const imageParts = lowerValue.split(SPLIT_COMMA_SEPARATED_WITH_PARANTHESES_REGEXP); let parsedValue = ''; for (const imagePart of imageParts) { - const parts = imagePart.trim().split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = imagePart.trim().split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); if (parsedValue) { parsedValue += ','; @@ -2808,7 +2811,7 @@ export default class CSSStyleDeclarationPropertySetParser { return { 'background-image': { value: lowerValue, important } }; } - const parts = value.split(SPLIT_COMMA_SEPARATED_REGEXP); + const parts = value.split(SPLIT_COMMA_SEPARATED_WITH_PARANTHESES_REGEXP); const parsed = []; for (const part of parts) { @@ -2917,7 +2920,9 @@ export default class CSSStyleDeclarationPropertySetParser { ...this.getLineHeight('normal', important) }; - const parts = value.replace(/\s*\/\s*/g, '/').split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = value + .replace(/\s*\/\s*/g, '/') + .split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); for (let i = 0, max = parts.length; i < max; i++) { const part = parts[i]; @@ -2985,7 +2990,7 @@ export default class CSSStyleDeclarationPropertySetParser { if (CSSStyleDeclarationValueParser.getGlobal(lowerValue) || FONT_STYLE.includes(lowerValue)) { return { 'font-style': { value: lowerValue, important } }; } - const parts = value.split(SPLIT_SPACE_SEPARATED_REGEXP); + const parts = value.split(SPLIT_SPACE_SEPARATED_WITH_PARANTHESES_REGEXP); if (parts.length === 2 && parts[0] === 'oblique') { const degree = CSSStyleDeclarationValueParser.getDegree(parts[1]); return degree ? { 'font-style': { value: lowerValue, important } } : null; @@ -3256,4 +3261,62 @@ export default class CSSStyleDeclarationPropertySetParser { } return null; } + + /** + * Returns aspect ratio. + * + * @param value Value. + * @param important Important. + * @returns Property + */ + public static getAspectRatio( + value: string, + important: boolean + ): { + [key: string]: ICSSStyleDeclarationPropertyValue; + } { + const variable = CSSStyleDeclarationValueParser.getVariable(value); + if (variable) { + return { 'aspect-ratio': { value: variable, important } }; + } + + const lowerValue = value.toLowerCase(); + + if (CSSStyleDeclarationValueParser.getGlobal(lowerValue)) { + return { 'aspect-ratio': { value: lowerValue, important } }; + } + + let parsedValue = value; + + const hasAuto = parsedValue.includes('auto'); + + if (hasAuto) { + parsedValue = parsedValue.replace('auto', ''); + } + + parsedValue = parsedValue.replace(WHITE_SPACE_GLOBAL_REGEXP, ''); + + if (!parsedValue) { + return { 'aspect-ratio': { value: 'auto', important } }; + } + + const aspectRatio = parsedValue.split('/'); + + if (aspectRatio.length > 3) { + return null; + } + + const width = Number(aspectRatio[0]); + const height = aspectRatio[1] ? Number(aspectRatio[1]) : 1; + + if (isNaN(width) || isNaN(height)) { + return null; + } + + if (hasAuto) { + return { 'aspect-ratio': { value: `auto ${width} / ${height}`, important } }; + } + + return { 'aspect-ratio': { value: `${width} / ${height}`, important } }; + } } diff --git a/packages/happy-dom/test/css/declaration/CSSStyleDeclaration.test.ts b/packages/happy-dom/test/css/declaration/CSSStyleDeclaration.test.ts index 5c0dc3c3c..757885e78 100644 --- a/packages/happy-dom/test/css/declaration/CSSStyleDeclaration.test.ts +++ b/packages/happy-dom/test/css/declaration/CSSStyleDeclaration.test.ts @@ -2728,6 +2728,46 @@ describe('CSSStyleDeclaration', () => { }); }); + describe('get aspectRatio()', () => { + it('Returns style property.', () => { + const declaration = new CSSStyleDeclaration(PropertySymbol.illegalConstructor, window, { + element + }); + + for (const value of [ + 'var(--test-variable)', + 'inherit', + 'initial', + 'revert', + 'unset', + 'auto', + '1 / 1', + '16 / 9', + '4 / 3', + '1 / 2', + '2 / 1', + '3 / 4', + '9 / 16' + ]) { + element.setAttribute('style', `aspect-ratio: ${value}`); + + expect(declaration.aspectRatio).toBe(value); + } + + element.setAttribute('style', 'aspect-ratio: 2'); + + expect(declaration.aspectRatio).toBe('2 / 1'); + + element.setAttribute('style', 'aspect-ratio: 16/9 auto'); + + expect(declaration.aspectRatio).toBe('auto 16 / 9'); + + element.setAttribute('style', 'aspect-ratio: 16/9'); + + expect(declaration.aspectRatio).toBe('16 / 9'); + }); + }); + describe('get length()', () => { it('Returns length when of styles on element.', () => { const declaration = new CSSStyleDeclaration(PropertySymbol.illegalConstructor, window, {