UNPKG

6.47 kBJavaScriptView Raw
1'use strict';
2
3const atRuleParamIndex = require('../../utils/atRuleParamIndex');
4const declarationValueIndex = require('../../utils/declarationValueIndex');
5const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
6const parseSelector = require('../../utils/parseSelector');
7const report = require('../../utils/report');
8const ruleMessages = require('../../utils/ruleMessages');
9const validateOptions = require('../../utils/validateOptions');
10const valueParser = require('postcss-value-parser');
11const { isBoolean, assertString } = require('../../utils/validateTypes');
12
13const ruleName = 'string-quotes';
14
15const messages = ruleMessages(ruleName, {
16 expected: (q) => `Expected ${q} quotes`,
17});
18
19const meta = {
20 url: 'https://stylelint.io/user-guide/rules/list/string-quotes',
21};
22
23const singleQuote = `'`;
24const doubleQuote = `"`;
25
26/** @type {import('stylelint').Rule} */
27const 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 * @param {import('postcss').Rule} ruleNode
73 * @returns {void}
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 /** @type {number[]} */
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 * @template {import('postcss').AtRule | import('postcss').Declaration} T
175 * @param {T} node
176 * @param {string} value
177 * @param {(node: T) => number} getIndex
178 * @returns {void}
179 */
180 function checkDeclOrAtRule(node, value, getIndex) {
181 /** @type {number[]} */
182 const fixPositions = [];
183
184 // Get out quickly if there are no erroneous quotes
185 if (!value.includes(erroneousQuote)) {
186 return;
187 }
188
189 if (node.type === 'atrule' && node.name === 'charset') {
190 // allow @charset rules to have double quotes, in spite of the configuration
191 // TODO: @charset should always use double-quotes, see https://github.com/stylelint/stylelint/issues/2788
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 // don't consider this an error
201 return;
202 }
203
204 const openIndex = valueNode.sourceIndex;
205
206 // we currently don't fix escapes
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 * @param {string} string
236 * @param {number} index
237 * @param {string} replace
238 * @returns {string}
239 */
240function replaceQuote(string, index, replace) {
241 return string.substring(0, index) + replace + string.substring(index + replace.length);
242}
243
244rule.ruleName = ruleName;
245rule.messages = messages;
246rule.meta = meta;
247module.exports = rule;