| import { |
| TokenType, |
| isTokenComment, |
| isTokenEOF, |
| isTokenIdent, |
| isTokenWhiteSpaceOrComment, |
| stringify, |
| tokenize, |
| } from '@csstools/css-tokenizer'; |
| |
| import { |
| displayInsideKeywords, |
| displayListItemKeyword, |
| displayOutsideKeywords, |
| } from '../../reference/keywords.mjs'; |
| import { declarationValueIndex } from '../../utils/nodeFieldIndices.mjs'; |
| import getDeclarationValue from '../../utils/getDeclarationValue.mjs'; |
| import isStandardSyntaxValue from '../../utils/isStandardSyntaxValue.mjs'; |
| import { mayIncludeRegexes } from '../../utils/regexes.mjs'; |
| import report from '../../utils/report.mjs'; |
| import ruleMessages from '../../utils/ruleMessages.mjs'; |
| import setDeclarationValue from '../../utils/setDeclarationValue.mjs'; |
| import validateOptions from '../../utils/validateOptions.mjs'; |
| |
| /** @import {CSSToken,TokenIdent} from '@csstools/css-tokenizer' */ |
| |
| const ruleName = 'display-notation'; |
| |
| const messages = ruleMessages(ruleName, { |
| expected: (unexpected, expected) => `Expected "${unexpected}" to be "${expected}"`, |
| }); |
| |
| const meta = { |
| url: 'https://stylelint.io/user-guide/rules/display-notation', |
| fixable: true, |
| }; |
| |
| const DISPLAY_OUTSIDE = new RegExp(`^(?:${[...displayOutsideKeywords.values()].join('|')})$`, 'i'); |
| const DISPLAY_INSIDE = new RegExp(`^(?:${[...displayInsideKeywords.values()].join('|')})$`, 'i'); |
| const DISPLAY_LIST_ITEM = new RegExp( |
| `^(?:${[...displayListItemKeyword.values()].join('|')})$`, |
| 'i', |
| ); |
| |
| const DISPLAY_PROPERTY = /^display$/i; |
| |
| /** @type {Record<string, Array<string>>} */ |
| const SHORT_TO_LONG = { |
| 'block list-item': ['block', 'flow', 'list-item'], |
| block: ['block', 'flow'], |
| flex: ['block', 'flex'], |
| 'flow list-item': ['block', 'flow', 'list-item'], |
| flow: ['block', 'flow'], |
| 'flow-root': ['block', 'flow-root'], |
| grid: ['block', 'grid'], |
| 'inline list-item': ['inline', 'flow', 'list-item'], |
| inline: ['inline', 'flow'], |
| 'inline-block': ['inline', 'flow-root'], |
| 'inline-flex': ['inline', 'flex'], |
| 'inline-grid': ['inline', 'grid'], |
| 'inline-table': ['inline', 'table'], |
| 'list-item': ['block', 'flow', 'list-item'], |
| ruby: ['inline', 'ruby'], |
| 'run-in': ['run-in', 'flow'], |
| table: ['block', 'table'], |
| }; |
| |
| /** @type {Record<string, Array<string>>} */ |
| const LONG_TO_SHORT = { |
| 'block flex': ['flex'], |
| 'block flow list-item': ['list-item'], |
| 'block flow': ['block'], |
| 'block flow-root': ['flow-root'], |
| 'block grid': ['grid'], |
| 'block list-item': ['list-item'], |
| 'block table': ['table'], |
| 'flow list-item': ['list-item'], |
| 'inline flex': ['inline-flex'], |
| 'inline flow list-item': ['inline', 'list-item'], |
| 'inline flow': ['inline'], |
| 'inline flow-root': ['inline-block'], |
| 'inline grid': ['inline-grid'], |
| 'inline ruby': ['ruby'], |
| 'inline table': ['inline-table'], |
| 'run-in flow': ['run-in'], |
| }; |
| |
| /** @type {import('stylelint').CoreRules[ruleName]} */ |
| const rule = (primary) => { |
| return (root, result) => { |
| const validOptions = validateOptions(result, ruleName, { |
| actual: primary, |
| possible: ['short', 'full'], |
| }); |
| |
| if (!validOptions) return; |
| |
| root.walkDecls(DISPLAY_PROPERTY, (decl) => { |
| const value = getDeclarationValue(decl); |
| |
| if (!mayIncludeRegexes.displayKeyword.test(value)) return; |
| |
| if (!isStandardSyntaxValue(value)) return; |
| |
| const tokens = tokenize({ css: value }); |
| |
| // ignore values that contain functions or other non-keyword values |
| const hasNonKeywordTokens = tokens.some( |
| (token) => !(isTokenWhiteSpaceOrComment(token) || isTokenEOF(token) || isTokenIdent(token)), |
| ); |
| |
| if (hasNonKeywordTokens) return; |
| |
| const keywords = tokens.filter(isTokenIdent); |
| |
| if (!keywords.length) return; |
| |
| const normalizedValue = normalizeValue(keywords); |
| |
| const replacementValue = |
| primary === 'short' ? LONG_TO_SHORT[normalizedValue] : SHORT_TO_LONG[normalizedValue]; |
| |
| if (!replacementValue) return; |
| |
| const firstKeyword = keywords.at(0); |
| const lastKeyword = keywords.at(-1); |
| |
| if (!firstKeyword || !lastKeyword) return; |
| |
| const index = declarationValueIndex(decl); |
| |
| report({ |
| message: messages.expected, |
| messageArgs: [normalizedValue, replacementValue.join(' ')], |
| node: decl, |
| index: index + firstKeyword[2], |
| endIndex: index + lastKeyword[3] + 1, |
| result, |
| ruleName, |
| fix: { |
| apply: () => { |
| setDeclarationValue( |
| decl, |
| fixValue(tokens, firstKeyword, lastKeyword, replacementValue), |
| ); |
| }, |
| node: decl, |
| }, |
| }); |
| }); |
| }; |
| }; |
| |
| /** |
| * @param {string} a |
| * @returns {number} |
| */ |
| function keywordOrder(a) { |
| if (DISPLAY_OUTSIDE.test(a)) return 1; |
| |
| if (DISPLAY_INSIDE.test(a)) return 2; |
| |
| if (DISPLAY_LIST_ITEM.test(a)) return 3; |
| |
| return 0; |
| } |
| |
| /** |
| * @param {string} a |
| * @param {string} b |
| * @returns {number} |
| */ |
| function compareKeywords(a, b) { |
| return keywordOrder(a) - keywordOrder(b); |
| } |
| |
| /** |
| * @param {Array<TokenIdent>} tokens |
| * @returns {string} |
| */ |
| function normalizeValue(tokens) { |
| return tokens |
| .map((token) => token[4].value) |
| .sort(compareKeywords) |
| .join(' ') |
| .toLowerCase(); |
| } |
| |
| /** |
| * @param {Array<CSSToken>} originalValue |
| * @param {TokenIdent} firstKeyword |
| * @param {TokenIdent} lastKeyword |
| * @param {Array<string>} replacement |
| * @returns {string} |
| */ |
| function fixValue(originalValue, firstKeyword, lastKeyword, replacement) { |
| const before = originalValue.slice(0, originalValue.indexOf(firstKeyword)); |
| const commentsBetweenKeywords = originalValue |
| .slice(originalValue.indexOf(firstKeyword), originalValue.indexOf(lastKeyword)) |
| .filter(isTokenComment); |
| const after = originalValue.slice(originalValue.indexOf(lastKeyword) + 1); |
| |
| return stringify( |
| ...before, |
| ...replacement.flatMap((keyword, index) => { |
| /** @type {Array<CSSToken>} */ |
| const tokens = []; |
| |
| if (index !== 0) { |
| tokens.push([TokenType.Whitespace, ' ', -1, -1, undefined]); |
| } |
| |
| tokens.push([TokenType.Ident, keyword, -1, -1, { value: keyword }]); |
| |
| return tokens; |
| }), |
| ...commentsBetweenKeywords, |
| ...after, |
| ); |
| } |
| |
| rule.ruleName = ruleName; |
| rule.messages = messages; |
| rule.meta = meta; |
| export default rule; |