postcss-merge-idents 9.0.3 → 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.
- package/package.json +5 -3
- package/src/data/mergeIdents.json +58 -0
- package/src/index.js +233 -100
- package/src/lib/grammar.js +271 -0
- package/src/lib/layerRegistry.js +224 -0
- package/src/lib/valueRewriter.js +258 -0
- package/types/index.d.ts +20 -0
- package/types/index.d.ts.map +1 -1
- package/types/lib/grammar.d.ts +43 -0
- package/types/lib/grammar.d.ts.map +1 -0
- package/types/lib/layerRegistry.d.ts +74 -0
- package/types/lib/layerRegistry.d.ts.map +1 -0
- package/types/lib/valueRewriter.d.ts +17 -0
- package/types/lib/valueRewriter.d.ts.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postcss-merge-idents",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.4",
|
|
4
4
|
"description": "Merge keyframe and counter style identifiers.",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"url": "git+https://github.com/cssnano/cssnano.git"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"
|
|
38
|
-
"
|
|
37
|
+
"@csstools/css-tokenizer": "^4.0.2",
|
|
38
|
+
"cssnano-utils": "^8.0.0"
|
|
39
39
|
},
|
|
40
40
|
"bugs": {
|
|
41
41
|
"url": "https://github.com/cssnano/cssnano/issues"
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"node": "^22.22.3 || ^24.15.0 || >=26.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
+
"@webref/css": "8.7.5",
|
|
47
48
|
"postcss": "^8.5.28"
|
|
48
49
|
},
|
|
49
50
|
"peerDependencies": {
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
},
|
|
52
53
|
"type": "module",
|
|
53
54
|
"scripts": {
|
|
55
|
+
"acquire": "node ./script/acquire.mjs",
|
|
54
56
|
"test": "node --test"
|
|
55
57
|
}
|
|
56
58
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cssWideKeywords": [
|
|
3
|
+
"inherit",
|
|
4
|
+
"initial",
|
|
5
|
+
"revert",
|
|
6
|
+
"revert-layer",
|
|
7
|
+
"revert-rule",
|
|
8
|
+
"unset"
|
|
9
|
+
],
|
|
10
|
+
"keyframes": {
|
|
11
|
+
"shorthandKeywords": [
|
|
12
|
+
"alternate",
|
|
13
|
+
"alternate-reverse",
|
|
14
|
+
"auto",
|
|
15
|
+
"backwards",
|
|
16
|
+
"both",
|
|
17
|
+
"ease",
|
|
18
|
+
"ease-in",
|
|
19
|
+
"ease-in-out",
|
|
20
|
+
"ease-out",
|
|
21
|
+
"forwards",
|
|
22
|
+
"infinite",
|
|
23
|
+
"linear",
|
|
24
|
+
"none",
|
|
25
|
+
"normal",
|
|
26
|
+
"paused",
|
|
27
|
+
"reverse",
|
|
28
|
+
"running",
|
|
29
|
+
"step-end",
|
|
30
|
+
"step-start"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"counterStyle": {
|
|
34
|
+
"keywords": [
|
|
35
|
+
"additive",
|
|
36
|
+
"alphabetic",
|
|
37
|
+
"auto",
|
|
38
|
+
"bullets",
|
|
39
|
+
"cyclic",
|
|
40
|
+
"extends",
|
|
41
|
+
"fixed",
|
|
42
|
+
"inside",
|
|
43
|
+
"none",
|
|
44
|
+
"numbers",
|
|
45
|
+
"numeric",
|
|
46
|
+
"outside",
|
|
47
|
+
"spell-out",
|
|
48
|
+
"symbolic",
|
|
49
|
+
"words"
|
|
50
|
+
],
|
|
51
|
+
"functions": {
|
|
52
|
+
"counter()": [1],
|
|
53
|
+
"counters()": [2],
|
|
54
|
+
"target-counter()": [2],
|
|
55
|
+
"target-counters()": [3]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,130 +1,263 @@
|
|
|
1
|
-
import valueParser from 'postcss-value-parser';
|
|
2
1
|
import cssnanoUtils from 'cssnano-utils';
|
|
2
|
+
import { LayerRegistry, compareAtRulePriority } from './lib/layerRegistry.js';
|
|
3
|
+
import {
|
|
4
|
+
classifyDeclaration,
|
|
5
|
+
getBody,
|
|
6
|
+
getContainer,
|
|
7
|
+
parseAtRuleName,
|
|
8
|
+
} from './lib/grammar.js';
|
|
9
|
+
import { rewriteDeclaration } from './lib/valueRewriter.js';
|
|
10
|
+
|
|
11
|
+
const { asciiLowerCase } = cssnanoUtils;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {{
|
|
15
|
+
* rule: import('postcss').AtRule,
|
|
16
|
+
* body?: string,
|
|
17
|
+
* layerPriority: number[],
|
|
18
|
+
* documentIndex: number,
|
|
19
|
+
* parsed: { isString: boolean, isReservedName: boolean, key: string, tokenText: string }
|
|
20
|
+
* }} AtRuleEntry
|
|
21
|
+
*
|
|
22
|
+
* @typedef {{
|
|
23
|
+
* entries: AtRuleEntry[],
|
|
24
|
+
* resolver?: (key: string) => { text: string, key: string } | undefined,
|
|
25
|
+
* definedNames?: Set<string>
|
|
26
|
+
* }} FamilyRecord
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Collects the entries that may join a cross-name merge group: repeat
|
|
31
|
+
* definitions of one name collapse to their highest priority copy first, and
|
|
32
|
+
* only when all copies share a body. Reserved names never merge because a
|
|
33
|
+
* stylesheet that spells one is not free to take another spelling.
|
|
34
|
+
*
|
|
35
|
+
* @param {Map<string, AtRuleEntry[]>} byName
|
|
36
|
+
* @param {Set<import('postcss').AtRule>} removals
|
|
37
|
+
* @return {AtRuleEntry[]}
|
|
38
|
+
*/
|
|
39
|
+
function collectEligibleEntries(byName, removals) {
|
|
40
|
+
/** @type {AtRuleEntry[]} */
|
|
41
|
+
const eligible = [];
|
|
42
|
+
|
|
43
|
+
for (const list of byName.values()) {
|
|
44
|
+
if (list.length === 1) {
|
|
45
|
+
if (!list[0].parsed.isReservedName) {
|
|
46
|
+
eligible.push(list[0]);
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const firstBody = getBody(list[0]);
|
|
51
|
+
if (!list.every((item) => getBody(item) === firstBody)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
let survivor = list[0];
|
|
55
|
+
for (let i = 1; i < list.length; i++) {
|
|
56
|
+
if (compareAtRulePriority(survivor, list[i]) < 0) {
|
|
57
|
+
survivor = list[i];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const item of list) {
|
|
61
|
+
if (item !== survivor) {
|
|
62
|
+
removals.add(item.rule);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!survivor.parsed.isReservedName) {
|
|
66
|
+
eligible.push(survivor);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return eligible;
|
|
71
|
+
}
|
|
3
72
|
|
|
4
|
-
const { sameParent } = cssnanoUtils;
|
|
5
|
-
const keyframesRegex = /keyframes/i;
|
|
6
|
-
const animationRegex = /animation/i;
|
|
7
|
-
const counterStyleRegex = /counter-style/i;
|
|
8
|
-
const listStyleSystemRegex = /(list-style|system)/i;
|
|
9
73
|
/**
|
|
10
|
-
* @param {
|
|
11
|
-
* @
|
|
74
|
+
* @param {AtRuleEntry[]} entries
|
|
75
|
+
* @param {Set<import('postcss').AtRule>} removals
|
|
76
|
+
* @return {{ resolver: (key: string) => { text: string, key: string } | undefined, definedNames: Set<string>, hasReplacements: boolean }}
|
|
12
77
|
*/
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
78
|
+
function processAtRuleEntries(entries, removals) {
|
|
79
|
+
const definedNames = new Set();
|
|
80
|
+
if (entries.length === 0) {
|
|
81
|
+
return {
|
|
82
|
+
resolver: () => undefined,
|
|
83
|
+
definedNames,
|
|
84
|
+
hasReplacements: false,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
definedNames.add(entry.parsed.key);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (entries.length === 1) {
|
|
93
|
+
return {
|
|
94
|
+
resolver: () => undefined,
|
|
95
|
+
definedNames,
|
|
96
|
+
hasReplacements: false,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @type {Map<string, AtRuleEntry[]>} */
|
|
101
|
+
const byName = new Map();
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
let nameList = byName.get(entry.parsed.key);
|
|
104
|
+
if (!nameList) {
|
|
105
|
+
nameList = [];
|
|
106
|
+
byName.set(entry.parsed.key, nameList);
|
|
26
107
|
}
|
|
108
|
+
nameList.push(entry);
|
|
109
|
+
}
|
|
27
110
|
|
|
28
|
-
|
|
111
|
+
const eligible = collectEligibleEntries(byName, removals);
|
|
29
112
|
|
|
30
|
-
|
|
113
|
+
/** @type {Map<string, AtRuleEntry[]>} */
|
|
114
|
+
const groupsByBody = new Map();
|
|
115
|
+
|
|
116
|
+
for (const item of eligible) {
|
|
117
|
+
const groupKey = (item.parsed.isString ? 's:' : 'i:') + getBody(item);
|
|
118
|
+
let group = groupsByBody.get(groupKey);
|
|
119
|
+
if (!group) {
|
|
120
|
+
group = [];
|
|
121
|
+
groupsByBody.set(groupKey, group);
|
|
122
|
+
}
|
|
123
|
+
group.push(item);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** @type {Map<string, { text: string, key: string }>} */
|
|
127
|
+
const resolved = new Map();
|
|
128
|
+
|
|
129
|
+
for (const group of groupsByBody.values()) {
|
|
130
|
+
if (group.length > 1) {
|
|
131
|
+
let survivor = group[0];
|
|
132
|
+
for (let i = 1; i < group.length; i++) {
|
|
133
|
+
if (compareAtRulePriority(survivor, group[i]) < 0) {
|
|
134
|
+
survivor = group[i];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const item of group) {
|
|
138
|
+
if (item !== survivor) {
|
|
139
|
+
removals.add(item.rule);
|
|
140
|
+
resolved.set(item.parsed.key, {
|
|
141
|
+
text: survivor.parsed.tokenText,
|
|
142
|
+
key: survivor.parsed.key,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
resolver: (key) => resolved.get(key),
|
|
151
|
+
definedNames,
|
|
152
|
+
hasReplacements: resolved.size > 0,
|
|
31
153
|
};
|
|
32
154
|
}
|
|
33
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Finds or creates the family record of one at-rule name in one container.
|
|
158
|
+
*
|
|
159
|
+
* @param {Map<import('postcss').Container, Map<string, FamilyRecord>>} scopes
|
|
160
|
+
* @param {import('postcss').Container} container
|
|
161
|
+
* @param {string} name
|
|
162
|
+
* @return {FamilyRecord}
|
|
163
|
+
*/
|
|
164
|
+
function familyRecordFor(scopes, container, name) {
|
|
165
|
+
let containerScope = scopes.get(container);
|
|
166
|
+
if (!containerScope) {
|
|
167
|
+
containerScope = new Map();
|
|
168
|
+
scopes.set(container, containerScope);
|
|
169
|
+
}
|
|
170
|
+
let familyData = containerScope.get(name);
|
|
171
|
+
if (!familyData) {
|
|
172
|
+
familyData = { entries: [] };
|
|
173
|
+
containerScope.set(name, familyData);
|
|
174
|
+
}
|
|
175
|
+
return familyData;
|
|
176
|
+
}
|
|
177
|
+
|
|
34
178
|
/**
|
|
35
179
|
* @param {import('postcss').Root} css
|
|
36
180
|
* @return {void}
|
|
37
181
|
*/
|
|
38
182
|
function mergeAtRules(css) {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
/** @type {import('postcss').AtRule[]} */
|
|
44
|
-
cache: [],
|
|
45
|
-
replacements: {},
|
|
46
|
-
/** @type {import('postcss').Declaration[]} */
|
|
47
|
-
decls: [],
|
|
48
|
-
/** @type {import('postcss').AtRule[]} */
|
|
49
|
-
removals: [],
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
atrule: counterStyleRegex,
|
|
53
|
-
decl: listStyleSystemRegex,
|
|
54
|
-
cache: [],
|
|
55
|
-
replacements: {},
|
|
56
|
-
decls: [],
|
|
57
|
-
removals: [],
|
|
58
|
-
},
|
|
59
|
-
];
|
|
183
|
+
const layerRegistry = new LayerRegistry();
|
|
184
|
+
let keyframesCount = 0;
|
|
185
|
+
let counterStyleCount = 0;
|
|
186
|
+
let documentIndex = 0;
|
|
60
187
|
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
-
*/
|
|
64
|
-
let relevant;
|
|
188
|
+
/** @type {Map<import('postcss').Container, Map<string, FamilyRecord>>} */
|
|
189
|
+
const scopes = new Map();
|
|
65
190
|
|
|
66
|
-
css.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
191
|
+
css.walkAtRules((atRule) => {
|
|
192
|
+
const name = asciiLowerCase(atRule.name);
|
|
193
|
+
if (name === 'layer') {
|
|
194
|
+
layerRegistry.declareLayerAtRule(atRule);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
71
197
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
198
|
+
const isKeyframes = name.endsWith('keyframes');
|
|
199
|
+
const isCounterStyle = name.endsWith('counter-style');
|
|
200
|
+
if (!isKeyframes && !isCounterStyle) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
75
203
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
for (const cached of relevant.cache) {
|
|
83
|
-
const cachedStringContent = cached.nodes
|
|
84
|
-
? cached.nodes.toString()
|
|
85
|
-
: '';
|
|
86
|
-
if (
|
|
87
|
-
cached.name.toLowerCase() === node.name.toLowerCase() &&
|
|
88
|
-
sameParent(cached, node) &&
|
|
89
|
-
cachedStringContent === toString
|
|
90
|
-
) {
|
|
91
|
-
relevant.removals.push(cached);
|
|
92
|
-
relevant.replacements[cached.params] = node.params;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
204
|
+
if (isKeyframes) {
|
|
205
|
+
keyframesCount++;
|
|
206
|
+
} else {
|
|
207
|
+
counterStyleCount++;
|
|
208
|
+
}
|
|
95
209
|
|
|
96
|
-
|
|
210
|
+
const container = getContainer(atRule);
|
|
211
|
+
const familyData = familyRecordFor(scopes, container, name);
|
|
97
212
|
|
|
98
|
-
|
|
99
|
-
|
|
213
|
+
const parsed = parseAtRuleName(atRule.params, name);
|
|
214
|
+
if (parsed) {
|
|
215
|
+
familyData.entries.push({
|
|
216
|
+
rule: atRule,
|
|
217
|
+
parsed,
|
|
218
|
+
layerPriority: layerRegistry.getPriority(atRule),
|
|
219
|
+
documentIndex: documentIndex++,
|
|
220
|
+
});
|
|
100
221
|
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (keyframesCount < 2 && counterStyleCount < 2) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
101
227
|
|
|
102
|
-
|
|
103
|
-
|
|
228
|
+
/** @type {Set<import('postcss').AtRule>} */
|
|
229
|
+
const removals = new Set();
|
|
230
|
+
let hasReplacements = false;
|
|
104
231
|
|
|
105
|
-
|
|
106
|
-
|
|
232
|
+
for (const containerScope of scopes.values()) {
|
|
233
|
+
for (const familyData of containerScope.values()) {
|
|
234
|
+
const result = processAtRuleEntries(familyData.entries, removals);
|
|
235
|
+
familyData.resolver = result.resolver;
|
|
236
|
+
familyData.definedNames = result.definedNames;
|
|
237
|
+
if (result.hasReplacements) {
|
|
238
|
+
hasReplacements = true;
|
|
107
239
|
}
|
|
108
|
-
|
|
109
|
-
relevant.decls.push(node);
|
|
110
240
|
}
|
|
111
|
-
}
|
|
241
|
+
}
|
|
112
242
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
243
|
+
if (removals.size === 0) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (hasReplacements) {
|
|
248
|
+
const singleScope =
|
|
249
|
+
scopes.size === 1 ? (scopes.values().next().value ?? null) : null;
|
|
250
|
+
|
|
251
|
+
css.walkDecls((decl) => {
|
|
252
|
+
const classification = classifyDeclaration(decl);
|
|
253
|
+
if (classification) {
|
|
254
|
+
rewriteDeclaration(decl, classification, scopes, singleScope);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const node of removals) {
|
|
260
|
+
node.remove();
|
|
128
261
|
}
|
|
129
262
|
}
|
|
130
263
|
|