index.mjs 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. import valueParser from 'postcss-value-parser';
  2. import { declarationValueIndex } from '../../utils/nodeFieldIndices.mjs';
  3. import getDeclarationValue from '../../utils/getDeclarationValue.mjs';
  4. import isStandardSyntaxValue from '../../utils/isStandardSyntaxValue.mjs';
  5. import report from '../../utils/report.mjs';
  6. import ruleMessages from '../../utils/ruleMessages.mjs';
  7. import setDeclarationValue from '../../utils/setDeclarationValue.mjs';
  8. import validateOptions from '../../utils/validateOptions.mjs';
  9. const ruleName = 'lightness-notation';
  10. const messages = ruleMessages(ruleName, {
  11. expected: (unfixed, fixed) => `Expected "${unfixed}" to be "${fixed}"`,
  12. });
  13. const meta = {
  14. url: 'https://stylelint.io/user-guide/rules/lightness-notation',
  15. fixable: true,
  16. };
  17. const LIGHTNESS_ZERO_TO_ONE_RANGE_FUNCS = new Set(['oklch', 'oklab']);
  18. const LIGHTNESS_ZERO_TO_HUNDRED_RANGE_FUNCS = new Set(['lch', 'lab']);
  19. const LIGHTNESS_FUNCS = new Set([
  20. ...LIGHTNESS_ZERO_TO_ONE_RANGE_FUNCS,
  21. ...LIGHTNESS_ZERO_TO_HUNDRED_RANGE_FUNCS,
  22. ]);
  23. const HAS_LIGHTNESS_COLOR_FUNC = new RegExp(`\\b(?:${[...LIGHTNESS_FUNCS].join('|')})\\(`, 'i');
  24. /** @type {import('stylelint').CoreRules[ruleName]} */
  25. const rule = (primary) => {
  26. return (root, result) => {
  27. const validOptions = validateOptions(result, ruleName, {
  28. actual: primary,
  29. possible: ['percentage', 'number'],
  30. });
  31. if (!validOptions) return;
  32. root.walkDecls((decl) => {
  33. if (!HAS_LIGHTNESS_COLOR_FUNC.test(decl.value)) return;
  34. const parsedValue = valueParser(getDeclarationValue(decl));
  35. parsedValue.walk((node) => {
  36. if (node.type !== 'function') return;
  37. const functionName = node.value.toLowerCase();
  38. if (!LIGHTNESS_FUNCS.has(functionName)) return;
  39. const lightness = findLightness(node);
  40. if (!lightness) return;
  41. const { value: unfixedValue } = lightness;
  42. if (!isStandardSyntaxValue(unfixedValue)) return;
  43. const dimension = valueParser.unit(unfixedValue);
  44. if (!dimension) return;
  45. const isPercentage = dimension.unit === `%`;
  46. const isNumber = dimension.unit === '';
  47. if (!isPercentage && !isNumber) return;
  48. if (primary === 'percentage' && isPercentage) return;
  49. if (primary === 'number' && isNumber) return;
  50. const fixedValue =
  51. primary === 'percentage'
  52. ? asPercentage(unfixedValue, functionName)
  53. : asNumber(unfixedValue, functionName);
  54. const valueIndex = declarationValueIndex(decl);
  55. const fix = () => {
  56. lightness.value = fixedValue;
  57. setDeclarationValue(decl, parsedValue.toString());
  58. };
  59. report({
  60. message: messages.expected,
  61. messageArgs: [unfixedValue, fixedValue],
  62. node: decl,
  63. index: valueIndex + lightness.sourceIndex,
  64. endIndex: valueIndex + lightness.sourceEndIndex,
  65. result,
  66. ruleName,
  67. fix: {
  68. apply: fix,
  69. node: decl,
  70. },
  71. });
  72. });
  73. });
  74. };
  75. };
  76. /**
  77. * @param {string} value
  78. * @param {import('postcss-value-parser').FunctionNode['value']} func
  79. */
  80. function asPercentage(value, func) {
  81. let num = Number.parseFloat(value);
  82. if (LIGHTNESS_ZERO_TO_HUNDRED_RANGE_FUNCS.has(func)) {
  83. return `${num}%`;
  84. }
  85. if (LIGHTNESS_ZERO_TO_ONE_RANGE_FUNCS.has(func)) {
  86. num *= 100;
  87. }
  88. if (Number.isInteger(num)) {
  89. return `${num}%`;
  90. }
  91. return `${roundToNumberOfDigits(num, value)}%`;
  92. }
  93. /**
  94. * @param {string} value
  95. * @param {import('postcss-value-parser').FunctionNode['value']} func
  96. */
  97. function asNumber(value, func) {
  98. let num = Number.parseFloat(value);
  99. if (LIGHTNESS_ZERO_TO_ONE_RANGE_FUNCS.has(func)) {
  100. num /= 100;
  101. return `${roundToNumberOfDigits(num, value)}`;
  102. }
  103. return `${num}`;
  104. }
  105. /**
  106. * @param {number} num
  107. * @param {string} value
  108. * @returns {string}
  109. */
  110. function roundToNumberOfDigits(num, value) {
  111. if (num === 0) return '0';
  112. const precision = value.replaceAll('%', '').replaceAll('.', '').length;
  113. if (precision === 0) return `${num}`;
  114. return num.toPrecision(precision).replace(/0+$/, ''); // trim trailing zeros
  115. }
  116. /**
  117. * @param {import('postcss-value-parser').FunctionNode} node
  118. */
  119. function findLightness({ nodes }) {
  120. return nodes.find(({ type }) => type === 'word' || type === 'function');
  121. }
  122. rule.ruleName = ruleName;
  123. rule.messages = messages;
  124. rule.meta = meta;
  125. export default rule;