postcss-merge-idents 9.0.2 → 9.0.4

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.
@@ -0,0 +1,271 @@
1
+ import { TokenType } from '@csstools/css-tokenizer';
2
+ import cssnanoUtils from 'cssnano-utils';
3
+ import specData from '../data/mergeIdents.json' with { type: 'json' };
4
+
5
+ const { asciiLowerCase, decoded, tokens } = cssnanoUtils;
6
+
7
+ const VENDOR_PREFIX = /^-(webkit|moz|o)-/v;
8
+
9
+ const CSS_WIDE_KEYWORDS = new Set(specData.cssWideKeywords);
10
+
11
+ const KEYFRAMES_NAME_RESERVED = new Set([
12
+ ...CSS_WIDE_KEYWORDS,
13
+ 'default',
14
+ 'none',
15
+ ]);
16
+
17
+ // css-animations-2 spells <single-animation-composition> only in the
18
+ // animation-composition property grammar; the shorthand grammar webref gives
19
+ // does not reach it yet, so these keywords stay hand-written.
20
+ const ANIMATION_COMPOSITION_KEYWORDS = new Set([
21
+ 'accumulate',
22
+ 'add',
23
+ 'replace',
24
+ ]);
25
+
26
+ const KEYFRAMES_SHORTHAND_KEYWORDS = new Set([
27
+ ...CSS_WIDE_KEYWORDS,
28
+ ...specData.keyframes.shorthandKeywords,
29
+ ...ANIMATION_COMPOSITION_KEYWORDS,
30
+ ]);
31
+
32
+ const COUNTER_STYLE_NAME_RESERVED = new Set([
33
+ ...CSS_WIDE_KEYWORDS,
34
+ 'default',
35
+ 'decimal',
36
+ 'none',
37
+ ]);
38
+
39
+ // The predefined counter style names are specified in prose (CSS Counter
40
+ // Styles Level 3 §6) rather than in any grammar, so webref cannot list them.
41
+ const PREDEFINED_COUNTER_STYLES = [
42
+ 'arabic-indic',
43
+ 'armenian',
44
+ 'bengali',
45
+ 'cambodian',
46
+ 'circle',
47
+ 'cjk-decimal',
48
+ 'cjk-earthly-branch',
49
+ 'cjk-heavenly-stem',
50
+ 'cjk-ideographic',
51
+ 'decimal',
52
+ 'decimal-leading-zero',
53
+ 'devanagari',
54
+ 'disc',
55
+ 'disclosure-closed',
56
+ 'disclosure-open',
57
+ 'ethiopic-numeric',
58
+ 'georgian',
59
+ 'gujarati',
60
+ 'gurmukhi',
61
+ 'hebrew',
62
+ 'hiragana',
63
+ 'hiragana-iroha',
64
+ 'japanese-formal',
65
+ 'japanese-informal',
66
+ 'kannada',
67
+ 'katakana',
68
+ 'katakana-iroha',
69
+ 'khmer',
70
+ 'korean-hangul-formal',
71
+ 'korean-hanja-formal',
72
+ 'korean-hanja-informal',
73
+ 'lao',
74
+ 'lower-alpha',
75
+ 'lower-armenian',
76
+ 'lower-greek',
77
+ 'lower-latin',
78
+ 'lower-roman',
79
+ 'malayalam',
80
+ 'mongolian',
81
+ 'myanmar',
82
+ 'oriya',
83
+ 'persian',
84
+ 'simp-chinese-formal',
85
+ 'simp-chinese-informal',
86
+ 'square',
87
+ 'tamil',
88
+ 'telugu',
89
+ 'thai',
90
+ 'tibetan',
91
+ 'trad-chinese-formal',
92
+ 'trad-chinese-informal',
93
+ 'upper-alpha',
94
+ 'upper-armenian',
95
+ 'upper-latin',
96
+ 'upper-roman',
97
+ ];
98
+
99
+ const COUNTER_STYLE_RESERVED = new Set([
100
+ ...CSS_WIDE_KEYWORDS,
101
+ ...specData.counterStyle.keywords,
102
+ ...PREDEFINED_COUNTER_STYLES,
103
+ ]);
104
+
105
+ const COUNTER_STYLE_FUNCTIONS = new Map(
106
+ Object.entries(specData.counterStyle.functions).map(([name, args]) => [
107
+ // Stylesheets spell a function without the trailing `()` webref names it
108
+ // by, preserving the historical lookup behavior.
109
+ name.slice(0, -2),
110
+ args,
111
+ ])
112
+ );
113
+
114
+ const CONDITIONAL_GROUP_RULES = new Set([
115
+ 'media',
116
+ 'supports',
117
+ 'container',
118
+ 'document',
119
+ ]);
120
+
121
+ /**
122
+ * @param {string} name
123
+ * @return {boolean}
124
+ */
125
+ function isConditionalGroupRule(name) {
126
+ const lower = asciiLowerCase(name);
127
+ return CONDITIONAL_GROUP_RULES.has(lower) || lower.endsWith('document');
128
+ }
129
+
130
+ /**
131
+ * @param {string} params
132
+ * @param {string} atRuleName
133
+ * @return {{ isString: boolean, isReservedName: boolean, key: string, tokenText: string } | null}
134
+ */
135
+ function parseAtRuleName(params, atRuleName) {
136
+ const trimmed = params.trim();
137
+ if (!trimmed) {
138
+ return null;
139
+ }
140
+ const tokenList = tokens(trimmed).filter(
141
+ (t) => t[0] !== TokenType.Whitespace && t[0] !== TokenType.Comment
142
+ );
143
+ if (tokenList.length !== 1) {
144
+ return null;
145
+ }
146
+ const isCounterStyle = atRuleName.endsWith('counter-style');
147
+ const first = tokenList[0];
148
+ if (first[0] === TokenType.Ident) {
149
+ const val = decoded(first);
150
+ const lower = asciiLowerCase(val);
151
+ const isReserved = isCounterStyle
152
+ ? COUNTER_STYLE_NAME_RESERVED.has(lower)
153
+ : KEYFRAMES_NAME_RESERVED.has(lower);
154
+ if (isReserved) {
155
+ return null;
156
+ }
157
+ const isReservedName = isCounterStyle
158
+ ? COUNTER_STYLE_RESERVED.has(lower)
159
+ : KEYFRAMES_SHORTHAND_KEYWORDS.has(lower) || val.startsWith('--');
160
+ return {
161
+ isString: false,
162
+ isReservedName,
163
+ key: `ident:${val}`,
164
+ tokenText: first[1],
165
+ };
166
+ }
167
+ if (!isCounterStyle && first[0] === TokenType.String) {
168
+ const val = /** @type {{value: string}} */ (first[4]).value;
169
+ return {
170
+ isString: true,
171
+ isReservedName: false,
172
+ key: `str:${val}`,
173
+ tokenText: first[1],
174
+ };
175
+ }
176
+ return null;
177
+ }
178
+
179
+ /**
180
+ * @param {import('postcss').Declaration} decl
181
+ * @return {{ targetAtRuleName: string, kind: 'animation-shorthand' | 'animation-name' | 'counter-style' | 'counter-style-func' } | null}
182
+ */
183
+ function classifyDeclaration(decl) {
184
+ const prop = asciiLowerCase(decl.prop);
185
+ if (prop.startsWith('--')) {
186
+ return null;
187
+ }
188
+
189
+ const prefixMatch = prop.match(VENDOR_PREFIX);
190
+ const prefix = prefixMatch ? prefixMatch[0] : '';
191
+ const unprefixed = prefix ? prop.slice(prefix.length) : prop;
192
+
193
+ if (unprefixed === 'animation') {
194
+ return {
195
+ targetAtRuleName: `${prefix}keyframes`,
196
+ kind: 'animation-shorthand',
197
+ };
198
+ }
199
+ if (unprefixed === 'animation-name') {
200
+ return { targetAtRuleName: `${prefix}keyframes`, kind: 'animation-name' };
201
+ }
202
+ if (unprefixed === 'list-style' || unprefixed === 'list-style-type') {
203
+ return { targetAtRuleName: 'counter-style', kind: 'counter-style' };
204
+ }
205
+
206
+ if (
207
+ decl.parent?.type === 'atrule' &&
208
+ asciiLowerCase(decl.parent.name).endsWith('counter-style')
209
+ ) {
210
+ if (prop === 'system' || prop === 'fallback' || prop === 'speak-as') {
211
+ return {
212
+ targetAtRuleName: asciiLowerCase(decl.parent.name),
213
+ kind: 'counter-style',
214
+ };
215
+ }
216
+ }
217
+
218
+ if (
219
+ unprefixed === 'content' ||
220
+ unprefixed === 'bookmark-label' ||
221
+ unprefixed === 'copy-into' ||
222
+ unprefixed === 'string-set'
223
+ ) {
224
+ return { targetAtRuleName: 'counter-style', kind: 'counter-style-func' };
225
+ }
226
+
227
+ return null;
228
+ }
229
+
230
+ /**
231
+ * @param {import('postcss').Node} node
232
+ * @return {import('postcss').Container}
233
+ */
234
+ function getContainer(node) {
235
+ let curr = node.parent;
236
+ while (curr && curr.type !== 'root') {
237
+ if (
238
+ curr.type === 'atrule' &&
239
+ isConditionalGroupRule(
240
+ /** @type {import('postcss').AtRule} */ (curr).name
241
+ )
242
+ ) {
243
+ return curr;
244
+ }
245
+ curr = curr.parent;
246
+ }
247
+ return curr ?? node.root();
248
+ }
249
+
250
+ /**
251
+ * @param {{ rule: import('postcss').AtRule, body?: string }} entry
252
+ * @return {string}
253
+ */
254
+ function getBody(entry) {
255
+ if (entry.body === undefined) {
256
+ entry.body = entry.rule.nodes ? entry.rule.nodes.toString() : '';
257
+ }
258
+ return entry.body;
259
+ }
260
+
261
+ export {
262
+ CSS_WIDE_KEYWORDS,
263
+ KEYFRAMES_SHORTHAND_KEYWORDS,
264
+ COUNTER_STYLE_RESERVED,
265
+ COUNTER_STYLE_FUNCTIONS,
266
+ isConditionalGroupRule,
267
+ parseAtRuleName,
268
+ classifyDeclaration,
269
+ getContainer,
270
+ getBody,
271
+ };
@@ -0,0 +1,224 @@
1
+ import { TokenType } from '@csstools/css-tokenizer';
2
+ import cssnanoUtils from 'cssnano-utils';
3
+
4
+ const { asciiLowerCase, decoded, tokens } = cssnanoUtils;
5
+
6
+ /**
7
+ * Parses a @layer parameter list into decoded layer-name paths. A period is a
8
+ * separator only as a Delim token between idents; an escaped dot is part of
9
+ * the preceding identifier's value, so `a\.b` is one segment named "a.b".
10
+ * Whitespace and comments around separators are allowed, preserving the
11
+ * previous handling of `a . b`.
12
+ *
13
+ * @param {string} params
14
+ * @return {string[][] | null} one decoded segment path per declared name
15
+ */
16
+ function parseLayerNameList(params) {
17
+ const tokenList = tokens(params).filter(
18
+ (t) => t[0] !== TokenType.Whitespace && t[0] !== TokenType.Comment
19
+ );
20
+ /** @type {string[][]} */
21
+ const names = [];
22
+ /** @type {string[] | null} */
23
+ let current = null;
24
+ let expectIdent = true;
25
+ for (const token of tokenList) {
26
+ const type = token[0];
27
+ if (type === TokenType.Comma) {
28
+ if (expectIdent || !current) {
29
+ return null;
30
+ }
31
+ names.push(current);
32
+ current = null;
33
+ expectIdent = true;
34
+ } else if (type === TokenType.Delim && token[1] === '.') {
35
+ if (expectIdent || !current) {
36
+ return null;
37
+ }
38
+ expectIdent = true;
39
+ } else if (type === TokenType.Ident && expectIdent) {
40
+ current ??= [];
41
+ current.push(decoded(token));
42
+ expectIdent = false;
43
+ } else {
44
+ return null;
45
+ }
46
+ }
47
+ if (expectIdent) {
48
+ return null;
49
+ }
50
+ if (current) {
51
+ names.push(current);
52
+ }
53
+ return names;
54
+ }
55
+
56
+ /**
57
+ * @param {import('postcss').Node} node
58
+ * @return {import('postcss').AtRule[]} enclosing @layer at-rules, outermost first
59
+ */
60
+ function enclosingLayerRules(node) {
61
+ /** @type {import('postcss').AtRule[]} */
62
+ const chain = [];
63
+ let curr = node.parent;
64
+ while (curr && curr.type !== 'root') {
65
+ if (
66
+ curr.type === 'atrule' &&
67
+ asciiLowerCase(/** @type {import('postcss').AtRule} */ (curr).name) ===
68
+ 'layer'
69
+ ) {
70
+ chain.unshift(/** @type {import('postcss').AtRule} */ (curr));
71
+ }
72
+ curr = curr.parent;
73
+ }
74
+ return chain;
75
+ }
76
+
77
+ class LayerRegistry {
78
+ constructor() {
79
+ this.nextIndex = 0;
80
+ /** @type {Map<string, { index: number, children: LayerRegistry }>} */
81
+ this.layers = new Map();
82
+ /** @type {Map<import('postcss').AtRule, { index: number, children: LayerRegistry }>} */
83
+ this.anonymous = new Map();
84
+ }
85
+
86
+ /**
87
+ * @param {string} name
88
+ * @return {{ index: number, children: LayerRegistry }}
89
+ */
90
+ getOrCreate(name) {
91
+ let entry = this.layers.get(name);
92
+ if (!entry) {
93
+ entry = { index: this.nextIndex++, children: new LayerRegistry() };
94
+ this.layers.set(name, entry);
95
+ }
96
+ return entry;
97
+ }
98
+
99
+ /**
100
+ * Entry for a layer with no name or an unparseable name: every occurrence
101
+ * is a unique cascade layer, but names nested inside still order beneath
102
+ * it through the child registry.
103
+ *
104
+ * @param {import('postcss').AtRule} layerRule
105
+ * @return {{ index: number, children: LayerRegistry }}
106
+ */
107
+ getOrCreateAnonymous(layerRule) {
108
+ let entry = this.anonymous.get(layerRule);
109
+ if (!entry) {
110
+ entry = { index: this.nextIndex++, children: new LayerRegistry() };
111
+ this.anonymous.set(layerRule, entry);
112
+ }
113
+ return entry;
114
+ }
115
+
116
+ /**
117
+ * The registry entry a single layer at-rule occupies, creating missing
118
+ * segments as needed. Statement lists and unparseable names fall back to a
119
+ * unique anonymous entry so they order but never match another spelling.
120
+ *
121
+ * @param {import('postcss').AtRule} layerRule
122
+ * @return {{ index: number, children: LayerRegistry }}
123
+ */
124
+ entryFor(layerRule) {
125
+ const names = layerRule.params.trim()
126
+ ? parseLayerNameList(layerRule.params)
127
+ : null;
128
+ if (names === null || names.length !== 1) {
129
+ return this.getOrCreateAnonymous(layerRule);
130
+ }
131
+ let entry = this.getOrCreate(names[0][0]);
132
+ for (let i = 1; i < names[0].length; i++) {
133
+ entry = entry.children.getOrCreate(names[0][i]);
134
+ }
135
+ return entry;
136
+ }
137
+
138
+ /**
139
+ * Registers the layers a @layer at-rule declares, in document order and
140
+ * nested beneath any enclosing layers, so later lookups observe the real
141
+ * layer tree.
142
+ *
143
+ * @param {import('postcss').AtRule} layerRule
144
+ * @return {void}
145
+ */
146
+ declareLayerAtRule(layerRule) {
147
+ /** @type {LayerRegistry} */
148
+ let current = this;
149
+ for (const parent of enclosingLayerRules(layerRule)) {
150
+ current = current.entryFor(parent).children;
151
+ }
152
+ if (!layerRule.params.trim()) {
153
+ // An unnamed block layer is itself a unique anonymous layer; register
154
+ // it so nested names order beneath it.
155
+ if (layerRule.nodes) {
156
+ current.getOrCreateAnonymous(layerRule);
157
+ }
158
+ return;
159
+ }
160
+ const names = parseLayerNameList(layerRule.params);
161
+ if (names === null) {
162
+ return;
163
+ }
164
+ for (const segments of names) {
165
+ let entry = current.getOrCreate(segments[0]);
166
+ for (let i = 1; i < segments.length; i++) {
167
+ entry = entry.children.getOrCreate(segments[i]);
168
+ }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * @param {import('postcss').Node} node
174
+ * @return {number[]}
175
+ */
176
+ getPriority(node) {
177
+ const chain = enclosingLayerRules(node);
178
+ if (chain.length === 0) {
179
+ return [Infinity];
180
+ }
181
+
182
+ /** @type {number[]} */
183
+ const priority = [];
184
+ /** @type {LayerRegistry} */
185
+ let current = this;
186
+ for (const layerRule of chain) {
187
+ const entry = current.entryFor(layerRule);
188
+ priority.push(entry.index);
189
+ current = entry.children;
190
+ }
191
+ priority.push(Infinity);
192
+ return priority;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * @param {number[]} p1
198
+ * @param {number[]} p2
199
+ * @return {number}
200
+ */
201
+ function compareLayerPriority(p1, p2) {
202
+ const len = Math.min(p1.length, p2.length);
203
+ for (let i = 0; i < len; i++) {
204
+ if (p1[i] !== p2[i]) {
205
+ return p1[i] - p2[i];
206
+ }
207
+ }
208
+ return p1.length - p2.length;
209
+ }
210
+
211
+ /**
212
+ * @param {{ layerPriority: number[], documentIndex: number }} a
213
+ * @param {{ layerPriority: number[], documentIndex: number }} b
214
+ * @return {number}
215
+ */
216
+ function compareAtRulePriority(a, b) {
217
+ const layerCmp = compareLayerPriority(a.layerPriority, b.layerPriority);
218
+ if (layerCmp !== 0) {
219
+ return layerCmp;
220
+ }
221
+ return a.documentIndex - b.documentIndex;
222
+ }
223
+
224
+ export { LayerRegistry, compareAtRulePriority };