Skip to content

Commit 57799cc

Browse files
committed
fix: handle size/position/attachment in background
Fixes [#3169](jsdom/jsdom#3169).
1 parent ae50b33 commit 57799cc

File tree

3 files changed

+223
-25
lines changed

3 files changed

+223
-25
lines changed

lib/CSSStyleDeclaration.test.js

+39
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,45 @@ describe('CSSStyleDeclaration', () => {
350350

351351
describe('properties', () => {
352352
describe('background', () => {
353+
test('invalid', () => {
354+
const style = new CSSStyleDeclaration();
355+
const invalid = [
356+
'left top',
357+
'red / 100% 100% 100%',
358+
'red / cover cover cover',
359+
'red / 100% 100% repeat 100%',
360+
'red / cover cover repeat cover',
361+
];
362+
invalid.forEach(value => {
363+
style.background = value;
364+
expect(style.background).toBe('');
365+
});
366+
});
367+
test('valid', () => {
368+
const style = new CSSStyleDeclaration();
369+
const canonical =
370+
'red url("bg.jpg") left 10% / 100px 100% no-repeat fixed content-box padding-box';
371+
style.background = canonical;
372+
expect(style.background).toBe(canonical);
373+
// Non canonical order
374+
style.background =
375+
'left 10% url("bg.jpg") red / no-repeat content-box padding-box 100px 100% fixed';
376+
expect(style.background).toBe(canonical);
377+
style.background =
378+
'url("bg.jpg") left 10% red / content-box padding-box no-repeat fixed 100px 100%';
379+
expect(style.background).toBe(canonical);
380+
style.background = 'red url("bg.jpg") left 10% / 100px 100% content-box no-repeat';
381+
expect(style.background).toBe(
382+
'red url("bg.jpg") left 10% / 100px 100% no-repeat content-box'
383+
);
384+
style.background = 'red url("bg.jpg") left 10% / 100px content-box padding-box no-repeat';
385+
expect(style.background).toBe(
386+
'red url("bg.jpg") left 10% / 100px no-repeat content-box padding-box'
387+
);
388+
// No space around separator
389+
style.background = 'red/100px';
390+
expect(style.background).toBe('red / 100px');
391+
});
353392
test('null should set property to empty string', () => {
354393
const style = new CSSStyleDeclaration();
355394
style.background = 'red';

lib/parsers.js

+31-14
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ const urlRegEx = new RegExp(`^url\\(${ws}([^"'() \\t${newline}\\\\]|${escape})+$
7373
const whitespaceRegEx = new RegExp(`^${whitespace}$`);
7474
const trailingWhitespaceRegEx = new RegExp(`.*${whitespace}$`);
7575

76+
exports.ws = ws;
77+
7678
/**
7779
* CSS types
7880
*
@@ -1390,9 +1392,18 @@ exports.shorthandParser = function parse(v, shorthand_for) {
13901392
return obj;
13911393
};
13921394

1393-
exports.shorthandSetter = function(property, shorthand_for) {
1395+
exports.shorthandSetter = function(
1396+
property,
1397+
shorthand_for,
1398+
shorthandParser = exports.shorthandParser,
1399+
separator
1400+
) {
1401+
let longhands = shorthand_for;
1402+
if (Array.isArray(shorthand_for)) {
1403+
shorthand_for = shorthand_for.reduce((obj, part) => ({ ...obj, ...part }), {});
1404+
}
13941405
return function(v) {
1395-
var obj = exports.shorthandParser(v, shorthand_for);
1406+
const obj = shorthandParser(v, shorthand_for);
13961407
if (obj === undefined) {
13971408
return;
13981409
}
@@ -1421,26 +1432,32 @@ exports.shorthandSetter = function(property, shorthand_for) {
14211432
// if it already exists, then call the shorthandGetter, if it's an empty
14221433
// string, don't set the property
14231434
this.removeProperty(property);
1424-
var calculated = exports.shorthandGetter(property, shorthand_for).call(this);
1435+
const calculated = exports.shorthandGetter(property, longhands, separator).call(this);
14251436
if (calculated !== '') {
14261437
this._setProperty(property, calculated);
14271438
}
14281439
};
14291440
};
14301441

1431-
exports.shorthandGetter = function(property, shorthand_for) {
1442+
exports.shorthandGetter = function(shorthand, longhands, separator = ' / ') {
14321443
return function() {
1433-
if (this._values[property] !== undefined) {
1434-
return this.getPropertyValue(property);
1444+
if (this._values[shorthand] !== undefined) {
1445+
return this.getPropertyValue(shorthand);
1446+
}
1447+
if (!Array.isArray(longhands)) {
1448+
longhands = [longhands];
14351449
}
1436-
return Object.keys(shorthand_for)
1437-
.map(function(subprop) {
1438-
return this.getPropertyValue(subprop);
1439-
}, this)
1440-
.filter(function(value) {
1441-
return value !== '';
1442-
})
1443-
.join(' ');
1450+
return longhands
1451+
.map(
1452+
longhands =>
1453+
Object.keys(longhands)
1454+
.map(longhand => this.getPropertyValue(longhand), this)
1455+
.filter(value => value !== '')
1456+
.join(' '),
1457+
this
1458+
)
1459+
.filter(value => value !== '')
1460+
.join(separator);
14441461
};
14451462
};
14461463

lib/properties/background.js

+153-11
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,161 @@
11
'use strict';
22

3-
var shorthandSetter = require('../parsers').shorthandSetter;
4-
var shorthandGetter = require('../parsers').shorthandGetter;
5-
6-
var shorthand_for = {
7-
'background-color': require('./backgroundColor'),
8-
'background-image': require('./backgroundImage'),
9-
'background-repeat': require('./backgroundRepeat'),
10-
'background-attachment': require('./backgroundAttachment'),
11-
'background-position': require('./backgroundPosition'),
3+
const { shorthandGetter, shorthandSetter, splitTokens, ws } = require('../parsers');
4+
const { parse: parseBackgroundAttachment } = require('./backgroundAttachment');
5+
const { parse: parseBackgroundColor } = require('./backgroundColor');
6+
const { parse: parseBackgroundImage } = require('./backgroundImage');
7+
const { parse: parseBackgroundPosition } = require('./backgroundPosition');
8+
const { parse: parseBackgroundSize } = require('./backgroundSize');
9+
const { parse: parseBackgroundRepeat } = require('./backgroundRepeat');
10+
const { parse: parseBackgroundOrigin } = require('./backgroundOrigin');
11+
const { parse: parseBackgroundClip } = require('./backgroundClip');
12+
13+
const before = {
14+
'background-color': '',
15+
'background-image': '',
16+
'background-position': '',
17+
};
18+
const after = {
19+
'background-size': '',
20+
'background-repeat': '',
21+
'background-attachment': '',
22+
'background-origin': '',
23+
'background-clip': '',
1224
};
25+
const separatorRegExp = new RegExp(`${ws}/${ws}`);
26+
const shorthandFor = [before, after];
27+
28+
function shorthandParser(v, shorthandFor) {
29+
if (v.toLowerCase() === 'inherit') {
30+
return {};
31+
}
32+
33+
const longhands = { ...shorthandFor };
34+
if (v === '') {
35+
return longhands;
36+
}
37+
38+
let [[argsBefore, argsAfter]] = splitTokens(v, separatorRegExp);
39+
40+
[argsBefore] = splitTokens(argsBefore);
41+
42+
const { length: argsBeforeLength } = argsBefore;
43+
let positionArg = [];
44+
let positionArgIndex = 0;
45+
let i = 0;
46+
for (; i < argsBeforeLength; i++) {
47+
const arg = argsBefore[i];
48+
const color = parseBackgroundColor(arg);
49+
if (color) {
50+
if (longhands['background-color']) {
51+
return undefined;
52+
}
53+
longhands['background-color'] = color;
54+
continue;
55+
}
56+
const image = parseBackgroundImage(arg);
57+
if (image) {
58+
if (longhands['background-image']) {
59+
return undefined;
60+
}
61+
longhands['background-image'] = image;
62+
continue;
63+
}
64+
// First or consecutive <position>
65+
if (positionArg.length === 0 || positionArgIndex - i === -1) {
66+
positionArgIndex = i;
67+
positionArg.push(arg);
68+
continue;
69+
}
70+
return undefined;
71+
}
72+
if (!(longhands['background-color'] || longhands['background-image'])) {
73+
return undefined;
74+
}
75+
if ((positionArg = positionArg.join(' '))) {
76+
const position = parseBackgroundPosition(positionArg);
77+
if (position === undefined) {
78+
return undefined;
79+
}
80+
longhands['background-position'] = position;
81+
}
82+
83+
if (argsAfter) {
84+
[argsAfter] = splitTokens(argsAfter);
85+
86+
const { length: argsAfterLength } = argsAfter;
87+
const sizeBoxArgs = [];
88+
let sizeBoxArgIndex = 0;
89+
for (let i = 0; i < argsAfterLength; i++) {
90+
const arg = argsAfter[i];
91+
const repeat = parseBackgroundRepeat(arg);
92+
if (repeat) {
93+
if (longhands['background-repeat']) {
94+
return undefined;
95+
}
96+
longhands['background-repeat'] = repeat;
97+
continue;
98+
}
99+
const attachment = parseBackgroundAttachment(arg);
100+
if (attachment) {
101+
if (longhands['background-attachment']) {
102+
return undefined;
103+
}
104+
longhands['background-attachment'] = attachment;
105+
continue;
106+
}
107+
// First or consecutive <box|size>
108+
if (sizeBoxArgs.length === 0 || sizeBoxArgs.length === 2 || sizeBoxArgIndex - i === -1) {
109+
sizeBoxArgIndex = i;
110+
sizeBoxArgs.push(arg);
111+
continue;
112+
}
113+
return undefined;
114+
}
115+
116+
const { length: sizeBoxArgsLength } = sizeBoxArgs;
117+
if (sizeBoxArgsLength > 4) {
118+
return undefined;
119+
}
120+
if (sizeBoxArgsLength > 0) {
121+
const [sizeX, sizeY = '', origin = '', clip = ''] = sizeBoxArgs;
122+
let parsedSize = parseBackgroundSize(`${sizeX}${sizeY ? ` ${sizeY}` : ''}`);
123+
let parsedOrigin = parseBackgroundOrigin(origin);
124+
let parsedClip = parseBackgroundClip(clip);
125+
if (parsedSize !== undefined && parsedOrigin !== undefined && parsedClip !== undefined) {
126+
longhands['background-size'] = parsedSize;
127+
longhands['background-origin'] = parsedOrigin;
128+
longhands['background-clip'] = parsedClip;
129+
return longhands;
130+
}
131+
parsedSize = parseBackgroundSize(`${origin}${clip ? ` ${clip}` : ''}`);
132+
parsedOrigin = parseBackgroundOrigin(sizeX);
133+
parsedClip = parseBackgroundOrigin(sizeY);
134+
if (parsedSize !== undefined && parsedOrigin !== undefined && parsedClip !== undefined) {
135+
longhands['background-size'] = parsedSize;
136+
longhands['background-origin'] = parsedOrigin;
137+
longhands['background-clip'] = parsedClip;
138+
return longhands;
139+
}
140+
parsedSize = parseBackgroundSize(sizeX);
141+
parsedOrigin = parseBackgroundOrigin(sizeY);
142+
parsedClip = parseBackgroundOrigin(origin);
143+
if (parsedSize !== undefined && parsedOrigin !== undefined && parsedClip !== undefined) {
144+
longhands['background-size'] = parsedSize;
145+
longhands['background-origin'] = parsedOrigin;
146+
longhands['background-clip'] = parsedClip;
147+
return longhands;
148+
}
149+
return undefined;
150+
}
151+
}
152+
153+
return longhands;
154+
}
13155

14156
module.exports.definition = {
15-
set: shorthandSetter('background', shorthand_for),
16-
get: shorthandGetter('background', shorthand_for),
157+
set: shorthandSetter('background', shorthandFor, shorthandParser),
158+
get: shorthandGetter('background', shorthandFor),
17159
enumerable: true,
18160
configurable: true,
19161
};

0 commit comments

Comments
 (0)