UNPKG

4.18 kBJavaScriptView Raw
1'use strict';
2
3const findFontFamily = require('../../utils/findFontFamily');
4const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue');
5const isVariable = require('../../utils/isVariable');
6const keywordSets = require('../../reference/keywordSets');
7const report = require('../../utils/report');
8const ruleMessages = require('../../utils/ruleMessages');
9const validateOptions = require('../../utils/validateOptions');
10
11const ruleName = 'font-family-name-quotes';
12
13const messages = ruleMessages(ruleName, {
14 expected: (family) => `Expected quotes around "${family}"`,
15 rejected: (family) => `Unexpected quotes around "${family}"`,
16});
17
18/**
19 * @param {string} font
20 * @returns {boolean}
21 */
22function isSystemFontKeyword(font) {
23 if (font.startsWith('-apple-')) {
24 return true;
25 }
26
27 if (font === 'BlinkMacSystemFont') {
28 return true;
29 }
30
31 return false;
32}
33
34/**
35 * "To avoid mistakes in escaping, it is recommended to quote font family names
36 * that contain white space, digits, or punctuation characters other than hyphens"
37 * (https://www.w3.org/TR/CSS2/fonts.html#font-family-prop)
38 *
39 * @param {string} family
40 * @returns {boolean}
41 */
42function quotesRecommended(family) {
43 return !/^[-a-zA-Z]+$/.test(family);
44}
45
46/**
47 * Quotes are required if the family is not a valid CSS identifier
48 * (regexes from https://mathiasbynens.be/notes/unquoted-font-family)
49 *
50 * @param {string} family
51 * @returns {boolean}
52 */
53function quotesRequired(family) {
54 return family
55 .split(/\s+/)
56 .some((word) => /^(?:-?\d|--)/.test(word) || !/^[-\w\u{00A0}-\u{10FFFF}]+$/u.test(word));
57}
58
59/** @type {import('stylelint').Rule} */
60const rule = (primary) => {
61 return (root, result) => {
62 const validOptions = validateOptions(result, ruleName, {
63 actual: primary,
64 possible: ['always-where-required', 'always-where-recommended', 'always-unless-keyword'],
65 });
66
67 if (!validOptions) {
68 return;
69 }
70
71 root.walkDecls(/^font(-family)?$/i, (decl) => {
72 const fontFamilies = findFontFamily(decl.value);
73
74 if (fontFamilies.length === 0) {
75 return;
76 }
77
78 fontFamilies.forEach((fontFamilyNode) => {
79 let rawFamily = fontFamilyNode.value;
80
81 if ('quote' in fontFamilyNode) {
82 rawFamily = fontFamilyNode.quote + rawFamily + fontFamilyNode.quote;
83 }
84
85 checkFamilyName(rawFamily, decl);
86 });
87 });
88
89 /**
90 * @param {string} rawFamily
91 * @param {import('postcss').Declaration} decl
92 */
93 function checkFamilyName(rawFamily, decl) {
94 if (!isStandardSyntaxValue(rawFamily)) {
95 return;
96 }
97
98 if (isVariable(rawFamily)) {
99 return;
100 }
101
102 const hasQuotes = rawFamily.startsWith("'") || rawFamily.startsWith('"');
103
104 // Clean the family of its quotes
105 const family = rawFamily.replace(/^['"]|['"]$/g, '');
106
107 // Disallow quotes around (case-insensitive) keywords
108 // and system font keywords in all cases
109 if (keywordSets.fontFamilyKeywords.has(family.toLowerCase()) || isSystemFontKeyword(family)) {
110 if (hasQuotes) {
111 return complain(messages.rejected(family), family, decl);
112 }
113
114 return;
115 }
116
117 const required = quotesRequired(family);
118 const recommended = quotesRecommended(family);
119
120 switch (primary) {
121 case 'always-unless-keyword':
122 if (!hasQuotes) {
123 return complain(messages.expected(family), family, decl);
124 }
125
126 return;
127
128 case 'always-where-recommended':
129 if (!recommended && hasQuotes) {
130 return complain(messages.rejected(family), family, decl);
131 }
132
133 if (recommended && !hasQuotes) {
134 return complain(messages.expected(family), family, decl);
135 }
136
137 return;
138
139 case 'always-where-required':
140 if (!required && hasQuotes) {
141 return complain(messages.rejected(family), family, decl);
142 }
143
144 if (required && !hasQuotes) {
145 return complain(messages.expected(family), family, decl);
146 }
147 }
148 }
149
150 /**
151 * @param {string} message
152 * @param {string} family
153 * @param {import('postcss').Declaration} decl
154 */
155 function complain(message, family, decl) {
156 report({
157 result,
158 ruleName,
159 message,
160 node: decl,
161 word: family,
162 });
163 }
164 };
165};
166
167rule.ruleName = ruleName;
168rule.messages = messages;
169module.exports = rule;