index.js 5.4 KB

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