index.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.calculationOperatorSpaceChecker = calculationOperatorSpaceChecker;
  6. exports["default"] = rule;
  7. exports.ruleName = exports.meta = exports.messages = void 0;
  8. var _postcssMediaQueryParser = _interopRequireDefault(require("postcss-media-query-parser"));
  9. var _stylelint = require("stylelint");
  10. var _utils = require("../../utils");
  11. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
  12. var ruleName = (0, _utils.namespace)("operator-no-unspaced");
  13. exports.ruleName = ruleName;
  14. var messages = _stylelint.utils.ruleMessages(ruleName, {
  15. expectedAfter: function expectedAfter(operator) {
  16. return "Expected single space after \"".concat(operator, "\"");
  17. },
  18. expectedBefore: function expectedBefore(operator) {
  19. return "Expected single space before \"".concat(operator, "\"");
  20. }
  21. });
  22. exports.messages = messages;
  23. var meta = {
  24. url: (0, _utils.ruleUrl)(ruleName)
  25. };
  26. /**
  27. * The actual check for are there (un)necessary whitespaces
  28. */
  29. exports.meta = meta;
  30. function checkSpaces(_ref) {
  31. var string = _ref.string,
  32. globalIndex = _ref.globalIndex,
  33. startIndex = _ref.startIndex,
  34. endIndex = _ref.endIndex,
  35. node = _ref.node,
  36. result = _ref.result;
  37. var symbol = string.substring(startIndex, endIndex + 1);
  38. var beforeOk = string[startIndex - 1] === " " && !(0, _utils.isWhitespace)(string[startIndex - 2]) || newlineBefore(string, startIndex - 1);
  39. if (!beforeOk) {
  40. _stylelint.utils.report({
  41. ruleName: ruleName,
  42. result: result,
  43. node: node,
  44. message: messages.expectedBefore(symbol),
  45. index: startIndex + globalIndex
  46. });
  47. }
  48. var afterOk = string[endIndex + 1] === " " && !(0, _utils.isWhitespace)(string[endIndex + 2]) || string[endIndex + 1] === "\n" || string.substr(endIndex + 1, 2) === "\r\n";
  49. if (!afterOk) {
  50. _stylelint.utils.report({
  51. ruleName: ruleName,
  52. result: result,
  53. node: node,
  54. message: messages.expectedAfter(symbol),
  55. index: endIndex + globalIndex
  56. });
  57. }
  58. }
  59. function newlineBefore(str, startIndex) {
  60. var index = startIndex;
  61. while (index && (0, _utils.isWhitespace)(str[index])) {
  62. if (str[index] === "\n") return true;
  63. index--;
  64. }
  65. return false;
  66. }
  67. function rule(expectation) {
  68. return function (root, result) {
  69. var validOptions = _stylelint.utils.validateOptions(result, ruleName, {
  70. actual: expectation
  71. });
  72. if (!validOptions) {
  73. return;
  74. }
  75. (0, _utils.eachRoot)(root, checkRoot);
  76. function checkRoot(root) {
  77. var rootString = root.source.input.css;
  78. if (rootString.trim() === "") {
  79. return;
  80. }
  81. calculationOperatorSpaceChecker({
  82. root: root,
  83. result: result,
  84. checker: checkSpaces
  85. });
  86. }
  87. };
  88. }
  89. rule.ruleName = ruleName;
  90. rule.messages = messages;
  91. rule.meta = meta;
  92. /**
  93. * The core rule logic function. This one can be imported by some other rules
  94. * that work with Sass operators
  95. *
  96. * @param {Object} args -- Named arguments object
  97. * @param {PostCSS Root} args.root
  98. * @param {PostCSS Result} args.result
  99. * @param {function} args.checker -- the function that is run against all the
  100. * operators found in the input. Takes these arguments:
  101. * {Object} cbArgs -- Named arguments object
  102. * {string} cbArgs.string -- the input string (suspected operation)
  103. * {number} cbArgs.globalIndex -- the string's index in a global input
  104. * {number} cbArgs.startIndex -- the start index of a symbol to inspect
  105. * {number} cbArgs.endIndex -- the end index of a symbol to inspect
  106. * (two indexes needed to allow for `==`, `!=`, etc.)
  107. * {PostCSS Node} cbArgs.node -- for stylelint.utils.report
  108. * {PostCSS Result} cbArgs.result -- for stylelint.utils.report
  109. */
  110. function calculationOperatorSpaceChecker(_ref2) {
  111. var root = _ref2.root,
  112. result = _ref2.result,
  113. checker = _ref2.checker;
  114. /**
  115. * Takes a string, finds all occurrences of Sass interpolation in it, then
  116. * finds all operators inside that interpolation
  117. *
  118. * @return {array} An array of objects { string, operators } - effectively,
  119. * a list of operators for each Sass interpolation occurrence
  120. */
  121. function findInterpolation(string, startIndex) {
  122. var interpolationRegex = /#{(.*?)}/g;
  123. var results = [];
  124. // Searching for interpolation
  125. var match = interpolationRegex.exec(string);
  126. startIndex = !isNaN(startIndex) ? Number(startIndex) : 0;
  127. while (match !== null) {
  128. results.push({
  129. source: match[0],
  130. operators: (0, _utils.findOperators)({
  131. string: match[0],
  132. globalIndex: match.index + startIndex
  133. })
  134. });
  135. match = interpolationRegex.exec(string);
  136. }
  137. return results;
  138. }
  139. var dataURIRegex = /^url\(\s*['"]?data:.+['"]?\s*\)/;
  140. root.walk(function (item) {
  141. if (item.prop === "unicode-range") {
  142. return;
  143. }
  144. var results = [];
  145. // Check a value (`10px` in `width: 10px;`)
  146. if (item.value !== undefined) {
  147. if (dataURIRegex.test(item.value)) {
  148. return results;
  149. }
  150. results.push({
  151. source: item.value,
  152. operators: (0, _utils.findOperators)({
  153. string: item.value,
  154. globalIndex: (0, _utils.declarationValueIndex)(item),
  155. // For Sass variable values some special rules apply
  156. isAfterColon: item.prop[0] === "$"
  157. })
  158. });
  159. }
  160. // Property name
  161. if (item.prop !== undefined) {
  162. results = results.concat(findInterpolation(item.prop));
  163. }
  164. // Selector
  165. if (item.selector !== undefined) {
  166. results = results.concat(findInterpolation(item.selector));
  167. }
  168. if (item.type === "atrule") {
  169. // @forward, @use and @at-root
  170. if (item.name === "forward" || item.name === "use" || item.name === "at-root") {
  171. return;
  172. }
  173. // Media queries
  174. if (item.name === "media" || item.name === "import") {
  175. (0, _postcssMediaQueryParser["default"])(item.params).walk(function (node) {
  176. var type = node.type;
  177. if (["keyword", "media-type", "media-feature"].includes(type)) {
  178. results = results.concat(findInterpolation(node.value, (0, _utils.atRuleParamIndex)(item) + node.sourceIndex));
  179. } else if (type === "value") {
  180. results.push({
  181. source: node.value,
  182. operators: (0, _utils.findOperators)({
  183. string: node.value,
  184. globalIndex: (0, _utils.atRuleParamIndex)(item) + node.sourceIndex,
  185. isAfterColon: true
  186. })
  187. });
  188. } else if (type === "url") {
  189. var isQuoted = node.value[0] === '"' || node.value[0] === "'";
  190. var containsWhitespace = node.value.search(/\s/) > -1;
  191. if (isQuoted || containsWhitespace) {
  192. // The argument to the url function is only parsed as SassScript if it is a quoted
  193. // string, or a _valid_ unquoted URL [1].
  194. //
  195. // [1] https://sass-lang.com/documentation/syntax/special-functions#url
  196. results.push({
  197. source: node.value,
  198. operators: (0, _utils.findOperators)({
  199. string: node.value,
  200. globalIndex: (0, _utils.atRuleParamIndex)(item) + node.sourceIndex,
  201. isAfterColon: true
  202. })
  203. });
  204. }
  205. }
  206. });
  207. } else {
  208. // Function and mixin definitions and other rules
  209. results.push({
  210. source: item.params,
  211. operators: (0, _utils.findOperators)({
  212. string: item.params,
  213. globalIndex: (0, _utils.atRuleParamIndex)(item),
  214. isAfterColon: true
  215. })
  216. });
  217. }
  218. }
  219. // All the strings have been parsed, now run whitespace checking
  220. results.forEach(function (el) {
  221. // Only if there are operators within a string
  222. if (el.operators && el.operators.length > 0) {
  223. el.operators.forEach(function (operator) {
  224. checker({
  225. string: el.source,
  226. globalIndex: operator.globalIndex,
  227. startIndex: operator.startIndex,
  228. endIndex: operator.endIndex,
  229. node: item,
  230. result: result
  231. });
  232. });
  233. }
  234. });
  235. });
  236. // Checking interpolation inside comments
  237. // We have to give up on PostCSS here because it skips some inline comments
  238. (0, _utils.findCommentsInRaws)(root.source.input.css).forEach(function (comment) {
  239. var startIndex = comment.source.start + comment.raws.startToken.length + comment.raws.left.length;
  240. if (comment.type !== "css") {
  241. return;
  242. }
  243. findInterpolation(comment.text).forEach(function (el) {
  244. // Only if there are operators within a string
  245. if (el.operators && el.operators.length > 0) {
  246. el.operators.forEach(function (operator) {
  247. checker({
  248. string: el.source,
  249. globalIndex: operator.globalIndex + startIndex,
  250. startIndex: operator.startIndex,
  251. endIndex: operator.endIndex,
  252. node: root,
  253. result: result
  254. });
  255. });
  256. }
  257. });
  258. });
  259. }