index.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. // @ts-nocheck
  2. 'use strict';
  3. const balancedMatch = require('balanced-match');
  4. const isWhitespace = require('../../utils/isWhitespace');
  5. const report = require('../../utils/report');
  6. const ruleMessages = require('../../utils/ruleMessages');
  7. const styleSearch = require('style-search');
  8. const validateOptions = require('../../utils/validateOptions');
  9. const valueParser = require('postcss-value-parser');
  10. const ruleName = 'function-calc-no-unspaced-operator';
  11. const messages = ruleMessages(ruleName, {
  12. expectedBefore: (operator) => `Expected single space before "${operator}" operator`,
  13. expectedAfter: (operator) => `Expected single space after "${operator}" operator`,
  14. expectedOperatorBeforeSign: (operator) => `Expected an operator before sign "${operator}"`,
  15. });
  16. function rule(actual) {
  17. return (root, result) => {
  18. const validOptions = validateOptions(result, ruleName, { actual });
  19. if (!validOptions) {
  20. return;
  21. }
  22. function complain(message, node, index) {
  23. report({ message, node, index, result, ruleName });
  24. }
  25. root.walkDecls((decl) => {
  26. valueParser(decl.value).walk((node) => {
  27. if (node.type !== 'function' || node.value.toLowerCase() !== 'calc') {
  28. return;
  29. }
  30. const nodeText = valueParser.stringify(node);
  31. const parensMatch = balancedMatch('(', ')', nodeText);
  32. if (!parensMatch) {
  33. throw new Error(`No parens match: "${nodeText}"`);
  34. }
  35. const rawExpression = parensMatch.body;
  36. const expressionIndex =
  37. decl.source.start.column +
  38. decl.prop.length +
  39. (decl.raws.between || '').length +
  40. node.sourceIndex;
  41. const expression = blurVariables(rawExpression);
  42. checkSymbol('+');
  43. checkSymbol('-');
  44. checkSymbol('*');
  45. checkSymbol('/');
  46. function checkSymbol(symbol) {
  47. const styleSearchOptions = {
  48. source: expression,
  49. target: symbol,
  50. functionArguments: 'skip',
  51. };
  52. styleSearch(styleSearchOptions, (match) => {
  53. const index = match.startIndex;
  54. // Deal with signs.
  55. // (@ and $ are considered "digits" here to allow for variable syntaxes
  56. // that permit signs in front of variables, e.g. `-$number`)
  57. // As is "." to deal with fractional numbers without a leading zero
  58. if ((symbol === '+' || symbol === '-') && /[\d@$.]/.test(expression[index + 1])) {
  59. const expressionBeforeSign = expression.substr(0, index);
  60. // Ignore signs that directly follow a opening bracket
  61. if (expressionBeforeSign[expressionBeforeSign.length - 1] === '(') {
  62. return;
  63. }
  64. // Ignore signs at the beginning of the expression
  65. if (/^\s*$/.test(expressionBeforeSign)) {
  66. return;
  67. }
  68. // Otherwise, ensure that there is a real operator preceding them
  69. if (/[*/+-]\s*$/.test(expressionBeforeSign)) {
  70. return;
  71. }
  72. // And if not, complain
  73. complain(messages.expectedOperatorBeforeSign(symbol), decl, expressionIndex + index);
  74. return;
  75. }
  76. const beforeOk =
  77. (expression[index - 1] === ' ' && !isWhitespace(expression[index - 2])) ||
  78. newlineBefore(expression, index - 1);
  79. if (!beforeOk) {
  80. complain(messages.expectedBefore(symbol), decl, expressionIndex + index);
  81. }
  82. const afterOk =
  83. (expression[index + 1] === ' ' && !isWhitespace(expression[index + 2])) ||
  84. expression[index + 1] === '\n' ||
  85. expression.substr(index + 1, 2) === '\r\n';
  86. if (!afterOk) {
  87. complain(messages.expectedAfter(symbol), decl, expressionIndex + index);
  88. }
  89. });
  90. }
  91. });
  92. });
  93. };
  94. }
  95. function blurVariables(source) {
  96. return source.replace(/[$@][^)\s]+|#{.+?}/g, '0');
  97. }
  98. function newlineBefore(str, startIndex) {
  99. let index = startIndex;
  100. while (index && isWhitespace(str[index])) {
  101. if (str[index] === '\n') return true;
  102. index--;
  103. }
  104. return false;
  105. }
  106. rule.ruleName = ruleName;
  107. rule.messages = messages;
  108. module.exports = rule;