index.js 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. 'use strict';
  2. const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
  3. const report = require('../../utils/report');
  4. const ruleMessages = require('../../utils/ruleMessages');
  5. const styleSearch = require('style-search');
  6. const validateOptions = require('../../utils/validateOptions');
  7. const whitespaceChecker = require('../../utils/whitespaceChecker');
  8. const ruleName = 'selector-list-comma-newline-after';
  9. const messages = ruleMessages(ruleName, {
  10. expectedAfter: () => 'Expected newline after ","',
  11. expectedAfterMultiLine: () => 'Expected newline after "," in a multi-line list',
  12. rejectedAfterMultiLine: () => 'Unexpected whitespace after "," in a multi-line list',
  13. });
  14. const meta = {
  15. url: 'https://stylelint.io/user-guide/rules/selector-list-comma-newline-after',
  16. fixable: true,
  17. deprecated: true,
  18. };
  19. /** @type {import('stylelint').Rule} */
  20. const rule = (primary, _secondaryOptions, context) => {
  21. const checker = whitespaceChecker('newline', primary, messages);
  22. return (root, result) => {
  23. const validOptions = validateOptions(result, ruleName, {
  24. actual: primary,
  25. possible: ['always', 'always-multi-line', 'never-multi-line'],
  26. });
  27. if (!validOptions) {
  28. return;
  29. }
  30. root.walkRules((ruleNode) => {
  31. if (!isStandardSyntaxRule(ruleNode)) {
  32. return;
  33. }
  34. // Get raw selector so we can allow end-of-line comments, e.g.
  35. // a, /* comment */
  36. // b {}
  37. const selector = ruleNode.raws.selector ? ruleNode.raws.selector.raw : ruleNode.selector;
  38. /** @type {number[]} */
  39. const fixIndices = [];
  40. styleSearch(
  41. {
  42. source: selector,
  43. target: ',',
  44. functionArguments: 'skip',
  45. },
  46. (match) => {
  47. const nextChars = selector.slice(match.endIndex);
  48. // If there's a // comment, that means there has to be a newline
  49. // ending the comment so we're fine
  50. if (/^\s+\/\//.test(nextChars)) {
  51. return;
  52. }
  53. // If there are spaces and then a comment begins, look for the newline
  54. const indextoCheckAfter = /^\s+\/\*/.test(nextChars)
  55. ? selector.indexOf('*/', match.endIndex) + 1
  56. : match.startIndex;
  57. checker.afterOneOnly({
  58. source: selector,
  59. index: indextoCheckAfter,
  60. err: (m) => {
  61. if (context.fix) {
  62. fixIndices.push(indextoCheckAfter + 1);
  63. return;
  64. }
  65. report({
  66. message: m,
  67. node: ruleNode,
  68. index: match.startIndex,
  69. result,
  70. ruleName,
  71. });
  72. },
  73. });
  74. },
  75. );
  76. if (fixIndices.length) {
  77. let fixedSelector = selector;
  78. for (const index of fixIndices.sort((a, b) => b - a)) {
  79. const beforeSelector = fixedSelector.slice(0, index);
  80. let afterSelector = fixedSelector.slice(index);
  81. if (primary.startsWith('always')) {
  82. afterSelector = context.newline + afterSelector;
  83. } else if (primary.startsWith('never-multi-line')) {
  84. afterSelector = afterSelector.replace(/^\s*/, '');
  85. }
  86. fixedSelector = beforeSelector + afterSelector;
  87. }
  88. if (ruleNode.raws.selector) {
  89. ruleNode.raws.selector.raw = fixedSelector;
  90. } else {
  91. ruleNode.selector = fixedSelector;
  92. }
  93. }
  94. });
  95. };
  96. };
  97. rule.ruleName = ruleName;
  98. rule.messages = messages;
  99. rule.meta = meta;
  100. module.exports = rule;