UNPKG

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