UNPKG

4.92 kBJavaScriptView Raw
1'use strict';
2
3const _ = require('lodash');
4const atRuleParamIndex = require('../../utils/atRuleParamIndex');
5const declarationValueIndex = require('../../utils/declarationValueIndex');
6const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
7const parseSelector = require('../../utils/parseSelector');
8const report = require('../../utils/report');
9const ruleMessages = require('../../utils/ruleMessages');
10const validateOptions = require('../../utils/validateOptions');
11const valueParser = require('postcss-value-parser');
12
13const ruleName = 'string-quotes';
14
15const messages = ruleMessages(ruleName, {
16 expected: (q) => `Expected ${q} quotes`,
17});
18
19const singleQuote = `'`;
20const doubleQuote = `"`;
21
22const 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 // don't consider this an error
81 return;
82 }
83
84 const openIndex =
85 // index of the start of our attribute node in our source
86 attributeNode.sourceIndex +
87 // length of our attribute
88 attributeNode.attribute.length +
89 // length of our operator , ie '='
90 attributeNode.operator.length +
91 // and the length of the quote
92 erroneousQuote.length;
93
94 // we currently don't fix escapes
95 if (context.fix && !needsEscape) {
96 const closeIndex =
97 // our initial index
98 openIndex +
99 // the length of our value
100 attributeNode.value.length -
101 // with the length of our quote subtracted
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 // Get out quickly if there are no erroneous quotes
126 if (value.indexOf(erroneousQuote) === -1) {
127 return;
128 } else if (node.type === 'atrule' && node.name === 'charset') {
129 // allow @charset rules to have double quotes, in spite of the configuration
130 // TODO: @charset should always use double-quotes, see https://github.com/stylelint/stylelint/issues/2788
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 // don't consider this an error
140 return;
141 }
142
143 const openIndex = valueNode.sourceIndex;
144
145 // we currently don't fix escapes
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
173function replaceQuote(string, index, replace) {
174 return string.substring(0, index) + replace + string.substring(index + replace.length);
175}
176
177rule.ruleName = ruleName;
178rule.messages = messages;
179module.exports = rule;