From 82bd4044621c4a42ed90c3c9687bfceb897f7649 Mon Sep 17 00:00:00 2001 From: Brian Orora Date: Fri, 4 Feb 2022 17:20:03 +0300 Subject: [PATCH] Add autofix to font-family-name-quotes (#5806) Co-authored-by: Masafumi Koba <473530+ybiquitous@users.noreply.github.com> --- docs/user-guide/rules/list.md | 2 +- lib/rules/font-family-name-quotes/README.md | 2 + .../__tests__/index.js | 298 ++++++++++++++++++ lib/rules/font-family-name-quotes/index.js | 125 ++++++-- 4 files changed, 408 insertions(+), 19 deletions(-) diff --git a/docs/user-guide/rules/list.md b/docs/user-guide/rules/list.md index 764620f501..1827a77167 100644 --- a/docs/user-guide/rules/list.md +++ b/docs/user-guide/rules/list.md @@ -110,7 +110,7 @@ Within each cateogory, the rules are grouped by the [_thing_](http://apps.workfl ### Font family -- [`font-family-name-quotes`](../../../lib/rules/font-family-name-quotes/README.md): Specify whether or not quotation marks should be used around font family names. +- [`font-family-name-quotes`](../../../lib/rules/font-family-name-quotes/README.md): Specify whether or not quotation marks should be used around font family names (Autofixable). ### Font weight diff --git a/lib/rules/font-family-name-quotes/README.md b/lib/rules/font-family-name-quotes/README.md index 9f024c17a1..2c19e7f723 100644 --- a/lib/rules/font-family-name-quotes/README.md +++ b/lib/rules/font-family-name-quotes/README.md @@ -13,6 +13,8 @@ This rule checks the `font` and `font-family` properties. This rule ignores `$sass`, `@less`, and `var(--custom-property)` variable syntaxes. +The [`fix` option](../../../docs/user-guide/usage/options.md#fix) can automatically fix most of the problems reported by this rule. + ## Options `string`: `"always-where-required"|"always-where-recommended"|"always-unless-keyword"` diff --git a/lib/rules/font-family-name-quotes/__tests__/index.js b/lib/rules/font-family-name-quotes/__tests__/index.js index 0c137f9d60..b8407d6a0d 100644 --- a/lib/rules/font-family-name-quotes/__tests__/index.js +++ b/lib/rules/font-family-name-quotes/__tests__/index.js @@ -390,3 +390,301 @@ testRule({ }, ], }); + +testRule({ + ruleName, + config: ['always-unless-keyword'], + fix: true, + reject: [ + { + code: 'a { font-family: "Lucida Grande", "Arial", "sans-serif"; }', + fixed: 'a { font-family: "Lucida Grande", "Arial", sans-serif; }', + message: messages.rejected('sans-serif'), + line: 1, + column: 45, + }, + { + code: 'a { font: 1em "Lucida Grande", "Arial", "sans-serif"; }', + fixed: 'a { font: 1em "Lucida Grande", "Arial", sans-serif; }', + message: messages.rejected('sans-serif'), + line: 1, + column: 42, + }, + { + code: 'a { fOnT-fAmIlY: "Lucida Grande", "Arial", "sans-serif"; }', + fixed: 'a { fOnT-fAmIlY: "Lucida Grande", "Arial", sans-serif; }', + message: messages.rejected('sans-serif'), + line: 1, + column: 45, + }, + { + code: 'a { font-family: Lucida Grande, "Arial", sans-serif; }', + fixed: 'a { font-family: "Lucida Grande", "Arial", sans-serif; }', + message: messages.expected('Lucida Grande'), + line: 1, + column: 18, + }, + { + code: "a { font-family: 'Lucida Grande', Arial, sans-serif; }", + fixed: 'a { font-family: \'Lucida Grande\', "Arial", sans-serif; }', + message: messages.expected('Arial'), + line: 1, + column: 35, + }, + { + code: 'a { font-family: "inherit"; }', + fixed: 'a { font-family: inherit; }', + message: messages.rejected('inherit'), + }, + { + code: "a { font-family: 'system-ui', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; }", + fixed: + "a { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; }", + message: messages.rejected('system-ui'), + }, + { + code: "a { font-family: system-ui, '-apple-system', BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; }", + fixed: + "a { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; }", + message: messages.rejected('-apple-system'), + }, + { + code: "a { font-family: system-ui, -apple-system, 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', sans-serif; }", + fixed: + "a { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; }", + message: messages.rejected('BlinkMacSystemFont'), + }, + { + code: 'a { font: italic 300 16px/30px Arial, serif; }', + fixed: 'a { font: italic 300 16px/30px "Arial", serif; }', + message: messages.expected('Arial'), + line: 1, + column: 32, + }, + { + code: 'a { font: italic 1000 16px/30px Arial, serif; }', + fixed: 'a { font: italic 1000 16px/30px "Arial", serif; }', + message: messages.expected('Arial'), + line: 1, + column: 33, + }, + { + code: 'a { font: italic 892 16px/30px Arial, serif; }', + fixed: 'a { font: italic 892 16px/30px "Arial", serif; }', + message: messages.expected('Arial'), + line: 1, + column: 32, + }, + { + code: 'a { font-family: \u1100; }', + fixed: 'a { font-family: "\u1100"; }', + message: messages.expected('\u1100'), + line: 1, + column: 18, + }, + { + code: 'a { font-family: ሀ; }', + fixed: 'a { font-family: "ሀ"; }', + message: messages.expected('ሀ'), + line: 1, + column: 18, + }, + { + code: "a { font-family: -apple-system, Hawaii 5-0, 'BlinkMacSystemFont', Segoe UI, 'Roboto', 'sans-serif'; }", + fixed: + 'a { font-family: -apple-system, "Hawaii 5-0", BlinkMacSystemFont, "Segoe UI", \'Roboto\', sans-serif; }', + description: 'multiple fixes on one declaration', + warnings: [ + { message: messages.expected('Hawaii 5-0') }, + { message: messages.rejected('BlinkMacSystemFont') }, + { message: messages.expected('Segoe UI') }, + { message: messages.rejected('sans-serif') }, + ], + }, + ], +}); + +testRule({ + ruleName, + config: ['always-where-recommended'], + fix: true, + + reject: [ + { + code: 'a { font: 1em Lucida Grande, Arial, sans-serif; }', + fixed: 'a { font: 1em "Lucida Grande", Arial, sans-serif; }', + message: messages.expected('Lucida Grande'), + line: 1, + column: 15, + }, + { + code: 'a { font-family: Lucida Grande, Arial, sans-serif; }', + fixed: 'a { font-family: "Lucida Grande", Arial, sans-serif; }', + message: messages.expected('Lucida Grande'), + line: 1, + column: 18, + }, + { + code: 'a { fOnT-fAmIlY: Lucida Grande, Arial, sans-serif; }', + fixed: 'a { fOnT-fAmIlY: "Lucida Grande", Arial, sans-serif; }', + message: messages.expected('Lucida Grande'), + line: 1, + column: 18, + }, + { + code: 'a { FONT-FAMILY: Lucida Grande, Arial, sans-serif; }', + fixed: 'a { FONT-FAMILY: "Lucida Grande", Arial, sans-serif; }', + message: messages.expected('Lucida Grande'), + line: 1, + column: 18, + }, + { + code: 'a { font-family: "Lucida Grande", Arial, "sans-serif"; }', + fixed: 'a { font-family: "Lucida Grande", Arial, sans-serif; }', + message: messages.rejected('sans-serif'), + line: 1, + column: 43, + }, + { + code: 'a { font-family: Red/Black, Arial, sans-serif; }', + fixed: 'a { font-family: "Red/Black", Arial, sans-serif; }', + message: messages.expected('Red/Black'), + }, + { + code: 'a { font-family: Arial, Ahem!, sans-serif; }', + fixed: 'a { font-family: Arial, "Ahem!", sans-serif; }', + message: messages.expected('Ahem!'), + }, + { + code: 'a { font-family: Hawaii 5-0, Arial, sans-serif; }', + fixed: 'a { font-family: "Hawaii 5-0", Arial, sans-serif; }', + message: messages.expected('Hawaii 5-0'), + }, + { + code: 'a { font-family: Times, Times New Roman, serif; }', + fixed: 'a { font-family: Times, "Times New Roman", serif; }', + message: messages.expected('Times New Roman'), + }, + { + code: 'a { font-family: Something6; }', + fixed: 'a { font-family: "Something6"; }', + message: messages.expected('Something6'), + }, + { + code: 'a { font-family: snake_case; }', + fixed: 'a { font-family: "snake_case"; }', + message: messages.expected('snake_case'), + }, + { + code: 'a { font-family: "Arial"; }', + fixed: 'a { font-family: Arial; }', + message: messages.rejected('Arial'), + }, + { + code: "a { font-family: '-apple-system', BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }", + fixed: + "a { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }", + message: messages.rejected('-apple-system'), + }, + { + code: "a { font-family: -apple-system, 'BlinkMacSystemFont', 'Segoe UI', Roboto, sans-serif; }", + fixed: + "a { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }", + message: messages.rejected('BlinkMacSystemFont'), + }, + { + code: "a { font-family: -apple-system, Hawaii 5-0, 'BlinkMacSystemFont', Segoe UI, 'Roboto', 'sans-serif'; }", + fixed: + 'a { font-family: -apple-system, "Hawaii 5-0", BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }', + description: 'multiple fixes on one declaration', + warnings: [ + { message: messages.expected('Hawaii 5-0') }, + { message: messages.rejected('BlinkMacSystemFont') }, + { message: messages.expected('Segoe UI') }, + { message: messages.rejected('Roboto') }, + { message: messages.rejected('sans-serif') }, + ], + }, + ], +}); + +testRule({ + ruleName, + config: ['always-where-required'], + fix: true, + + reject: [ + { + code: 'a { font: 1em "Lucida Grande", Arial, sans-serif; }', + fixed: 'a { font: 1em Lucida Grande, Arial, sans-serif; }', + message: messages.rejected('Lucida Grande'), + line: 1, + column: 16, + }, + { + code: 'a { font-family: "Lucida Grande", Arial, sans-serif; }', + fixed: 'a { font-family: Lucida Grande, Arial, sans-serif; }', + message: messages.rejected('Lucida Grande'), + line: 1, + column: 19, + }, + { + code: 'a { fOnT-fAmIlY: "Lucida Grande", Arial, sans-serif; }', + fixed: 'a { fOnT-fAmIlY: Lucida Grande, Arial, sans-serif; }', + message: messages.rejected('Lucida Grande'), + line: 1, + column: 19, + }, + { + code: 'a { FONT-FAMILY: "Lucida Grande", Arial, sans-serif; }', + fixed: 'a { FONT-FAMILY: Lucida Grande, Arial, sans-serif; }', + message: messages.rejected('Lucida Grande'), + line: 1, + column: 19, + }, + { + code: 'a { font-family: Lucida Grande, Arial, "sans-serif"; }', + fixed: 'a { font-family: Lucida Grande, Arial, sans-serif; }', + message: messages.rejected('sans-serif'), + line: 1, + column: 41, + }, + { + code: 'a { font-family: Red/Black, Arial, sans-serif; }', + fixed: 'a { font-family: "Red/Black", Arial, sans-serif; }', + message: messages.expected('Red/Black'), + }, + { + code: 'a { font-family: Arial, Ahem!, sans-serif; }', + fixed: 'a { font-family: Arial, "Ahem!", sans-serif; }', + message: messages.expected('Ahem!'), + }, + { + code: 'a { font-family: Hawaii 5-0, Arial, sans-serif; }', + fixed: 'a { font-family: "Hawaii 5-0", Arial, sans-serif; }', + message: messages.expected('Hawaii 5-0'), + }, + { + code: "a { font-family: '-apple-system', BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; }", + fixed: 'a { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; }', + message: messages.rejected('-apple-system'), + }, + { + code: "a { font-family: -apple-system, 'BlinkMacSystemFont', Segoe UI, Roboto, sans-serif; }", + fixed: 'a { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; }', + message: messages.rejected('BlinkMacSystemFont'), + }, + { + code: "a { font-family: -apple-system, Hawaii 5-0, 'BlinkMacSystemFont', Segoe UI, 'Roboto', 'sans-serif'; }", + fixed: + 'a { font-family: -apple-system, "Hawaii 5-0", BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; }', + description: 'multiple fixes on one declaration', + warnings: [ + { message: messages.expected('Hawaii 5-0') }, + { message: messages.rejected('BlinkMacSystemFont') }, + { message: messages.rejected('Roboto') }, + { message: messages.rejected('sans-serif') }, + ], + }, + ], +}); diff --git a/lib/rules/font-family-name-quotes/index.js b/lib/rules/font-family-name-quotes/index.js index 48eaf4f4ea..ee43fc5ec7 100644 --- a/lib/rules/font-family-name-quotes/index.js +++ b/lib/rules/font-family-name-quotes/index.js @@ -60,8 +60,68 @@ function quotesRequired(family) { .some((word) => /^(?:-?\d|--)/.test(word) || !/^[-\w\u{00A0}-\u{10FFFF}]+$/u.test(word)); } +/** + * @typedef {{ + * name: string, + * hasQuotes: boolean, + * sourceIndex: number, + * 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 newNode = { + name: fontFamily.value, + sourceIndex: fontFamily.sourceIndex, + hasQuotes: 'quote' in fontFamily, + resetIndexes: (/** @type {number} */ 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) => { +const rule = (primary, _secondary, context) => { return (root, result) => { const validOptions = validateOptions(result, ruleName, { actual: primary, @@ -73,28 +133,26 @@ const rule = (primary) => { } root.walkDecls(/^font(-family)?$/i, (decl) => { - const fontFamilies = findFontFamily(decl.value); + let fontFamilyNodes = makeMutableFontFamilies(findFontFamily(decl.value), decl); - if (fontFamilies.length === 0) { + if (fontFamilyNodes.length === 0) { return; } - for (const fontFamilyNode of fontFamilies) { - let rawFamily = fontFamilyNode.value; - - if ('quote' in fontFamilyNode) { - rawFamily = fontFamilyNode.quote + rawFamily + fontFamilyNode.quote; - } - - checkFamilyName(rawFamily, decl); + for (const fontFamilyNode of fontFamilyNodes) { + checkFamilyName(fontFamilyNode, decl); } }); /** - * @param {string} rawFamily + * @param {MutableNode} fontFamilyNode * @param {import('postcss').Declaration} decl */ - function checkFamilyName(rawFamily, decl) { + function checkFamilyName(fontFamilyNode, decl) { + const hasQuotes = fontFamilyNode.hasQuotes; + const family = fontFamilyNode.name; + const rawFamily = hasQuotes ? `"${family}"` : family; + if (!isStandardSyntaxValue(rawFamily)) { return; } @@ -103,15 +161,16 @@ const rule = (primary) => { return; } - const hasQuotes = rawFamily.startsWith("'") || rawFamily.startsWith('"'); - - // Clean the family of its quotes - const family = rawFamily.replace(/^['"]|['"]$/g, ''); - // 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), family, decl); } @@ -124,6 +183,12 @@ const rule = (primary) => { switch (primary) { case 'always-unless-keyword': if (!hasQuotes) { + if (context.fix) { + fontFamilyNode.addQuotes(); + + return; + } + return complain(messages.expected(family), family, decl); } @@ -131,10 +196,22 @@ const rule = (primary) => { case 'always-where-recommended': if (!recommended && hasQuotes) { + if (context.fix) { + fontFamilyNode.removeQuotes(); + + return; + } + return complain(messages.rejected(family), family, decl); } if (recommended && !hasQuotes) { + if (context.fix) { + fontFamilyNode.addQuotes(); + + return; + } + return complain(messages.expected(family), family, decl); } @@ -142,10 +219,22 @@ const rule = (primary) => { case 'always-where-required': if (!required && hasQuotes) { + if (context.fix) { + fontFamilyNode.removeQuotes(); + + return; + } + return complain(messages.rejected(family), family, decl); } if (required && !hasQuotes) { + if (context.fix) { + fontFamilyNode.addQuotes(); + + return; + } + return complain(messages.expected(family), family, decl); } }