From 57799cc98aeed718333e7c7667362e58b78794eb Mon Sep 17 00:00:00 2001 From: Guillaume Date: Sun, 23 May 2021 18:18:41 +0200 Subject: [PATCH] fix: handle size/position/attachment in background Fixes [#3169](https://github.com/jsdom/jsdom/issues/3169). --- lib/CSSStyleDeclaration.test.js | 39 ++++++++ lib/parsers.js | 45 ++++++--- lib/properties/background.js | 164 +++++++++++++++++++++++++++++--- 3 files changed, 223 insertions(+), 25 deletions(-) diff --git a/lib/CSSStyleDeclaration.test.js b/lib/CSSStyleDeclaration.test.js index 202a63de..27152477 100644 --- a/lib/CSSStyleDeclaration.test.js +++ b/lib/CSSStyleDeclaration.test.js @@ -350,6 +350,45 @@ describe('CSSStyleDeclaration', () => { describe('properties', () => { describe('background', () => { + test('invalid', () => { + const style = new CSSStyleDeclaration(); + const invalid = [ + 'left top', + 'red / 100% 100% 100%', + 'red / cover cover cover', + 'red / 100% 100% repeat 100%', + 'red / cover cover repeat cover', + ]; + invalid.forEach(value => { + style.background = value; + expect(style.background).toBe(''); + }); + }); + test('valid', () => { + const style = new CSSStyleDeclaration(); + const canonical = + 'red url("bg.jpg") left 10% / 100px 100% no-repeat fixed content-box padding-box'; + style.background = canonical; + expect(style.background).toBe(canonical); + // Non canonical order + style.background = + 'left 10% url("bg.jpg") red / no-repeat content-box padding-box 100px 100% fixed'; + expect(style.background).toBe(canonical); + style.background = + 'url("bg.jpg") left 10% red / content-box padding-box no-repeat fixed 100px 100%'; + expect(style.background).toBe(canonical); + style.background = 'red url("bg.jpg") left 10% / 100px 100% content-box no-repeat'; + expect(style.background).toBe( + 'red url("bg.jpg") left 10% / 100px 100% no-repeat content-box' + ); + style.background = 'red url("bg.jpg") left 10% / 100px content-box padding-box no-repeat'; + expect(style.background).toBe( + 'red url("bg.jpg") left 10% / 100px no-repeat content-box padding-box' + ); + // No space around separator + style.background = 'red/100px'; + expect(style.background).toBe('red / 100px'); + }); test('null should set property to empty string', () => { const style = new CSSStyleDeclaration(); style.background = 'red'; diff --git a/lib/parsers.js b/lib/parsers.js index 6e2fe4d8..e59ab606 100644 --- a/lib/parsers.js +++ b/lib/parsers.js @@ -73,6 +73,8 @@ const urlRegEx = new RegExp(`^url\\(${ws}([^"'() \\t${newline}\\\\]|${escape})+$ const whitespaceRegEx = new RegExp(`^${whitespace}$`); const trailingWhitespaceRegEx = new RegExp(`.*${whitespace}$`); +exports.ws = ws; + /** * CSS types * @@ -1390,9 +1392,18 @@ exports.shorthandParser = function parse(v, shorthand_for) { return obj; }; -exports.shorthandSetter = function(property, shorthand_for) { +exports.shorthandSetter = function( + property, + shorthand_for, + shorthandParser = exports.shorthandParser, + separator +) { + let longhands = shorthand_for; + if (Array.isArray(shorthand_for)) { + shorthand_for = shorthand_for.reduce((obj, part) => ({ ...obj, ...part }), {}); + } return function(v) { - var obj = exports.shorthandParser(v, shorthand_for); + const obj = shorthandParser(v, shorthand_for); if (obj === undefined) { return; } @@ -1421,26 +1432,32 @@ exports.shorthandSetter = function(property, shorthand_for) { // if it already exists, then call the shorthandGetter, if it's an empty // string, don't set the property this.removeProperty(property); - var calculated = exports.shorthandGetter(property, shorthand_for).call(this); + const calculated = exports.shorthandGetter(property, longhands, separator).call(this); if (calculated !== '') { this._setProperty(property, calculated); } }; }; -exports.shorthandGetter = function(property, shorthand_for) { +exports.shorthandGetter = function(shorthand, longhands, separator = ' / ') { return function() { - if (this._values[property] !== undefined) { - return this.getPropertyValue(property); + if (this._values[shorthand] !== undefined) { + return this.getPropertyValue(shorthand); + } + if (!Array.isArray(longhands)) { + longhands = [longhands]; } - return Object.keys(shorthand_for) - .map(function(subprop) { - return this.getPropertyValue(subprop); - }, this) - .filter(function(value) { - return value !== ''; - }) - .join(' '); + return longhands + .map( + longhands => + Object.keys(longhands) + .map(longhand => this.getPropertyValue(longhand), this) + .filter(value => value !== '') + .join(' '), + this + ) + .filter(value => value !== '') + .join(separator); }; }; diff --git a/lib/properties/background.js b/lib/properties/background.js index b843e0c7..51392c37 100644 --- a/lib/properties/background.js +++ b/lib/properties/background.js @@ -1,19 +1,161 @@ 'use strict'; -var shorthandSetter = require('../parsers').shorthandSetter; -var shorthandGetter = require('../parsers').shorthandGetter; - -var shorthand_for = { - 'background-color': require('./backgroundColor'), - 'background-image': require('./backgroundImage'), - 'background-repeat': require('./backgroundRepeat'), - 'background-attachment': require('./backgroundAttachment'), - 'background-position': require('./backgroundPosition'), +const { shorthandGetter, shorthandSetter, splitTokens, ws } = require('../parsers'); +const { parse: parseBackgroundAttachment } = require('./backgroundAttachment'); +const { parse: parseBackgroundColor } = require('./backgroundColor'); +const { parse: parseBackgroundImage } = require('./backgroundImage'); +const { parse: parseBackgroundPosition } = require('./backgroundPosition'); +const { parse: parseBackgroundSize } = require('./backgroundSize'); +const { parse: parseBackgroundRepeat } = require('./backgroundRepeat'); +const { parse: parseBackgroundOrigin } = require('./backgroundOrigin'); +const { parse: parseBackgroundClip } = require('./backgroundClip'); + +const before = { + 'background-color': '', + 'background-image': '', + 'background-position': '', +}; +const after = { + 'background-size': '', + 'background-repeat': '', + 'background-attachment': '', + 'background-origin': '', + 'background-clip': '', }; +const separatorRegExp = new RegExp(`${ws}/${ws}`); +const shorthandFor = [before, after]; + +function shorthandParser(v, shorthandFor) { + if (v.toLowerCase() === 'inherit') { + return {}; + } + + const longhands = { ...shorthandFor }; + if (v === '') { + return longhands; + } + + let [[argsBefore, argsAfter]] = splitTokens(v, separatorRegExp); + + [argsBefore] = splitTokens(argsBefore); + + const { length: argsBeforeLength } = argsBefore; + let positionArg = []; + let positionArgIndex = 0; + let i = 0; + for (; i < argsBeforeLength; i++) { + const arg = argsBefore[i]; + const color = parseBackgroundColor(arg); + if (color) { + if (longhands['background-color']) { + return undefined; + } + longhands['background-color'] = color; + continue; + } + const image = parseBackgroundImage(arg); + if (image) { + if (longhands['background-image']) { + return undefined; + } + longhands['background-image'] = image; + continue; + } + // First or consecutive + if (positionArg.length === 0 || positionArgIndex - i === -1) { + positionArgIndex = i; + positionArg.push(arg); + continue; + } + return undefined; + } + if (!(longhands['background-color'] || longhands['background-image'])) { + return undefined; + } + if ((positionArg = positionArg.join(' '))) { + const position = parseBackgroundPosition(positionArg); + if (position === undefined) { + return undefined; + } + longhands['background-position'] = position; + } + + if (argsAfter) { + [argsAfter] = splitTokens(argsAfter); + + const { length: argsAfterLength } = argsAfter; + const sizeBoxArgs = []; + let sizeBoxArgIndex = 0; + for (let i = 0; i < argsAfterLength; i++) { + const arg = argsAfter[i]; + const repeat = parseBackgroundRepeat(arg); + if (repeat) { + if (longhands['background-repeat']) { + return undefined; + } + longhands['background-repeat'] = repeat; + continue; + } + const attachment = parseBackgroundAttachment(arg); + if (attachment) { + if (longhands['background-attachment']) { + return undefined; + } + longhands['background-attachment'] = attachment; + continue; + } + // First or consecutive + if (sizeBoxArgs.length === 0 || sizeBoxArgs.length === 2 || sizeBoxArgIndex - i === -1) { + sizeBoxArgIndex = i; + sizeBoxArgs.push(arg); + continue; + } + return undefined; + } + + const { length: sizeBoxArgsLength } = sizeBoxArgs; + if (sizeBoxArgsLength > 4) { + return undefined; + } + if (sizeBoxArgsLength > 0) { + const [sizeX, sizeY = '', origin = '', clip = ''] = sizeBoxArgs; + let parsedSize = parseBackgroundSize(`${sizeX}${sizeY ? ` ${sizeY}` : ''}`); + let parsedOrigin = parseBackgroundOrigin(origin); + let parsedClip = parseBackgroundClip(clip); + if (parsedSize !== undefined && parsedOrigin !== undefined && parsedClip !== undefined) { + longhands['background-size'] = parsedSize; + longhands['background-origin'] = parsedOrigin; + longhands['background-clip'] = parsedClip; + return longhands; + } + parsedSize = parseBackgroundSize(`${origin}${clip ? ` ${clip}` : ''}`); + parsedOrigin = parseBackgroundOrigin(sizeX); + parsedClip = parseBackgroundOrigin(sizeY); + if (parsedSize !== undefined && parsedOrigin !== undefined && parsedClip !== undefined) { + longhands['background-size'] = parsedSize; + longhands['background-origin'] = parsedOrigin; + longhands['background-clip'] = parsedClip; + return longhands; + } + parsedSize = parseBackgroundSize(sizeX); + parsedOrigin = parseBackgroundOrigin(sizeY); + parsedClip = parseBackgroundOrigin(origin); + if (parsedSize !== undefined && parsedOrigin !== undefined && parsedClip !== undefined) { + longhands['background-size'] = parsedSize; + longhands['background-origin'] = parsedOrigin; + longhands['background-clip'] = parsedClip; + return longhands; + } + return undefined; + } + } + + return longhands; +} module.exports.definition = { - set: shorthandSetter('background', shorthand_for), - get: shorthandGetter('background', shorthand_for), + set: shorthandSetter('background', shorthandFor, shorthandParser), + get: shorthandGetter('background', shorthandFor), enumerable: true, configurable: true, };