UNPKG

5.89 kBJavaScriptView Raw
1'use strict';
2const parser = require('postcss-selector-parser');
3const canUnquote = require('./lib/canUnquote.js');
4
5const pseudoElements = new Set([
6 '::before',
7 '::after',
8 '::first-letter',
9 '::first-line',
10]);
11
12/**
13 * @param {parser.Attribute} selector
14 * @return {void}
15 */
16function attribute(selector) {
17 if (selector.value) {
18 if (selector.raws.value) {
19 // Join selectors that are split over new lines
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 = /** @type {parser.AttributeOperator} */ (
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 * @param {parser.Combinator} selector
71 * @return {void}
72 */
73function 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
82const 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 * @param {parser.Pseudo} selector
91 * @return {void}
92 */
93function 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: /** @type {string} */ (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 selector.walk((child) => {
135 if (child.type === 'selector' && child.parent) {
136 const uniques = new Set();
137 child.parent.each((sibling) => {
138 const siblingStr = String(sibling);
139
140 if (!uniques.has(siblingStr)) {
141 uniques.add(siblingStr);
142 } else {
143 sibling.remove();
144 }
145 });
146 }
147 });
148
149 if (pseudoElements.has(value)) {
150 selector.value = selector.value.slice(1);
151 }
152}
153
154const tagReplacements = new Map([
155 ['from', '0%'],
156 ['100%', 'to'],
157]);
158
159/**
160 * @param {parser.Tag} selector
161 * @return {void}
162 */
163function tag(selector) {
164 const value = selector.value.toLowerCase();
165
166 if (tagReplacements.has(value)) {
167 selector.value = /** @type {string} */ (tagReplacements.get(value));
168 }
169}
170
171/**
172 * @param {parser.Universal} selector
173 * @return {void}
174 */
175function universal(selector) {
176 const next = selector.next();
177
178 if (next && next.type !== 'combinator') {
179 selector.remove();
180 }
181}
182
183const reducers = new Map(
184 /** @type {[string, ((selector: parser.Node) => void)][]}*/ ([
185 ['attribute', attribute],
186 ['combinator', combinator],
187 ['pseudo', pseudo],
188 ['tag', tag],
189 ['universal', universal],
190 ])
191);
192
193/**
194 * @type {import('postcss').PluginCreator<void>}
195 * @return {import('postcss').Plugin}
196 */
197function pluginCreator() {
198 return {
199 postcssPlugin: 'postcss-minify-selectors',
200
201 OnceExit(css) {
202 const cache = new Map();
203 const processor = parser((selectors) => {
204 const uniqueSelectors = new Set();
205
206 selectors.walk((sel) => {
207 // Trim whitespace around the value
208 sel.spaces.before = sel.spaces.after = '';
209 const reducer = reducers.get(sel.type);
210 if (reducer !== undefined) {
211 reducer(sel);
212 return;
213 }
214
215 const toString = String(sel);
216
217 if (
218 sel.type === 'selector' &&
219 sel.parent &&
220 sel.parent.type !== 'pseudo'
221 ) {
222 if (!uniqueSelectors.has(toString)) {
223 uniqueSelectors.add(toString);
224 } else {
225 sel.remove();
226 }
227 }
228 });
229 selectors.nodes.sort();
230 });
231
232 css.walkRules((rule) => {
233 const selector =
234 rule.raws.selector && rule.raws.selector.value === rule.selector
235 ? rule.raws.selector.raw
236 : rule.selector;
237
238 // If the selector ends with a ':' it is likely a part of a custom mixin,
239 // so just pass through.
240 if (selector[selector.length - 1] === ':') {
241 return;
242 }
243
244 if (cache.has(selector)) {
245 rule.selector = cache.get(selector);
246
247 return;
248 }
249
250 const optimizedSelector = processor.processSync(selector);
251
252 rule.selector = optimizedSelector;
253 cache.set(selector, optimizedSelector);
254 });
255 },
256 };
257}
258
259pluginCreator.postcss = true;
260module.exports = pluginCreator;