index.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. 'use strict';
  2. const optionsMatches = require('../../utils/optionsMatches');
  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 { isNumber } = require('../../utils/validateTypes');
  8. const ruleName = 'max-empty-lines';
  9. const messages = ruleMessages(ruleName, {
  10. expected: (max) => `Expected no more than ${max} empty ${max === 1 ? 'line' : 'lines'}`,
  11. });
  12. const meta = {
  13. url: 'https://stylelint.io/user-guide/rules/max-empty-lines',
  14. fixable: true,
  15. };
  16. /** @type {import('stylelint').Rule} */
  17. const rule = (primary, secondaryOptions, context) => {
  18. let emptyLines = 0;
  19. let lastIndex = -1;
  20. return (root, result) => {
  21. const validOptions = validateOptions(
  22. result,
  23. ruleName,
  24. {
  25. actual: primary,
  26. possible: isNumber,
  27. },
  28. {
  29. actual: secondaryOptions,
  30. possible: {
  31. ignore: ['comments'],
  32. },
  33. optional: true,
  34. },
  35. );
  36. if (!validOptions) {
  37. return;
  38. }
  39. const ignoreComments = optionsMatches(secondaryOptions, 'ignore', 'comments');
  40. const getChars = replaceEmptyLines.bind(null, primary);
  41. /**
  42. * 1. walk nodes & replace enterchar
  43. * 2. deal with special case.
  44. */
  45. if (context.fix) {
  46. root.walk((node) => {
  47. if (node.type === 'comment' && !ignoreComments) {
  48. node.raws.left = getChars(node.raws.left);
  49. node.raws.right = getChars(node.raws.right);
  50. }
  51. if (node.raws.before) {
  52. node.raws.before = getChars(node.raws.before);
  53. }
  54. });
  55. // first node
  56. const firstNodeRawsBefore = root.first && root.first.raws.before;
  57. // root raws
  58. const rootRawsAfter = root.raws.after;
  59. // not document node
  60. // @ts-expect-error -- TS2339: Property 'document' does not exist on type 'Root'.
  61. if ((root.document && root.document.constructor.name) !== 'Document') {
  62. if (firstNodeRawsBefore) {
  63. root.first.raws.before = getChars(firstNodeRawsBefore, true);
  64. }
  65. if (rootRawsAfter) {
  66. // when max setted 0, should be treated as 1 in this situation.
  67. root.raws.after = replaceEmptyLines(primary === 0 ? 1 : primary, rootRawsAfter, true);
  68. }
  69. } else if (rootRawsAfter) {
  70. // `css in js` or `html`
  71. root.raws.after = replaceEmptyLines(primary === 0 ? 1 : primary, rootRawsAfter);
  72. }
  73. return;
  74. }
  75. emptyLines = 0;
  76. lastIndex = -1;
  77. const rootString = root.toString();
  78. styleSearch(
  79. {
  80. source: rootString,
  81. target: /\r\n/.test(rootString) ? '\r\n' : '\n',
  82. comments: ignoreComments ? 'skip' : 'check',
  83. },
  84. (match) => {
  85. checkMatch(rootString, match.startIndex, match.endIndex, root);
  86. },
  87. );
  88. /**
  89. * @param {string} source
  90. * @param {number} matchStartIndex
  91. * @param {number} matchEndIndex
  92. * @param {import('postcss').Root} node
  93. */
  94. function checkMatch(source, matchStartIndex, matchEndIndex, node) {
  95. const eof = matchEndIndex === source.length;
  96. let problem = false;
  97. // Additional check for beginning of file
  98. if (!matchStartIndex || lastIndex === matchStartIndex) {
  99. emptyLines++;
  100. } else {
  101. emptyLines = 0;
  102. }
  103. lastIndex = matchEndIndex;
  104. if (emptyLines > primary) problem = true;
  105. if (!eof && !problem) return;
  106. if (problem) {
  107. report({
  108. message: messages.expected(primary),
  109. node,
  110. index: matchStartIndex,
  111. result,
  112. ruleName,
  113. });
  114. }
  115. // Additional check for end of file
  116. if (eof && primary) {
  117. emptyLines++;
  118. if (emptyLines > primary && isEofNode(result.root, node)) {
  119. report({
  120. message: messages.expected(primary),
  121. node,
  122. index: matchEndIndex,
  123. result,
  124. ruleName,
  125. });
  126. }
  127. }
  128. }
  129. /**
  130. * @param {number} maxLines
  131. * @param {unknown} str
  132. * @param {boolean?} isSpecialCase
  133. */
  134. function replaceEmptyLines(maxLines, str, isSpecialCase = false) {
  135. const repeatTimes = isSpecialCase ? maxLines : maxLines + 1;
  136. if (repeatTimes === 0 || typeof str !== 'string') {
  137. return '';
  138. }
  139. const emptyLFLines = '\n'.repeat(repeatTimes);
  140. const emptyCRLFLines = '\r\n'.repeat(repeatTimes);
  141. return /(?:\r\n)+/.test(str)
  142. ? str.replace(/(\r\n)+/g, ($1) => {
  143. if ($1.length / 2 > repeatTimes) {
  144. return emptyCRLFLines;
  145. }
  146. return $1;
  147. })
  148. : str.replace(/(\n)+/g, ($1) => {
  149. if ($1.length > repeatTimes) {
  150. return emptyLFLines;
  151. }
  152. return $1;
  153. });
  154. }
  155. };
  156. };
  157. /**
  158. * Checks whether the given node is the last node of file.
  159. * @param {import('stylelint').PostcssResult['root']} document - the document node with `postcss-html` and `postcss-jsx`.
  160. * @param {import('postcss').Root} root - the root node of css
  161. */
  162. function isEofNode(document, root) {
  163. if (!document || document.constructor.name !== 'Document' || !('type' in document)) {
  164. return true;
  165. }
  166. // In the `postcss-html` and `postcss-jsx` syntax, checks that there is text after the given node.
  167. let after;
  168. if (root === document.last) {
  169. after = document.raws && document.raws.codeAfter;
  170. } else {
  171. // @ts-expect-error -- TS2345: Argument of type 'Root' is not assignable to parameter of type 'number | ChildNode'.
  172. const rootIndex = document.index(root);
  173. const nextNode = document.nodes[rootIndex + 1];
  174. after = nextNode && nextNode.raws && nextNode.raws.codeBefore;
  175. }
  176. return !String(after).trim();
  177. }
  178. rule.ruleName = ruleName;
  179. rule.messages = messages;
  180. rule.meta = meta;
  181. module.exports = rule;