first commit
This commit is contained in:
+340
@@ -0,0 +1,340 @@
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user