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