diff --git a/docs/rules/font-family-fallbacks.md b/docs/rules/font-family-fallbacks.md index a286da21..a59abc32 100644 --- a/docs/rules/font-family-fallbacks.md +++ b/docs/rules/font-family-fallbacks.md @@ -12,7 +12,7 @@ It is a best practice to use a fallback font and a generic font last in the `fon ## Rule Details -This rule enforces the use of fallback fonts and a generic font last in `font-family` and `font` property. +This rule enforces the use of fallback fonts and a generic font last in `font-family` and `font` property. Global values (`inherit`, `initial`, `revert`, `revert-layer`, `unset`) are always allowed. Example of **incorrect** code: @@ -56,9 +56,13 @@ b { .foo { font-family: var(--my-font); } + +.bar { + font-family: inherit; +} ``` -Fonts can also be specified using the `font` property, which acts as a shorthand for several font-related properties, including `font-family`. You must specify both the `font-size` and `font-family` when using `font` property. +Fonts can also be specified using the `font` property, which acts as a shorthand for several font-related properties, including `font-family`. You must specify both the `font-size` and `font-family` when using `font` property, unless you are using a CSS-wide keyword. Example of **incorrect** code: @@ -116,6 +120,10 @@ b { 1em var(--font), monospace; } + +.baz { + font: unset; +} ``` ## When Not to Use It diff --git a/src/rules/font-family-fallbacks.js b/src/rules/font-family-fallbacks.js index e3cc6a8b..3a36a722 100644 --- a/src/rules/font-family-fallbacks.js +++ b/src/rules/font-family-fallbacks.js @@ -33,6 +33,29 @@ const genericFonts = new Set([ "fangsong", ]); +/** + * Check if the value is a CSS-wide keyword. + * @param {string} value The value to check. + * @param {Set} cssWideKeywords The CSS-wide keywords to check against. + * @returns {boolean} True if the value is a CSS-wide keyword, false otherwise. + */ +function isCSSWideKeyword(value, cssWideKeywords) { + return cssWideKeywords.has(value.trim().toLowerCase()); +} + +/** + * Check if the node is an identifier with a CSS-wide keyword. + * @param {Object} node The node to check. + * @param {Set} cssWideKeywords The CSS-wide keywords to check against. + * @returns {boolean} True if the node is a CSS-wide keyword identifier, false otherwise. + */ +function isCSSWideKeywordIdentifier(node, cssWideKeywords) { + return ( + node.type === "Identifier" && + isCSSWideKeyword(node.name, cssWideKeywords) + ); +} + /** * Check if the node is a CSS variable function. * @param {Object} node The node to check. @@ -47,6 +70,7 @@ function isVarFunction(node) { * @param {string} fontPropertyValues The font property values to check. * @param {Object} context The ESLint context object. * @param {Object} node The CSS node being checked. + * @param {Set} cssWideKeywords The CSS-wide keywords to check against. * @returns {void} * @private */ @@ -54,7 +78,12 @@ function reportFontWithoutFallbacksInFontProperty( fontPropertyValues, context, node, + cssWideKeywords, ) { + if (isCSSWideKeyword(fontPropertyValues, cssWideKeywords)) { + return; + } + const valueList = fontPropertyValues.split(",").map(v => v.trim()); if (valueList.length === 1) { @@ -104,6 +133,11 @@ export default { create(context) { const sourceCode = context.sourceCode; + const cssWideKeywords = new Set( + sourceCode.lexer.cssWideKeywords.map(keyword => + keyword.toLowerCase(), + ), + ); const variableMap = new Map(); return { @@ -120,6 +154,12 @@ export default { const valueArr = node.children; if (valueArr.length === 1) { + if ( + isCSSWideKeywordIdentifier(valueArr[0], cssWideKeywords) + ) { + return; + } + if ( valueArr[0].type === "Function" && valueArr[0].name === "var" @@ -133,6 +173,10 @@ export default { return; } + if (isCSSWideKeyword(variableValue, cssWideKeywords)) { + return; + } + const variableList = variableValue .split(",") .map(v => v.trim()); @@ -272,6 +316,7 @@ export default { variableValue, context, node, + cssWideKeywords, ); } } else { @@ -397,6 +442,7 @@ export default { variableValue, context, node, + cssWideKeywords, ); } else { if ( @@ -421,6 +467,7 @@ export default { fontPropertyValues, context, node, + cssWideKeywords, ); } } diff --git a/tests/rules/font-family-fallbacks.test.js b/tests/rules/font-family-fallbacks.test.js index 34c5471c..8880f26e 100644 --- a/tests/rules/font-family-fallbacks.test.js +++ b/tests/rules/font-family-fallbacks.test.js @@ -10,8 +10,20 @@ const ruleTester = new RuleTester({ plugins: { css }, language: "css/css" }); ruleTester.run("font-family-fallbacks", rule, { valid: [ + "a { font-family: inherit; }", + "a { font-family: initial; }", + "a { font-family: revert; }", + "a { font-family: revert-layer; }", + "a { font-family: unset; }", + "a { font: inherit; }", + "a { font: initial; }", + "a { font: revert; }", + "a { font: revert-layer; }", + "a { font: unset; }", ":root { --my-font: sans-serif; } a { font-family: var(--my-font); }", ":root { --foo: 3rem; } a { font-family: var(--my-font); }", + ":root { --my-font: inherit; } a { font-family: var(--my-font); }", + ":root { --my-font-value: inherit; } a { font: var(--my-font-value); }", ":root { --my-font: 'Arial', sans-serif; } a { font-family: var(--my-font); }", ":root { --my-font: 'Arial', 'Segoe UI Emoji', serif; } a { font-family: var(--my-font); }", "a { font-family: serif; }", @@ -36,6 +48,14 @@ ruleTester.run("font-family-fallbacks", rule, { "a { font: var(--font-size) 'Open Sans', var(--my-font); }", ":root { --my-font: Verdana, Arial, Helvetica; } a { font: var(--font-size) 'Open Sans', var(--my-font), serif; }", ":root { --my-font: sans-serif; } a { font: var(--font-weight) var(--font-size)/var(--line-height) var(--font-family); }", + { + code: "a { font-family: custom-inherit; }", + languageOptions: { + customSyntax: { + cssWideKeywords: ["custom-inherit"], + }, + }, + }, ], invalid: [ {