postcss-merge-rules 5.0.4 → 5.1.0
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 +8 -9
- package/{dist → src}/index.js +161 -160
- package/{dist → src}/lib/ensureCompatibility.js +77 -70
- package/types/index.d.ts +9 -0
- package/types/lib/ensureCompatibility.d.ts +71 -0
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postcss-merge-rules",
|
|
3
|
-
"version": "5.0
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "Merge CSS rules with PostCSS.",
|
|
5
|
-
"
|
|
5
|
+
"types": "types/index.d.ts",
|
|
6
|
+
"main": "src/index.js",
|
|
6
7
|
"files": [
|
|
7
8
|
"LICENSE-MIT",
|
|
8
|
-
"
|
|
9
|
+
"src",
|
|
10
|
+
"types"
|
|
9
11
|
],
|
|
10
12
|
"keywords": [
|
|
11
13
|
"css",
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
"dependencies": {
|
|
25
27
|
"browserslist": "^4.16.6",
|
|
26
28
|
"caniuse-api": "^3.0.0",
|
|
27
|
-
"cssnano-utils": "^3.
|
|
29
|
+
"cssnano-utils": "^3.1.0",
|
|
28
30
|
"postcss-selector-parser": "^6.0.5"
|
|
29
31
|
},
|
|
30
32
|
"bugs": {
|
|
@@ -34,15 +36,12 @@
|
|
|
34
36
|
"node": "^10 || ^12 || >=14.0"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
39
|
+
"@types/caniuse-api": "^3.0.2",
|
|
37
40
|
"postcss": "^8.2.15",
|
|
38
|
-
"postcss-discard-comments": "^5.0
|
|
41
|
+
"postcss-discard-comments": "^5.1.0"
|
|
39
42
|
},
|
|
40
43
|
"peerDependencies": {
|
|
41
44
|
"postcss": "^8.2.15"
|
|
42
45
|
},
|
|
43
|
-
"scripts": {
|
|
44
|
-
"prebuild": "rimraf dist",
|
|
45
|
-
"build": "babel src --config-file ../../babel.config.json --out-dir dist --ignore \"**/__tests__/\""
|
|
46
|
-
},
|
|
47
46
|
"readme": "# [postcss][postcss]-merge-rules\n\n> Merge CSS rules with PostCSS.\n\n## Install\n\nWith [npm](https://npmjs.org/package/postcss-merge-rules) do:\n\n```\nnpm install postcss-merge-rules --save\n```\n\n## Examples\n\nThis module will attempt to merge *adjacent* CSS rules:\n\n### By declarations\n\n#### Input\n\n```css\na {\n color: blue;\n font-weight: bold\n}\n\np {\n color: blue;\n font-weight: bold\n}\n```\n\n#### Output\n\n```css\na,p {\n color: blue;\n font-weight: bold\n}\n```\n\n### By selectors\n\n#### Input\n\n```css\na {\n color: blue\n}\n\na {\n font-weight: bold\n}\n```\n\n#### Output\n\n```css\na {\n color: blue;\n font-weight: bold\n}\n```\n\n### By partial declarations\n\n#### Input\n\n```css\na {\n font-weight: bold\n}\n\np {\n color: blue;\n font-weight: bold\n}\n```\n\n#### Output\n\n```css\na,p {\n font-weight: bold\n}\n\np {\n color: blue\n}\n```\n\n## Usage\n\nSee the [PostCSS documentation](https://github.com/postcss/postcss#usage) for\nexamples for your environment.\n\n## Contributors\n\nSee [CONTRIBUTORS.md](https://github.com/cssnano/cssnano/blob/master/CONTRIBUTORS.md).\n\n## License\n\nMIT © [Ben Briggs](http://beneb.info)\n\n[postcss]: https://github.com/postcss/postcss\n"
|
|
48
47
|
}
|
package/{dist → src}/index.js
RENAMED
|
@@ -1,226 +1,214 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
var _cssnanoUtils = require("cssnano-utils");
|
|
11
|
-
|
|
12
|
-
var _ensureCompatibility = require("./lib/ensureCompatibility");
|
|
13
|
-
|
|
14
|
-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
1
|
+
'use strict';
|
|
2
|
+
const browserslist = require('browserslist');
|
|
3
|
+
const { sameParent } = require('cssnano-utils');
|
|
4
|
+
const {
|
|
5
|
+
ensureCompatibility,
|
|
6
|
+
sameVendor,
|
|
7
|
+
noVendor,
|
|
8
|
+
} = require('./lib/ensureCompatibility');
|
|
15
9
|
|
|
16
10
|
/**
|
|
17
|
-
* @param {postcss.Declaration} a
|
|
18
|
-
* @param {postcss.Declaration} b
|
|
11
|
+
* @param {import('postcss').Declaration} a
|
|
12
|
+
* @param {import('postcss').Declaration} b
|
|
19
13
|
* @return {boolean}
|
|
20
14
|
*/
|
|
21
15
|
function declarationIsEqual(a, b) {
|
|
22
|
-
return
|
|
16
|
+
return (
|
|
17
|
+
a.important === b.important && a.prop === b.prop && a.value === b.value
|
|
18
|
+
);
|
|
23
19
|
}
|
|
20
|
+
|
|
24
21
|
/**
|
|
25
|
-
* @param {postcss.Declaration[]} array
|
|
26
|
-
* @param {postcss.Declaration} decl
|
|
22
|
+
* @param {import('postcss').Declaration[]} array
|
|
23
|
+
* @param {import('postcss').Declaration} decl
|
|
27
24
|
* @return {number}
|
|
28
25
|
*/
|
|
29
|
-
|
|
30
|
-
|
|
31
26
|
function indexOfDeclaration(array, decl) {
|
|
32
|
-
return array.findIndex(d => declarationIsEqual(d, decl));
|
|
27
|
+
return array.findIndex((d) => declarationIsEqual(d, decl));
|
|
33
28
|
}
|
|
29
|
+
|
|
34
30
|
/**
|
|
35
31
|
* Returns filtered array of matched or unmatched declarations
|
|
36
|
-
* @param {postcss.Declaration[]} a
|
|
37
|
-
* @param {postcss.Declaration[]} b
|
|
32
|
+
* @param {import('postcss').Declaration[]} a
|
|
33
|
+
* @param {import('postcss').Declaration[]} b
|
|
38
34
|
* @param {boolean} [not=false]
|
|
39
|
-
* @return {postcss.Declaration[]}
|
|
35
|
+
* @return {import('postcss').Declaration[]}
|
|
40
36
|
*/
|
|
41
|
-
|
|
42
|
-
|
|
43
37
|
function intersect(a, b, not) {
|
|
44
|
-
return a.filter(c => {
|
|
38
|
+
return a.filter((c) => {
|
|
45
39
|
const index = ~indexOfDeclaration(b, c);
|
|
46
40
|
return not ? !index : index;
|
|
47
41
|
});
|
|
48
42
|
}
|
|
43
|
+
|
|
49
44
|
/**
|
|
50
|
-
* @param {postcss.Declaration[]} a
|
|
51
|
-
* @param {postcss.Declaration[]} b
|
|
45
|
+
* @param {import('postcss').Declaration[]} a
|
|
46
|
+
* @param {import('postcss').Declaration[]} b
|
|
52
47
|
* @return {boolean}
|
|
53
48
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
56
49
|
function sameDeclarationsAndOrder(a, b) {
|
|
57
50
|
if (a.length !== b.length) {
|
|
58
51
|
return false;
|
|
59
52
|
}
|
|
60
|
-
|
|
61
53
|
return a.every((d, index) => declarationIsEqual(d, b[index]));
|
|
62
54
|
}
|
|
55
|
+
|
|
63
56
|
/**
|
|
64
|
-
* @param {postcss.Rule} ruleA
|
|
65
|
-
* @param {postcss.Rule} ruleB
|
|
57
|
+
* @param {import('postcss').Rule} ruleA
|
|
58
|
+
* @param {import('postcss').Rule} ruleB
|
|
66
59
|
* @param {string[]=} browsers
|
|
67
60
|
* @param {Map<string, boolean>=} compatibilityCache
|
|
68
61
|
* @return {boolean}
|
|
69
62
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
72
63
|
function canMerge(ruleA, ruleB, browsers, compatibilityCache) {
|
|
73
64
|
const a = ruleA.selectors;
|
|
74
65
|
const b = ruleB.selectors;
|
|
66
|
+
|
|
75
67
|
const selectors = a.concat(b);
|
|
76
68
|
|
|
77
|
-
if (!
|
|
69
|
+
if (!ensureCompatibility(selectors, browsers, compatibilityCache)) {
|
|
78
70
|
return false;
|
|
79
71
|
}
|
|
80
72
|
|
|
81
|
-
const parent =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (parent && name &&
|
|
73
|
+
const parent = sameParent(
|
|
74
|
+
/** @type {any} */ (ruleA),
|
|
75
|
+
/** @type {any} */ (ruleB)
|
|
76
|
+
);
|
|
77
|
+
const { name } = /** @type {any} */ (ruleA).parent;
|
|
78
|
+
if (parent && name && name.includes('keyframes')) {
|
|
87
79
|
return false;
|
|
88
80
|
}
|
|
89
|
-
|
|
90
|
-
return parent && (selectors.every(_ensureCompatibility.noVendor) || (0, _ensureCompatibility.sameVendor)(a, b));
|
|
81
|
+
return parent && (selectors.every(noVendor) || sameVendor(a, b));
|
|
91
82
|
}
|
|
83
|
+
|
|
92
84
|
/**
|
|
93
|
-
* @param {postcss.Rule} rule
|
|
94
|
-
* @return {postcss.Declaration[]}
|
|
85
|
+
* @param {import('postcss').Rule} rule
|
|
86
|
+
* @return {import('postcss').Declaration[]}
|
|
95
87
|
*/
|
|
96
|
-
|
|
97
|
-
|
|
98
88
|
function getDecls(rule) {
|
|
99
|
-
return
|
|
89
|
+
return /** @type {import('postcss').Declaration[]} */ (
|
|
90
|
+
rule.nodes.filter((node) => node.type === 'decl')
|
|
91
|
+
);
|
|
100
92
|
}
|
|
101
93
|
|
|
102
|
-
|
|
94
|
+
/** @type {(...rules: import('postcss').Rule[]) => string} */
|
|
95
|
+
const joinSelectors = (...rules) => rules.map((s) => s.selector).join();
|
|
103
96
|
|
|
97
|
+
/**
|
|
98
|
+
* @param {...import('postcss').Rule} rules
|
|
99
|
+
* @return {number}
|
|
100
|
+
*/
|
|
104
101
|
function ruleLength(...rules) {
|
|
105
|
-
return rules.map(r => r.nodes.length ? String(r) : '').join('').length;
|
|
102
|
+
return rules.map((r) => (r.nodes.length ? String(r) : '')).join('').length;
|
|
106
103
|
}
|
|
104
|
+
|
|
107
105
|
/**
|
|
108
106
|
* @param {string} prop
|
|
109
|
-
* @return {{prefix: string
|
|
107
|
+
* @return {{prefix: string?, base:string?, rest:string[]}}
|
|
110
108
|
*/
|
|
111
|
-
|
|
112
|
-
|
|
113
109
|
function splitProp(prop) {
|
|
114
110
|
// Treat vendor prefixed properties as if they were unprefixed;
|
|
115
111
|
// moving them when combined with non-prefixed properties can
|
|
116
112
|
// cause issues. e.g. moving -webkit-background-clip when there
|
|
117
113
|
// is a background shorthand definition.
|
|
118
|
-
const parts = prop.split('-');
|
|
119
114
|
|
|
115
|
+
const parts = prop.split('-');
|
|
120
116
|
if (prop[0] !== '-') {
|
|
121
117
|
return {
|
|
122
118
|
prefix: '',
|
|
123
119
|
base: parts[0],
|
|
124
|
-
rest: parts.slice(1)
|
|
120
|
+
rest: parts.slice(1),
|
|
125
121
|
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
122
|
+
}
|
|
123
|
+
// Don't split css variables
|
|
129
124
|
if (prop[1] === '-') {
|
|
130
125
|
return {
|
|
131
126
|
prefix: null,
|
|
132
127
|
base: null,
|
|
133
|
-
rest: [prop]
|
|
128
|
+
rest: [prop],
|
|
134
129
|
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
}
|
|
131
|
+
// Found prefix
|
|
138
132
|
return {
|
|
139
133
|
prefix: parts[1],
|
|
140
134
|
base: parts[2],
|
|
141
|
-
rest: parts.slice(3)
|
|
135
|
+
rest: parts.slice(3),
|
|
142
136
|
};
|
|
143
137
|
}
|
|
138
|
+
|
|
144
139
|
/**
|
|
145
140
|
* @param {string} propA
|
|
146
141
|
* @param {string} propB
|
|
147
142
|
*/
|
|
148
|
-
|
|
149
|
-
|
|
150
143
|
function isConflictingProp(propA, propB) {
|
|
151
144
|
if (propA === propB) {
|
|
152
145
|
// Same specificity
|
|
153
146
|
return true;
|
|
154
147
|
}
|
|
155
|
-
|
|
156
148
|
const a = splitProp(propA);
|
|
157
|
-
const b = splitProp(propB);
|
|
158
|
-
|
|
149
|
+
const b = splitProp(propB);
|
|
150
|
+
// Don't resort css variables
|
|
159
151
|
if (!a.base && !b.base) {
|
|
160
152
|
return true;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
153
|
+
}
|
|
154
|
+
// Different base;
|
|
164
155
|
if (a.base !== b.base) {
|
|
165
156
|
return false;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
157
|
+
}
|
|
158
|
+
// Conflict if rest-count mismatches
|
|
169
159
|
if (a.rest.length !== b.rest.length) {
|
|
170
160
|
return true;
|
|
171
|
-
}
|
|
172
|
-
|
|
161
|
+
}
|
|
173
162
|
|
|
163
|
+
// Conflict if rest parameters are equal (same but unprefixed)
|
|
174
164
|
return a.rest.every((s, index) => b.rest[index] === s);
|
|
175
165
|
}
|
|
166
|
+
|
|
176
167
|
/**
|
|
177
|
-
* @param {postcss.Rule} first
|
|
178
|
-
* @param {postcss.Rule} second
|
|
168
|
+
* @param {import('postcss').Rule} first
|
|
169
|
+
* @param {import('postcss').Rule} second
|
|
179
170
|
* @return {boolean} merged
|
|
180
171
|
*/
|
|
181
|
-
|
|
182
|
-
|
|
183
172
|
function mergeParents(first, second) {
|
|
184
173
|
// Null check for detached rules
|
|
185
174
|
if (!first.parent || !second.parent) {
|
|
186
175
|
return false;
|
|
187
|
-
}
|
|
188
|
-
|
|
176
|
+
}
|
|
189
177
|
|
|
178
|
+
// Check if parents share node
|
|
190
179
|
if (first.parent === second.parent) {
|
|
191
180
|
return false;
|
|
192
|
-
}
|
|
181
|
+
}
|
|
193
182
|
|
|
183
|
+
// sameParent() already called by canMerge()
|
|
194
184
|
|
|
195
185
|
second.remove();
|
|
196
186
|
first.parent.append(second);
|
|
197
187
|
return true;
|
|
198
188
|
}
|
|
189
|
+
|
|
199
190
|
/**
|
|
200
|
-
* @param {postcss.Rule} first
|
|
201
|
-
* @param {postcss.Rule} second
|
|
202
|
-
* @return {postcss.Rule} mergedRule
|
|
191
|
+
* @param {import('postcss').Rule} first
|
|
192
|
+
* @param {import('postcss').Rule} second
|
|
193
|
+
* @return {import('postcss').Rule} mergedRule
|
|
203
194
|
*/
|
|
204
|
-
|
|
205
|
-
|
|
206
195
|
function partialMerge(first, second) {
|
|
207
196
|
let intersection = intersect(getDecls(first), getDecls(second));
|
|
208
|
-
|
|
209
197
|
if (!intersection.length) {
|
|
210
198
|
return second;
|
|
211
199
|
}
|
|
212
|
-
|
|
213
200
|
let nextRule = second.next();
|
|
214
|
-
|
|
215
201
|
if (!nextRule) {
|
|
216
202
|
// Grab next cousin
|
|
217
|
-
|
|
203
|
+
/** @type {any} */
|
|
204
|
+
const parentSibling =
|
|
205
|
+
/** @type {import('postcss').Container<import('postcss').ChildNode>} */ (
|
|
206
|
+
second.parent
|
|
207
|
+
).next();
|
|
218
208
|
nextRule = parentSibling && parentSibling.nodes && parentSibling.nodes[0];
|
|
219
209
|
}
|
|
220
|
-
|
|
221
210
|
if (nextRule && nextRule.type === 'rule' && canMerge(second, nextRule)) {
|
|
222
211
|
let nextIntersection = intersect(getDecls(second), getDecls(nextRule));
|
|
223
|
-
|
|
224
212
|
if (nextIntersection.length > intersection.length) {
|
|
225
213
|
mergeParents(second, nextRule);
|
|
226
214
|
first = second;
|
|
@@ -229,45 +217,52 @@ function partialMerge(first, second) {
|
|
|
229
217
|
}
|
|
230
218
|
}
|
|
231
219
|
|
|
232
|
-
const firstDecls = getDecls(first);
|
|
220
|
+
const firstDecls = getDecls(first);
|
|
233
221
|
|
|
222
|
+
// Filter out intersections with later conflicts in First
|
|
234
223
|
intersection = intersection.filter((decl, intersectIndex) => {
|
|
235
224
|
const indexOfDecl = indexOfDeclaration(firstDecls, decl);
|
|
236
|
-
const nextConflictInFirst = firstDecls
|
|
237
|
-
|
|
225
|
+
const nextConflictInFirst = firstDecls
|
|
226
|
+
.slice(indexOfDecl + 1)
|
|
227
|
+
.filter((d) => isConflictingProp(d.prop, decl.prop));
|
|
238
228
|
if (!nextConflictInFirst.length) {
|
|
239
229
|
return true;
|
|
240
230
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
231
|
+
const nextConflictInIntersection = intersection
|
|
232
|
+
.slice(intersectIndex + 1)
|
|
233
|
+
.filter((d) => isConflictingProp(d.prop, decl.prop));
|
|
244
234
|
if (!nextConflictInIntersection.length) {
|
|
245
235
|
return false;
|
|
246
236
|
}
|
|
247
|
-
|
|
248
237
|
if (nextConflictInFirst.length !== nextConflictInIntersection.length) {
|
|
249
238
|
return false;
|
|
250
239
|
}
|
|
240
|
+
return nextConflictInFirst.every((d, index) =>
|
|
241
|
+
declarationIsEqual(d, nextConflictInIntersection[index])
|
|
242
|
+
);
|
|
243
|
+
});
|
|
251
244
|
|
|
252
|
-
|
|
253
|
-
}); // Filter out intersections with previous conflicts in Second
|
|
254
|
-
|
|
245
|
+
// Filter out intersections with previous conflicts in Second
|
|
255
246
|
const secondDecls = getDecls(second);
|
|
256
|
-
intersection = intersection.filter(decl => {
|
|
257
|
-
const nextConflictIndex = secondDecls.findIndex(d =>
|
|
258
|
-
|
|
247
|
+
intersection = intersection.filter((decl) => {
|
|
248
|
+
const nextConflictIndex = secondDecls.findIndex((d) =>
|
|
249
|
+
isConflictingProp(d.prop, decl.prop)
|
|
250
|
+
);
|
|
259
251
|
if (nextConflictIndex === -1) {
|
|
260
252
|
return false;
|
|
261
253
|
}
|
|
262
|
-
|
|
263
254
|
if (!declarationIsEqual(secondDecls[nextConflictIndex], decl)) {
|
|
264
255
|
return false;
|
|
265
256
|
}
|
|
266
|
-
|
|
267
|
-
|
|
257
|
+
if (
|
|
258
|
+
decl.prop.toLowerCase() !== 'direction' &&
|
|
259
|
+
decl.prop.toLowerCase() !== 'unicode-bidi' &&
|
|
260
|
+
secondDecls.some(
|
|
261
|
+
(declaration) => declaration.prop.toLowerCase() === 'all'
|
|
262
|
+
)
|
|
263
|
+
) {
|
|
268
264
|
return false;
|
|
269
265
|
}
|
|
270
|
-
|
|
271
266
|
secondDecls.splice(nextConflictIndex, 1);
|
|
272
267
|
return true;
|
|
273
268
|
});
|
|
@@ -280,58 +275,60 @@ function partialMerge(first, second) {
|
|
|
280
275
|
const receivingBlock = second.clone();
|
|
281
276
|
receivingBlock.selector = joinSelectors(first, second);
|
|
282
277
|
receivingBlock.nodes = [];
|
|
283
|
-
|
|
278
|
+
|
|
279
|
+
/** @type {import('postcss').Container<import('postcss').ChildNode>} */ (
|
|
280
|
+
second.parent
|
|
281
|
+
).insertBefore(second, receivingBlock);
|
|
282
|
+
|
|
284
283
|
const firstClone = first.clone();
|
|
285
284
|
const secondClone = second.clone();
|
|
285
|
+
|
|
286
286
|
/**
|
|
287
|
-
* @param {function(postcss.Declaration):void} callback
|
|
288
|
-
* @
|
|
287
|
+
* @param {function(import('postcss').Declaration):void} callback
|
|
288
|
+
* @this {import('postcss').Rule}
|
|
289
|
+
* @return {function(import('postcss').Declaration)}
|
|
289
290
|
*/
|
|
290
|
-
|
|
291
291
|
function moveDecl(callback) {
|
|
292
|
-
return decl => {
|
|
292
|
+
return (decl) => {
|
|
293
293
|
if (~indexOfDeclaration(intersection, decl)) {
|
|
294
294
|
callback.call(this, decl);
|
|
295
295
|
}
|
|
296
296
|
};
|
|
297
297
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
298
|
+
firstClone.walkDecls(
|
|
299
|
+
moveDecl((decl) => {
|
|
300
|
+
decl.remove();
|
|
301
|
+
receivingBlock.append(decl);
|
|
302
|
+
})
|
|
303
|
+
);
|
|
304
|
+
secondClone.walkDecls(moveDecl((decl) => decl.remove()));
|
|
304
305
|
const merged = ruleLength(firstClone, receivingBlock, secondClone);
|
|
305
306
|
const original = ruleLength(first, second);
|
|
306
|
-
|
|
307
307
|
if (merged < original) {
|
|
308
308
|
first.replaceWith(firstClone);
|
|
309
309
|
second.replaceWith(secondClone);
|
|
310
|
-
[firstClone, receivingBlock, secondClone].forEach(r => {
|
|
310
|
+
[firstClone, receivingBlock, secondClone].forEach((r) => {
|
|
311
311
|
if (!r.nodes.length) {
|
|
312
312
|
r.remove();
|
|
313
313
|
}
|
|
314
314
|
});
|
|
315
|
-
|
|
316
315
|
if (!secondClone.parent) {
|
|
317
316
|
return receivingBlock;
|
|
318
317
|
}
|
|
319
|
-
|
|
320
318
|
return secondClone;
|
|
321
319
|
} else {
|
|
322
320
|
receivingBlock.remove();
|
|
323
321
|
return second;
|
|
324
322
|
}
|
|
325
323
|
}
|
|
324
|
+
|
|
326
325
|
/**
|
|
327
326
|
* @param {string[]} browsers
|
|
328
327
|
* @param {Map<string, boolean>} compatibilityCache
|
|
329
|
-
* @return {function(postcss.Rule)}
|
|
328
|
+
* @return {function(import('postcss').Rule)}
|
|
330
329
|
*/
|
|
331
|
-
|
|
332
|
-
|
|
333
330
|
function selectorMerger(browsers, compatibilityCache) {
|
|
334
|
-
/** @type {postcss.Rule} */
|
|
331
|
+
/** @type {import('postcss').Rule | null} */
|
|
335
332
|
let cache = null;
|
|
336
333
|
return function (rule) {
|
|
337
334
|
// Prime the cache with the first rule, or alternately ensure that it is
|
|
@@ -339,71 +336,75 @@ function selectorMerger(browsers, compatibilityCache) {
|
|
|
339
336
|
if (!cache || !canMerge(rule, cache, browsers, compatibilityCache)) {
|
|
340
337
|
cache = rule;
|
|
341
338
|
return;
|
|
342
|
-
}
|
|
339
|
+
}
|
|
340
|
+
// Ensure that we don't deduplicate the same rule; this is sometimes
|
|
343
341
|
// caused by a partial merge
|
|
344
|
-
|
|
345
|
-
|
|
346
342
|
if (cache === rule) {
|
|
347
343
|
cache = rule;
|
|
348
344
|
return;
|
|
349
|
-
}
|
|
345
|
+
}
|
|
350
346
|
|
|
347
|
+
// Parents merge: check if the rules have same parents, but not same parent nodes
|
|
348
|
+
mergeParents(cache, rule);
|
|
351
349
|
|
|
352
|
-
|
|
350
|
+
// Merge when declarations are exactly equal
|
|
353
351
|
// e.g. h1 { color: red } h2 { color: red }
|
|
354
|
-
|
|
355
352
|
if (sameDeclarationsAndOrder(getDecls(rule), getDecls(cache))) {
|
|
356
353
|
rule.selector = joinSelectors(cache, rule);
|
|
357
354
|
cache.remove();
|
|
358
355
|
cache = rule;
|
|
359
356
|
return;
|
|
360
|
-
}
|
|
357
|
+
}
|
|
358
|
+
// Merge when both selectors are exactly equal
|
|
361
359
|
// e.g. a { color: blue } a { font-weight: bold }
|
|
362
|
-
|
|
363
|
-
|
|
364
360
|
if (cache.selector === rule.selector) {
|
|
365
361
|
const cached = getDecls(cache);
|
|
366
|
-
rule.walk(decl => {
|
|
367
|
-
if (
|
|
368
|
-
|
|
362
|
+
rule.walk((decl) => {
|
|
363
|
+
if (
|
|
364
|
+
~indexOfDeclaration(
|
|
365
|
+
cached,
|
|
366
|
+
/** @type {import('postcss').Declaration} */ (decl)
|
|
367
|
+
)
|
|
368
|
+
) {
|
|
369
|
+
decl.remove();
|
|
370
|
+
return;
|
|
369
371
|
}
|
|
370
|
-
|
|
371
|
-
cache.append(decl);
|
|
372
|
+
/** @type {import('postcss').Rule} */ (cache).append(decl);
|
|
372
373
|
});
|
|
373
374
|
rule.remove();
|
|
374
375
|
return;
|
|
375
|
-
}
|
|
376
|
+
}
|
|
377
|
+
// Partial merge: check if the rule contains a subset of the last; if
|
|
376
378
|
// so create a joined selector with the subset, if smaller.
|
|
377
|
-
|
|
378
|
-
|
|
379
379
|
cache = partialMerge(cache, rule);
|
|
380
380
|
};
|
|
381
381
|
}
|
|
382
|
-
|
|
382
|
+
/**
|
|
383
|
+
* @type {import('postcss').PluginCreator<void>}
|
|
384
|
+
* @return {import('postcss').Plugin}
|
|
385
|
+
*/
|
|
383
386
|
function pluginCreator() {
|
|
384
387
|
return {
|
|
385
388
|
postcssPlugin: 'postcss-merge-rules',
|
|
386
389
|
|
|
387
390
|
prepare(result) {
|
|
391
|
+
/** @type {typeof result.opts & browserslist.Options} */
|
|
388
392
|
const resultOpts = result.opts || {};
|
|
389
|
-
const browsers = (
|
|
393
|
+
const browsers = browserslist(null, {
|
|
390
394
|
stats: resultOpts.stats,
|
|
391
395
|
path: __dirname,
|
|
392
|
-
env: resultOpts.env
|
|
396
|
+
env: resultOpts.env,
|
|
393
397
|
});
|
|
398
|
+
|
|
394
399
|
const compatibilityCache = new Map();
|
|
395
400
|
return {
|
|
396
401
|
OnceExit(css) {
|
|
397
402
|
css.walkRules(selectorMerger(browsers, compatibilityCache));
|
|
398
|
-
}
|
|
399
|
-
|
|
403
|
+
},
|
|
400
404
|
};
|
|
401
|
-
}
|
|
402
|
-
|
|
405
|
+
},
|
|
403
406
|
};
|
|
404
407
|
}
|
|
405
408
|
|
|
406
409
|
pluginCreator.postcss = true;
|
|
407
|
-
|
|
408
|
-
exports.default = _default;
|
|
409
|
-
module.exports = exports.default;
|
|
410
|
+
module.exports = pluginCreator;
|
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.ensureCompatibility = ensureCompatibility;
|
|
7
|
-
exports.noVendor = noVendor;
|
|
8
|
-
exports.pseudoElements = void 0;
|
|
9
|
-
exports.sameVendor = sameVendor;
|
|
10
|
-
|
|
11
|
-
var _caniuseApi = require("caniuse-api");
|
|
12
|
-
|
|
13
|
-
var _postcssSelectorParser = _interopRequireDefault(require("postcss-selector-parser"));
|
|
14
|
-
|
|
15
|
-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
1
|
+
'use strict';
|
|
2
|
+
const { isSupported } = require('caniuse-api');
|
|
3
|
+
const selectorParser = require('postcss-selector-parser');
|
|
16
4
|
|
|
17
5
|
const simpleSelectorRe = /^#?[-._a-z0-9 ]+$/i;
|
|
6
|
+
|
|
18
7
|
const cssSel2 = 'css-sel2';
|
|
19
8
|
const cssSel3 = 'css-sel3';
|
|
20
9
|
const cssGencontent = 'css-gencontent';
|
|
@@ -22,33 +11,50 @@ const cssFirstLetter = 'css-first-letter';
|
|
|
22
11
|
const cssFirstLine = 'css-first-line';
|
|
23
12
|
const cssInOutOfRange = 'css-in-out-of-range';
|
|
24
13
|
const formValidation = 'form-validation';
|
|
25
|
-
|
|
14
|
+
|
|
15
|
+
const vendorPrefix =
|
|
16
|
+
/-(ah|apple|atsc|epub|hp|khtml|moz|ms|o|rim|ro|tc|wap|webkit|xv)-/;
|
|
17
|
+
|
|
18
|
+
const level2Sel = new Set(['=', '~=', '|=']);
|
|
19
|
+
const level3Sel = new Set(['^=', '$=', '*=']);
|
|
20
|
+
|
|
26
21
|
/**
|
|
27
22
|
* @param {string} selector
|
|
28
|
-
* @return {
|
|
23
|
+
* @return {RegExpMatchArray | null}
|
|
29
24
|
*/
|
|
30
|
-
|
|
31
25
|
function filterPrefixes(selector) {
|
|
32
26
|
return selector.match(vendorPrefix);
|
|
33
|
-
}
|
|
34
|
-
// Microsoft Edge use ::-ms-input-placeholder.
|
|
35
|
-
|
|
27
|
+
}
|
|
36
28
|
|
|
37
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Internet Explorer use :-ms-input-placeholder.
|
|
31
|
+
* Microsoft Edge use ::-ms-input-placeholder.
|
|
32
|
+
*
|
|
33
|
+
* @type {(selector: string) => number}
|
|
34
|
+
*/
|
|
35
|
+
const findMsInputPlaceholder = (selector) =>
|
|
36
|
+
~selector.search(/-ms-input-placeholder/i);
|
|
38
37
|
|
|
38
|
+
/**
|
|
39
|
+
* @param {string[]} selectorsA
|
|
40
|
+
* @param {string[]} selectorsB
|
|
41
|
+
* @return {boolean}
|
|
42
|
+
*/
|
|
39
43
|
function sameVendor(selectorsA, selectorsB) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return
|
|
44
|
+
/** @type {(selectors: string[]) => string} */
|
|
45
|
+
let same = (selectors) => selectors.map(filterPrefixes).join();
|
|
46
|
+
/** @type {(selectors: string[]) => string | undefined} */
|
|
47
|
+
let findMsVendor = (selectors) => selectors.find(findMsInputPlaceholder);
|
|
48
|
+
return (
|
|
49
|
+
same(selectorsA) === same(selectorsB) &&
|
|
50
|
+
!(findMsVendor(selectorsA) && findMsVendor(selectorsB))
|
|
51
|
+
);
|
|
45
52
|
}
|
|
53
|
+
|
|
46
54
|
/**
|
|
47
55
|
* @param {string} selector
|
|
48
56
|
* @return {boolean}
|
|
49
57
|
*/
|
|
50
|
-
|
|
51
|
-
|
|
52
58
|
function noVendor(selector) {
|
|
53
59
|
return !vendorPrefix.test(selector);
|
|
54
60
|
}
|
|
@@ -104,109 +110,110 @@ const pseudoElements = {
|
|
|
104
110
|
'::placeholder': 'css-placeholder',
|
|
105
111
|
'::selection': 'css-selection',
|
|
106
112
|
':valid': formValidation,
|
|
107
|
-
':visited': cssSel2
|
|
113
|
+
':visited': cssSel2,
|
|
108
114
|
};
|
|
109
|
-
exports.pseudoElements = pseudoElements;
|
|
110
115
|
|
|
116
|
+
/**
|
|
117
|
+
* @param {string} selector
|
|
118
|
+
* @return {boolean}
|
|
119
|
+
*/
|
|
111
120
|
function isCssMixin(selector) {
|
|
112
121
|
return selector[selector.length - 1] === ':';
|
|
113
122
|
}
|
|
114
123
|
|
|
124
|
+
/**
|
|
125
|
+
* @param {string} selector
|
|
126
|
+
* @return {boolean}
|
|
127
|
+
*/
|
|
115
128
|
function isHostPseudoClass(selector) {
|
|
116
129
|
return selector.includes(':host');
|
|
117
130
|
}
|
|
118
131
|
|
|
119
|
-
const isSupportedCache = new Map();
|
|
132
|
+
const isSupportedCache = new Map();
|
|
120
133
|
|
|
134
|
+
// Move to util in future
|
|
135
|
+
/**
|
|
136
|
+
* @param {string} feature
|
|
137
|
+
* @param {string[] | undefined} browsers
|
|
138
|
+
*/
|
|
121
139
|
function isSupportedCached(feature, browsers) {
|
|
122
|
-
const key = JSON.stringify({
|
|
123
|
-
feature,
|
|
124
|
-
browsers
|
|
125
|
-
});
|
|
140
|
+
const key = JSON.stringify({ feature, browsers });
|
|
126
141
|
let result = isSupportedCache.get(key);
|
|
127
142
|
|
|
128
143
|
if (!result) {
|
|
129
|
-
result =
|
|
144
|
+
result = isSupported(feature, /** @type {string[]} */ (browsers));
|
|
130
145
|
isSupportedCache.set(key, result);
|
|
131
146
|
}
|
|
132
147
|
|
|
133
148
|
return result;
|
|
134
149
|
}
|
|
135
150
|
|
|
151
|
+
/**
|
|
152
|
+
* @param {string[]} selectors
|
|
153
|
+
* @param{string[]=} browsers
|
|
154
|
+
* @param{Map<string,boolean>=} compatibilityCache
|
|
155
|
+
* @return {boolean}
|
|
156
|
+
*/
|
|
136
157
|
function ensureCompatibility(selectors, browsers, compatibilityCache) {
|
|
137
158
|
// Should not merge mixins
|
|
138
159
|
if (selectors.some(isCssMixin)) {
|
|
139
160
|
return false;
|
|
140
|
-
}
|
|
141
|
-
|
|
161
|
+
}
|
|
142
162
|
|
|
163
|
+
// Should not merge :host selector https://github.com/angular/angular-cli/issues/18672
|
|
143
164
|
if (selectors.some(isHostPseudoClass)) {
|
|
144
165
|
return false;
|
|
145
166
|
}
|
|
146
|
-
|
|
147
|
-
return selectors.every(selector => {
|
|
167
|
+
return selectors.every((selector) => {
|
|
148
168
|
if (simpleSelectorRe.test(selector)) {
|
|
149
169
|
return true;
|
|
150
170
|
}
|
|
151
|
-
|
|
152
171
|
if (compatibilityCache && compatibilityCache.has(selector)) {
|
|
153
172
|
return compatibilityCache.get(selector);
|
|
154
173
|
}
|
|
155
|
-
|
|
156
174
|
let compatible = true;
|
|
157
|
-
(
|
|
158
|
-
ast.walk(node => {
|
|
159
|
-
const {
|
|
160
|
-
type,
|
|
161
|
-
value
|
|
162
|
-
} = node;
|
|
163
|
-
|
|
175
|
+
selectorParser((ast) => {
|
|
176
|
+
ast.walk((node) => {
|
|
177
|
+
const { type, value } = node;
|
|
164
178
|
if (type === 'pseudo') {
|
|
165
|
-
const entry =
|
|
166
|
-
|
|
179
|
+
const entry =
|
|
180
|
+
pseudoElements[/** @type {keyof pseudoElements} */ (value)];
|
|
167
181
|
if (!entry && noVendor(value)) {
|
|
168
182
|
compatible = false;
|
|
169
183
|
}
|
|
170
|
-
|
|
171
184
|
if (entry && compatible) {
|
|
172
185
|
compatible = isSupportedCached(entry, browsers);
|
|
173
186
|
}
|
|
174
187
|
}
|
|
175
|
-
|
|
176
188
|
if (type === 'combinator') {
|
|
177
|
-
if (
|
|
189
|
+
if (value.includes('~')) {
|
|
178
190
|
compatible = isSupportedCached(cssSel3, browsers);
|
|
179
191
|
}
|
|
180
|
-
|
|
181
|
-
if (~value.indexOf('>') || ~value.indexOf('+')) {
|
|
192
|
+
if (value.includes('>') || value.includes('+')) {
|
|
182
193
|
compatible = isSupportedCached(cssSel2, browsers);
|
|
183
194
|
}
|
|
184
195
|
}
|
|
185
|
-
|
|
186
196
|
if (type === 'attribute' && node.attribute) {
|
|
187
197
|
// [foo]
|
|
188
198
|
if (!node.operator) {
|
|
189
199
|
compatible = isSupportedCached(cssSel2, browsers);
|
|
190
200
|
}
|
|
191
|
-
|
|
192
201
|
if (value) {
|
|
193
202
|
// [foo="bar"], [foo~="bar"], [foo|="bar"]
|
|
194
|
-
if (
|
|
203
|
+
if (level2Sel.has(/** @type {string} */ (node.operator))) {
|
|
195
204
|
compatible = isSupportedCached(cssSel2, browsers);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (~['^=', '$=', '*='].indexOf(node.operator)) {
|
|
205
|
+
}
|
|
206
|
+
// [foo^="bar"], [foo$="bar"], [foo*="bar"]
|
|
207
|
+
if (level3Sel.has(/** @type {string} */ (node.operator))) {
|
|
200
208
|
compatible = isSupportedCached(cssSel3, browsers);
|
|
201
209
|
}
|
|
202
|
-
}
|
|
203
|
-
|
|
210
|
+
}
|
|
204
211
|
|
|
212
|
+
// [foo="bar" i]
|
|
205
213
|
if (node.insensitive) {
|
|
206
214
|
compatible = isSupportedCached('css-case-insensitive', browsers);
|
|
207
215
|
}
|
|
208
216
|
}
|
|
209
|
-
|
|
210
217
|
if (!compatible) {
|
|
211
218
|
// If this node was not compatible,
|
|
212
219
|
// break out early from walking the rest
|
|
@@ -214,11 +221,11 @@ function ensureCompatibility(selectors, browsers, compatibilityCache) {
|
|
|
214
221
|
}
|
|
215
222
|
});
|
|
216
223
|
}).processSync(selector);
|
|
217
|
-
|
|
218
224
|
if (compatibilityCache) {
|
|
219
225
|
compatibilityCache.set(selector, compatible);
|
|
220
226
|
}
|
|
221
|
-
|
|
222
227
|
return compatible;
|
|
223
228
|
});
|
|
224
|
-
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = { sameVendor, noVendor, pseudoElements, ensureCompatibility };
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string[]} selectorsA
|
|
3
|
+
* @param {string[]} selectorsB
|
|
4
|
+
* @return {boolean}
|
|
5
|
+
*/
|
|
6
|
+
export function sameVendor(selectorsA: string[], selectorsB: string[]): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} selector
|
|
9
|
+
* @return {boolean}
|
|
10
|
+
*/
|
|
11
|
+
export function noVendor(selector: string): boolean;
|
|
12
|
+
export const pseudoElements: {
|
|
13
|
+
':active': string;
|
|
14
|
+
':after': string;
|
|
15
|
+
':any-link': string;
|
|
16
|
+
':before': string;
|
|
17
|
+
':checked': string;
|
|
18
|
+
':default': string;
|
|
19
|
+
':dir': string;
|
|
20
|
+
':disabled': string;
|
|
21
|
+
':empty': string;
|
|
22
|
+
':enabled': string;
|
|
23
|
+
':first-child': string;
|
|
24
|
+
':first-letter': string;
|
|
25
|
+
':first-line': string;
|
|
26
|
+
':first-of-type': string;
|
|
27
|
+
':focus': string;
|
|
28
|
+
':focus-within': string;
|
|
29
|
+
':focus-visible': string;
|
|
30
|
+
':has': string;
|
|
31
|
+
':hover': string;
|
|
32
|
+
':in-range': string;
|
|
33
|
+
':indeterminate': string;
|
|
34
|
+
':invalid': string;
|
|
35
|
+
':is': string;
|
|
36
|
+
':lang': string;
|
|
37
|
+
':last-child': string;
|
|
38
|
+
':last-of-type': string;
|
|
39
|
+
':link': string;
|
|
40
|
+
':matches': string;
|
|
41
|
+
':not': string;
|
|
42
|
+
':nth-child': string;
|
|
43
|
+
':nth-last-child': string;
|
|
44
|
+
':nth-last-of-type': string;
|
|
45
|
+
':nth-of-type': string;
|
|
46
|
+
':only-child': string;
|
|
47
|
+
':only-of-type': string;
|
|
48
|
+
':optional': string;
|
|
49
|
+
':out-of-range': string;
|
|
50
|
+
':placeholder-shown': string;
|
|
51
|
+
':required': string;
|
|
52
|
+
':root': string;
|
|
53
|
+
':target': string;
|
|
54
|
+
'::after': string;
|
|
55
|
+
'::backdrop': string;
|
|
56
|
+
'::before': string;
|
|
57
|
+
'::first-letter': string;
|
|
58
|
+
'::first-line': string;
|
|
59
|
+
'::marker': string;
|
|
60
|
+
'::placeholder': string;
|
|
61
|
+
'::selection': string;
|
|
62
|
+
':valid': string;
|
|
63
|
+
':visited': string;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* @param {string[]} selectors
|
|
67
|
+
* @param{string[]=} browsers
|
|
68
|
+
* @param{Map<string,boolean>=} compatibilityCache
|
|
69
|
+
* @return {boolean}
|
|
70
|
+
*/
|
|
71
|
+
export function ensureCompatibility(selectors: string[], browsers?: string[] | undefined, compatibilityCache?: Map<string, boolean> | undefined): boolean;
|