1 | 'use strict';
|
2 |
|
3 | const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule');
|
4 | const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
|
5 | const report = require('../../utils/report');
|
6 | const ruleMessages = require('../../utils/ruleMessages');
|
7 | const styleSearch = require('style-search');
|
8 | const validateOptions = require('../../utils/validateOptions');
|
9 |
|
10 | const ruleName = 'no-extra-semicolons';
|
11 |
|
12 | const messages = ruleMessages(ruleName, {
|
13 | rejected: 'Unexpected extra semicolon',
|
14 | });
|
15 |
|
16 | const meta = {
|
17 | url: 'https://stylelint.io/user-guide/rules/list/no-extra-semicolons',
|
18 | };
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 | function getOffsetByNode(node) {
|
25 |
|
26 | if (node.parent && node.parent.document) {
|
27 | return 0;
|
28 | }
|
29 |
|
30 | const root = node.root();
|
31 |
|
32 | if (!root.source) throw new Error('The root node must have a source');
|
33 |
|
34 | if (!node.source) throw new Error('The node must have a source');
|
35 |
|
36 | if (!node.source.start) throw new Error('The source must have a start position');
|
37 |
|
38 | const string = root.source.input.css;
|
39 | const nodeColumn = node.source.start.column;
|
40 | const nodeLine = node.source.start.line;
|
41 | let line = 1;
|
42 | let column = 1;
|
43 | let index = 0;
|
44 |
|
45 | for (let i = 0; i < string.length; i++) {
|
46 | if (column === nodeColumn && nodeLine === line) {
|
47 | index = i;
|
48 | break;
|
49 | }
|
50 |
|
51 | if (string[i] === '\n') {
|
52 | column = 1;
|
53 | line += 1;
|
54 | } else {
|
55 | column += 1;
|
56 | }
|
57 | }
|
58 |
|
59 | return index;
|
60 | }
|
61 |
|
62 |
|
63 | const rule = (primary, _secondaryOptions, context) => {
|
64 | return (root, result) => {
|
65 | const validOptions = validateOptions(result, ruleName, { actual: primary });
|
66 |
|
67 | if (!validOptions) {
|
68 | return;
|
69 | }
|
70 |
|
71 | if (root.raws.after && root.raws.after.trim().length !== 0) {
|
72 | const rawAfterRoot = root.raws.after;
|
73 |
|
74 |
|
75 | const fixSemiIndices = [];
|
76 |
|
77 | styleSearch({ source: rawAfterRoot, target: ';' }, (match) => {
|
78 | if (context.fix) {
|
79 | fixSemiIndices.push(match.startIndex);
|
80 |
|
81 | return;
|
82 | }
|
83 |
|
84 | if (!root.source) throw new Error('The root node must have a source');
|
85 |
|
86 | complain(root.source.input.css.length - rawAfterRoot.length + match.startIndex);
|
87 | });
|
88 |
|
89 |
|
90 | if (fixSemiIndices.length) {
|
91 | root.raws.after = removeIndices(rawAfterRoot, fixSemiIndices);
|
92 | }
|
93 | }
|
94 |
|
95 | root.walk((node) => {
|
96 | if (node.type === 'atrule' && !isStandardSyntaxAtRule(node)) {
|
97 | return;
|
98 | }
|
99 |
|
100 | if (node.type === 'rule' && !isStandardSyntaxRule(node)) {
|
101 | return;
|
102 | }
|
103 |
|
104 | if (node.raws.before && node.raws.before.trim().length !== 0) {
|
105 | const rawBeforeNode = node.raws.before;
|
106 | const allowedSemi = 0;
|
107 |
|
108 | const rawBeforeIndexStart = 0;
|
109 |
|
110 |
|
111 | const fixSemiIndices = [];
|
112 |
|
113 | styleSearch({ source: rawBeforeNode, target: ';' }, (match, count) => {
|
114 | if (count === allowedSemi) {
|
115 | return;
|
116 | }
|
117 |
|
118 | if (context.fix) {
|
119 | fixSemiIndices.push(match.startIndex - rawBeforeIndexStart);
|
120 |
|
121 | return;
|
122 | }
|
123 |
|
124 | complain(getOffsetByNode(node) - rawBeforeNode.length + match.startIndex);
|
125 | });
|
126 |
|
127 |
|
128 | if (fixSemiIndices.length) {
|
129 | node.raws.before = removeIndices(rawBeforeNode, fixSemiIndices);
|
130 | }
|
131 | }
|
132 |
|
133 | if (typeof node.raws.after === 'string' && node.raws.after.trim().length !== 0) {
|
134 | const rawAfterNode = node.raws.after;
|
135 |
|
136 | |
137 |
|
138 |
|
139 |
|
140 |
|
141 | if (
|
142 | 'last' in node &&
|
143 | node.last &&
|
144 | node.last.type === 'atrule' &&
|
145 | !isStandardSyntaxAtRule(node.last)
|
146 | ) {
|
147 | return;
|
148 | }
|
149 |
|
150 |
|
151 | const fixSemiIndices = [];
|
152 |
|
153 | styleSearch({ source: rawAfterNode, target: ';' }, (match) => {
|
154 | if (context.fix) {
|
155 | fixSemiIndices.push(match.startIndex);
|
156 |
|
157 | return;
|
158 | }
|
159 |
|
160 | const index =
|
161 | getOffsetByNode(node) +
|
162 | node.toString().length -
|
163 | 1 -
|
164 | rawAfterNode.length +
|
165 | match.startIndex;
|
166 |
|
167 | complain(index);
|
168 | });
|
169 |
|
170 |
|
171 | if (fixSemiIndices.length) {
|
172 | node.raws.after = removeIndices(rawAfterNode, fixSemiIndices);
|
173 | }
|
174 | }
|
175 |
|
176 | if (typeof node.raws.ownSemicolon === 'string') {
|
177 | const rawOwnSemicolon = node.raws.ownSemicolon;
|
178 | const allowedSemi = 0;
|
179 |
|
180 |
|
181 | const fixSemiIndices = [];
|
182 |
|
183 | styleSearch({ source: rawOwnSemicolon, target: ';' }, (match, count) => {
|
184 | if (count === allowedSemi) {
|
185 | return;
|
186 | }
|
187 |
|
188 | if (context.fix) {
|
189 | fixSemiIndices.push(match.startIndex);
|
190 |
|
191 | return;
|
192 | }
|
193 |
|
194 | const index =
|
195 | getOffsetByNode(node) +
|
196 | node.toString().length -
|
197 | rawOwnSemicolon.length +
|
198 | match.startIndex;
|
199 |
|
200 | complain(index);
|
201 | });
|
202 |
|
203 |
|
204 | if (fixSemiIndices.length) {
|
205 | node.raws.ownSemicolon = removeIndices(rawOwnSemicolon, fixSemiIndices);
|
206 | }
|
207 | }
|
208 | });
|
209 |
|
210 | |
211 |
|
212 |
|
213 | function complain(index) {
|
214 | report({
|
215 | message: messages.rejected,
|
216 | node: root,
|
217 | index,
|
218 | result,
|
219 | ruleName,
|
220 | });
|
221 | }
|
222 |
|
223 | |
224 |
|
225 |
|
226 |
|
227 |
|
228 | function removeIndices(str, indices) {
|
229 | for (const index of indices.reverse()) {
|
230 | str = str.slice(0, index) + str.slice(index + 1);
|
231 | }
|
232 |
|
233 | return str;
|
234 | }
|
235 | };
|
236 | };
|
237 |
|
238 | rule.ruleName = ruleName;
|
239 | rule.messages = messages;
|
240 | rule.meta = meta;
|
241 | module.exports = rule;
|