UNPKG

5.8 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 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
153const tagReplacements = new Map([
154 ['from', '0%'],
155 ['100%', 'to'],
156]);
157
158/**
159 * @param {parser.Tag} selector
160 * @return {void}
161 */
162function tag(selector) {
163 const value = selector.value.toLowerCase();
164
165 if (tagReplacements.has(value)) {
166 selector.value = /** @type {string} */ (tagReplacements.get(value));
167 }
168}
169
170/**
171 * @param {parser.Universal} selector
172 * @return {void}
173 */
174function universal(selector) {
175 const next = selector.next();
176
177 if (next && next.type !== 'combinator') {
178 selector.remove();
179 }
180}
181
182const reducers = new Map(
183 /** @type {[string, ((selector: parser.Node) => void)][]}*/ ([
184 ['attribute', attribute],
185 ['combinator', combinator],
186 ['pseudo', pseudo],
187 ['tag', tag],
188 ['universal', universal],
189 ])
190);
191
192/**
193 * @type {import('postcss').PluginCreator<void>}
194 * @return {import('postcss').Plugin}
195 */
196function 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 // Trim whitespace around the value
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 // If the selector ends with a ':' it is likely a part of a custom mixin,
238 // so just pass through.
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
258pluginCreator.postcss = true;
259module.exports = pluginCreator;