[6a3a178] | 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; |
---|