UNPKG

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