index.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. 'use strict';
  2. const execall = require('execall');
  3. const optionsMatches = require('../../utils/optionsMatches');
  4. const report = require('../../utils/report');
  5. const ruleMessages = require('../../utils/ruleMessages');
  6. const styleSearch = require('style-search');
  7. const validateOptions = require('../../utils/validateOptions');
  8. const { isNumber, isRegExp, isString } = require('../../utils/validateTypes');
  9. const ruleName = 'max-line-length';
  10. const EXCLUDED_PATTERNS = [
  11. /url\(\s*(\S.*\S)\s*\)/gi, // allow tab, whitespace in url content
  12. /@import\s+(['"].*['"])/gi,
  13. ];
  14. const messages = ruleMessages(ruleName, {
  15. expected: (max) =>
  16. `Expected line length to be no more than ${max} ${max === 1 ? 'character' : 'characters'}`,
  17. });
  18. const meta = {
  19. url: 'https://stylelint.io/user-guide/rules/list/max-line-length',
  20. };
  21. /** @type {import('stylelint').Rule} */
  22. const rule = (primary, secondaryOptions, context) => {
  23. return (root, result) => {
  24. const validOptions = validateOptions(
  25. result,
  26. ruleName,
  27. {
  28. actual: primary,
  29. possible: isNumber,
  30. },
  31. {
  32. actual: secondaryOptions,
  33. possible: {
  34. ignore: ['non-comments', 'comments'],
  35. ignorePattern: [isString, isRegExp],
  36. },
  37. optional: true,
  38. },
  39. );
  40. if (!validOptions) {
  41. return;
  42. }
  43. if (root.source == null) {
  44. throw new Error('The root node must have a source');
  45. }
  46. const ignoreNonComments = optionsMatches(secondaryOptions, 'ignore', 'non-comments');
  47. const ignoreComments = optionsMatches(secondaryOptions, 'ignore', 'comments');
  48. const rootString = context.fix ? root.toString() : root.source.input.css;
  49. // Array of skipped sub strings, i.e `url(...)`, `@import "..."`
  50. /** @type {Array<[number, number]>} */
  51. let skippedSubStrings = [];
  52. let skippedSubStringsIndex = 0;
  53. for (const pattern of EXCLUDED_PATTERNS)
  54. for (const match of execall(pattern, rootString)) {
  55. const subMatch = match.subMatches[0] || '';
  56. const startOfSubString = match.index + match.match.indexOf(subMatch);
  57. skippedSubStrings.push([startOfSubString, startOfSubString + subMatch.length]);
  58. continue;
  59. }
  60. skippedSubStrings = skippedSubStrings.sort((a, b) => a[0] - b[0]);
  61. // Check first line
  62. checkNewline({ endIndex: 0 });
  63. // Check subsequent lines
  64. styleSearch({ source: rootString, target: ['\n'], comments: 'check' }, (match) =>
  65. checkNewline(match),
  66. );
  67. /**
  68. * @param {number} index
  69. */
  70. function complain(index) {
  71. report({
  72. index,
  73. result,
  74. ruleName,
  75. message: messages.expected(primary),
  76. node: root,
  77. });
  78. }
  79. /**
  80. * @param {number} start
  81. * @param {number} end
  82. */
  83. function tryToPopSubString(start, end) {
  84. const [startSubString, endSubString] = skippedSubStrings[skippedSubStringsIndex];
  85. // Excluded substring does not presented in current line
  86. if (end < startSubString) {
  87. return 0;
  88. }
  89. // Compute excluded substring size regarding to current line indexes
  90. const excluded = Math.min(end, endSubString) - Math.max(start, startSubString);
  91. // Current substring is out of range for next lines
  92. if (endSubString <= end) {
  93. skippedSubStringsIndex++;
  94. }
  95. return excluded;
  96. }
  97. /**
  98. * @param {import('style-search').StyleSearchMatch | { endIndex: number }} match
  99. */
  100. function checkNewline(match) {
  101. let nextNewlineIndex = rootString.indexOf('\n', match.endIndex);
  102. if (rootString[nextNewlineIndex - 1] === '\r') {
  103. nextNewlineIndex -= 1;
  104. }
  105. // Accommodate last line
  106. if (nextNewlineIndex === -1) {
  107. nextNewlineIndex = rootString.length;
  108. }
  109. const rawLineLength = nextNewlineIndex - match.endIndex;
  110. const excludedLength = skippedSubStrings[skippedSubStringsIndex]
  111. ? tryToPopSubString(match.endIndex, nextNewlineIndex)
  112. : 0;
  113. const lineText = rootString.slice(match.endIndex, nextNewlineIndex);
  114. // Case sensitive ignorePattern match
  115. if (optionsMatches(secondaryOptions, 'ignorePattern', lineText)) {
  116. return;
  117. }
  118. // If the line's length is less than or equal to the specified
  119. // max, ignore it ... So anything below is liable to be complained about.
  120. // **Note that the length of any url arguments or import urls
  121. // are excluded from the calculation.**
  122. if (rawLineLength - excludedLength <= primary) {
  123. return;
  124. }
  125. const complaintIndex = nextNewlineIndex - 1;
  126. if (ignoreComments) {
  127. if ('insideComment' in match && match.insideComment) {
  128. return;
  129. }
  130. // This trimming business is to notice when the line starts a
  131. // comment but that comment is indented, e.g.
  132. // /* something here */
  133. const nextTwoChars = rootString.slice(match.endIndex).trim().slice(0, 2);
  134. if (nextTwoChars === '/*' || nextTwoChars === '//') {
  135. return;
  136. }
  137. }
  138. if (ignoreNonComments) {
  139. if ('insideComment' in match && match.insideComment) {
  140. return complain(complaintIndex);
  141. }
  142. // This trimming business is to notice when the line starts a
  143. // comment but that comment is indented, e.g.
  144. // /* something here */
  145. const nextTwoChars = rootString.slice(match.endIndex).trim().slice(0, 2);
  146. if (nextTwoChars !== '/*' && nextTwoChars !== '//') {
  147. return;
  148. }
  149. return complain(complaintIndex);
  150. }
  151. // If there are no spaces besides initial (indent) spaces, ignore it
  152. const lineString = rootString.slice(match.endIndex, nextNewlineIndex);
  153. if (!lineString.replace(/^\s+/, '').includes(' ')) {
  154. return;
  155. }
  156. return complain(complaintIndex);
  157. }
  158. };
  159. };
  160. rule.ruleName = ruleName;
  161. rule.messages = messages;
  162. rule.meta = meta;
  163. module.exports = rule;