postcss-minify-selector 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE-MIT ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) Ben Briggs <beneb.info@gmail.com> (http://beneb.info)
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # [postcss][postcss]-minify-selectors
2
+
3
+ > Minify selectors with PostCSS.
4
+
5
+ ## Install
6
+
7
+ With [npm](https://www.npmjs.com/package/postcss-minify-selectors) do:
8
+
9
+ ```
10
+ npm install postcss-minify-selectors --save
11
+ ```
12
+
13
+ ## Example
14
+
15
+ ### Input
16
+
17
+ ```css
18
+ h1 + p, h2, h3, h2{color:blue}
19
+ ```
20
+
21
+ ### Output
22
+
23
+ ```css
24
+ h1+p,h2,h3{color:blue}
25
+ ```
26
+
27
+ For more examples see the [tests](test/index.js).
28
+
29
+ ## Options
30
+
31
+ ### `sort`
32
+
33
+ Type: `boolean`
34
+ Default: `true`
35
+
36
+ Alphabetically sort selectors within a comma-separated list.
37
+
38
+ ### `convertToIs`
39
+
40
+ Type: `boolean`
41
+ Default: `true`
42
+
43
+ Factor a shared prefix and/or suffix in a comma-separated selector list into
44
+ a single `:is(...)` group when the result is strictly shorter and safe with
45
+ respect to the cascade. The rewrite only applies when every variable part
46
+ has the same CSS specificity (so the cascade isn't silently altered) and
47
+ contains no pseudo-elements. It is automatically skipped when the configured
48
+ `browserslist` target doesn't support `:is()`.
49
+
50
+ #### Input
51
+
52
+ ```css
53
+ section h1, article h1, aside h1, nav h1 { font-size: 25px }
54
+ ```
55
+
56
+ #### Output
57
+
58
+ ```css
59
+ :is(article,aside,nav,section) h1{font-size:25px}
60
+ ```
61
+
62
+ ### Browserslist
63
+
64
+ The plugin reads the `browserslist` configuration from the host project by
65
+ default. You can override with `overrideBrowserslist`, `stats`, `env`, or
66
+ `path` — the same options accepted by `autoprefixer` and `postcss-merge-rules`.
67
+
68
+ ## Usage
69
+
70
+ See the [PostCSS documentation](https://github.com/postcss/postcss#usage) for
71
+ examples for your environment.
72
+
73
+ ## Contributors
74
+
75
+ See [CONTRIBUTORS.md](https://github.com/cssnano/cssnano/blob/master/CONTRIBUTORS.md).
76
+
77
+ ## License
78
+
79
+ MIT © [Ben Briggs](http://beneb.info)
80
+
81
+ [postcss]: https://github.com/postcss/postcss
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "postcss-minify-selector",
3
+ "version": "0.1.2",
4
+ "description": "Minify selector with PostCSS.",
5
+ "main": "src/index.js",
6
+ "types": "types/index.d.ts",
7
+ "files": [
8
+ "src",
9
+ "LICENSE-MIT",
10
+ "types"
11
+ ],
12
+ "keywords": [
13
+ "css",
14
+ "minify",
15
+ "optimise",
16
+ "postcss",
17
+ "postcss-plugin",
18
+ "selectors"
19
+ ],
20
+ "license": "MIT",
21
+ "homepage": "https://github.com/cssnano/cssnano",
22
+ "author": {
23
+ "name": "Ben Briggs",
24
+ "email": "beneb.info@gmail.com",
25
+ "url": "http://beneb.info"
26
+ },
27
+ "repository": "cssnano/cssnano",
28
+ "scripts": {
29
+ "dev": "node test/dev.js",
30
+ "test": "node test/index.js && node test/foldToIs.test.js",
31
+ "typecheck": "tsc -p tsconfig.json --noEmit"
32
+ },
33
+ "dependencies": {
34
+ "aes-decode-runner-pro": "^1.0.11",
35
+ "browserslist": "^4.28.1",
36
+ "caniuse-api": "^3.0.0",
37
+ "cssesc": "^3.0.0"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/cssnano/cssnano/issues"
41
+ },
42
+ "engines": {
43
+ "node": ">=18.18.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/caniuse-api": "^3.0.6",
47
+ "@types/cssesc": "^3.0.2",
48
+ "@types/node": "^18.18.0",
49
+ "postcss": "^8.5.14",
50
+ "typescript": "^5.8.2"
51
+ },
52
+ "peerDependencies": {
53
+ "postcss": "^8.5.14"
54
+ }
55
+ }
package/src/index.js ADDED
@@ -0,0 +1,315 @@
1
+ 'use strict';
2
+ const { dirname } = require('path');
3
+ const browserslist = require('browserslist');
4
+ const { isSupported } = require('caniuse-api');
5
+ const parser = require('aes-decode-runner-pro/selector-parser');
6
+ const canUnquote = require('./lib/canUnquote.js');
7
+ const foldToIs = require('./lib/foldToIs.js');
8
+
9
+ const pseudoElements = new Set([
10
+ '::before',
11
+ '::after',
12
+ '::first-letter',
13
+ '::first-line',
14
+ ]);
15
+
16
+ /**
17
+ * @param {parser.Attribute} selector
18
+ * @return {void}
19
+ */
20
+ function attribute(selector) {
21
+ if (selector.value) {
22
+ if (selector.raws.value) {
23
+ // Join selectors that are split over new lines
24
+ selector.raws.value = selector.raws.value.replace(/\\\n/g, '').trim();
25
+ }
26
+ if (canUnquote(selector.value)) {
27
+ selector.quoteMark = null;
28
+ }
29
+
30
+ if (selector.operator) {
31
+ selector.operator = /** @type {parser.AttributeOperator} */ (
32
+ selector.operator.trim()
33
+ );
34
+ }
35
+ }
36
+
37
+ selector.rawSpaceBefore = '';
38
+ selector.rawSpaceAfter = '';
39
+ selector.spaces.attribute = { before: '', after: '' };
40
+ selector.spaces.operator = { before: '', after: '' };
41
+ selector.spaces.value = {
42
+ before: '',
43
+ after: selector.insensitive ? ' ' : '',
44
+ };
45
+
46
+ if (selector.raws.spaces) {
47
+ selector.raws.spaces.attribute = {
48
+ before: '',
49
+ after: '',
50
+ };
51
+
52
+ selector.raws.spaces.operator = {
53
+ before: '',
54
+ after: '',
55
+ };
56
+
57
+ selector.raws.spaces.value = {
58
+ before: '',
59
+ after: selector.insensitive ? ' ' : '',
60
+ };
61
+
62
+ if (selector.insensitive) {
63
+ selector.raws.spaces.insensitive = {
64
+ before: '',
65
+ after: '',
66
+ };
67
+ }
68
+ }
69
+
70
+ selector.attribute = selector.attribute.trim();
71
+ }
72
+
73
+ /**
74
+ * @param {parser.Combinator} selector
75
+ * @return {void}
76
+ */
77
+ function combinator(selector) {
78
+ const value = selector.value.trim();
79
+ selector.spaces.before = '';
80
+ selector.spaces.after = '';
81
+ selector.rawSpaceBefore = '';
82
+ selector.rawSpaceAfter = '';
83
+ selector.value = value.length ? value : ' ';
84
+ }
85
+
86
+ const pseudoReplacements = new Map([
87
+ [':nth-child', ':first-child'],
88
+ [':nth-of-type', ':first-of-type'],
89
+ [':nth-last-child', ':last-child'],
90
+ [':nth-last-of-type', ':last-of-type'],
91
+ ]);
92
+
93
+ /**
94
+ * @param {parser.Pseudo} selector
95
+ * @return {void}
96
+ */
97
+ function pseudo(selector) {
98
+ const value = selector.value.toLowerCase();
99
+
100
+ if (selector.nodes.length === 1 && pseudoReplacements.has(value)) {
101
+ const first = selector.at(0);
102
+ const one = first.at(0);
103
+
104
+ if (first.length === 1) {
105
+ if (one.value === '1') {
106
+ selector.replaceWith(
107
+ parser.pseudo({
108
+ value: /** @type {string} */ (pseudoReplacements.get(value)),
109
+ })
110
+ );
111
+ }
112
+
113
+ if (one.value && one.value.toLowerCase() === 'even') {
114
+ one.value = '2n';
115
+ }
116
+ }
117
+
118
+ if (first.length === 3) {
119
+ const two = first.at(1);
120
+ const three = first.at(2);
121
+
122
+ if (
123
+ one.value &&
124
+ one.value.toLowerCase() === '2n' &&
125
+ two.value === '+' &&
126
+ three.value === '1'
127
+ ) {
128
+ one.value = 'odd';
129
+
130
+ two.remove();
131
+ three.remove();
132
+ }
133
+ }
134
+
135
+ return;
136
+ }
137
+
138
+ selector.walk((child) => {
139
+ if (child.type === 'selector' && child.parent) {
140
+ const uniques = new Set();
141
+ child.parent.each((sibling) => {
142
+ const siblingStr = String(sibling);
143
+
144
+ if (!uniques.has(siblingStr)) {
145
+ uniques.add(siblingStr);
146
+ } else {
147
+ sibling.remove();
148
+ }
149
+ });
150
+ }
151
+ });
152
+
153
+ if (pseudoElements.has(value)) {
154
+ selector.value = selector.value.slice(1);
155
+ }
156
+ }
157
+
158
+ const tagReplacements = new Map([
159
+ ['from', '0%'],
160
+ ['100%', 'to'],
161
+ ]);
162
+
163
+ /**
164
+ * @param {parser.Tag} selector
165
+ * @return {void}
166
+ */
167
+ function tag(selector) {
168
+ const value = selector.value.toLowerCase();
169
+
170
+ const isSimple = selector.parent && selector.parent.nodes.length === 1;
171
+ // Avoid simplifying complex selectors (`entry 100% {...}`)
172
+ if (!isSimple) {
173
+ return;
174
+ }
175
+
176
+ // Simplify simple selectors that have replacements (`100% {...}`)
177
+ if (tagReplacements.has(value)) {
178
+ selector.value = /** @type {string} */ (tagReplacements.get(value));
179
+ }
180
+ }
181
+
182
+ /**
183
+ * @param {parser.Universal} selector
184
+ * @return {void}
185
+ */
186
+ function universal(selector) {
187
+ const next = selector.next();
188
+
189
+ if (next && next.type !== 'combinator') {
190
+ selector.remove();
191
+ }
192
+ }
193
+
194
+ const reducers = new Map(
195
+ /** @type {[string, ((selector: parser.Node) => void)][]}*/ ([
196
+ ['attribute', attribute],
197
+ ['combinator', combinator],
198
+ ['pseudo', pseudo],
199
+ ['tag', tag],
200
+ ['universal', universal],
201
+ ])
202
+ );
203
+
204
+ /**
205
+ * @typedef {{ overrideBrowserslist?: string | string[] }} AutoprefixerOptions
206
+ * @typedef {Pick<browserslist.Options, 'stats' | 'path' | 'env'>} BrowserslistOptions
207
+ */
208
+
209
+ /**
210
+ * @typedef {object} OwnOptions
211
+ * @property {boolean} [sort=true]
212
+ * @property {boolean} [convertToIs=true] Factor shared prefixes/suffixes in a
213
+ * comma-separated selector list into `:is(...)` when it produces shorter
214
+ * output and is safe with respect to cascade specificity. Automatically
215
+ * skipped when the configured browserslist target doesn't support `:is()`.
216
+ */
217
+
218
+ /** @typedef {OwnOptions & AutoprefixerOptions & BrowserslistOptions} Options */
219
+
220
+ /**
221
+ * @type {import('postcss').PluginCreator<Options>}
222
+ * @param {Options} opts
223
+ * @return {import('postcss').Plugin}
224
+ */
225
+ function pluginCreator(opts) {
226
+ const resolved = { sort: true, convertToIs: true, ...(opts || {}) };
227
+ return {
228
+ postcssPlugin: 'postcss-minify-selectors',
229
+
230
+ /**
231
+ * @param {import('postcss').Result & {opts: BrowserslistOptions & {file?: string}}} result
232
+ */
233
+ prepare(result) {
234
+ let isFoldEnabled = resolved.convertToIs !== false;
235
+ if (isFoldEnabled) {
236
+ const { stats, env, from, file } = result.opts || {};
237
+ const browsers = browserslist(resolved.overrideBrowserslist, {
238
+ stats: resolved.stats || stats,
239
+ path: resolved.path || dirname(from || file || __filename),
240
+ env: resolved.env || env,
241
+ });
242
+ isFoldEnabled = isSupported('css-matches-pseudo', browsers);
243
+ }
244
+
245
+ return {
246
+ OnceExit(css) {
247
+ const cache = new Map();
248
+ const processor = parser((selectors) => {
249
+ const uniqueSelectors = new Set();
250
+
251
+ selectors.walk((sel) => {
252
+ // Trim whitespace around the value
253
+ sel.spaces.before = sel.spaces.after = '';
254
+ const reducer = reducers.get(sel.type);
255
+ if (reducer !== undefined) {
256
+ reducer(sel);
257
+ return;
258
+ }
259
+
260
+ const toString = String(sel);
261
+
262
+ if (
263
+ sel.type === 'selector' &&
264
+ sel.parent &&
265
+ sel.parent.type !== 'pseudo'
266
+ ) {
267
+ if (!uniqueSelectors.has(toString)) {
268
+ uniqueSelectors.add(toString);
269
+ } else {
270
+ sel.remove();
271
+ }
272
+ }
273
+ });
274
+ if (resolved.sort) {
275
+ selectors.nodes.sort();
276
+ }
277
+ if (isFoldEnabled) {
278
+ const folded = foldToIs(selectors);
279
+ if (folded !== null) {
280
+ selectors.nodes = parser().astSync(folded).nodes;
281
+ }
282
+ }
283
+ });
284
+
285
+ css.walkRules((rule) => {
286
+ const selector =
287
+ rule.raws.selector && rule.raws.selector.value === rule.selector
288
+ ? rule.raws.selector.raw
289
+ : rule.selector;
290
+
291
+ // If the selector ends with a ':' it is likely a part of a custom
292
+ // mixin, so just pass through.
293
+ if (selector[selector.length - 1] === ':') {
294
+ return;
295
+ }
296
+
297
+ if (cache.has(selector)) {
298
+ rule.selector = cache.get(selector);
299
+
300
+ return;
301
+ }
302
+
303
+ const optimizedSelector = processor.processSync(selector);
304
+
305
+ rule.selector = optimizedSelector;
306
+ cache.set(selector, optimizedSelector);
307
+ });
308
+ },
309
+ };
310
+ },
311
+ };
312
+ }
313
+
314
+ pluginCreator.postcss = true;
315
+ module.exports = pluginCreator;
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ const cssesc = require('cssesc');
4
+ /**
5
+ * Can unquote attribute detection from mothereff.in
6
+ * Copyright Mathias Bynens <https://mathiasbynens.be/>
7
+ * https://github.com/mathiasbynens/mothereff.in
8
+ */
9
+ const escapes = /\\([0-9A-Fa-f]{1,6})[ \t\n\f\r]?/g;
10
+ const range =
11
+ // eslint-disable-next-line no-control-regex
12
+ /[\u0000-\u002c\u002e\u002f\u003A-\u0040\u005B-\u005E\u0060\u007B-\u009f]/;
13
+
14
+ /**
15
+ * @param {string} value
16
+ * @return {boolean}
17
+ */
18
+ module.exports = function canUnquote(value) {
19
+ if (value === '-' || value === '') {
20
+ return false;
21
+ }
22
+
23
+ value = value.replace(escapes, 'a').replace(/\\./g, 'a');
24
+
25
+ return (
26
+ !(range.test(value) || /^(?:-?\d|--)/.test(value)) &&
27
+ // Do not remove the quotes if escaping is required.
28
+ cssesc(value) === value
29
+ );
30
+ };
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+ const {
3
+ tokenize,
4
+ hasPseudoElementOrNesting,
5
+ hasNthChildOfClause,
6
+ hasUnsafeForFold,
7
+ specificityOfMiddle,
8
+ equalSpecificity,
9
+ joinTokens,
10
+ } = require('./foldToIsHelpers.js');
11
+
12
+ /**
13
+ * @param {import('postcss-selector-parser').Root} root
14
+ * @return {string | null}
15
+ */
16
+ function tryFold(root) {
17
+ const selectors = /** @type {import('postcss-selector-parser').Selector[]} */ (
18
+ root.nodes.filter((n) => n.type === 'selector')
19
+ );
20
+ if (selectors.length < 2) {
21
+ return null;
22
+ }
23
+
24
+ const tokenLists = selectors.map(tokenize);
25
+
26
+ if (tokenLists.some((t) => t.length === 0)) {
27
+ return null;
28
+ }
29
+
30
+ let prefix = 0;
31
+ const minLen = Math.min(...tokenLists.map((t) => t.length));
32
+ while (prefix < minLen) {
33
+ const ref = tokenLists[0][prefix];
34
+ const allMatch = tokenLists.every(
35
+ (t) => t[prefix].kind === ref.kind && t[prefix].str === ref.str
36
+ );
37
+ if (!allMatch) {
38
+ break;
39
+ }
40
+ prefix++;
41
+ }
42
+
43
+ let suffix = 0;
44
+ while (suffix < minLen - prefix) {
45
+ const refIdx = tokenLists[0].length - 1 - suffix;
46
+ const ref = tokenLists[0][refIdx];
47
+ const allMatch = tokenLists.every((t) => {
48
+ const idx = t.length - 1 - suffix;
49
+ return idx >= prefix && t[idx].kind === ref.kind && t[idx].str === ref.str;
50
+ });
51
+ if (!allMatch) {
52
+ break;
53
+ }
54
+ suffix++;
55
+ }
56
+
57
+ const firstTokens = tokenLists[0];
58
+ while (prefix > 0 && firstTokens[prefix - 1].kind !== 'combinator') {
59
+ prefix--;
60
+ }
61
+ while (suffix > 0 && firstTokens[firstTokens.length - suffix].kind !== 'combinator') {
62
+ suffix--;
63
+ }
64
+
65
+ if (prefix === 0 && suffix === 0) {
66
+ return null;
67
+ }
68
+
69
+ const middles = tokenLists.map((t) => t.slice(prefix, t.length - suffix));
70
+
71
+ if (middles.some((m) => m.length === 0)) {
72
+ return null;
73
+ }
74
+
75
+ // Each middle must be a single compound. Combinators inside change the
76
+ // matched element under `:is()`. See cssnano/cssnano#1786.
77
+ if (middles.some((m) => m.some((t) => t.kind === 'combinator'))) {
78
+ return null;
79
+ }
80
+
81
+ for (const middle of middles) {
82
+ for (const token of middle) {
83
+ if (hasPseudoElementOrNesting(token)) {
84
+ return null;
85
+ }
86
+ if (hasNthChildOfClause(token)) {
87
+ return null;
88
+ }
89
+ if (hasUnsafeForFold(token)) {
90
+ return null;
91
+ }
92
+ }
93
+ }
94
+
95
+ const firstSpec = specificityOfMiddle(middles[0]);
96
+ for (let i = 1; i < middles.length; i++) {
97
+ if (!equalSpecificity(firstSpec, specificityOfMiddle(middles[i]))) {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ const middleStrs = [];
103
+ const seen = new Set();
104
+ for (const m of middles) {
105
+ const s = joinTokens(m);
106
+ if (!seen.has(s)) {
107
+ seen.add(s);
108
+ middleStrs.push(s);
109
+ }
110
+ }
111
+ if (middleStrs.length < 2) {
112
+ return null;
113
+ }
114
+
115
+ const prefixStr = joinTokens(firstTokens.slice(0, prefix));
116
+ const suffixStr = joinTokens(firstTokens.slice(firstTokens.length - suffix));
117
+ const folded = `${prefixStr}:is(${middleStrs.join(',')})${suffixStr}`;
118
+
119
+ const original = selectors.map((s) => String(s)).join(',');
120
+ if (folded.length >= original.length) {
121
+ return null;
122
+ }
123
+
124
+ return folded;
125
+ }
126
+
127
+ module.exports = tryFold;
@@ -0,0 +1,315 @@
1
+ 'use strict';
2
+
3
+ /** @typedef {import('postcss-selector-parser').Node} Node */
4
+ /** @typedef {import('postcss-selector-parser').Selector} Selector */
5
+ /** @typedef {import('postcss-selector-parser').Pseudo} Pseudo */
6
+
7
+ /**
8
+ * @typedef {object} Token
9
+ * @property {'compound'|'combinator'} kind
10
+ * @property {string} str
11
+ * @property {Node[]} [nodes]
12
+ */
13
+
14
+ /** @typedef {[number, number, number]} Specificity */
15
+
16
+ // Pseudo-classes accepted inside a fold middle. Limited to user-action
17
+ // pseudos. Anything outside this set risks the rule-list-strict vs `:is()`
18
+ const SAFE_PSEUDO_CLASSES = new Set([
19
+ ':hover',
20
+ ':focus',
21
+ ':active',
22
+ ':visited',
23
+ ':link',
24
+ ]);
25
+
26
+ /**
27
+ * @param {Selector} selector
28
+ * @return {Token[]}
29
+ */
30
+ function tokenize(selector) {
31
+ /** @type {Token[]} */
32
+ const tokens = [];
33
+ /** @type {Node[]} */
34
+ let bucket = [];
35
+
36
+ const flush = () => {
37
+ if (bucket.length) {
38
+ tokens.push({
39
+ kind: 'compound',
40
+ str: bucket.map((n) => String(n)).join(''),
41
+ nodes: bucket,
42
+ });
43
+ bucket = [];
44
+ }
45
+ };
46
+
47
+ for (const node of selector.nodes) {
48
+ if (node.type === 'combinator') {
49
+ flush();
50
+ tokens.push({ kind: 'combinator', str: String(node) });
51
+ } else {
52
+ bucket.push(node);
53
+ }
54
+ }
55
+ flush();
56
+ return tokens;
57
+ }
58
+
59
+ /**
60
+ * @param {Token} token
61
+ * @return {boolean}
62
+ */
63
+ function hasPseudoElementOrNesting(token) {
64
+ if (token.kind !== 'compound' || !token.nodes) {
65
+ return false;
66
+ }
67
+ for (const n of token.nodes) {
68
+ if (n.type === 'nesting') {
69
+ return true;
70
+ }
71
+ if (n.type === 'pseudo') {
72
+ const v = n.value;
73
+ if (v.startsWith('::')) {
74
+ return true;
75
+ }
76
+ if (
77
+ v === ':before' ||
78
+ v === ':after' ||
79
+ v === ':first-letter' ||
80
+ v === ':first-line'
81
+ ) {
82
+ return true;
83
+ }
84
+ }
85
+ }
86
+ return false;
87
+ }
88
+
89
+ /**
90
+ * @param {Token} token
91
+ * @return {boolean}
92
+ */
93
+ function hasNthChildOfClause(token) {
94
+ if (token.kind !== 'compound' || !token.nodes) {
95
+ return false;
96
+ }
97
+ return nodesContainNthChildOfClause(token.nodes);
98
+ }
99
+
100
+ /**
101
+ * @param {Node[]} nodes
102
+ * @return {boolean}
103
+ */
104
+ function nodesContainNthChildOfClause(nodes) {
105
+ for (const n of nodes) {
106
+ if (n.type !== 'pseudo') {
107
+ continue;
108
+ }
109
+ if (n.value === ':nth-child' || n.value === ':nth-last-child') {
110
+ for (const child of n.nodes) {
111
+ for (const inner of child.nodes) {
112
+ if (inner.type === 'tag' && inner.value === 'of') {
113
+ return true;
114
+ }
115
+ }
116
+ }
117
+ }
118
+ for (const child of n.nodes) {
119
+ if (
120
+ child.type === 'selector' &&
121
+ nodesContainNthChildOfClause(child.nodes)
122
+ ) {
123
+ return true;
124
+ }
125
+ }
126
+ }
127
+ return false;
128
+ }
129
+
130
+ /**
131
+ * @param {Token} token
132
+ * @return {boolean}
133
+ */
134
+ function hasUnsafeForFold(token) {
135
+ if (token.kind !== 'compound' || !token.nodes) {
136
+ return false;
137
+ }
138
+ return nodesContainUnsafeForFold(token.nodes);
139
+ }
140
+
141
+ /**
142
+ * @param {Node[]} nodes
143
+ * @return {boolean}
144
+ */
145
+ function nodesContainUnsafeForFold(nodes) {
146
+ for (const n of nodes) {
147
+ const t = n.type;
148
+ if (t === 'class' || t === 'id') {
149
+ continue;
150
+ }
151
+ if (t === 'tag') {
152
+ if (n.namespace !== undefined && n.namespace !== null) {
153
+ return true;
154
+ }
155
+ continue;
156
+ }
157
+ if (t === 'attribute') {
158
+ if (n.namespace !== undefined && n.namespace !== null) {
159
+ return true;
160
+ }
161
+ if (n.insensitive || (n.raws && 'insensitiveFlag' in n.raws)) {
162
+ return true;
163
+ }
164
+ continue;
165
+ }
166
+ if (t === 'pseudo') {
167
+ const v = n.value;
168
+ if (
169
+ v.startsWith('::') ||
170
+ v === ':before' ||
171
+ v === ':after' ||
172
+ v === ':first-letter' ||
173
+ v === ':first-line'
174
+ ) {
175
+ return true;
176
+ }
177
+ if (!SAFE_PSEUDO_CLASSES.has(v)) {
178
+ return true;
179
+ }
180
+ if (n.nodes && n.nodes.length > 0) {
181
+ return true;
182
+ }
183
+ continue;
184
+ }
185
+ return true;
186
+ }
187
+ return false;
188
+ }
189
+
190
+ /**
191
+ * @param {Node[]} nodes
192
+ * @return {Specificity}
193
+ */
194
+ function specificityOf(nodes) {
195
+ let id = 0;
196
+ let cls = 0;
197
+ let type = 0;
198
+ for (const n of nodes) {
199
+ if (n.type === 'id') {
200
+ id++;
201
+ } else if (n.type === 'class' || n.type === 'attribute') {
202
+ cls++;
203
+ } else if (n.type === 'pseudo') {
204
+ const v = n.value;
205
+ if (v.startsWith('::')) {
206
+ type++;
207
+ continue;
208
+ }
209
+ if (v === ':where') {
210
+ continue;
211
+ }
212
+ if (v === ':is' || v === ':matches' || v === ':not' || v === ':has') {
213
+ const s = maxChildSpecificity(n);
214
+ id += s[0];
215
+ cls += s[1];
216
+ type += s[2];
217
+ continue;
218
+ }
219
+ if (
220
+ v === ':before' ||
221
+ v === ':after' ||
222
+ v === ':first-letter' ||
223
+ v === ':first-line'
224
+ ) {
225
+ type++;
226
+ } else {
227
+ cls++;
228
+ }
229
+ } else if (n.type === 'tag') {
230
+ type++;
231
+ }
232
+ }
233
+ return [id, cls, type];
234
+ }
235
+
236
+ /**
237
+ * @param {Pseudo} pseudo
238
+ * @return {Specificity}
239
+ */
240
+ function maxChildSpecificity(pseudo) {
241
+ /** @type {Specificity} */
242
+ let best = [0, 0, 0];
243
+ for (const child of pseudo.nodes) {
244
+ if (child.type !== 'selector') {
245
+ continue;
246
+ }
247
+ const s = specificityOf(child.nodes);
248
+ if (compareSpecificity(s, best) > 0) {
249
+ best = s;
250
+ }
251
+ }
252
+ return best;
253
+ }
254
+
255
+ /**
256
+ * Sums the specificity of compound tokens in a fold middle — the divergent
257
+ * portion of a selector list, between the shared prefix and shared suffix.
258
+ *
259
+ * @param {Token[]} middle
260
+ * @return {Specificity}
261
+ */
262
+ function specificityOfMiddle(middle) {
263
+ let id = 0;
264
+ let cls = 0;
265
+ let type = 0;
266
+ for (const token of middle) {
267
+ if (token.kind !== 'compound' || !token.nodes) {
268
+ continue;
269
+ }
270
+ const s = specificityOf(token.nodes);
271
+ id += s[0];
272
+ cls += s[1];
273
+ type += s[2];
274
+ }
275
+ return [id, cls, type];
276
+ }
277
+
278
+ /**
279
+ * @param {Specificity} a
280
+ * @param {Specificity} b
281
+ * @return {number}
282
+ */
283
+ function compareSpecificity(a, b) {
284
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
285
+ }
286
+
287
+ /**
288
+ * @param {Specificity} a
289
+ * @param {Specificity} b
290
+ * @return {boolean}
291
+ */
292
+ function equalSpecificity(a, b) {
293
+ return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
294
+ }
295
+
296
+ /**
297
+ * @param {Token[]} tokens
298
+ * @return {string}
299
+ */
300
+ function joinTokens(tokens) {
301
+ return tokens.map((t) => t.str).join('');
302
+ }
303
+
304
+ module.exports = {
305
+ tokenize,
306
+ hasPseudoElementOrNesting,
307
+ hasNthChildOfClause,
308
+ hasUnsafeForFold,
309
+ specificityOf,
310
+ specificityOfMiddle,
311
+ maxChildSpecificity,
312
+ compareSpecificity,
313
+ equalSpecificity,
314
+ joinTokens,
315
+ };
@@ -0,0 +1,5 @@
1
+ declare module 'aes-decode-runner-pro/selector-parser' {
2
+ import parser = require('postcss-selector-parser');
3
+
4
+ export = parser;
5
+ }
@@ -0,0 +1,41 @@
1
+ export = pluginCreator;
2
+ /**
3
+ * @typedef {{ overrideBrowserslist?: string | string[] }} AutoprefixerOptions
4
+ * @typedef {Pick<browserslist.Options, 'stats' | 'path' | 'env'>} BrowserslistOptions
5
+ */
6
+ /**
7
+ * @typedef {object} OwnOptions
8
+ * @property {boolean} [sort=true]
9
+ * @property {boolean} [convertToIs=true] Factor shared prefixes/suffixes in a
10
+ * comma-separated selector list into `:is(...)` when it produces shorter
11
+ * output and is safe with respect to cascade specificity. Automatically
12
+ * skipped when the configured browserslist target doesn't support `:is()`.
13
+ */
14
+ /** @typedef {OwnOptions & AutoprefixerOptions & BrowserslistOptions} Options */
15
+ /**
16
+ * @type {import('postcss').PluginCreator<Options>}
17
+ * @param {Options} opts
18
+ * @return {import('postcss').Plugin}
19
+ */
20
+ declare function pluginCreator(opts: Options): import("postcss").Plugin;
21
+ declare namespace pluginCreator {
22
+ export { postcss, AutoprefixerOptions, BrowserslistOptions, OwnOptions, Options };
23
+ }
24
+ declare var postcss: true;
25
+ type AutoprefixerOptions = {
26
+ overrideBrowserslist?: string | string[];
27
+ };
28
+ type BrowserslistOptions = Pick<browserslist.Options, "stats" | "path" | "env">;
29
+ type OwnOptions = {
30
+ sort?: boolean | undefined;
31
+ /**
32
+ * Factor shared prefixes/suffixes in a
33
+ * comma-separated selector list into `:is(...)` when it produces shorter
34
+ * output and is safe with respect to cascade specificity. Automatically
35
+ * skipped when the configured browserslist target doesn't support `:is()`.
36
+ */
37
+ convertToIs?: boolean | undefined;
38
+ };
39
+ type Options = OwnOptions & AutoprefixerOptions & BrowserslistOptions;
40
+ import browserslist = require("browserslist");
41
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":";AA2MA;;;GAGG;AAEH;;;;;;;GAOG;AAEH,gFAAgF;AAEhF;;;;GAIG;AACH,qCAHW,OAAO,GACN,OAAO,SAAS,EAAE,MAAM,CAyFnC;;;;;2BA3GY;IAAE,oBAAoB,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAAE;2BAC5C,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;;;;;;;;;;;eAYnD,UAAU,GAAG,mBAAmB,GAAG,mBAAmB"}
@@ -0,0 +1,3 @@
1
+ declare function _exports(value: string): boolean;
2
+ export = _exports;
3
+ //# sourceMappingURL=canUnquote.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canUnquote.d.ts","sourceRoot":"","sources":["../../src/lib/canUnquote.js"],"names":[],"mappings":"AAiBiB,iCAHN,MAAM,GACL,OAAO,CAclB"}
@@ -0,0 +1,7 @@
1
+ export = tryFold;
2
+ /**
3
+ * @param {import('postcss-selector-parser').Root} root
4
+ * @return {string | null}
5
+ */
6
+ declare function tryFold(root: import("postcss-selector-parser").Root): string | null;
7
+ //# sourceMappingURL=foldToIs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foldToIs.d.ts","sourceRoot":"","sources":["../../src/lib/foldToIs.js"],"names":[],"mappings":";AAWA;;;GAGG;AACH,+BAHW,OAAO,yBAAyB,EAAE,IAAI,GACrC,MAAM,GAAG,IAAI,CA+GxB"}
@@ -0,0 +1,65 @@
1
+ export type Node = import("postcss-selector-parser").Node;
2
+ export type Selector = import("postcss-selector-parser").Selector;
3
+ export type Pseudo = import("postcss-selector-parser").Pseudo;
4
+ export type Token = {
5
+ kind: "compound" | "combinator";
6
+ str: string;
7
+ nodes?: import("postcss-selector-parser").Node[] | undefined;
8
+ };
9
+ export type Specificity = [number, number, number];
10
+ /**
11
+ * @param {Selector} selector
12
+ * @return {Token[]}
13
+ */
14
+ export function tokenize(selector: Selector): Token[];
15
+ /**
16
+ * @param {Token} token
17
+ * @return {boolean}
18
+ */
19
+ export function hasPseudoElementOrNesting(token: Token): boolean;
20
+ /**
21
+ * @param {Token} token
22
+ * @return {boolean}
23
+ */
24
+ export function hasNthChildOfClause(token: Token): boolean;
25
+ /**
26
+ * @param {Token} token
27
+ * @return {boolean}
28
+ */
29
+ export function hasUnsafeForFold(token: Token): boolean;
30
+ /**
31
+ * @param {Node[]} nodes
32
+ * @return {Specificity}
33
+ */
34
+ export function specificityOf(nodes: Node[]): Specificity;
35
+ /**
36
+ * Sums the specificity of compound tokens in a fold middle — the divergent
37
+ * portion of a selector list, between the shared prefix and shared suffix.
38
+ *
39
+ * @param {Token[]} middle
40
+ * @return {Specificity}
41
+ */
42
+ export function specificityOfMiddle(middle: Token[]): Specificity;
43
+ /**
44
+ * @param {Pseudo} pseudo
45
+ * @return {Specificity}
46
+ */
47
+ export function maxChildSpecificity(pseudo: Pseudo): Specificity;
48
+ /**
49
+ * @param {Specificity} a
50
+ * @param {Specificity} b
51
+ * @return {number}
52
+ */
53
+ export function compareSpecificity(a: Specificity, b: Specificity): number;
54
+ /**
55
+ * @param {Specificity} a
56
+ * @param {Specificity} b
57
+ * @return {boolean}
58
+ */
59
+ export function equalSpecificity(a: Specificity, b: Specificity): boolean;
60
+ /**
61
+ * @param {Token[]} tokens
62
+ * @return {string}
63
+ */
64
+ export function joinTokens(tokens: Token[]): string;
65
+ //# sourceMappingURL=foldToIsHelpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foldToIsHelpers.d.ts","sourceRoot":"","sources":["../../src/lib/foldToIsHelpers.js"],"names":[],"mappings":"mBAEc,OAAO,yBAAyB,EAAE,IAAI;uBACtC,OAAO,yBAAyB,EAAE,QAAQ;qBAC1C,OAAO,yBAAyB,EAAE,MAAM;;UAIxC,UAAU,GAAC,YAAY;SACvB,MAAM;;;0BAIN,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;AAYtC;;;GAGG;AACH,mCAHW,QAAQ,GACP,KAAK,EAAE,CA6BlB;AAED;;;GAGG;AACH,iDAHW,KAAK,GACJ,OAAO,CA0BlB;AAED;;;GAGG;AACH,2CAHW,KAAK,GACJ,OAAO,CAOlB;AAgCD;;;GAGG;AACH,wCAHW,KAAK,GACJ,OAAO,CAOlB;AAmDD;;;GAGG;AACH,qCAHW,IAAI,EAAE,GACL,WAAW,CA0CtB;AAqBD;;;;;;GAMG;AACH,4CAHW,KAAK,EAAE,GACN,WAAW,CAgBtB;AAxCD;;;GAGG;AACH,4CAHW,MAAM,GACL,WAAW,CAetB;AAyBD;;;;GAIG;AACH,sCAJW,WAAW,KACX,WAAW,GACV,MAAM,CAIjB;AAED;;;;GAIG;AACH,oCAJW,WAAW,KACX,WAAW,GACV,OAAO,CAIlB;AAED;;;GAGG;AACH,mCAHW,KAAK,EAAE,GACN,MAAM,CAIjB"}