1 |
|
2 |
|
3 | 'use strict';
|
4 |
|
5 | const _ = require('lodash');
|
6 | const execall = require('execall');
|
7 | const optionsMatches = require('../../utils/optionsMatches');
|
8 | const report = require('../../utils/report');
|
9 | const ruleMessages = require('../../utils/ruleMessages');
|
10 | const styleSearch = require('style-search');
|
11 | const validateOptions = require('../../utils/validateOptions');
|
12 |
|
13 | const ruleName = 'max-line-length';
|
14 | const EXCLUDED_PATTERNS = [
|
15 | /url\(\s*(\S.*\S)\s*\)/gi,
|
16 | /@import\s+(['"].*['"])/gi,
|
17 | ];
|
18 |
|
19 | const messages = ruleMessages(ruleName, {
|
20 | expected: (max) =>
|
21 | `Expected line length to be no more than ${max} ${max === 1 ? 'character' : 'characters'}`,
|
22 | });
|
23 |
|
24 | function 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 |
|
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 |
|
69 | checkNewline(rootString, { endIndex: 0 }, root);
|
70 |
|
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 |
|
89 | if (end < startSubString) {
|
90 | return 0;
|
91 | }
|
92 |
|
93 |
|
94 | const excluded = Math.min(end, endSubString) - Math.max(start, startSubString);
|
95 |
|
96 |
|
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 |
|
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 |
|
123 | if (optionsMatches(options, 'ignorePattern', lineText)) {
|
124 | return;
|
125 | }
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
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 |
|
143 |
|
144 |
|
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 |
|
158 |
|
159 |
|
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 |
|
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 |
|
181 | rule.ruleName = ruleName;
|
182 | rule.messages = messages;
|
183 | module.exports = rule;
|