source: frontend/node_modules/postcss-merge-rules/src/index.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

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