1 | 'use strict';
|
2 |
|
3 | const atRuleParamIndex = require('../../utils/atRuleParamIndex');
|
4 | const declarationValueIndex = require('../../utils/declarationValueIndex');
|
5 | const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
|
6 | const parseSelector = require('../../utils/parseSelector');
|
7 | const report = require('../../utils/report');
|
8 | const ruleMessages = require('../../utils/ruleMessages');
|
9 | const validateOptions = require('../../utils/validateOptions');
|
10 | const valueParser = require('postcss-value-parser');
|
11 | const { isBoolean, assertString } = require('../../utils/validateTypes');
|
12 |
|
13 | const ruleName = 'string-quotes';
|
14 |
|
15 | const messages = ruleMessages(ruleName, {
|
16 | expected: (q) => `Expected ${q} quotes`,
|
17 | });
|
18 |
|
19 | const meta = {
|
20 | url: 'https://stylelint.io/user-guide/rules/list/string-quotes',
|
21 | };
|
22 |
|
23 | const singleQuote = `'`;
|
24 | const doubleQuote = `"`;
|
25 |
|
26 |
|
27 | const rule = (primary, secondaryOptions, context) => {
|
28 | const correctQuote = primary === 'single' ? singleQuote : doubleQuote;
|
29 | const erroneousQuote = primary === 'single' ? doubleQuote : singleQuote;
|
30 |
|
31 | return (root, result) => {
|
32 | const validOptions = validateOptions(
|
33 | result,
|
34 | ruleName,
|
35 | {
|
36 | actual: primary,
|
37 | possible: ['single', 'double'],
|
38 | },
|
39 | {
|
40 | actual: secondaryOptions,
|
41 | possible: {
|
42 | avoidEscape: [isBoolean],
|
43 | },
|
44 | optional: true,
|
45 | },
|
46 | );
|
47 |
|
48 | if (!validOptions) {
|
49 | return;
|
50 | }
|
51 |
|
52 | const avoidEscape =
|
53 | secondaryOptions && secondaryOptions.avoidEscape !== undefined
|
54 | ? secondaryOptions.avoidEscape
|
55 | : true;
|
56 |
|
57 | root.walk((node) => {
|
58 | switch (node.type) {
|
59 | case 'atrule':
|
60 | checkDeclOrAtRule(node, node.params, atRuleParamIndex);
|
61 | break;
|
62 | case 'decl':
|
63 | checkDeclOrAtRule(node, node.value, declarationValueIndex);
|
64 | break;
|
65 | case 'rule':
|
66 | checkRule(node);
|
67 | break;
|
68 | }
|
69 | });
|
70 |
|
71 | |
72 |
|
73 |
|
74 |
|
75 | function checkRule(ruleNode) {
|
76 | if (!isStandardSyntaxRule(ruleNode)) {
|
77 | return;
|
78 | }
|
79 |
|
80 | if (!ruleNode.selector.includes('[') || !ruleNode.selector.includes('=')) {
|
81 | return;
|
82 | }
|
83 |
|
84 |
|
85 | const fixPositions = [];
|
86 |
|
87 | parseSelector(ruleNode.selector, result, ruleNode, (selectorTree) => {
|
88 | let selectorFixed = false;
|
89 |
|
90 | selectorTree.walkAttributes((attributeNode) => {
|
91 | if (!attributeNode.quoted) {
|
92 | return;
|
93 | }
|
94 |
|
95 | if (attributeNode.quoteMark === correctQuote && avoidEscape) {
|
96 | assertString(attributeNode.value);
|
97 | const needsCorrectEscape = attributeNode.value.includes(correctQuote);
|
98 | const needsOtherEscape = attributeNode.value.includes(erroneousQuote);
|
99 |
|
100 | if (needsOtherEscape) {
|
101 | return;
|
102 | }
|
103 |
|
104 | if (needsCorrectEscape) {
|
105 | if (context.fix) {
|
106 | selectorFixed = true;
|
107 | attributeNode.quoteMark = erroneousQuote;
|
108 | } else {
|
109 | report({
|
110 | message: messages.expected(primary === 'single' ? 'double' : primary),
|
111 | node: ruleNode,
|
112 | index: attributeNode.sourceIndex + attributeNode.offsetOf('value'),
|
113 | result,
|
114 | ruleName,
|
115 | });
|
116 | }
|
117 | }
|
118 | }
|
119 |
|
120 | if (attributeNode.quoteMark === erroneousQuote) {
|
121 | if (avoidEscape) {
|
122 | assertString(attributeNode.value);
|
123 | const needsCorrectEscape = attributeNode.value.includes(correctQuote);
|
124 | const needsOtherEscape = attributeNode.value.includes(erroneousQuote);
|
125 |
|
126 | if (needsOtherEscape) {
|
127 | if (context.fix) {
|
128 | selectorFixed = true;
|
129 | attributeNode.quoteMark = correctQuote;
|
130 | } else {
|
131 | report({
|
132 | message: messages.expected(primary),
|
133 | node: ruleNode,
|
134 | index: attributeNode.sourceIndex + attributeNode.offsetOf('value'),
|
135 | result,
|
136 | ruleName,
|
137 | });
|
138 | }
|
139 |
|
140 | return;
|
141 | }
|
142 |
|
143 | if (needsCorrectEscape) {
|
144 | return;
|
145 | }
|
146 | }
|
147 |
|
148 | if (context.fix) {
|
149 | selectorFixed = true;
|
150 | attributeNode.quoteMark = correctQuote;
|
151 | } else {
|
152 | report({
|
153 | message: messages.expected(primary),
|
154 | node: ruleNode,
|
155 | index: attributeNode.sourceIndex + attributeNode.offsetOf('value'),
|
156 | result,
|
157 | ruleName,
|
158 | });
|
159 | }
|
160 | }
|
161 | });
|
162 |
|
163 | if (selectorFixed) {
|
164 | ruleNode.selector = selectorTree.toString();
|
165 | }
|
166 | });
|
167 |
|
168 | for (const fixIndex of fixPositions) {
|
169 | ruleNode.selector = replaceQuote(ruleNode.selector, fixIndex, correctQuote);
|
170 | }
|
171 | }
|
172 |
|
173 | |
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 | function checkDeclOrAtRule(node, value, getIndex) {
|
181 |
|
182 | const fixPositions = [];
|
183 |
|
184 |
|
185 | if (!value.includes(erroneousQuote)) {
|
186 | return;
|
187 | }
|
188 |
|
189 | if (node.type === 'atrule' && node.name === 'charset') {
|
190 |
|
191 |
|
192 | return;
|
193 | }
|
194 |
|
195 | valueParser(value).walk((valueNode) => {
|
196 | if (valueNode.type === 'string' && valueNode.quote === erroneousQuote) {
|
197 | const needsEscape = valueNode.value.includes(correctQuote);
|
198 |
|
199 | if (avoidEscape && needsEscape) {
|
200 |
|
201 | return;
|
202 | }
|
203 |
|
204 | const openIndex = valueNode.sourceIndex;
|
205 |
|
206 |
|
207 | if (context.fix && !needsEscape) {
|
208 | const closeIndex = openIndex + valueNode.value.length + erroneousQuote.length;
|
209 |
|
210 | fixPositions.push(openIndex, closeIndex);
|
211 | } else {
|
212 | report({
|
213 | message: messages.expected(primary),
|
214 | node,
|
215 | index: getIndex(node) + openIndex,
|
216 | result,
|
217 | ruleName,
|
218 | });
|
219 | }
|
220 | }
|
221 | });
|
222 |
|
223 | for (const fixIndex of fixPositions) {
|
224 | if (node.type === 'atrule') {
|
225 | node.params = replaceQuote(node.params, fixIndex, correctQuote);
|
226 | } else {
|
227 | node.value = replaceQuote(node.value, fixIndex, correctQuote);
|
228 | }
|
229 | }
|
230 | }
|
231 | };
|
232 | };
|
233 |
|
234 |
|
235 |
|
236 |
|
237 |
|
238 |
|
239 |
|
240 | function replaceQuote(string, index, replace) {
|
241 | return string.substring(0, index) + replace + string.substring(index + replace.length);
|
242 | }
|
243 |
|
244 | rule.ruleName = ruleName;
|
245 | rule.messages = messages;
|
246 | rule.meta = meta;
|
247 | module.exports = rule;
|