index.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. 'use strict';
  2. const valueParser = require('postcss-value-parser');
  3. const declarationValueIndex = require('../../utils/declarationValueIndex');
  4. const getDeclarationValue = require('../../utils/getDeclarationValue');
  5. const isNumbery = require('../../utils/isNumbery');
  6. const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue');
  7. const isVariable = require('../../utils/isVariable');
  8. const {
  9. fontWeightNonNumericKeywords,
  10. fontWeightRelativeKeywords,
  11. } = require('../../reference/keywords');
  12. const optionsMatches = require('../../utils/optionsMatches');
  13. const report = require('../../utils/report');
  14. const ruleMessages = require('../../utils/ruleMessages');
  15. const setDeclarationValue = require('../../utils/setDeclarationValue');
  16. const validateOptions = require('../../utils/validateOptions');
  17. const { assertString } = require('../../utils/validateTypes');
  18. const ruleName = 'font-weight-notation';
  19. const messages = ruleMessages(ruleName, {
  20. expected: (type) => `Expected ${type} font-weight notation`,
  21. expectedWithActual: (actual, expected) => `Expected "${actual}" to be "${expected}"`,
  22. });
  23. const meta = {
  24. url: 'https://stylelint.io/user-guide/rules/font-weight-notation',
  25. fixable: true,
  26. };
  27. const NORMAL_KEYWORD = 'normal';
  28. const NAMED_TO_NUMERIC = new Map([
  29. ['normal', '400'],
  30. ['bold', '700'],
  31. ]);
  32. const NUMERIC_TO_NAMED = new Map([
  33. ['400', 'normal'],
  34. ['700', 'bold'],
  35. ]);
  36. /** @type {import('stylelint').Rule<'numeric' | 'named-where-possible'>} */
  37. const rule = (primary, secondaryOptions, context) => {
  38. return (root, result) => {
  39. const validOptions = validateOptions(
  40. result,
  41. ruleName,
  42. {
  43. actual: primary,
  44. possible: ['numeric', 'named-where-possible'],
  45. },
  46. {
  47. actual: secondaryOptions,
  48. possible: {
  49. ignore: ['relative'],
  50. },
  51. optional: true,
  52. },
  53. );
  54. if (!validOptions) {
  55. return;
  56. }
  57. const ignoreRelative = optionsMatches(secondaryOptions, 'ignore', 'relative');
  58. root.walkDecls(/^font(-weight)?$/i, (decl) => {
  59. const isFontShorthandProp = decl.prop.toLowerCase() === 'font';
  60. const parsedValue = valueParser(getDeclarationValue(decl));
  61. const valueNodes = parsedValue.nodes;
  62. const hasNumericFontWeight = valueNodes.some((node, index, nodes) => {
  63. return isNumbery(node.value) && !isDivNode(nodes[index - 1]);
  64. });
  65. for (const [index, valueNode] of valueNodes.entries()) {
  66. if (!isPossibleFontWeightNode(valueNode, index, valueNodes)) continue;
  67. const { value } = valueNode;
  68. if (isFontShorthandProp) {
  69. if (value.toLowerCase() === NORMAL_KEYWORD && hasNumericFontWeight) {
  70. continue; // Not `normal` for font-weight
  71. }
  72. if (checkWeight(decl, valueNode)) {
  73. break; // Stop traverse if font-weight is processed
  74. }
  75. }
  76. checkWeight(decl, valueNode);
  77. }
  78. if (context.fix) {
  79. // Autofix after the loop ends can prevent value nodes from changing their positions during the loop.
  80. setDeclarationValue(decl, parsedValue.toString());
  81. }
  82. });
  83. /**
  84. * @param {import('postcss').Declaration} decl
  85. * @param {import('postcss-value-parser').Node} weightValueNode
  86. * @returns {true | undefined}
  87. */
  88. function checkWeight(decl, weightValueNode) {
  89. const weightValue = weightValueNode.value;
  90. if (!isStandardSyntaxValue(weightValue)) {
  91. return;
  92. }
  93. if (isVariable(weightValue)) {
  94. return;
  95. }
  96. const lowerWeightValue = weightValue.toLowerCase();
  97. if (ignoreRelative && fontWeightRelativeKeywords.has(lowerWeightValue)) {
  98. return;
  99. }
  100. if (primary === 'numeric') {
  101. if (!isNumbery(lowerWeightValue) && fontWeightNonNumericKeywords.has(lowerWeightValue)) {
  102. const numericValue = NAMED_TO_NUMERIC.get(lowerWeightValue);
  103. if (context.fix) {
  104. if (numericValue) {
  105. weightValueNode.value = numericValue;
  106. return true;
  107. }
  108. }
  109. const msg = numericValue
  110. ? messages.expectedWithActual(weightValue, numericValue)
  111. : messages.expected('numeric');
  112. complain(msg, weightValueNode);
  113. return true;
  114. }
  115. }
  116. if (primary === 'named-where-possible') {
  117. if (isNumbery(lowerWeightValue) && NUMERIC_TO_NAMED.has(lowerWeightValue)) {
  118. const namedValue = NUMERIC_TO_NAMED.get(lowerWeightValue);
  119. assertString(namedValue);
  120. if (context.fix) {
  121. weightValueNode.value = namedValue;
  122. return true;
  123. }
  124. complain(messages.expectedWithActual(weightValue, namedValue), weightValueNode);
  125. return true;
  126. }
  127. }
  128. /**
  129. * @param {string} message
  130. * @param {import('postcss-value-parser').Node} valueNode
  131. */
  132. function complain(message, valueNode) {
  133. const index = declarationValueIndex(decl) + valueNode.sourceIndex;
  134. const endIndex = index + valueNode.value.length;
  135. report({
  136. ruleName,
  137. result,
  138. message,
  139. node: decl,
  140. index,
  141. endIndex,
  142. });
  143. }
  144. }
  145. };
  146. };
  147. /**
  148. * @param {import('postcss-value-parser').Node | undefined} node
  149. * @returns {boolean}
  150. */
  151. function isDivNode(node) {
  152. return node !== undefined && node.type === 'div';
  153. }
  154. /**
  155. * @param {import('postcss-value-parser').Node} node
  156. * @param {number} index
  157. * @param {import('postcss-value-parser').Node[]} nodes
  158. * @returns {boolean}
  159. */
  160. function isPossibleFontWeightNode(node, index, nodes) {
  161. if (node.type !== 'word') return false;
  162. // Exclude `<font-size>/<line-height>` format like `16px/3`.
  163. if (isDivNode(nodes[index - 1])) return false;
  164. if (isDivNode(nodes[index + 1])) return false;
  165. return true;
  166. }
  167. rule.ruleName = ruleName;
  168. rule.messages = messages;
  169. rule.meta = meta;
  170. module.exports = rule;