1 | 'use strict';
|
2 |
|
3 | const _ = require('lodash');
|
4 | const atRuleParamIndex = require('../../utils/atRuleParamIndex');
|
5 | const declarationValueIndex = require('../../utils/declarationValueIndex');
|
6 | const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
|
7 | const parseSelector = require('../../utils/parseSelector');
|
8 | const report = require('../../utils/report');
|
9 | const ruleMessages = require('../../utils/ruleMessages');
|
10 | const validateOptions = require('../../utils/validateOptions');
|
11 | const valueParser = require('postcss-value-parser');
|
12 |
|
13 | const ruleName = 'string-quotes';
|
14 |
|
15 | const messages = ruleMessages(ruleName, {
|
16 | expected: (q) => `Expected ${q} quotes`,
|
17 | });
|
18 |
|
19 | const singleQuote = `'`;
|
20 | const doubleQuote = `"`;
|
21 |
|
22 | const rule = function(expectation, secondary, context) {
|
23 | const correctQuote = expectation === 'single' ? singleQuote : doubleQuote;
|
24 | const erroneousQuote = expectation === 'single' ? doubleQuote : singleQuote;
|
25 |
|
26 | return (root, result) => {
|
27 | const validOptions = validateOptions(
|
28 | result,
|
29 | ruleName,
|
30 | {
|
31 | actual: expectation,
|
32 | possible: ['single', 'double'],
|
33 | },
|
34 | {
|
35 | actual: secondary,
|
36 | possible: {
|
37 | avoidEscape: _.isBoolean,
|
38 | },
|
39 | optional: true,
|
40 | },
|
41 | );
|
42 |
|
43 | if (!validOptions) {
|
44 | return;
|
45 | }
|
46 |
|
47 | const avoidEscape = _.get(secondary, 'avoidEscape', true);
|
48 |
|
49 | root.walk((node) => {
|
50 | switch (node.type) {
|
51 | case 'atrule':
|
52 | checkDeclOrAtRule(node, node.params, atRuleParamIndex);
|
53 | break;
|
54 | case 'decl':
|
55 | checkDeclOrAtRule(node, node.value, declarationValueIndex);
|
56 | break;
|
57 | case 'rule':
|
58 | checkRule(node);
|
59 | break;
|
60 | }
|
61 | });
|
62 |
|
63 | function checkRule(rule) {
|
64 | if (!isStandardSyntaxRule(rule)) {
|
65 | return;
|
66 | }
|
67 |
|
68 | if (rule.selector.indexOf('[') === -1 || rule.selector.indexOf('=') === -1) {
|
69 | return;
|
70 | }
|
71 |
|
72 | const fixPositions = [];
|
73 |
|
74 | parseSelector(rule.selector, result, rule, (selectorTree) => {
|
75 | selectorTree.walkAttributes((attributeNode) => {
|
76 | if (attributeNode.quoted && attributeNode.value.indexOf(erroneousQuote) !== -1) {
|
77 | const needsEscape = attributeNode.value.indexOf(correctQuote) !== -1;
|
78 |
|
79 | if (avoidEscape && needsEscape) {
|
80 |
|
81 | return;
|
82 | }
|
83 |
|
84 | const openIndex =
|
85 |
|
86 | attributeNode.sourceIndex +
|
87 |
|
88 | attributeNode.attribute.length +
|
89 |
|
90 | attributeNode.operator.length +
|
91 |
|
92 | erroneousQuote.length;
|
93 |
|
94 |
|
95 | if (context.fix && !needsEscape) {
|
96 | const closeIndex =
|
97 |
|
98 | openIndex +
|
99 |
|
100 | attributeNode.value.length -
|
101 |
|
102 | erroneousQuote.length;
|
103 |
|
104 | fixPositions.push(openIndex, closeIndex);
|
105 | } else {
|
106 | report({
|
107 | message: messages.expected(expectation),
|
108 | node: rule,
|
109 | index: openIndex,
|
110 | result,
|
111 | ruleName,
|
112 | });
|
113 | }
|
114 | }
|
115 | });
|
116 | });
|
117 | fixPositions.forEach((fixIndex) => {
|
118 | rule.selector = replaceQuote(rule.selector, fixIndex, correctQuote);
|
119 | });
|
120 | }
|
121 |
|
122 | function checkDeclOrAtRule(node, value, getIndex) {
|
123 | const fixPositions = [];
|
124 |
|
125 |
|
126 | if (value.indexOf(erroneousQuote) === -1) {
|
127 | return;
|
128 | } else if (node.type === 'atrule' && node.name === 'charset') {
|
129 |
|
130 |
|
131 | return;
|
132 | }
|
133 |
|
134 | valueParser(value).walk((valueNode) => {
|
135 | if (valueNode.type === 'string' && valueNode.quote === erroneousQuote) {
|
136 | const needsEscape = valueNode.value.indexOf(correctQuote) !== -1;
|
137 |
|
138 | if (avoidEscape && needsEscape) {
|
139 |
|
140 | return;
|
141 | }
|
142 |
|
143 | const openIndex = valueNode.sourceIndex;
|
144 |
|
145 |
|
146 | if (context.fix && !needsEscape) {
|
147 | const closeIndex = openIndex + valueNode.value.length + erroneousQuote.length;
|
148 |
|
149 | fixPositions.push(openIndex, closeIndex);
|
150 | } else {
|
151 | report({
|
152 | message: messages.expected(expectation),
|
153 | node,
|
154 | index: getIndex(node) + openIndex,
|
155 | result,
|
156 | ruleName,
|
157 | });
|
158 | }
|
159 | }
|
160 | });
|
161 |
|
162 | fixPositions.forEach((fixIndex) => {
|
163 | if (node.type === 'atrule') {
|
164 | node.params = replaceQuote(node.params, fixIndex, correctQuote);
|
165 | } else {
|
166 | node.value = replaceQuote(node.value, fixIndex, correctQuote);
|
167 | }
|
168 | });
|
169 | }
|
170 | };
|
171 | };
|
172 |
|
173 | function replaceQuote(string, index, replace) {
|
174 | return string.substring(0, index) + replace + string.substring(index + replace.length);
|
175 | }
|
176 |
|
177 | rule.ruleName = ruleName;
|
178 | rule.messages = messages;
|
179 | module.exports = rule;
|