index.js 3.2 KB

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