1 |
|
2 |
|
3 | 'use strict';
|
4 |
|
5 | const _ = require('lodash');
|
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 styleSearch = require('style-search');
|
11 | const validateOptions = require('../../utils/validateOptions');
|
12 |
|
13 | const ruleName = 'selector-attribute-brackets-space-inside';
|
14 |
|
15 | const messages = ruleMessages(ruleName, {
|
16 | expectedOpening: 'Expected single space after "["',
|
17 | rejectedOpening: 'Unexpected whitespace after "["',
|
18 | expectedClosing: 'Expected single space before "]"',
|
19 | rejectedClosing: 'Unexpected whitespace before "]"',
|
20 | });
|
21 |
|
22 | function rule(expectation, options, context) {
|
23 | return (root, result) => {
|
24 | const validOptions = validateOptions(result, ruleName, {
|
25 | actual: expectation,
|
26 | possible: ['always', 'never'],
|
27 | });
|
28 |
|
29 | if (!validOptions) {
|
30 | return;
|
31 | }
|
32 |
|
33 | root.walkRules((rule) => {
|
34 | if (!isStandardSyntaxRule(rule)) {
|
35 | return;
|
36 | }
|
37 |
|
38 | if (!rule.selector.includes('[')) {
|
39 | return;
|
40 | }
|
41 |
|
42 | const selector = rule.raws.selector ? rule.raws.selector.raw : rule.selector;
|
43 |
|
44 | let hasFixed;
|
45 | const fixedSelector = parseSelector(selector, result, rule, (selectorTree) => {
|
46 | selectorTree.walkAttributes((attributeNode) => {
|
47 | const attributeSelectorString = attributeNode.toString();
|
48 |
|
49 | styleSearch({ source: attributeSelectorString, target: '[' }, (match) => {
|
50 | const nextCharIsSpace = attributeSelectorString[match.startIndex + 1] === ' ';
|
51 | const index = attributeNode.sourceIndex + match.startIndex + 1;
|
52 |
|
53 | if (nextCharIsSpace && expectation === 'never') {
|
54 | if (context.fix) {
|
55 | hasFixed = true;
|
56 | fixBefore(attributeNode);
|
57 |
|
58 | return;
|
59 | }
|
60 |
|
61 | complain(messages.rejectedOpening, index);
|
62 | }
|
63 |
|
64 | if (!nextCharIsSpace && expectation === 'always') {
|
65 | if (context.fix) {
|
66 | hasFixed = true;
|
67 | fixBefore(attributeNode);
|
68 |
|
69 | return;
|
70 | }
|
71 |
|
72 | complain(messages.expectedOpening, index);
|
73 | }
|
74 | });
|
75 |
|
76 | styleSearch({ source: attributeSelectorString, target: ']' }, (match) => {
|
77 | const prevCharIsSpace = attributeSelectorString[match.startIndex - 1] === ' ';
|
78 | const index = attributeNode.sourceIndex + match.startIndex - 1;
|
79 |
|
80 | if (prevCharIsSpace && expectation === 'never') {
|
81 | if (context.fix) {
|
82 | hasFixed = true;
|
83 | fixAfter(attributeNode);
|
84 |
|
85 | return;
|
86 | }
|
87 |
|
88 | complain(messages.rejectedClosing, index);
|
89 | }
|
90 |
|
91 | if (!prevCharIsSpace && expectation === 'always') {
|
92 | if (context.fix) {
|
93 | hasFixed = true;
|
94 | fixAfter(attributeNode);
|
95 |
|
96 | return;
|
97 | }
|
98 |
|
99 | complain(messages.expectedClosing, index);
|
100 | }
|
101 | });
|
102 | });
|
103 | });
|
104 |
|
105 | if (hasFixed) {
|
106 | if (!rule.raws.selector) {
|
107 | rule.selector = fixedSelector;
|
108 | } else {
|
109 | rule.raws.selector.raw = fixedSelector;
|
110 | }
|
111 | }
|
112 |
|
113 | function complain(message, index) {
|
114 | report({
|
115 | message,
|
116 | index,
|
117 | result,
|
118 | ruleName,
|
119 | node: rule,
|
120 | });
|
121 | }
|
122 | });
|
123 | };
|
124 |
|
125 | function fixBefore(attributeNode) {
|
126 | const rawAttrBefore = _.get(attributeNode, 'raws.spaces.attribute.before');
|
127 | const { attrBefore, setAttrBefore } = rawAttrBefore
|
128 | ? {
|
129 | attrBefore: rawAttrBefore,
|
130 | setAttrBefore(fixed) {
|
131 | attributeNode.raws.spaces.attribute.before = fixed;
|
132 | },
|
133 | }
|
134 | : {
|
135 | attrBefore: _.get(attributeNode, 'spaces.attribute.before', ''),
|
136 | setAttrBefore(fixed) {
|
137 | _.set(attributeNode, 'spaces.attribute.before', fixed);
|
138 | },
|
139 | };
|
140 |
|
141 | if (expectation === 'always') {
|
142 | setAttrBefore(attrBefore.replace(/^\s*/, ' '));
|
143 | } else if (expectation === 'never') {
|
144 | setAttrBefore(attrBefore.replace(/^\s*/, ''));
|
145 | }
|
146 | }
|
147 |
|
148 | function fixAfter(attributeNode) {
|
149 | let key;
|
150 |
|
151 | if (attributeNode.operator) {
|
152 | if (attributeNode.insensitive) {
|
153 | key = 'insensitive';
|
154 | } else {
|
155 | key = 'value';
|
156 | }
|
157 | } else {
|
158 | key = 'attribute';
|
159 | }
|
160 |
|
161 | const rawAfter = _.get(attributeNode, `raws.spaces.${key}.after`);
|
162 | const { after, setAfter } = rawAfter
|
163 | ? {
|
164 | after: rawAfter,
|
165 | setAfter(fixed) {
|
166 | attributeNode.raws.spaces[key].after = fixed;
|
167 | },
|
168 | }
|
169 | : {
|
170 | after: _.get(attributeNode, `spaces.${key}.after`, ''),
|
171 | setAfter(fixed) {
|
172 | _.set(attributeNode, `spaces.${key}.after`, fixed);
|
173 | },
|
174 | };
|
175 |
|
176 | if (expectation === 'always') {
|
177 | setAfter(after.replace(/\s*$/, ' '));
|
178 | } else if (expectation === 'never') {
|
179 | setAfter(after.replace(/\s*$/, ''));
|
180 | }
|
181 | }
|
182 | }
|
183 |
|
184 | rule.ruleName = ruleName;
|
185 | rule.messages = messages;
|
186 | module.exports = rule;
|