Skip to content

Commit 030a609

Browse files
authored
Update font-family support and font shorthand support
Fixes #212. Fixes jsdom/jsdom#3021. Closes #121 by superseding it.
1 parent 7ed8c6c commit 030a609

File tree

5 files changed

+375
-105
lines changed

5 files changed

+375
-105
lines changed

lib/properties/font.js

Lines changed: 94 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,38 +26,103 @@ module.exports.parse = function parse(v) {
2626
const [fontBlock, ...families] = parsers.splitValue(v, {
2727
delimiter: ","
2828
});
29-
let blockA, blockB;
30-
if (fontBlock.includes("/")) {
31-
[blockA, blockB] = parsers.splitValue(fontBlock, {
32-
delimiter: "/"
33-
});
34-
} else {
35-
blockA = fontBlock.trim();
36-
}
37-
const obj = parsers.parseShorthand(blockA, shorthandFor, true);
38-
if (!obj) {
39-
return;
40-
}
41-
const font = {};
29+
const [fontBlockA, fontBlockB] = parsers.splitValue(fontBlock, {
30+
delimiter: "/"
31+
});
32+
const font = {
33+
"font-style": "normal",
34+
"font-variant": "normal",
35+
"font-weight": "normal"
36+
};
4237
const fontFamilies = new Set();
43-
for (const [property, value] of Object.entries(obj)) {
44-
if (property === "font-family") {
45-
if (!blockB) {
46-
fontFamilies.add(value);
47-
}
38+
if (fontBlockB) {
39+
const [lineB, ...familiesB] = fontBlockB.trim().split(" ");
40+
if (!lineB || !lineHeight.isValid(lineB) || !familiesB.length) {
41+
return;
42+
}
43+
const lineHeightB = lineHeight.parse(lineB);
44+
const familyB = familiesB.join(" ");
45+
if (fontFamily.isValid(familyB)) {
46+
fontFamilies.add(fontFamily.parse(familyB));
4847
} else {
49-
font[property] = value;
48+
return;
5049
}
51-
}
52-
// blockB, if matched, includes line-height and first font-family
53-
if (blockB) {
54-
const [lineheight, family] = parsers.splitValue(blockB);
55-
if (lineHeight.isValid(lineheight)) {
56-
font["line-height"] = lineHeight.parse(lineheight);
50+
const parts = parsers.splitValue(fontBlockA.trim());
51+
const properties = ["font-style", "font-variant", "font-weight", "font-size"];
52+
for (const part of parts) {
53+
if (part === "normal") {
54+
continue;
55+
} else {
56+
for (const property of properties) {
57+
switch (property) {
58+
case "font-style":
59+
case "font-variant":
60+
case "font-weight": {
61+
const value = shorthandFor.get(property);
62+
if (value.isValid(part)) {
63+
font[property] = value.parse(part);
64+
}
65+
break;
66+
}
67+
case "font-size": {
68+
const value = shorthandFor.get(property);
69+
if (value.isValid(part)) {
70+
font[property] = value.parse(part);
71+
}
72+
break;
73+
}
74+
default:
75+
}
76+
}
77+
}
78+
}
79+
if (Object.hasOwn(font, "font-size")) {
80+
font["line-height"] = lineHeightB;
5781
} else {
5882
return;
5983
}
60-
if (fontFamily.isValid(family)) {
84+
} else {
85+
// FIXME: Switch to toReversed() when we can drop Node.js 18 support.
86+
const revParts = [...parsers.splitValue(fontBlockA.trim())].reverse();
87+
const revFontFamily = [];
88+
const properties = ["font-style", "font-variant", "font-weight", "line-height"];
89+
font["font-style"] = "normal";
90+
font["font-variant"] = "normal";
91+
font["font-weight"] = "normal";
92+
font["line-height"] = "normal";
93+
let fontSizeA;
94+
for (const part of revParts) {
95+
if (fontSizeA) {
96+
if (part === "normal") {
97+
continue;
98+
} else {
99+
for (const property of properties) {
100+
switch (property) {
101+
case "font-style":
102+
case "font-variant":
103+
case "font-weight":
104+
case "line-height": {
105+
const value = shorthandFor.get(property);
106+
if (value.isValid(part)) {
107+
font[property] = value.parse(part);
108+
}
109+
break;
110+
}
111+
default:
112+
}
113+
}
114+
}
115+
} else if (fontSize.isValid(part)) {
116+
fontSizeA = fontSize.parse(part);
117+
} else if (fontFamily.isValid(part)) {
118+
revFontFamily.push(part);
119+
} else {
120+
return;
121+
}
122+
}
123+
const family = revFontFamily.reverse().join(" ");
124+
if (fontSizeA && fontFamily.isValid(family)) {
125+
font["font-size"] = fontSizeA;
61126
fontFamilies.add(fontFamily.parse(family));
62127
} else {
63128
return;
@@ -77,7 +142,7 @@ module.exports.parse = function parse(v) {
77142
module.exports.definition = {
78143
set(v) {
79144
v = parsers.prepareValue(v, this._global);
80-
if (parsers.hasVarFunc(v)) {
145+
if (v === "" || parsers.hasVarFunc(v)) {
81146
for (const [key] of shorthandFor) {
82147
this._setProperty(key, "");
83148
}
@@ -92,7 +157,7 @@ module.exports.definition = {
92157
const val = obj[key];
93158
if (typeof val === "string") {
94159
this._setProperty(key, val);
95-
if (val && !str.has(val)) {
160+
if (val && val !== "normal" && !str.has(val)) {
96161
if (key === "line-height") {
97162
str.add(`/ ${val}`);
98163
} else {
@@ -115,7 +180,7 @@ module.exports.definition = {
115180
if (parsers.hasVarFunc(v)) {
116181
return "";
117182
}
118-
if (v && !str.has(v)) {
183+
if (v && v !== "normal" && !str.has(v)) {
119184
if (key === "line-height") {
120185
str.add(`/ ${v}`);
121186
} else {

lib/properties/fontFamily.js

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,19 @@ module.exports.parse = function parse(v) {
66
if (v === "") {
77
return v;
88
}
9-
const keywords = ["serif", "sans-serif", "system-ui", "cursive", "fantasy", "monospace"];
9+
const keywords = [
10+
"serif",
11+
"sans-serif",
12+
"cursive",
13+
"fantasy",
14+
"monospace",
15+
"system-ui",
16+
"math",
17+
"ui-serif",
18+
"ui-sans-serif",
19+
"ui-monospace",
20+
"ui-rounded"
21+
];
1022
const val = parsers.splitValue(v, {
1123
delimiter: ","
1224
});
@@ -25,16 +37,18 @@ module.exports.parse = function parse(v) {
2537
valid = true;
2638
continue;
2739
}
28-
// This implementation does not strictly follow the specification. The spec
29-
// does not require the first letter of the font-family to be capitalized.
30-
// Also, unquoted font-family names are not restricted to ASCII only.
40+
// This implementation does not strictly follow the specification.
41+
// The spec does not require the first letter of the font-family to be
42+
// capitalized, and unquoted font-family names are not restricted to ASCII.
3143
// However, in the real world, the first letter of the ASCII font-family
32-
// names are always capitalized, and unquoted font-family names do not
33-
// contain spaces, e.g. `Times`, and AFAIK, non-ASCII font-family names are
34-
// always quoted even without spaces, e.g. `"メイリオ"`.
35-
// Therefore, it is unlikely that this implementation will cause problems.
44+
// names are capitalized, and unquoted font-family names do not contain
45+
// spaces, e.g. `Times`. And non-ASCII font-family names are quoted even
46+
// without spaces, e.g. `"メイリオ"`.
3647
// @see https://drafts.csswg.org/css-fonts/#font-family-prop
37-
if (/^\s*(?:[A-Z][A-Za-z\d\s-]+)\s*$/.test(i)) {
48+
if (
49+
i !== "undefined" &&
50+
/^(?:[A-Z][A-Za-z\d-]+(?:\s+[A-Z][A-Za-z\d-]+)*|-?[a-z][a-z-]+)$/.test(i)
51+
) {
3852
font.push(i.trim());
3953
valid = true;
4054
continue;

test/CSSStyleDeclaration.test.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,3 +1053,50 @@ describe("regression test for https://github.com/jsdom/cssstyle/issues/124", ()
10531053
assert.strictEqual(style.borderWidth, "1px");
10541054
});
10551055
});
1056+
1057+
describe("regression test for https://github.com/jsdom/cssstyle/issues/212", () => {
1058+
it("should support <generic-family> keywords", () => {
1059+
const keywords = [
1060+
"serif",
1061+
"sans-serif",
1062+
"cursive",
1063+
"fantasy",
1064+
"monospace",
1065+
"system-ui",
1066+
"math",
1067+
"ui-serif",
1068+
"ui-sans-serif",
1069+
"ui-monospace",
1070+
"ui-rounded"
1071+
];
1072+
const style = new CSSStyleDeclaration();
1073+
for (const keyword of keywords) {
1074+
style.fontFamily = keyword;
1075+
assert.strictEqual(style.fontFamily, keyword);
1076+
}
1077+
});
1078+
1079+
// see https://drafts.csswg.org/css-fonts-4/#changes-2021-12-21
1080+
it("should support removed generic keywords as non generic family name", () => {
1081+
const keywords = ["emoji", "fangsong"];
1082+
const style = new CSSStyleDeclaration();
1083+
for (const keyword of keywords) {
1084+
style.fontFamily = keyword;
1085+
assert.strictEqual(style.fontFamily, keyword);
1086+
}
1087+
});
1088+
1089+
it("should support `-webkit-` prefixed family name", () => {
1090+
const style = new CSSStyleDeclaration();
1091+
style.fontFamily = "-webkit-body";
1092+
assert.strictEqual(style.fontFamily, "-webkit-body");
1093+
});
1094+
});
1095+
1096+
describe("regression test for https://github.com/jsdom/jsdom/issues/3021", () => {
1097+
it("should get normalized value for font shorthand", () => {
1098+
const style = new CSSStyleDeclaration();
1099+
style.font = "normal bold 4px sans-serif";
1100+
assert.strictEqual(style.font, "bold 4px sans-serif");
1101+
});
1102+
});

test/parsers.test.js

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -965,70 +965,6 @@ describe("parseShorthand", () => {
965965
"flex-basis": ""
966966
});
967967
});
968-
969-
const fontStyle = require("../lib/properties/fontStyle");
970-
const fontVariant = require("../lib/properties/fontVariant");
971-
const fontWeight = require("../lib/properties/fontWeight");
972-
const fontSize = require("../lib/properties/fontSize");
973-
const lineHeight = require("../lib/properties/lineHeight");
974-
const fontFamily = require("../lib/properties/fontFamily");
975-
976-
const shorthandForFont = new Map([
977-
["font-style", fontStyle],
978-
["font-variant", fontVariant],
979-
["font-weight", fontWeight],
980-
["font-size", fontSize],
981-
["line-height", lineHeight],
982-
["font-family", fontFamily]
983-
]);
984-
985-
it("should return undefined for invalid font-family", () => {
986-
const input = "medium foo";
987-
const output = parsers.parseShorthand(input, shorthandForFont, true);
988-
989-
assert.deepEqual(output, undefined);
990-
});
991-
992-
it("should return object", () => {
993-
const input = "normal medium sans-serif";
994-
const output = parsers.parseShorthand(input, shorthandForFont, true);
995-
996-
assert.deepEqual(output, {
997-
"font-style": "normal",
998-
"font-variant": "normal",
999-
"font-weight": "normal",
1000-
"font-size": "medium",
1001-
"line-height": "normal",
1002-
"font-family": "sans-serif"
1003-
});
1004-
});
1005-
1006-
it("should return object", () => {
1007-
const input = "italic bold calc(3em/2) serif";
1008-
const output = parsers.parseShorthand(input, shorthandForFont, true);
1009-
1010-
assert.deepEqual(output, {
1011-
"font-style": "italic",
1012-
"font-weight": "bold",
1013-
"font-size": "calc(1.5em)",
1014-
"line-height": "calc(1.5em)",
1015-
"font-family": "serif"
1016-
});
1017-
});
1018-
1019-
it("should return object", () => {
1020-
const input = "var(--foo) medium serif";
1021-
const output = parsers.parseShorthand(input, shorthandForFont, true);
1022-
1023-
assert.deepEqual(output, {
1024-
"font-style": "",
1025-
"font-variant": "",
1026-
"font-weight": "",
1027-
"font-size": "",
1028-
"line-height": "",
1029-
"font-family": ""
1030-
});
1031-
});
1032968
});
1033969

1034970
describe("isValidColor", () => {

0 commit comments

Comments
 (0)