3 const findFontFamily = require("../../utils/findFontFamily");
4 const isStandardSyntaxValue = require("../../utils/isStandardSyntaxValue");
5 const isVariable = require("../../utils/isVariable");
6 const keywordSets = require("../../reference/keywordSets");
7 const report = require("../../utils/report");
8 const ruleMessages = require("../../utils/ruleMessages");
9 const validateOptions = require("../../utils/validateOptions");
11 const ruleName = "font-family-name-quotes";
13 const messages = ruleMessages(ruleName, {
14 expected: family => `Expected quotes around "${family}"`,
15 rejected: family => `Unexpected quotes around "${family}"`
18 function isSystemFontKeyword(font) {
19 if (font.indexOf("-apple-") === 0) {
22 if (font === "BlinkMacSystemFont") {
28 // "To avoid mistakes in escaping, it is recommended to quote font family names
29 // that contain white space, digits, or punctuation characters other than hyphens"
30 // (https://www.w3.org/TR/CSS2/fonts.html#font-family-prop)
31 function quotesRecommended(family) {
32 return !/^[-a-zA-Z]+$/.test(family);
35 // Quotes are required if the family is not a valid CSS identifier
36 // (regexes from https://mathiasbynens.be/notes/unquoted-font-family)
37 function quotesRequired(family) {
38 return family.split(/\s+/).some(word => {
40 /^(-?\d|--)/.test(word) || !/^[-_a-zA-Z0-9\u00A0-\u10FFFF]+$/.test(word)
45 const rule = function(expectation) {
46 return (root, result) => {
47 const validOptions = validateOptions(result, ruleName, {
50 "always-where-required",
51 "always-where-recommended",
52 "always-unless-keyword"
59 root.walkDecls(/^font(-family)?$/i, decl => {
60 const fontFamilies = findFontFamily(decl.value);
62 if (fontFamilies.length === 0) {
66 fontFamilies.forEach(fontFamilyNode => {
67 let rawFamily = fontFamilyNode.value;
69 if (fontFamilyNode.quote) {
70 rawFamily = fontFamilyNode.quote + rawFamily + fontFamilyNode.quote;
73 checkFamilyName(rawFamily, decl);
77 function checkFamilyName(rawFamily, decl) {
78 if (!isStandardSyntaxValue(rawFamily)) {
81 if (isVariable(rawFamily)) {
85 const hasQuotes = rawFamily[0] === "'" || rawFamily[0] === '"';
87 // Clean the family of its quotes
88 const family = rawFamily.replace(/^['"]|['"]$/g, "");
90 // Disallow quotes around (case-insensitive) keywords
91 // and system font keywords in all cases
93 keywordSets.fontFamilyKeywords.has(family.toLowerCase()) ||
94 isSystemFontKeyword(family)
97 return complain(messages.rejected(family), family, decl);
102 const required = quotesRequired(family);
103 const recommended = quotesRecommended(family);
105 switch (expectation) {
106 case "always-unless-keyword":
108 return complain(messages.expected(family), family, decl);
112 case "always-where-recommended":
113 if (!recommended && hasQuotes) {
114 return complain(messages.rejected(family), family, decl);
116 if (recommended && !hasQuotes) {
117 return complain(messages.expected(family), family, decl);
121 case "always-where-required":
122 if (!required && hasQuotes) {
123 return complain(messages.rejected(family), family, decl);
125 if (required && !hasQuotes) {
126 return complain(messages.expected(family), family, decl);
132 function complain(message, family, decl) {
144 rule.ruleName = ruleName;
145 rule.messages = messages;
146 module.exports = rule;