index.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. 'use strict';
  2. const valueParser = require('postcss-value-parser');
  3. const declarationValueIndex = require('../../utils/declarationValueIndex');
  4. const getDeclarationValue = require('../../utils/getDeclarationValue');
  5. const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue');
  6. const optionsMatches = require('../../utils/optionsMatches');
  7. const report = require('../../utils/report');
  8. const ruleMessages = require('../../utils/ruleMessages');
  9. const setDeclarationValue = require('../../utils/setDeclarationValue');
  10. const validateOptions = require('../../utils/validateOptions');
  11. const { isRegExp, isString, assert } = require('../../utils/validateTypes');
  12. const ruleName = 'alpha-value-notation';
  13. const messages = ruleMessages(ruleName, {
  14. expected: (unfixed, fixed) => `Expected "${unfixed}" to be "${fixed}"`,
  15. });
  16. const meta = {
  17. url: 'https://stylelint.io/user-guide/rules/list/alpha-value-notation',
  18. };
  19. const ALPHA_PROPS = new Set(['opacity', 'shape-image-threshold']);
  20. const ALPHA_FUNCS = new Set(['hsl', 'hsla', 'hwb', 'lab', 'lch', 'rgb', 'rgba']);
  21. /** @type {import('stylelint').Rule} */
  22. const rule = (primary, secondaryOptions, context) => {
  23. return (root, result) => {
  24. const validOptions = validateOptions(
  25. result,
  26. ruleName,
  27. {
  28. actual: primary,
  29. possible: ['number', 'percentage'],
  30. },
  31. {
  32. actual: secondaryOptions,
  33. possible: {
  34. exceptProperties: [isString, isRegExp],
  35. },
  36. optional: true,
  37. },
  38. );
  39. if (!validOptions) return;
  40. const optionFuncs = Object.freeze({
  41. number: {
  42. expFunc: isNumber,
  43. fixFunc: asNumber,
  44. },
  45. percentage: {
  46. expFunc: isPercentage,
  47. fixFunc: asPercentage,
  48. },
  49. });
  50. root.walkDecls((decl) => {
  51. let needsFix = false;
  52. const parsedValue = valueParser(getDeclarationValue(decl));
  53. parsedValue.walk((node) => {
  54. /** @type {import('postcss-value-parser').Node | undefined} */
  55. let alpha;
  56. if (ALPHA_PROPS.has(decl.prop.toLowerCase())) {
  57. alpha = findAlphaInValue(node);
  58. } else {
  59. if (node.type !== 'function') return;
  60. if (!ALPHA_FUNCS.has(node.value.toLowerCase())) return;
  61. alpha = findAlphaInFunction(node);
  62. }
  63. if (!alpha) return;
  64. const { value } = alpha;
  65. if (!isStandardSyntaxValue(value)) return;
  66. if (!isNumber(value) && !isPercentage(value)) return;
  67. /** @type {'number' | 'percentage'} */
  68. let expectation = primary;
  69. if (optionsMatches(secondaryOptions, 'exceptProperties', decl.prop)) {
  70. if (expectation === 'number') {
  71. expectation = 'percentage';
  72. } else if (expectation === 'percentage') {
  73. expectation = 'number';
  74. }
  75. }
  76. if (optionFuncs[expectation].expFunc(value)) return;
  77. const fixed = optionFuncs[expectation].fixFunc(value);
  78. const unfixed = value;
  79. if (context.fix) {
  80. alpha.value = String(fixed);
  81. needsFix = true;
  82. return;
  83. }
  84. report({
  85. message: messages.expected(unfixed, fixed),
  86. node: decl,
  87. index: declarationValueIndex(decl) + alpha.sourceIndex,
  88. result,
  89. ruleName,
  90. });
  91. });
  92. if (needsFix) {
  93. setDeclarationValue(decl, parsedValue.toString());
  94. }
  95. });
  96. };
  97. };
  98. /**
  99. * @param {string} value
  100. * @returns {string}
  101. */
  102. function asPercentage(value) {
  103. const number = Number(value);
  104. return `${Number((number * 100).toPrecision(3))}%`;
  105. }
  106. /**
  107. * @param {string} value
  108. * @returns {string}
  109. */
  110. function asNumber(value) {
  111. const dimension = valueParser.unit(value);
  112. assert(dimension);
  113. const number = Number(dimension.number);
  114. return Number((number / 100).toPrecision(3)).toString();
  115. }
  116. /**
  117. * @template {import('postcss-value-parser').Node} T
  118. * @param {T} node
  119. * @returns {T | undefined}
  120. */
  121. function findAlphaInValue(node) {
  122. return node.type === 'word' || node.type === 'function' ? node : undefined;
  123. }
  124. /**
  125. * @param {import('postcss-value-parser').FunctionNode} node
  126. * @returns {import('postcss-value-parser').Node | undefined}
  127. */
  128. function findAlphaInFunction(node) {
  129. const args = node.nodes.filter(({ type }) => type === 'word' || type === 'function');
  130. if (args.length === 4) return args[3];
  131. const slashNodeIndex = node.nodes.findIndex(({ type, value }) => type === 'div' && value === '/');
  132. if (slashNodeIndex !== -1) {
  133. const nodesAfterSlash = node.nodes.slice(slashNodeIndex + 1, node.nodes.length);
  134. return nodesAfterSlash.find(({ type }) => type === 'word');
  135. }
  136. return undefined;
  137. }
  138. /**
  139. * @param {string} value
  140. * @returns {boolean}
  141. */
  142. function isPercentage(value) {
  143. const dimension = valueParser.unit(value);
  144. return dimension && dimension.unit === '%';
  145. }
  146. /**
  147. * @param {string} value
  148. * @returns {boolean}
  149. */
  150. function isNumber(value) {
  151. const dimension = valueParser.unit(value);
  152. return dimension && dimension.unit === '';
  153. }
  154. rule.ruleName = ruleName;
  155. rule.messages = messages;
  156. rule.meta = meta;
  157. module.exports = rule;