Skip to content

Update font-family support and font shorthand support #213

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 100 additions & 29 deletions lib/properties/font.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,38 +26,109 @@ module.exports.parse = function parse(v) {
const [fontBlock, ...families] = parsers.splitValue(v, {
delimiter: ","
});
let blockA, blockB;
if (fontBlock.includes("/")) {
[blockA, blockB] = parsers.splitValue(fontBlock, {
delimiter: "/"
});
} else {
blockA = fontBlock.trim();
}
const obj = parsers.parseShorthand(blockA, shorthandFor, true);
if (!obj) {
return;
}
const font = {};
const [fontBlockA, fontBlockB] = parsers.splitValue(fontBlock, {
delimiter: "/"
});
const font = {
"font-style": "normal",
"font-variant": "normal",
"font-weight": "normal"
};
const fontFamilies = new Set();
for (const [property, value] of Object.entries(obj)) {
if (property === "font-family") {
if (!blockB) {
fontFamilies.add(value);
}
if (fontBlockB) {
const [lineB, ...familiesB] = fontBlockB.trim().split(" ");
if (!lineB || !lineHeight.isValid(lineB) || !familiesB.length) {
return;
}
const lineHeightB = lineHeight.parse(lineB);
const familyB = familiesB.join(" ");
if (fontFamily.isValid(familyB)) {
fontFamilies.add(fontFamily.parse(familyB));
} else {
font[property] = value;
return;
}
}
// blockB, if matched, includes line-height and first font-family
if (blockB) {
const [lineheight, family] = parsers.splitValue(blockB);
if (lineHeight.isValid(lineheight)) {
font["line-height"] = lineHeight.parse(lineheight);
const parts = parsers.splitValue(fontBlockA.trim());
const properties = ["font-style", "font-variant", "font-weight", "font-size"];
for (const part of parts) {
if (part === "normal") {
continue;
} else {
for (const property of properties) {
switch (property) {
case "font-style":
case "font-variant":
case "font-weight": {
const value = shorthandFor.get(property);
if (value.isValid(part)) {
font[property] = value.parse(part);
}
break;
}
case "font-size": {
const value = shorthandFor.get(property);
if (value.isValid(part)) {
font[property] = value.parse(part);
}
break;
}
default:
}
}
}
}
if (Object.hasOwn(font, "font-size")) {
font["line-height"] = lineHeightB;
} else {
return;
}
if (fontFamily.isValid(family)) {
} else {
// FIXME: Switch to toReversed() when we can drop Node.js 18 support.
const revParts = [...parsers.splitValue(fontBlockA.trim())].reverse();
const revFontFamily = [];
const properties = ["font-style", "font-variant", "font-weight", "line-height"];
font["font-style"] = "normal";
font["font-variant"] = "normal";
font["font-weight"] = "normal";
font["line-height"] = "normal";
let fontSizeA;
for (const part of revParts) {
if (fontSizeA) {
if (part === "normal") {
continue;
} else {
for (const property of properties) {
switch (property) {
case "font-style":
case "font-variant":
case "font-weight": {
const value = shorthandFor.get(property);
if (value.isValid(part)) {
font[property] = value.parse(part);
}
break;
}
case "line-height": {
const value = shorthandFor.get(property);
if (value.isValid(part)) {
font[property] = value.parse(part);
}
break;
}
default:
}
}
}
} else if (fontSize.isValid(part)) {
fontSizeA = fontSize.parse(part);
} else if (fontFamily.isValid(part)) {
revFontFamily.push(part);
} else {
return;
}
}
const family = revFontFamily.reverse().join(" ");
if (fontSizeA && fontFamily.isValid(family)) {
font["font-size"] = fontSizeA;
fontFamilies.add(fontFamily.parse(family));
} else {
return;
Expand All @@ -77,7 +148,7 @@ module.exports.parse = function parse(v) {
module.exports.definition = {
set(v) {
v = parsers.prepareValue(v, this._global);
if (parsers.hasVarFunc(v)) {
if (v === "" || parsers.hasVarFunc(v)) {
for (const [key] of shorthandFor) {
this._setProperty(key, "");
}
Expand All @@ -92,7 +163,7 @@ module.exports.definition = {
const val = obj[key];
if (typeof val === "string") {
this._setProperty(key, val);
if (val && !str.has(val)) {
if (val && val !== "normal" && !str.has(val)) {
if (key === "line-height") {
str.add(`/ ${val}`);
} else {
Expand All @@ -115,7 +186,7 @@ module.exports.definition = {
if (parsers.hasVarFunc(v)) {
return "";
}
if (v && !str.has(v)) {
if (v && v !== "normal" && !str.has(v)) {
if (key === "line-height") {
str.add(`/ ${v}`);
} else {
Expand Down
32 changes: 23 additions & 9 deletions lib/properties/fontFamily.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,19 @@ module.exports.parse = function parse(v) {
if (v === "") {
return v;
}
const keywords = ["serif", "sans-serif", "system-ui", "cursive", "fantasy", "monospace"];
const keywords = [
"serif",
"sans-serif",
"cursive",
"fantasy",
"monospace",
"system-ui",
"math",
"ui-serif",
"ui-sans-serif",
"ui-monospace",
"ui-rounded"
];
const val = parsers.splitValue(v, {
delimiter: ","
});
Expand All @@ -25,16 +37,18 @@ module.exports.parse = function parse(v) {
valid = true;
continue;
}
// This implementation does not strictly follow the specification. The spec
// does not require the first letter of the font-family to be capitalized.
// Also, unquoted font-family names are not restricted to ASCII only.
// This implementation does not strictly follow the specification.
// The spec does not require the first letter of the font-family to be
// capitalized, and unquoted font-family names are not restricted to ASCII.
// However, in the real world, the first letter of the ASCII font-family
// names are always capitalized, and unquoted font-family names do not
// contain spaces, e.g. `Times`, and AFAIK, non-ASCII font-family names are
// always quoted even without spaces, e.g. `"メイリオ"`.
// Therefore, it is unlikely that this implementation will cause problems.
// names are capitalized, and unquoted font-family names do not contain
// spaces, e.g. `Times`. And non-ASCII font-family names are quoted even
// without spaces, e.g. `"メイリオ"`.
// @see https://drafts.csswg.org/css-fonts/#font-family-prop
if (/^\s*(?:[A-Z][A-Za-z\d\s-]+)\s*$/.test(i)) {
if (
i !== "undefined" &&
/^(?:[A-Z][A-Za-z\d-]+(?:\s+[A-Z][A-Za-z\d-]+)*|-?[a-z][a-z-]+)$/.test(i)
) {
font.push(i.trim());
valid = true;
continue;
Expand Down
47 changes: 47 additions & 0 deletions test/CSSStyleDeclaration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1053,3 +1053,50 @@ describe("regression test for https://github.com/jsdom/cssstyle/issues/124", ()
assert.strictEqual(style.borderWidth, "1px");
});
});

describe("regression test for https://github.com/jsdom/cssstyle/issues/212", () => {
it("should support <generic-family> keywords", () => {
const keywords = [
"serif",
"sans-serif",
"cursive",
"fantasy",
"monospace",
"system-ui",
"math",
"ui-serif",
"ui-sans-serif",
"ui-monospace",
"ui-rounded"
];
const style = new CSSStyleDeclaration();
for (const keyword of keywords) {
style.fontFamily = keyword;
assert.strictEqual(style.fontFamily, keyword);
}
});

// see https://drafts.csswg.org/css-fonts-4/#changes-2021-12-21
it("should support removed generic keywords as non generic family name", () => {
const keywords = ["emoji", "fangsong"];
const style = new CSSStyleDeclaration();
for (const keyword of keywords) {
style.fontFamily = keyword;
assert.strictEqual(style.fontFamily, keyword);
}
});

it("should support `-webkit-` prefixed family name", () => {
const style = new CSSStyleDeclaration();
style.fontFamily = "-webkit-body";
assert.strictEqual(style.fontFamily, "-webkit-body");
});
});

describe("regression test for https://github.com/jsdom/jsdom/issues/3021", () => {
it("should get normalized value for font shorthand", () => {
const style = new CSSStyleDeclaration();
style.font = "normal bold 4px sans-serif";
assert.strictEqual(style.font, "bold 4px sans-serif");
});
});
64 changes: 0 additions & 64 deletions test/parsers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -965,70 +965,6 @@ describe("parseShorthand", () => {
"flex-basis": ""
});
});

const fontStyle = require("../lib/properties/fontStyle");
const fontVariant = require("../lib/properties/fontVariant");
const fontWeight = require("../lib/properties/fontWeight");
const fontSize = require("../lib/properties/fontSize");
const lineHeight = require("../lib/properties/lineHeight");
const fontFamily = require("../lib/properties/fontFamily");

const shorthandForFont = new Map([
["font-style", fontStyle],
["font-variant", fontVariant],
["font-weight", fontWeight],
["font-size", fontSize],
["line-height", lineHeight],
["font-family", fontFamily]
]);

it("should return undefined for invalid font-family", () => {
const input = "medium foo";
const output = parsers.parseShorthand(input, shorthandForFont, true);

assert.deepEqual(output, undefined);
});

it("should return object", () => {
const input = "normal medium sans-serif";
const output = parsers.parseShorthand(input, shorthandForFont, true);

assert.deepEqual(output, {
"font-style": "normal",
"font-variant": "normal",
"font-weight": "normal",
"font-size": "medium",
"line-height": "normal",
"font-family": "sans-serif"
});
});

it("should return object", () => {
const input = "italic bold calc(3em/2) serif";
const output = parsers.parseShorthand(input, shorthandForFont, true);

assert.deepEqual(output, {
"font-style": "italic",
"font-weight": "bold",
"font-size": "calc(1.5em)",
"line-height": "calc(1.5em)",
"font-family": "serif"
});
});

it("should return object", () => {
const input = "var(--foo) medium serif";
const output = parsers.parseShorthand(input, shorthandForFont, true);

assert.deepEqual(output, {
"font-style": "",
"font-variant": "",
"font-weight": "",
"font-size": "",
"line-height": "",
"font-family": ""
});
});
});

describe("isValidColor", () => {
Expand Down
Loading