123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- 'use strict';
- const findFontFamily = require('../../utils/findFontFamily');
- const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue');
- const isVariable = require('../../utils/isVariable');
- const keywordSets = require('../../reference/keywordSets');
- const report = require('../../utils/report');
- const ruleMessages = require('../../utils/ruleMessages');
- const validateOptions = require('../../utils/validateOptions');
- const ruleName = 'font-family-name-quotes';
- const messages = ruleMessages(ruleName, {
- expected: (family) => `Expected quotes around "${family}"`,
- rejected: (family) => `Unexpected quotes around "${family}"`,
- });
- const meta = {
- url: 'https://stylelint.io/user-guide/rules/list/font-family-name-quotes',
- };
- /**
- * @param {string} font
- * @returns {boolean}
- */
- function isSystemFontKeyword(font) {
- if (font.startsWith('-apple-')) {
- return true;
- }
- if (font === 'BlinkMacSystemFont') {
- return true;
- }
- return false;
- }
- /**
- * "To avoid mistakes in escaping, it is recommended to quote font family names
- * that contain white space, digits, or punctuation characters other than hyphens"
- * (https://www.w3.org/TR/CSS2/fonts.html#font-family-prop)
- *
- * @param {string} family
- * @returns {boolean}
- */
- function quotesRecommended(family) {
- return !/^[-a-zA-Z]+$/.test(family);
- }
- /**
- * Quotes are required if the family is not a valid CSS identifier
- * (regexes from https://mathiasbynens.be/notes/unquoted-font-family)
- *
- * @param {string} family
- * @returns {boolean}
- */
- function quotesRequired(family) {
- return family
- .split(/\s+/)
- .some((word) => /^(?:-?\d|--)/.test(word) || !/^[-\w\u{00A0}-\u{10FFFF}]+$/u.test(word));
- }
- /**
- * @typedef {{
- * name: string,
- * rawName: string,
- * hasQuotes: boolean,
- * sourceIndex: number,
- * resetIndexes: (offset: number) => void,
- * removeQuotes: () => void,
- * addQuotes: () => void,
- * }} MutableNode
- */
- /**
- *
- * @param {import('postcss-value-parser').Node[]} fontFamilies
- * @param {import('postcss').Declaration} decl
- * @returns {MutableNode[]}
- */
- const makeMutableFontFamilies = (fontFamilies, decl) => {
- /**
- * @type {MutableNode[]}
- */
- const mutableNodes = [];
- fontFamilies.forEach((fontFamily, idx) => {
- const quote = 'quote' in fontFamily && fontFamily.quote;
- const name = fontFamily.value;
- /** @type {MutableNode} */
- const newNode = {
- name,
- rawName: quote ? `${quote}${name}${quote}` : name,
- sourceIndex: fontFamily.sourceIndex,
- hasQuotes: Boolean(quote),
- resetIndexes(offset) {
- mutableNodes.slice(idx + 1).forEach((n) => (n.sourceIndex += offset));
- },
- removeQuotes() {
- if (this.hasQuotes === false) return;
- const openIndex = this.sourceIndex;
- const closeIndex = openIndex + this.name.length + 2;
- this.hasQuotes = false;
- decl.value = decl.value.slice(0, openIndex) + this.name + decl.value.substring(closeIndex);
- this.resetIndexes(-2);
- },
- addQuotes() {
- if (this.hasQuotes === true) return;
- const openIndex = this.sourceIndex;
- const closeIndex = openIndex + this.name.length;
- this.hasQuotes = true;
- const fixedName = `"${this.name}"`;
- decl.value = decl.value.slice(0, openIndex) + fixedName + decl.value.substring(closeIndex);
- this.resetIndexes(2);
- },
- };
- mutableNodes.push(newNode);
- });
- return mutableNodes;
- };
- /** @type {import('stylelint').Rule} */
- const rule = (primary, _secondary, context) => {
- return (root, result) => {
- const validOptions = validateOptions(result, ruleName, {
- actual: primary,
- possible: ['always-where-required', 'always-where-recommended', 'always-unless-keyword'],
- });
- if (!validOptions) {
- return;
- }
- root.walkDecls(/^font(-family)?$/i, (decl) => {
- let fontFamilyNodes = makeMutableFontFamilies(findFontFamily(decl.value), decl);
- if (fontFamilyNodes.length === 0) {
- return;
- }
- for (const fontFamilyNode of fontFamilyNodes) {
- checkFamilyName(fontFamilyNode, decl);
- }
- });
- /**
- * @param {MutableNode} fontFamilyNode
- * @param {import('postcss').Declaration} decl
- */
- function checkFamilyName(fontFamilyNode, decl) {
- const { name: family, rawName: rawFamily, hasQuotes } = fontFamilyNode;
- if (!isStandardSyntaxValue(rawFamily)) {
- return;
- }
- if (isVariable(rawFamily)) {
- return;
- }
- // Disallow quotes around (case-insensitive) keywords
- // and system font keywords in all cases
- if (keywordSets.fontFamilyKeywords.has(family.toLowerCase()) || isSystemFontKeyword(family)) {
- if (hasQuotes) {
- if (context.fix) {
- fontFamilyNode.removeQuotes();
- return;
- }
- return complain(messages.rejected(family), rawFamily, decl);
- }
- return;
- }
- const required = quotesRequired(family);
- const recommended = quotesRecommended(family);
- switch (primary) {
- case 'always-unless-keyword':
- if (!hasQuotes) {
- if (context.fix) {
- fontFamilyNode.addQuotes();
- return;
- }
- return complain(messages.expected(family), rawFamily, decl);
- }
- return;
- case 'always-where-recommended':
- if (!recommended && hasQuotes) {
- if (context.fix) {
- fontFamilyNode.removeQuotes();
- return;
- }
- return complain(messages.rejected(family), rawFamily, decl);
- }
- if (recommended && !hasQuotes) {
- if (context.fix) {
- fontFamilyNode.addQuotes();
- return;
- }
- return complain(messages.expected(family), rawFamily, decl);
- }
- return;
- case 'always-where-required':
- if (!required && hasQuotes) {
- if (context.fix) {
- fontFamilyNode.removeQuotes();
- return;
- }
- return complain(messages.rejected(family), rawFamily, decl);
- }
- if (required && !hasQuotes) {
- if (context.fix) {
- fontFamilyNode.addQuotes();
- return;
- }
- return complain(messages.expected(family), rawFamily, decl);
- }
- }
- }
- /**
- * @param {string} message
- * @param {string} family
- * @param {import('postcss').Declaration} decl
- */
- function complain(message, family, decl) {
- report({
- result,
- ruleName,
- message,
- node: decl,
- word: family,
- });
- }
- };
- };
- rule.ruleName = ruleName;
- rule.messages = messages;
- rule.meta = meta;
- module.exports = rule;
|