1 | 'use strict';
|
2 | const parser = require('postcss-selector-parser');
|
3 | const canUnquote = require('./lib/canUnquote.js');
|
4 |
|
5 | const pseudoElements = new Set([
|
6 | '::before',
|
7 | '::after',
|
8 | '::first-letter',
|
9 | '::first-line',
|
10 | ]);
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | function attribute(selector) {
|
17 | if (selector.value) {
|
18 | if (selector.raws.value) {
|
19 |
|
20 | selector.raws.value = selector.raws.value.replace(/\\\n/g, '').trim();
|
21 | }
|
22 | if (canUnquote(selector.value)) {
|
23 | selector.quoteMark = null;
|
24 | }
|
25 |
|
26 | if (selector.operator) {
|
27 | selector.operator = (
|
28 | selector.operator.trim()
|
29 | );
|
30 | }
|
31 | }
|
32 |
|
33 | selector.rawSpaceBefore = '';
|
34 | selector.rawSpaceAfter = '';
|
35 | selector.spaces.attribute = { before: '', after: '' };
|
36 | selector.spaces.operator = { before: '', after: '' };
|
37 | selector.spaces.value = {
|
38 | before: '',
|
39 | after: selector.insensitive ? ' ' : '',
|
40 | };
|
41 |
|
42 | if (selector.raws.spaces) {
|
43 | selector.raws.spaces.attribute = {
|
44 | before: '',
|
45 | after: '',
|
46 | };
|
47 |
|
48 | selector.raws.spaces.operator = {
|
49 | before: '',
|
50 | after: '',
|
51 | };
|
52 |
|
53 | selector.raws.spaces.value = {
|
54 | before: '',
|
55 | after: selector.insensitive ? ' ' : '',
|
56 | };
|
57 |
|
58 | if (selector.insensitive) {
|
59 | selector.raws.spaces.insensitive = {
|
60 | before: '',
|
61 | after: '',
|
62 | };
|
63 | }
|
64 | }
|
65 |
|
66 | selector.attribute = selector.attribute.trim();
|
67 | }
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 | function combinator(selector) {
|
74 | const value = selector.value.trim();
|
75 | selector.spaces.before = '';
|
76 | selector.spaces.after = '';
|
77 | selector.rawSpaceBefore = '';
|
78 | selector.rawSpaceAfter = '';
|
79 | selector.value = value.length ? value : ' ';
|
80 | }
|
81 |
|
82 | const pseudoReplacements = new Map([
|
83 | [':nth-child', ':first-child'],
|
84 | [':nth-of-type', ':first-of-type'],
|
85 | [':nth-last-child', ':last-child'],
|
86 | [':nth-last-of-type', ':last-of-type'],
|
87 | ]);
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 | function pseudo(selector) {
|
94 | const value = selector.value.toLowerCase();
|
95 |
|
96 | if (selector.nodes.length === 1 && pseudoReplacements.has(value)) {
|
97 | const first = selector.at(0);
|
98 | const one = first.at(0);
|
99 |
|
100 | if (first.length === 1) {
|
101 | if (one.value === '1') {
|
102 | selector.replaceWith(
|
103 | parser.pseudo({
|
104 | value: (pseudoReplacements.get(value)),
|
105 | })
|
106 | );
|
107 | }
|
108 |
|
109 | if (one.value && one.value.toLowerCase() === 'even') {
|
110 | one.value = '2n';
|
111 | }
|
112 | }
|
113 |
|
114 | if (first.length === 3) {
|
115 | const two = first.at(1);
|
116 | const three = first.at(2);
|
117 |
|
118 | if (
|
119 | one.value &&
|
120 | one.value.toLowerCase() === '2n' &&
|
121 | two.value === '+' &&
|
122 | three.value === '1'
|
123 | ) {
|
124 | one.value = 'odd';
|
125 |
|
126 | two.remove();
|
127 | three.remove();
|
128 | }
|
129 | }
|
130 |
|
131 | return;
|
132 | }
|
133 |
|
134 | const uniques = new Set();
|
135 |
|
136 | selector.walk((child) => {
|
137 | if (child.type === 'selector') {
|
138 | const childStr = String(child);
|
139 |
|
140 | if (!uniques.has(childStr)) {
|
141 | uniques.add(childStr);
|
142 | } else {
|
143 | child.remove();
|
144 | }
|
145 | }
|
146 | });
|
147 |
|
148 | if (pseudoElements.has(value)) {
|
149 | selector.value = selector.value.slice(1);
|
150 | }
|
151 | }
|
152 |
|
153 | const tagReplacements = new Map([
|
154 | ['from', '0%'],
|
155 | ['100%', 'to'],
|
156 | ]);
|
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 | function tag(selector) {
|
163 | const value = selector.value.toLowerCase();
|
164 |
|
165 | if (tagReplacements.has(value)) {
|
166 | selector.value = (tagReplacements.get(value));
|
167 | }
|
168 | }
|
169 |
|
170 |
|
171 |
|
172 |
|
173 |
|
174 | function universal(selector) {
|
175 | const next = selector.next();
|
176 |
|
177 | if (next && next.type !== 'combinator') {
|
178 | selector.remove();
|
179 | }
|
180 | }
|
181 |
|
182 | const reducers = new Map(
|
183 | ([
|
184 | ['attribute', attribute],
|
185 | ['combinator', combinator],
|
186 | ['pseudo', pseudo],
|
187 | ['tag', tag],
|
188 | ['universal', universal],
|
189 | ])
|
190 | );
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 | function pluginCreator() {
|
197 | return {
|
198 | postcssPlugin: 'postcss-minify-selectors',
|
199 |
|
200 | OnceExit(css) {
|
201 | const cache = new Map();
|
202 | const processor = parser((selectors) => {
|
203 | const uniqueSelectors = new Set();
|
204 |
|
205 | selectors.walk((sel) => {
|
206 |
|
207 | sel.spaces.before = sel.spaces.after = '';
|
208 | const reducer = reducers.get(sel.type);
|
209 | if (reducer !== undefined) {
|
210 | reducer(sel);
|
211 | return;
|
212 | }
|
213 |
|
214 | const toString = String(sel);
|
215 |
|
216 | if (
|
217 | sel.type === 'selector' &&
|
218 | sel.parent &&
|
219 | sel.parent.type !== 'pseudo'
|
220 | ) {
|
221 | if (!uniqueSelectors.has(toString)) {
|
222 | uniqueSelectors.add(toString);
|
223 | } else {
|
224 | sel.remove();
|
225 | }
|
226 | }
|
227 | });
|
228 | selectors.nodes.sort();
|
229 | });
|
230 |
|
231 | css.walkRules((rule) => {
|
232 | const selector =
|
233 | rule.raws.selector && rule.raws.selector.value === rule.selector
|
234 | ? rule.raws.selector.raw
|
235 | : rule.selector;
|
236 |
|
237 |
|
238 |
|
239 | if (selector[selector.length - 1] === ':') {
|
240 | return;
|
241 | }
|
242 |
|
243 | if (cache.has(selector)) {
|
244 | rule.selector = cache.get(selector);
|
245 |
|
246 | return;
|
247 | }
|
248 |
|
249 | const optimizedSelector = processor.processSync(selector);
|
250 |
|
251 | rule.selector = optimizedSelector;
|
252 | cache.set(selector, optimizedSelector);
|
253 | });
|
254 | },
|
255 | };
|
256 | }
|
257 |
|
258 | pluginCreator.postcss = true;
|
259 | module.exports = pluginCreator;
|