1 | "use strict";
|
---|
2 |
|
---|
3 | Object.defineProperty(exports, "__esModule", {
|
---|
4 | value: true
|
---|
5 | });
|
---|
6 | exports.default = void 0;
|
---|
7 |
|
---|
8 | var _browserslist = _interopRequireDefault(require("browserslist"));
|
---|
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 }; }
|
---|
15 |
|
---|
16 | /**
|
---|
17 | * @param {postcss.Declaration} a
|
---|
18 | * @param {postcss.Declaration} b
|
---|
19 | * @return {boolean}
|
---|
20 | */
|
---|
21 | function declarationIsEqual(a, b) {
|
---|
22 | return a.important === b.important && a.prop === b.prop && a.value === b.value;
|
---|
23 | }
|
---|
24 | /**
|
---|
25 | * @param {postcss.Declaration[]} array
|
---|
26 | * @param {postcss.Declaration} decl
|
---|
27 | * @return {number}
|
---|
28 | */
|
---|
29 |
|
---|
30 |
|
---|
31 | function indexOfDeclaration(array, decl) {
|
---|
32 | return array.findIndex(d => declarationIsEqual(d, decl));
|
---|
33 | }
|
---|
34 | /**
|
---|
35 | * Returns filtered array of matched or unmatched declarations
|
---|
36 | * @param {postcss.Declaration[]} a
|
---|
37 | * @param {postcss.Declaration[]} b
|
---|
38 | * @param {boolean} [not=false]
|
---|
39 | * @return {postcss.Declaration[]}
|
---|
40 | */
|
---|
41 |
|
---|
42 |
|
---|
43 | function intersect(a, b, not) {
|
---|
44 | return a.filter(c => {
|
---|
45 | const index = ~indexOfDeclaration(b, c);
|
---|
46 | return not ? !index : index;
|
---|
47 | });
|
---|
48 | }
|
---|
49 | /**
|
---|
50 | * @param {postcss.Declaration[]} a
|
---|
51 | * @param {postcss.Declaration[]} b
|
---|
52 | * @return {boolean}
|
---|
53 | */
|
---|
54 |
|
---|
55 |
|
---|
56 | function sameDeclarationsAndOrder(a, b) {
|
---|
57 | if (a.length !== b.length) {
|
---|
58 | return false;
|
---|
59 | }
|
---|
60 |
|
---|
61 | return a.every((d, index) => declarationIsEqual(d, b[index]));
|
---|
62 | }
|
---|
63 | /**
|
---|
64 | * @param {postcss.Rule} ruleA
|
---|
65 | * @param {postcss.Rule} ruleB
|
---|
66 | * @param {string[]=} browsers
|
---|
67 | * @param {Object.<string, boolean>=} compatibilityCache
|
---|
68 | * @return {boolean}
|
---|
69 | */
|
---|
70 |
|
---|
71 |
|
---|
72 | function canMerge(ruleA, ruleB, browsers, compatibilityCache) {
|
---|
73 | const a = ruleA.selectors;
|
---|
74 | const b = ruleB.selectors;
|
---|
75 | const selectors = a.concat(b);
|
---|
76 |
|
---|
77 | if (!(0, _ensureCompatibility.ensureCompatibility)(selectors, browsers, compatibilityCache)) {
|
---|
78 | return false;
|
---|
79 | }
|
---|
80 |
|
---|
81 | const parent = (0, _cssnanoUtils.sameParent)(ruleA, ruleB);
|
---|
82 | const {
|
---|
83 | name
|
---|
84 | } = ruleA.parent;
|
---|
85 |
|
---|
86 | if (parent && name && ~name.indexOf('keyframes')) {
|
---|
87 | return false;
|
---|
88 | }
|
---|
89 |
|
---|
90 | return parent && (selectors.every(_ensureCompatibility.noVendor) || (0, _ensureCompatibility.sameVendor)(a, b));
|
---|
91 | }
|
---|
92 | /**
|
---|
93 | * @param {postcss.Rule} rule
|
---|
94 | * @return {postcss.Declaration[]}
|
---|
95 | */
|
---|
96 |
|
---|
97 |
|
---|
98 | function getDecls(rule) {
|
---|
99 | return rule.nodes.filter(node => node.type === 'decl');
|
---|
100 | }
|
---|
101 |
|
---|
102 | const joinSelectors = (...rules) => rules.map(s => s.selector).join();
|
---|
103 |
|
---|
104 | function ruleLength(...rules) {
|
---|
105 | return rules.map(r => r.nodes.length ? String(r) : '').join('').length;
|
---|
106 | }
|
---|
107 | /**
|
---|
108 | * @param {string} prop
|
---|
109 | * @return {{prefix: string, base:string, rest:string[]}}
|
---|
110 | */
|
---|
111 |
|
---|
112 |
|
---|
113 | function splitProp(prop) {
|
---|
114 | // Treat vendor prefixed properties as if they were unprefixed;
|
---|
115 | // moving them when combined with non-prefixed properties can
|
---|
116 | // cause issues. e.g. moving -webkit-background-clip when there
|
---|
117 | // is a background shorthand definition.
|
---|
118 | const parts = prop.split('-');
|
---|
119 |
|
---|
120 | if (prop[0] !== '-') {
|
---|
121 | return {
|
---|
122 | prefix: '',
|
---|
123 | base: parts[0],
|
---|
124 | rest: parts.slice(1)
|
---|
125 | };
|
---|
126 | } // Don't split css variables
|
---|
127 |
|
---|
128 |
|
---|
129 | if (prop[1] === '-') {
|
---|
130 | return {
|
---|
131 | prefix: null,
|
---|
132 | base: null,
|
---|
133 | rest: [prop]
|
---|
134 | };
|
---|
135 | } // Found prefix
|
---|
136 |
|
---|
137 |
|
---|
138 | return {
|
---|
139 | prefix: parts[1],
|
---|
140 | base: parts[2],
|
---|
141 | rest: parts.slice(3)
|
---|
142 | };
|
---|
143 | }
|
---|
144 | /**
|
---|
145 | * @param {string} propA
|
---|
146 | * @param {string} propB
|
---|
147 | */
|
---|
148 |
|
---|
149 |
|
---|
150 | function isConflictingProp(propA, propB) {
|
---|
151 | if (propA === propB) {
|
---|
152 | // Same specificity
|
---|
153 | return true;
|
---|
154 | }
|
---|
155 |
|
---|
156 | const a = splitProp(propA);
|
---|
157 | const b = splitProp(propB); // Don't resort css variables
|
---|
158 |
|
---|
159 | if (!a.base && !b.base) {
|
---|
160 | return true;
|
---|
161 | } // Different base;
|
---|
162 |
|
---|
163 |
|
---|
164 | if (a.base !== b.base) {
|
---|
165 | return false;
|
---|
166 | } // Conflict if rest-count mismatches
|
---|
167 |
|
---|
168 |
|
---|
169 | if (a.rest.length !== b.rest.length) {
|
---|
170 | return true;
|
---|
171 | } // Conflict if rest parameters are equal (same but unprefixed)
|
---|
172 |
|
---|
173 |
|
---|
174 | return a.rest.every((s, index) => b.rest[index] === s);
|
---|
175 | }
|
---|
176 | /**
|
---|
177 | * @param {postcss.Rule} first
|
---|
178 | * @param {postcss.Rule} second
|
---|
179 | * @return {boolean} merged
|
---|
180 | */
|
---|
181 |
|
---|
182 |
|
---|
183 | function mergeParents(first, second) {
|
---|
184 | // Null check for detached rules
|
---|
185 | if (!first.parent || !second.parent) {
|
---|
186 | return false;
|
---|
187 | } // Check if parents share node
|
---|
188 |
|
---|
189 |
|
---|
190 | if (first.parent === second.parent) {
|
---|
191 | return false;
|
---|
192 | } // sameParent() already called by canMerge()
|
---|
193 |
|
---|
194 |
|
---|
195 | second.remove();
|
---|
196 | first.parent.append(second);
|
---|
197 | return true;
|
---|
198 | }
|
---|
199 | /**
|
---|
200 | * @param {postcss.Rule} first
|
---|
201 | * @param {postcss.Rule} second
|
---|
202 | * @return {postcss.Rule} mergedRule
|
---|
203 | */
|
---|
204 |
|
---|
205 |
|
---|
206 | function partialMerge(first, second) {
|
---|
207 | let intersection = intersect(getDecls(first), getDecls(second));
|
---|
208 |
|
---|
209 | if (!intersection.length) {
|
---|
210 | return second;
|
---|
211 | }
|
---|
212 |
|
---|
213 | let nextRule = second.next();
|
---|
214 |
|
---|
215 | if (!nextRule) {
|
---|
216 | // Grab next cousin
|
---|
217 | const parentSibling = second.parent.next();
|
---|
218 | nextRule = parentSibling && parentSibling.nodes && parentSibling.nodes[0];
|
---|
219 | }
|
---|
220 |
|
---|
221 | if (nextRule && nextRule.type === 'rule' && canMerge(second, nextRule)) {
|
---|
222 | let nextIntersection = intersect(getDecls(second), getDecls(nextRule));
|
---|
223 |
|
---|
224 | if (nextIntersection.length > intersection.length) {
|
---|
225 | mergeParents(second, nextRule);
|
---|
226 | first = second;
|
---|
227 | second = nextRule;
|
---|
228 | intersection = nextIntersection;
|
---|
229 | }
|
---|
230 | }
|
---|
231 |
|
---|
232 | const firstDecls = getDecls(first); // Filter out intersections with later conflicts in First
|
---|
233 |
|
---|
234 | intersection = intersection.filter((decl, intersectIndex) => {
|
---|
235 | const indexOfDecl = indexOfDeclaration(firstDecls, decl);
|
---|
236 | const nextConflictInFirst = firstDecls.slice(indexOfDecl + 1).filter(d => isConflictingProp(d.prop, decl.prop));
|
---|
237 |
|
---|
238 | if (!nextConflictInFirst.length) {
|
---|
239 | return true;
|
---|
240 | }
|
---|
241 |
|
---|
242 | const nextConflictInIntersection = intersection.slice(intersectIndex + 1).filter(d => isConflictingProp(d.prop, decl.prop));
|
---|
243 |
|
---|
244 | if (!nextConflictInIntersection.length) {
|
---|
245 | return false;
|
---|
246 | }
|
---|
247 |
|
---|
248 | if (nextConflictInFirst.length !== nextConflictInIntersection.length) {
|
---|
249 | return false;
|
---|
250 | }
|
---|
251 |
|
---|
252 | return nextConflictInFirst.every((d, index) => declarationIsEqual(d, nextConflictInIntersection[index]));
|
---|
253 | }); // Filter out intersections with previous conflicts in Second
|
---|
254 |
|
---|
255 | const secondDecls = getDecls(second);
|
---|
256 | intersection = intersection.filter(decl => {
|
---|
257 | const nextConflictIndex = secondDecls.findIndex(d => isConflictingProp(d.prop, decl.prop));
|
---|
258 |
|
---|
259 | if (nextConflictIndex === -1) {
|
---|
260 | return false;
|
---|
261 | }
|
---|
262 |
|
---|
263 | if (!declarationIsEqual(secondDecls[nextConflictIndex], decl)) {
|
---|
264 | return false;
|
---|
265 | }
|
---|
266 |
|
---|
267 | if (decl.prop.toLowerCase() !== 'direction' && decl.prop.toLowerCase() !== 'unicode-bidi' && secondDecls.some(declaration => declaration.prop.toLowerCase() === 'all')) {
|
---|
268 | return false;
|
---|
269 | }
|
---|
270 |
|
---|
271 | secondDecls.splice(nextConflictIndex, 1);
|
---|
272 | return true;
|
---|
273 | });
|
---|
274 |
|
---|
275 | if (!intersection.length) {
|
---|
276 | // Nothing to merge
|
---|
277 | return second;
|
---|
278 | }
|
---|
279 |
|
---|
280 | const receivingBlock = second.clone();
|
---|
281 | receivingBlock.selector = joinSelectors(first, second);
|
---|
282 | receivingBlock.nodes = [];
|
---|
283 | second.parent.insertBefore(second, receivingBlock);
|
---|
284 | const firstClone = first.clone();
|
---|
285 | const secondClone = second.clone();
|
---|
286 | /**
|
---|
287 | * @param {function(postcss.Declaration):void} callback
|
---|
288 | * @return {function(postcss.Declaration)}
|
---|
289 | */
|
---|
290 |
|
---|
291 | function moveDecl(callback) {
|
---|
292 | return decl => {
|
---|
293 | if (~indexOfDeclaration(intersection, decl)) {
|
---|
294 | callback.call(this, decl);
|
---|
295 | }
|
---|
296 | };
|
---|
297 | }
|
---|
298 |
|
---|
299 | firstClone.walkDecls(moveDecl(decl => {
|
---|
300 | decl.remove();
|
---|
301 | receivingBlock.append(decl);
|
---|
302 | }));
|
---|
303 | secondClone.walkDecls(moveDecl(decl => decl.remove()));
|
---|
304 | const merged = ruleLength(firstClone, receivingBlock, secondClone);
|
---|
305 | const original = ruleLength(first, second);
|
---|
306 |
|
---|
307 | if (merged < original) {
|
---|
308 | first.replaceWith(firstClone);
|
---|
309 | second.replaceWith(secondClone);
|
---|
310 | [firstClone, receivingBlock, secondClone].forEach(r => {
|
---|
311 | if (!r.nodes.length) {
|
---|
312 | r.remove();
|
---|
313 | }
|
---|
314 | });
|
---|
315 |
|
---|
316 | if (!secondClone.parent) {
|
---|
317 | return receivingBlock;
|
---|
318 | }
|
---|
319 |
|
---|
320 | return secondClone;
|
---|
321 | } else {
|
---|
322 | receivingBlock.remove();
|
---|
323 | return second;
|
---|
324 | }
|
---|
325 | }
|
---|
326 | /**
|
---|
327 | * @param {string[]} browsers
|
---|
328 | * @param {Object.<string, boolean>} compatibilityCache
|
---|
329 | * @return {function(postcss.Rule)}
|
---|
330 | */
|
---|
331 |
|
---|
332 |
|
---|
333 | function selectorMerger(browsers, compatibilityCache) {
|
---|
334 | /** @type {postcss.Rule} */
|
---|
335 | let cache = null;
|
---|
336 | return function (rule) {
|
---|
337 | // Prime the cache with the first rule, or alternately ensure that it is
|
---|
338 | // safe to merge both declarations before continuing
|
---|
339 | if (!cache || !canMerge(rule, cache, browsers, compatibilityCache)) {
|
---|
340 | cache = rule;
|
---|
341 | return;
|
---|
342 | } // Ensure that we don't deduplicate the same rule; this is sometimes
|
---|
343 | // caused by a partial merge
|
---|
344 |
|
---|
345 |
|
---|
346 | if (cache === rule) {
|
---|
347 | cache = rule;
|
---|
348 | return;
|
---|
349 | } // Parents merge: check if the rules have same parents, but not same parent nodes
|
---|
350 |
|
---|
351 |
|
---|
352 | mergeParents(cache, rule); // Merge when declarations are exactly equal
|
---|
353 | // e.g. h1 { color: red } h2 { color: red }
|
---|
354 |
|
---|
355 | if (sameDeclarationsAndOrder(getDecls(rule), getDecls(cache))) {
|
---|
356 | rule.selector = joinSelectors(cache, rule);
|
---|
357 | cache.remove();
|
---|
358 | cache = rule;
|
---|
359 | return;
|
---|
360 | } // Merge when both selectors are exactly equal
|
---|
361 | // e.g. a { color: blue } a { font-weight: bold }
|
---|
362 |
|
---|
363 |
|
---|
364 | if (cache.selector === rule.selector) {
|
---|
365 | const cached = getDecls(cache);
|
---|
366 | rule.walk(decl => {
|
---|
367 | if (~indexOfDeclaration(cached, decl)) {
|
---|
368 | return decl.remove();
|
---|
369 | }
|
---|
370 |
|
---|
371 | cache.append(decl);
|
---|
372 | });
|
---|
373 | rule.remove();
|
---|
374 | return;
|
---|
375 | } // Partial merge: check if the rule contains a subset of the last; if
|
---|
376 | // so create a joined selector with the subset, if smaller.
|
---|
377 |
|
---|
378 |
|
---|
379 | cache = partialMerge(cache, rule);
|
---|
380 | };
|
---|
381 | }
|
---|
382 |
|
---|
383 | function pluginCreator() {
|
---|
384 | return {
|
---|
385 | postcssPlugin: 'postcss-merge-rules',
|
---|
386 |
|
---|
387 | prepare(result) {
|
---|
388 | const resultOpts = result.opts || {};
|
---|
389 | const browsers = (0, _browserslist.default)(null, {
|
---|
390 | stats: resultOpts.stats,
|
---|
391 | path: __dirname,
|
---|
392 | env: resultOpts.env
|
---|
393 | });
|
---|
394 | const compatibilityCache = {};
|
---|
395 | return {
|
---|
396 | OnceExit(css) {
|
---|
397 | css.walkRules(selectorMerger(browsers, compatibilityCache));
|
---|
398 | }
|
---|
399 |
|
---|
400 | };
|
---|
401 | }
|
---|
402 |
|
---|
403 | };
|
---|
404 | }
|
---|
405 |
|
---|
406 | pluginCreator.postcss = true;
|
---|
407 | var _default = pluginCreator;
|
---|
408 | exports.default = _default;
|
---|
409 | module.exports = exports.default; |
---|