index.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. 'use strict';
  2. const styleSearch = require('style-search');
  3. const isOnlyWhitespace = require('../../utils/isOnlyWhitespace');
  4. const isStandardSyntaxComment = require('../../utils/isStandardSyntaxComment');
  5. const optionsMatches = require('../../utils/optionsMatches');
  6. const report = require('../../utils/report');
  7. const ruleMessages = require('../../utils/ruleMessages');
  8. const { isAtRule, isComment, isDeclaration, isRule } = require('../../utils/typeGuards');
  9. const validateOptions = require('../../utils/validateOptions');
  10. const ruleName = 'no-eol-whitespace';
  11. const messages = ruleMessages(ruleName, {
  12. rejected: 'Unexpected whitespace at end of line',
  13. });
  14. const meta = {
  15. url: 'https://stylelint.io/user-guide/rules/list/no-eol-whitespace',
  16. };
  17. const whitespacesToReject = new Set([' ', '\t']);
  18. /**
  19. * @param {string} str
  20. * @returns {string}
  21. */
  22. function fixString(str) {
  23. return str.replace(/[ \t]+$/, '');
  24. }
  25. /**
  26. * @param {number} lastEOLIndex
  27. * @param {string} string
  28. * @param {{ ignoreEmptyLines?: boolean, isRootFirst?: boolean }} [options]
  29. * @returns {number}
  30. */
  31. function findErrorStartIndex(
  32. lastEOLIndex,
  33. string,
  34. { ignoreEmptyLines, isRootFirst } = {
  35. ignoreEmptyLines: false,
  36. isRootFirst: false,
  37. },
  38. ) {
  39. const eolWhitespaceIndex = lastEOLIndex - 1;
  40. // If the character before newline is not whitespace, ignore
  41. if (!whitespacesToReject.has(string[eolWhitespaceIndex])) {
  42. return -1;
  43. }
  44. if (ignoreEmptyLines) {
  45. // If there is only whitespace between the previous newline and
  46. // this newline, ignore
  47. const beforeNewlineIndex = string.lastIndexOf('\n', eolWhitespaceIndex);
  48. if (beforeNewlineIndex >= 0 || isRootFirst) {
  49. const line = string.substring(beforeNewlineIndex, eolWhitespaceIndex);
  50. if (isOnlyWhitespace(line)) {
  51. return -1;
  52. }
  53. }
  54. }
  55. return eolWhitespaceIndex;
  56. }
  57. /** @type {import('stylelint').Rule} */
  58. const rule = (primary, secondaryOptions, context) => {
  59. return (root, result) => {
  60. const validOptions = validateOptions(
  61. result,
  62. ruleName,
  63. {
  64. actual: primary,
  65. },
  66. {
  67. optional: true,
  68. actual: secondaryOptions,
  69. possible: {
  70. ignore: ['empty-lines'],
  71. },
  72. },
  73. );
  74. if (!validOptions) {
  75. return;
  76. }
  77. const ignoreEmptyLines = optionsMatches(secondaryOptions, 'ignore', 'empty-lines');
  78. if (context.fix) {
  79. fix(root);
  80. }
  81. const rootString = context.fix ? root.toString() : (root.source && root.source.input.css) || '';
  82. /**
  83. * @param {number} index
  84. */
  85. const reportFromIndex = (index) => {
  86. report({
  87. message: messages.rejected,
  88. node: root,
  89. index,
  90. result,
  91. ruleName,
  92. });
  93. };
  94. eachEolWhitespace(rootString, reportFromIndex, true);
  95. const errorIndex = findErrorStartIndex(rootString.length, rootString, {
  96. ignoreEmptyLines,
  97. isRootFirst: true,
  98. });
  99. if (errorIndex > -1) {
  100. reportFromIndex(errorIndex);
  101. }
  102. /**
  103. * Iterate each whitespace at the end of each line of the given string.
  104. * @param {string} string - the source code string
  105. * @param {(index: number) => void} callback - callback the whitespace index at the end of each line.
  106. * @param {boolean} isRootFirst - set `true` if the given string is the first token of the root.
  107. * @returns {void}
  108. */
  109. function eachEolWhitespace(string, callback, isRootFirst) {
  110. styleSearch(
  111. {
  112. source: string,
  113. target: ['\n', '\r'],
  114. comments: 'check',
  115. },
  116. (match) => {
  117. const index = findErrorStartIndex(match.startIndex, string, {
  118. ignoreEmptyLines,
  119. isRootFirst,
  120. });
  121. if (index > -1) {
  122. callback(index);
  123. }
  124. },
  125. );
  126. }
  127. /**
  128. * @param {import('postcss').Root} rootNode
  129. */
  130. function fix(rootNode) {
  131. let isRootFirst = true;
  132. rootNode.walk((node) => {
  133. fixText(
  134. node.raws.before,
  135. (fixed) => {
  136. node.raws.before = fixed;
  137. },
  138. isRootFirst,
  139. );
  140. isRootFirst = false;
  141. if (isAtRule(node)) {
  142. fixText(node.raws.afterName, (fixed) => {
  143. node.raws.afterName = fixed;
  144. });
  145. const rawsParams = node.raws.params;
  146. if (rawsParams) {
  147. fixText(rawsParams.raw, (fixed) => {
  148. rawsParams.raw = fixed;
  149. });
  150. } else {
  151. fixText(node.params, (fixed) => {
  152. node.params = fixed;
  153. });
  154. }
  155. }
  156. if (isRule(node)) {
  157. const rawsSelector = node.raws.selector;
  158. if (rawsSelector) {
  159. fixText(rawsSelector.raw, (fixed) => {
  160. rawsSelector.raw = fixed;
  161. });
  162. } else {
  163. fixText(node.selector, (fixed) => {
  164. node.selector = fixed;
  165. });
  166. }
  167. }
  168. if (isAtRule(node) || isRule(node) || isDeclaration(node)) {
  169. fixText(node.raws.between, (fixed) => {
  170. node.raws.between = fixed;
  171. });
  172. }
  173. if (isDeclaration(node)) {
  174. const rawsValue = node.raws.value;
  175. if (rawsValue) {
  176. fixText(rawsValue.raw, (fixed) => {
  177. rawsValue.raw = fixed;
  178. });
  179. } else {
  180. fixText(node.value, (fixed) => {
  181. node.value = fixed;
  182. });
  183. }
  184. }
  185. if (isComment(node)) {
  186. fixText(node.raws.left, (fixed) => {
  187. node.raws.left = fixed;
  188. });
  189. if (!isStandardSyntaxComment(node)) {
  190. node.raws.right = node.raws.right && fixString(node.raws.right);
  191. } else {
  192. fixText(node.raws.right, (fixed) => {
  193. node.raws.right = fixed;
  194. });
  195. }
  196. fixText(node.text, (fixed) => {
  197. node.text = fixed;
  198. });
  199. }
  200. if (isAtRule(node) || isRule(node)) {
  201. fixText(node.raws.after, (fixed) => {
  202. node.raws.after = fixed;
  203. });
  204. }
  205. });
  206. fixText(
  207. rootNode.raws.after,
  208. (fixed) => {
  209. rootNode.raws.after = fixed;
  210. },
  211. isRootFirst,
  212. );
  213. if (typeof rootNode.raws.after === 'string') {
  214. const lastEOL = Math.max(
  215. rootNode.raws.after.lastIndexOf('\n'),
  216. rootNode.raws.after.lastIndexOf('\r'),
  217. );
  218. if (lastEOL !== rootNode.raws.after.length - 1) {
  219. rootNode.raws.after =
  220. rootNode.raws.after.slice(0, lastEOL + 1) +
  221. fixString(rootNode.raws.after.slice(lastEOL + 1));
  222. }
  223. }
  224. }
  225. /**
  226. * @param {string | undefined} value
  227. * @param {(text: string) => void} fixFn
  228. * @param {boolean} isRootFirst
  229. */
  230. function fixText(value, fixFn, isRootFirst = false) {
  231. if (!value) {
  232. return;
  233. }
  234. let fixed = '';
  235. let lastIndex = 0;
  236. eachEolWhitespace(
  237. value,
  238. (index) => {
  239. const newlineIndex = index + 1;
  240. fixed += fixString(value.slice(lastIndex, newlineIndex));
  241. lastIndex = newlineIndex;
  242. },
  243. isRootFirst,
  244. );
  245. if (lastIndex) {
  246. fixed += value.slice(lastIndex);
  247. fixFn(fixed);
  248. }
  249. }
  250. };
  251. };
  252. rule.ruleName = ruleName;
  253. rule.messages = messages;
  254. rule.meta = meta;
  255. module.exports = rule;