index.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. 'use strict';
  2. const declarationValueIndex = require('../../utils/declarationValueIndex');
  3. const isNumbery = require('../../utils/isNumbery');
  4. const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue');
  5. const isVariable = require('../../utils/isVariable');
  6. const keywordSets = require('../../reference/keywordSets');
  7. const optionsMatches = require('../../utils/optionsMatches');
  8. const postcss = require('postcss');
  9. const report = require('../../utils/report');
  10. const ruleMessages = require('../../utils/ruleMessages');
  11. const validateOptions = require('../../utils/validateOptions');
  12. const { isAtRule } = require('../../utils/typeGuards');
  13. const ruleName = 'font-weight-notation';
  14. const messages = ruleMessages(ruleName, {
  15. expected: (type) => `Expected ${type} font-weight notation`,
  16. invalidNamed: (name) => `Unexpected invalid font-weight name "${name}"`,
  17. });
  18. const meta = {
  19. url: 'https://stylelint.io/user-guide/rules/list/font-weight-notation',
  20. };
  21. const INHERIT_KEYWORD = 'inherit';
  22. const INITIAL_KEYWORD = 'initial';
  23. const NORMAL_KEYWORD = 'normal';
  24. const WEIGHTS_WITH_KEYWORD_EQUIVALENTS = new Set(['400', '700']);
  25. /** @type {import('stylelint').Rule} */
  26. const rule = (primary, secondaryOptions) => {
  27. return (root, result) => {
  28. const validOptions = validateOptions(
  29. result,
  30. ruleName,
  31. {
  32. actual: primary,
  33. possible: ['numeric', 'named-where-possible'],
  34. },
  35. {
  36. actual: secondaryOptions,
  37. possible: {
  38. ignore: ['relative'],
  39. },
  40. optional: true,
  41. },
  42. );
  43. if (!validOptions) {
  44. return;
  45. }
  46. root.walkDecls((decl) => {
  47. if (decl.prop.toLowerCase() === 'font-weight') {
  48. checkWeight(decl.value, decl);
  49. }
  50. if (decl.prop.toLowerCase() === 'font') {
  51. checkFont(decl);
  52. }
  53. });
  54. /**
  55. * @param {import('postcss').Declaration} decl
  56. */
  57. function checkFont(decl) {
  58. const valueList = postcss.list.space(decl.value);
  59. // We do not need to more carefully distinguish font-weight
  60. // numbers from unitless line-heights because line-heights in
  61. // `font` values need to be part of a font-size/line-height pair
  62. const hasNumericFontWeight = valueList.some((value) => isNumbery(value));
  63. for (const value of postcss.list.space(decl.value)) {
  64. if (
  65. (value.toLowerCase() === NORMAL_KEYWORD && !hasNumericFontWeight) ||
  66. isNumbery(value) ||
  67. (value.toLowerCase() !== NORMAL_KEYWORD &&
  68. keywordSets.fontWeightKeywords.has(value.toLowerCase()))
  69. ) {
  70. checkWeight(value, decl);
  71. return;
  72. }
  73. }
  74. }
  75. /**
  76. * @param {string} weightValue
  77. * @param {import('postcss').Declaration} decl
  78. */
  79. function checkWeight(weightValue, decl) {
  80. if (!isStandardSyntaxValue(weightValue)) {
  81. return;
  82. }
  83. if (isVariable(weightValue)) {
  84. return;
  85. }
  86. if (
  87. weightValue.toLowerCase() === INHERIT_KEYWORD ||
  88. weightValue.toLowerCase() === INITIAL_KEYWORD
  89. ) {
  90. return;
  91. }
  92. if (
  93. optionsMatches(secondaryOptions, 'ignore', 'relative') &&
  94. keywordSets.fontWeightRelativeKeywords.has(weightValue.toLowerCase())
  95. ) {
  96. return;
  97. }
  98. const weightValueOffset = decl.value.indexOf(weightValue);
  99. if (primary === 'numeric') {
  100. const parent = decl.parent;
  101. if (parent && isAtRule(parent) && parent.name.toLowerCase() === 'font-face') {
  102. const weightValueNumbers = postcss.list.space(weightValue);
  103. if (!weightValueNumbers.every((value) => isNumbery(value))) {
  104. return complain(messages.expected('numeric'));
  105. }
  106. return;
  107. }
  108. if (!isNumbery(weightValue)) {
  109. return complain(messages.expected('numeric'));
  110. }
  111. }
  112. if (primary === 'named-where-possible') {
  113. if (isNumbery(weightValue)) {
  114. if (WEIGHTS_WITH_KEYWORD_EQUIVALENTS.has(weightValue)) {
  115. complain(messages.expected('named'));
  116. }
  117. return;
  118. }
  119. if (
  120. !keywordSets.fontWeightKeywords.has(weightValue.toLowerCase()) &&
  121. weightValue.toLowerCase() !== NORMAL_KEYWORD
  122. ) {
  123. return complain(messages.invalidNamed(weightValue));
  124. }
  125. }
  126. /**
  127. * @param {string} message
  128. */
  129. function complain(message) {
  130. report({
  131. ruleName,
  132. result,
  133. message,
  134. node: decl,
  135. index: declarationValueIndex(decl) + weightValueOffset,
  136. });
  137. }
  138. }
  139. };
  140. };
  141. rule.ruleName = ruleName;
  142. rule.messages = messages;
  143. rule.meta = meta;
  144. module.exports = rule;