1 |
|
2 |
|
3 | 'use strict';
|
4 |
|
5 | const _ = require('lodash');
|
6 | const atRuleParamIndex = require('../../utils/atRuleParamIndex');
|
7 | const declarationValueIndex = require('../../utils/declarationValueIndex');
|
8 | const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
|
9 | const parseSelector = require('../../utils/parseSelector');
|
10 | const report = require('../../utils/report');
|
11 | const ruleMessages = require('../../utils/ruleMessages');
|
12 | const validateOptions = require('../../utils/validateOptions');
|
13 | const valueParser = require('postcss-value-parser');
|
14 |
|
15 | const ruleName = 'string-quotes';
|
16 |
|
17 | const messages = ruleMessages(ruleName, {
|
18 | expected: (q) => `Expected ${q} quotes`,
|
19 | });
|
20 |
|
21 | const singleQuote = `'`;
|
22 | const doubleQuote = `"`;
|
23 |
|
24 | function 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 |
|
166 | if (!value.includes(erroneousQuote)) {
|
167 | return;
|
168 | }
|
169 |
|
170 | if (node.type === 'atrule' && node.name === 'charset') {
|
171 |
|
172 |
|
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 |
|
182 | return;
|
183 | }
|
184 |
|
185 | const openIndex = valueNode.sourceIndex;
|
186 |
|
187 |
|
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 |
|
215 | function replaceQuote(string, index, replace) {
|
216 | return string.substring(0, index) + replace + string.substring(index + replace.length);
|
217 | }
|
218 |
|
219 | rule.ruleName = ruleName;
|
220 | rule.messages = messages;
|
221 | module.exports = rule;
|