Files
LearnNode/node-course/node_modules/@eslint/css/dist/rules/font-family-fallbacks.js
T
2026-08-06 12:00:39 +10:00

341 lines
16 KiB
JavaScript

/**
* @fileoverview Rule to enforce the use of fallback fonts and a generic font last.
* @author Tanuj Kanti
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { CSSRuleDefinition } from "../types.js"
* @typedef {"useFallbackFonts" | "useGenericFont"} FontFamilyFallbacksMessageIds
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: FontFamilyFallbacksMessageIds }>} FontFamilyFallbacksRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const genericFonts = new Set([
"serif",
"sans-serif",
"monospace",
"cursive",
"fantasy",
"system-ui",
"ui-serif",
"ui-sans-serif",
"ui-monospace",
"ui-rounded",
"emoji",
"math",
"fangsong",
]);
/**
* Check if the value is a CSS-wide keyword.
* @param {string} value The value to check.
* @param {Set<string>} 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<string>} 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.
* @returns {boolean} True if the node is a variable function, false otherwise.
*/
function isVarFunction(node) {
return node.type === "Function" && node.name === "var";
}
/**
* Report an error if the font property values do not have fallbacks or a generic font.
* @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<string>} cssWideKeywords The CSS-wide keywords to check against.
* @returns {void}
* @private
*/
function reportFontWithoutFallbacksInFontProperty(fontPropertyValues, context, node, cssWideKeywords) {
if (isCSSWideKeyword(fontPropertyValues, cssWideKeywords)) {
return;
}
const valueList = fontPropertyValues.split(",").map(v => v.trim());
if (valueList.length === 1) {
const containsGenericFont = Array.from(genericFonts).some(font => valueList[0].includes(font));
if (!containsGenericFont) {
context.report({
loc: node.loc,
messageId: "useFallbackFonts",
});
}
}
else {
if (!genericFonts.has(valueList.at(-1))) {
context.report({
loc: node.loc,
messageId: "useGenericFont",
});
}
}
}
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {FontFamilyFallbacksRuleDefinition} */ ({
meta: {
type: "suggestion",
docs: {
description: "Enforce use of fallback fonts and a generic font last",
recommended: true,
url: "https://github.com/eslint/css/blob/main/docs/rules/font-family-fallbacks.md",
},
messages: {
useFallbackFonts: "Use fallback fonts and a generic font last.",
useGenericFont: "Use a generic font last.",
},
},
create(context) {
const sourceCode = context.sourceCode;
const cssWideKeywords = new Set(sourceCode.lexer.cssWideKeywords.map(keyword => keyword.toLowerCase()));
const variableMap = new Map();
return {
"Rule > Block > Declaration"(node) {
if (node.property.startsWith("--")) {
const variableName = node.property;
const variableValue = node.value.type === "Raw" && node.value.value;
variableMap.set(variableName, variableValue);
}
},
"Rule > Block > Declaration[property='font-family'] > Value"(node) {
const valueArr = node.children;
if (valueArr.length === 1) {
if (isCSSWideKeywordIdentifier(valueArr[0], cssWideKeywords)) {
return;
}
if (valueArr[0].type === "Function" &&
valueArr[0].name === "var") {
const variableName = valueArr[0].children[0].type === "Identifier" &&
valueArr[0].children[0].name;
const variableValue = variableMap.get(variableName);
if (!variableValue) {
return;
}
if (isCSSWideKeyword(variableValue, cssWideKeywords)) {
return;
}
const variableList = variableValue
.split(",")
.map(v => v.trim());
if (variableList.length === 1 &&
!genericFonts.has(variableList[0])) {
context.report({
loc: node.loc,
messageId: "useFallbackFonts",
});
}
else if (!genericFonts.has(variableList.at(-1))) {
context.report({
loc: node.loc,
messageId: "useGenericFont",
});
}
}
else {
if (valueArr[0].type === "Identifier" &&
genericFonts.has(valueArr[0].name)) {
return;
}
context.report({
loc: node.loc,
messageId: "useFallbackFonts",
});
}
}
else {
const isUsingVariable = valueArr.some(child => isVarFunction(child));
if (isUsingVariable) {
const fontsList = [];
const lastNode = valueArr.at(-1);
if (lastNode.type === "Function" &&
lastNode.name === "var") {
const variableName = lastNode.children[0].type === "Identifier" &&
lastNode.children[0].name;
const lastVariable = variableMap.get(variableName);
if (!lastVariable) {
return;
}
}
valueArr.forEach(child => {
if (child.type === "String") {
fontsList.push(child.value);
}
if (child.type === "Identifier") {
fontsList.push(child.name);
}
if (child.type === "Function" &&
child.name === "var") {
const variableName = child.children[0].type === "Identifier" &&
child.children[0].name;
const variableValue = variableMap.get(variableName);
if (variableValue) {
const variableList = variableValue
.split(",")
.map(v => v.trim());
fontsList.push(...variableList);
}
}
});
if (fontsList.length > 0 &&
!genericFonts.has(fontsList.at(-1))) {
context.report({
loc: node.loc,
messageId: "useGenericFont",
});
}
}
else {
const lastFont = valueArr.at(-1);
if (!(lastFont.type === "Identifier" &&
genericFonts.has(lastFont.name))) {
context.report({
loc: node.loc,
messageId: "useGenericFont",
});
}
}
}
},
"Rule > Block > Declaration[property='font'] > Value"(node) {
const valueArr = node.children;
if (valueArr.length === 1) {
const firstValue = valueArr[0];
// If it font is set to system font, we don't need to check for fallbacks
if (firstValue.type === "Identifier") {
return;
}
// If the value is a variable function, we need to check the variable value
if (firstValue.type === "Function" &&
firstValue.name === "var") {
// Check if the function is a variable
const variableName = firstValue.children[0].type === "Identifier" &&
firstValue.children[0].name;
const variableValue = variableMap.get(variableName);
if (!variableValue) {
return;
}
reportFontWithoutFallbacksInFontProperty(variableValue, context, node, cssWideKeywords);
}
}
else {
const isUsingVariable = valueArr.some(child => isVarFunction(child));
if (isUsingVariable) {
const beforOperator = [];
const afterOperator = [];
const operator = valueArr.find(child => child.type === "Operator" &&
child.value === ",");
const operatorOffset = operator && operator.loc.end.offset;
if (operatorOffset) {
valueArr.forEach(child => {
if (child.loc.end.offset < operatorOffset) {
beforOperator.push(sourceCode.getText(child).trim());
}
else if (child.loc.end.offset > operatorOffset) {
afterOperator.push(sourceCode.getText(child).trim());
}
});
if (afterOperator.length !== 0) {
const usingVar = afterOperator.some(value => value.startsWith("var"));
if (!usingVar) {
if (!genericFonts.has(afterOperator.at(-1))) {
context.report({
loc: node.loc,
messageId: "useGenericFont",
});
}
}
else {
if (afterOperator.at(-1).startsWith("var")) {
const lastNode = valueArr.at(-1);
const isFunctionVar = lastNode.type === "Function" &&
lastNode.name === "var";
const variableName = isFunctionVar &&
lastNode.children[0].type ===
"Identifier" &&
lastNode.children[0].name;
const variableValue = variableMap.get(variableName);
if (!variableValue) {
return;
}
const variableList = variableValue
.split(",")
.map(v => v.trim());
if (variableList.length > 0 &&
!genericFonts.has(variableList.at(-1))) {
context.report({
loc: node.loc,
messageId: "useGenericFont",
});
}
}
else {
if (!genericFonts.has(afterOperator.at(-1))) {
context.report({
loc: node.loc,
messageId: "useGenericFont",
});
}
}
}
}
}
else {
if (sourceCode
.getText(valueArr.at(-1))
.trim()
.startsWith("var")) {
const lastNode = valueArr.at(-1);
const isFunctionVar = lastNode.type === "Function" &&
lastNode.name === "var";
const variableName = isFunctionVar &&
lastNode.children[0].type ===
"Identifier" &&
lastNode.children[0].name;
const variableValue = variableMap.get(variableName);
if (!variableValue) {
return;
}
reportFontWithoutFallbacksInFontProperty(variableValue, context, node, cssWideKeywords);
}
else {
if (!genericFonts.has(sourceCode
.getText(valueArr.at(-1))
.trim())) {
context.report({
loc: node.loc,
messageId: "useFallbackFonts",
});
}
}
}
}
else {
const fontPropertyValues = sourceCode.getText(node);
if (fontPropertyValues) {
reportFontWithoutFallbacksInFontProperty(fontPropertyValues, context, node, cssWideKeywords);
}
}
}
},
};
},
});