diff --git a/lib/properties/font.js b/lib/properties/font.js index 9bb5b439..65ba44de 100644 --- a/lib/properties/font.js +++ b/lib/properties/font.js @@ -26,38 +26,103 @@ 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": + 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; @@ -77,7 +142,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, ""); } @@ -92,7 +157,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 { @@ -115,7 +180,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 { diff --git a/lib/properties/fontFamily.js b/lib/properties/fontFamily.js index 98f752c5..6fb5d491 100644 --- a/lib/properties/fontFamily.js +++ b/lib/properties/fontFamily.js @@ -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: "," }); @@ -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; diff --git a/test/CSSStyleDeclaration.test.js b/test/CSSStyleDeclaration.test.js index 2fcdd356..67c12e7b 100644 --- a/test/CSSStyleDeclaration.test.js +++ b/test/CSSStyleDeclaration.test.js @@ -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 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"); + }); +}); diff --git a/test/parsers.test.js b/test/parsers.test.js index c272f54b..13910ff0 100644 --- a/test/parsers.test.js +++ b/test/parsers.test.js @@ -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", () => { diff --git a/test/properties.test.js b/test/properties.test.js index 7233ee91..88855f56 100644 --- a/test/properties.test.js +++ b/test/properties.test.js @@ -1347,11 +1347,59 @@ describe("font", () => { ); }); + it("font-family should set / get family values", () => { + testPropertyValue( + "font-family", + '"Gill Sans Extrabold", sans-serif', + '"Gill Sans Extrabold", sans-serif' + ); + }); + + it("font-family should set / get family values", () => { + testPropertyValue( + "font-family", + '"Goudy Bookletter 1911", sans-serif', + '"Goudy Bookletter 1911", sans-serif' + ); + }); + + it("font-family should not set / get invalid family values", () => { + testPropertyValue("font-family", "Goudy Bookletter 1911, sans-serif", ""); + }); + + it("font-family should not set / get invalid family values", () => { + testPropertyValue("font-family", "Red/Black, sans-serif", ""); + }); + + it("font-family should not set / get invalid family values", () => { + testPropertyValue("font-family", '"Lucida" Grande, sans-serif', ""); + }); + + it("font-family should not set / get invalid family values", () => { + testPropertyValue("font-family", 'Lucida "Grande", sans-serif', ""); + }); + + it("font-family should not set / get invalid family values", () => { + testPropertyValue("font-family", "Ahem!, sans-serif", ""); + }); + + it("font-family should not set / get invalid family values", () => { + testPropertyValue("font-family", "test@foo, sans-serif", ""); + }); + + it("font-family should not set / get invalid family values", () => { + testPropertyValue("font-family", "#POUND, sans-serif", ""); + }); + + it("font-family should not set / get invalid family values", () => { + testPropertyValue("font-family", "Hawaii 5-0, sans-serif", ""); + }); + it("font shorthand should set / get values", () => { testImplicitPropertyValue( "font", 'normal medium Times, "Times New Roman", Georgia, serif', - 'normal medium Times, "Times New Roman", Georgia, serif', + 'medium Times, "Times New Roman", Georgia, serif', new Map([ ["font-style", "normal"], ["font-variant", "normal"], @@ -1363,6 +1411,166 @@ describe("font", () => { ); }); + it("font shorthand should set / get values", () => { + testImplicitPropertyValue( + "font", + "normal medium Gill Sans Extrabold, sans-serif", + "medium Gill Sans Extrabold, sans-serif", + new Map([ + ["font-style", "normal"], + ["font-variant", "normal"], + ["font-weight", "normal"], + ["font-size", "medium"], + ["line-height", "normal"], + ["font-family", "Gill Sans Extrabold, sans-serif"] + ]) + ); + }); + + it("font shorthand should set / get values", () => { + testImplicitPropertyValue( + "font", + 'normal medium "Goudy Bookletter 1911", sans-serif', + 'medium "Goudy Bookletter 1911", sans-serif', + new Map([ + ["font-style", "normal"], + ["font-variant", "normal"], + ["font-weight", "normal"], + ["font-size", "medium"], + ["line-height", "normal"], + ["font-family", '"Goudy Bookletter 1911", sans-serif'] + ]) + ); + }); + + it("font shorthand should not set / get invalid values", () => { + testImplicitPropertyValue( + "font", + "normal medium Goudy Bookletter 1911, sans-serif", + "", + new Map([ + ["font-style", ""], + ["font-variant", ""], + ["font-weight", ""], + ["font-size", ""], + ["line-height", ""], + ["font-family", ""] + ]) + ); + }); + + it("font shorthand should not set / get invalid values", () => { + testImplicitPropertyValue( + "font", + "normal medium Red/Black, sans-serif", + "", + new Map([ + ["font-style", ""], + ["font-variant", ""], + ["font-weight", ""], + ["font-size", ""], + ["line-height", ""], + ["font-family", ""] + ]) + ); + }); + + it("font shorthand should not set / get invalid values", () => { + testImplicitPropertyValue( + "font", + 'normal medium "Lucida" Grande, sans-serif', + "", + new Map([ + ["font-style", ""], + ["font-variant", ""], + ["font-weight", ""], + ["font-size", ""], + ["line-height", ""], + ["font-family", ""] + ]) + ); + }); + + it("font shorthand should not set / get invalid values", () => { + testImplicitPropertyValue( + "font", + 'normal medium Lucida "Grande", sans-serif', + "", + new Map([ + ["font-style", ""], + ["font-variant", ""], + ["font-weight", ""], + ["font-size", ""], + ["line-height", ""], + ["font-family", ""] + ]) + ); + }); + + it("font shorthand should not set / get invalid values", () => { + testImplicitPropertyValue( + "font", + "normal medium Ahem!, sans-serif", + "", + new Map([ + ["font-style", ""], + ["font-variant", ""], + ["font-weight", ""], + ["font-size", ""], + ["line-height", ""], + ["font-family", ""] + ]) + ); + }); + + it("font shorthand should not set / get invalid values", () => { + testImplicitPropertyValue( + "font", + "normal medium test@foo, sans-serif", + "", + new Map([ + ["font-style", ""], + ["font-variant", ""], + ["font-weight", ""], + ["font-size", ""], + ["line-height", ""], + ["font-family", ""] + ]) + ); + }); + + it("font shorthand should not set / get invalid values", () => { + testImplicitPropertyValue( + "font", + "normal medium #POUND, sans-serif", + "", + new Map([ + ["font-style", ""], + ["font-variant", ""], + ["font-weight", ""], + ["font-size", ""], + ["line-height", ""], + ["font-family", ""] + ]) + ); + }); + + it("font shorthand should not set / get invalid values", () => { + testImplicitPropertyValue( + "font", + "normal medium Hawaii 5-0, sans-serif", + "", + new Map([ + ["font-style", ""], + ["font-variant", ""], + ["font-weight", ""], + ["font-size", ""], + ["line-height", ""], + ["font-family", ""] + ]) + ); + }); + it("font shorthand should set / get values", () => { testImplicitPropertyValue( "font", @@ -1370,7 +1578,7 @@ describe("font", () => { 'italic bold medium / 1.2 Times, "Times New Roman", Georgia, serif', new Map([ ["font-style", "italic"], - ["font-variant", ""], + ["font-variant", "normal"], ["font-weight", "bold"], ["font-size", "medium"], ["line-height", "1.2"], @@ -1386,7 +1594,7 @@ describe("font", () => { 'italic bold calc(1.5em) / 1.2 Times, "Times New Roman", Georgia, serif', new Map([ ["font-style", "italic"], - ["font-variant", ""], + ["font-variant", "normal"], ["font-weight", "bold"], ["font-size", "calc(1.5em)"], ["line-height", "1.2"],