8889841cPK=[1K  README.mdnuW+A# [postcss][postcss]-merge-rules > Merge CSS rules with PostCSS. ## Install With [npm](https://npmjs.org/package/postcss-merge-rules) do: ``` npm install postcss-merge-rules --save ``` ## Examples This module will attempt to merge *adjacent* CSS rules: ### By declarations #### Input ```css a { color: blue; font-weight: bold } p { color: blue; font-weight: bold } ``` #### Output ```css a,p { color: blue; font-weight: bold } ``` ### By selectors #### Input ```css a { color: blue } a { font-weight: bold } ``` #### Output ```css a { color: blue; font-weight: bold } ``` ### By partial declarations #### Input ```css a { font-weight: bold } p { color: blue; font-weight: bold } ``` #### Output ```css a,p { font-weight: bold } p { color: blue } ``` ## Usage See the [PostCSS documentation](https://github.com/postcss/postcss#usage) for examples for your environment. ## Contributors See [CONTRIBUTORS.md](https://github.com/cssnano/cssnano/blob/master/CONTRIBUTORS.md). ## License MIT © [Ben Briggs](http://beneb.info) [postcss]: https://github.com/postcss/postcss PK=[T7`,, src/index.jsnuW+A'use strict'; const browserslist = require('browserslist'); const { sameParent } = require('cssnano-utils'); const { ensureCompatibility, sameVendor, noVendor, } = require('./lib/ensureCompatibility'); /** * @param {import('postcss').Declaration} a * @param {import('postcss').Declaration} b * @return {boolean} */ function declarationIsEqual(a, b) { return ( a.important === b.important && a.prop === b.prop && a.value === b.value ); } /** * @param {import('postcss').Declaration[]} array * @param {import('postcss').Declaration} decl * @return {number} */ function indexOfDeclaration(array, decl) { return array.findIndex((d) => declarationIsEqual(d, decl)); } /** * Returns filtered array of matched or unmatched declarations * @param {import('postcss').Declaration[]} a * @param {import('postcss').Declaration[]} b * @param {boolean} [not=false] * @return {import('postcss').Declaration[]} */ function intersect(a, b, not) { return a.filter((c) => { const index = indexOfDeclaration(b, c) !== -1; return not ? !index : index; }); } /** * @param {import('postcss').Declaration[]} a * @param {import('postcss').Declaration[]} b * @return {boolean} */ function sameDeclarationsAndOrder(a, b) { if (a.length !== b.length) { return false; } return a.every((d, index) => declarationIsEqual(d, b[index])); } /** * @param {import('postcss').Rule} ruleA * @param {import('postcss').Rule} ruleB * @param {string[]=} browsers * @param {Map=} compatibilityCache * @return {boolean} */ function canMerge(ruleA, ruleB, browsers, compatibilityCache) { const a = ruleA.selectors; const b = ruleB.selectors; const selectors = a.concat(b); if (!ensureCompatibility(selectors, browsers, compatibilityCache)) { return false; } const parent = sameParent( /** @type {any} */ (ruleA), /** @type {any} */ (ruleB) ); if ( parent && ruleA.parent && ruleA.parent.type === 'atrule' && /** @type {import('postcss').AtRule} */ (ruleA.parent).name.includes( 'keyframes' ) ) { return false; } return parent && (selectors.every(noVendor) || sameVendor(a, b)); } /** * @param {import('postcss').ChildNode} node * @return {node is import('postcss').Declaration} */ function isDeclaration(node) { return node.type === 'decl'; } /** * @param {import('postcss').Rule} rule * @return {import('postcss').Declaration[]} */ function getDecls(rule) { return rule.nodes.filter(isDeclaration); } /** @type {(...rules: import('postcss').Rule[]) => string} */ const joinSelectors = (...rules) => rules.map((s) => s.selector).join(); /** * @param {...import('postcss').Rule} rules * @return {number} */ function ruleLength(...rules) { return rules.map((r) => (r.nodes.length ? String(r) : '')).join('').length; } /** * @param {string} prop * @return {{prefix: string?, base:string?, rest:string[]}} */ function splitProp(prop) { // Treat vendor prefixed properties as if they were unprefixed; // moving them when combined with non-prefixed properties can // cause issues. e.g. moving -webkit-background-clip when there // is a background shorthand definition. const parts = prop.split('-'); if (prop[0] !== '-') { return { prefix: '', base: parts[0], rest: parts.slice(1), }; } // Don't split css variables if (prop[1] === '-') { return { prefix: null, base: null, rest: [prop], }; } // Found prefix return { prefix: parts[1], base: parts[2], rest: parts.slice(3), }; } /** * @param {string} propA * @param {string} propB * @return {boolean} */ function isConflictingProp(propA, propB) { if (propA === propB) { // Same specificity return true; } const a = splitProp(propA); const b = splitProp(propB); // Don't resort css variables if (!a.base && !b.base) { return true; } // Different base and none is `place`; if (a.base !== b.base && a.base !== 'place' && b.base !== 'place') { return false; } // Conflict if rest-count mismatches if (a.rest.length !== b.rest.length) { return true; } /* Do not merge conflicting border properties */ if (a.base === 'border') { const allRestProps = new Set([...a.rest, ...b.rest]); if ( allRestProps.has('image') || allRestProps.has('width') || allRestProps.has('color') || allRestProps.has('style') ) { return true; } } // Conflict if rest parameters are equal (same but unprefixed) return a.rest.every((s, index) => b.rest[index] === s); } /** * @param {import('postcss').Rule} first * @param {import('postcss').Rule} second * @return {boolean} merged */ function mergeParents(first, second) { // Null check for detached rules if (!first.parent || !second.parent) { return false; } // Check if parents share node if (first.parent === second.parent) { return false; } // sameParent() already called by canMerge() second.remove(); first.parent.append(second); return true; } /** * @param {import('postcss').Rule} first * @param {import('postcss').Rule} second * @return {import('postcss').Rule} mergedRule */ function partialMerge(first, second) { let intersection = intersect(getDecls(first), getDecls(second)); if (intersection.length === 0) { return second; } let nextRule = second.next(); if (!nextRule) { // Grab next cousin /** @type {any} */ const parentSibling = /** @type {import('postcss').Container} */ ( second.parent ).next(); nextRule = parentSibling && parentSibling.nodes && parentSibling.nodes[0]; } if (nextRule && nextRule.type === 'rule' && canMerge(second, nextRule)) { let nextIntersection = intersect(getDecls(second), getDecls(nextRule)); if (nextIntersection.length > intersection.length) { mergeParents(second, nextRule); first = second; second = nextRule; intersection = nextIntersection; } } const firstDecls = getDecls(first); // Filter out intersections with later conflicts in First intersection = intersection.filter((decl, intersectIndex) => { const indexOfDecl = indexOfDeclaration(firstDecls, decl); const nextConflictInFirst = firstDecls .slice(indexOfDecl + 1) .filter((d) => isConflictingProp(d.prop, decl.prop)); if (nextConflictInFirst.length === 0) { return true; } const nextConflictInIntersection = intersection .slice(intersectIndex + 1) .filter((d) => isConflictingProp(d.prop, decl.prop)); if (nextConflictInFirst.length !== nextConflictInIntersection.length) { return false; } return nextConflictInFirst.every((d, index) => declarationIsEqual(d, nextConflictInIntersection[index]) ); }); // Filter out intersections with previous conflicts in Second const secondDecls = getDecls(second); intersection = intersection.filter((decl) => { const nextConflictIndex = secondDecls.findIndex((d) => isConflictingProp(d.prop, decl.prop) ); if (nextConflictIndex === -1) { return false; } if (!declarationIsEqual(secondDecls[nextConflictIndex], decl)) { return false; } if ( decl.prop.toLowerCase() !== 'direction' && decl.prop.toLowerCase() !== 'unicode-bidi' && secondDecls.some( (declaration) => declaration.prop.toLowerCase() === 'all' ) ) { return false; } secondDecls.splice(nextConflictIndex, 1); return true; }); if (intersection.length === 0) { // Nothing to merge return second; } const receivingBlock = second.clone(); receivingBlock.selector = joinSelectors(first, second); receivingBlock.nodes = []; /** @type {import('postcss').Container} */ ( second.parent ).insertBefore(second, receivingBlock); const firstClone = first.clone(); const secondClone = second.clone(); /** * @param {function(import('postcss').Declaration):void} callback * @this {import('postcss').Rule} * @return {function(import('postcss').Declaration)} */ function moveDecl(callback) { return (decl) => { if (indexOfDeclaration(intersection, decl) !== -1) { callback.call(this, decl); } }; } firstClone.walkDecls( moveDecl((decl) => { decl.remove(); receivingBlock.append(decl); }) ); secondClone.walkDecls(moveDecl((decl) => decl.remove())); const merged = ruleLength(firstClone, receivingBlock, secondClone); const original = ruleLength(first, second); if (merged < original) { first.replaceWith(firstClone); second.replaceWith(secondClone); [firstClone, receivingBlock, secondClone].forEach((r) => { if (r.nodes.length === 0) { r.remove(); } }); if (!secondClone.parent) { return receivingBlock; } return secondClone; } else { receivingBlock.remove(); return second; } } /** * @param {string[]} browsers * @param {Map} compatibilityCache * @return {function(import('postcss').Rule)} */ function selectorMerger(browsers, compatibilityCache) { /** @type {import('postcss').Rule | null} */ let cache = null; return function (rule) { // Prime the cache with the first rule, or alternately ensure that it is // safe to merge both declarations before continuing if (!cache || !canMerge(rule, cache, browsers, compatibilityCache)) { cache = rule; return; } // Ensure that we don't deduplicate the same rule; this is sometimes // caused by a partial merge if (cache === rule) { cache = rule; return; } // Parents merge: check if the rules have same parents, but not same parent nodes mergeParents(cache, rule); // Merge when declarations are exactly equal // e.g. h1 { color: red } h2 { color: red } if (sameDeclarationsAndOrder(getDecls(rule), getDecls(cache))) { rule.selector = joinSelectors(cache, rule); cache.remove(); cache = rule; return; } // Merge when both selectors are exactly equal // e.g. a { color: blue } a { font-weight: bold } if (cache.selector === rule.selector) { const cached = getDecls(cache); rule.walk((node) => { if (node.type === 'decl' && indexOfDeclaration(cached, node) !== -1) { node.remove(); return; } /** @type {import('postcss').Rule} */ (cache).append(node); }); rule.remove(); return; } // Partial merge: check if the rule contains a subset of the last; if // so create a joined selector with the subset, if smaller. cache = partialMerge(cache, rule); }; } /** * @type {import('postcss').PluginCreator} * @return {import('postcss').Plugin} */ function pluginCreator() { return { postcssPlugin: 'postcss-merge-rules', prepare(result) { /** @type {typeof result.opts & browserslist.Options} */ const resultOpts = result.opts || {}; const browsers = browserslist(null, { stats: resultOpts.stats, path: __dirname, env: resultOpts.env, }); const compatibilityCache = new Map(); return { OnceExit(css) { css.walkRules(selectorMerger(browsers, compatibilityCache)); }, }; }, }; } pluginCreator.postcss = true; module.exports = pluginCreator; PK=[crrsrc/lib/ensureCompatibility.jsnuW+A'use strict'; const { isSupported } = require('caniuse-api'); const selectorParser = require('postcss-selector-parser'); const simpleSelectorRe = /^#?[-._a-z0-9 ]+$/i; const cssSel2 = 'css-sel2'; const cssSel3 = 'css-sel3'; const cssGencontent = 'css-gencontent'; const cssFirstLetter = 'css-first-letter'; const cssFirstLine = 'css-first-line'; const cssInOutOfRange = 'css-in-out-of-range'; const formValidation = 'form-validation'; const vendorPrefix = /-(ah|apple|atsc|epub|hp|khtml|moz|ms|o|rim|ro|tc|wap|webkit|xv)-/; const level2Sel = new Set(['=', '~=', '|=']); const level3Sel = new Set(['^=', '$=', '*=']); /** * @param {string} selector * @return {RegExpMatchArray | null} */ function filterPrefixes(selector) { return selector.match(vendorPrefix); } /** * Internet Explorer use :-ms-input-placeholder. * Microsoft Edge use ::-ms-input-placeholder. * * @type {(selector: string) => number} */ const findMsInputPlaceholder = (selector) => ~selector.search(/-ms-input-placeholder/i); /** * @param {string[]} selectorsA * @param {string[]} selectorsB * @return {boolean} */ function sameVendor(selectorsA, selectorsB) { /** @type {(selectors: string[]) => string} */ let same = (selectors) => selectors.map(filterPrefixes).join(); /** @type {(selectors: string[]) => string | undefined} */ let findMsVendor = (selectors) => selectors.find(findMsInputPlaceholder); return ( same(selectorsA) === same(selectorsB) && !(findMsVendor(selectorsA) && findMsVendor(selectorsB)) ); } /** * @param {string} selector * @return {boolean} */ function noVendor(selector) { return !vendorPrefix.test(selector); } const pseudoElements = { ':active': cssSel2, ':after': cssGencontent, ':any-link': 'css-any-link', ':before': cssGencontent, ':checked': cssSel3, ':default': 'css-default-pseudo', ':dir': 'css-dir-pseudo', ':disabled': cssSel3, ':empty': cssSel3, ':enabled': cssSel3, ':first-child': cssSel2, ':first-letter': cssFirstLetter, ':first-line': cssFirstLine, ':first-of-type': cssSel3, ':focus': cssSel2, ':focus-within': 'css-focus-within', ':focus-visible': 'css-focus-visible', ':has': 'css-has', ':hover': cssSel2, ':in-range': cssInOutOfRange, ':indeterminate': 'css-indeterminate-pseudo', ':invalid': formValidation, ':is': 'css-matches-pseudo', ':lang': cssSel2, ':last-child': cssSel3, ':last-of-type': cssSel3, ':link': cssSel2, ':matches': 'css-matches-pseudo', ':not': cssSel3, ':nth-child': cssSel3, ':nth-last-child': cssSel3, ':nth-last-of-type': cssSel3, ':nth-of-type': cssSel3, ':only-child': cssSel3, ':only-of-type': cssSel3, ':optional': 'css-optional-pseudo', ':out-of-range': cssInOutOfRange, ':placeholder-shown': 'css-placeholder-shown', ':required': formValidation, ':root': cssSel3, ':target': cssSel3, '::after': cssGencontent, '::backdrop': 'dialog', '::before': cssGencontent, '::first-letter': cssFirstLetter, '::first-line': cssFirstLine, '::marker': 'css-marker-pseudo', '::placeholder': 'css-placeholder', '::selection': 'css-selection', ':valid': formValidation, ':visited': cssSel2, }; /** * @param {string} selector * @return {boolean} */ function isCssMixin(selector) { return selector[selector.length - 1] === ':'; } /** * @param {string} selector * @return {boolean} */ function isHostPseudoClass(selector) { return selector.includes(':host'); } const isSupportedCache = new Map(); // Move to util in future /** * @param {string} feature * @param {string[] | undefined} browsers */ function isSupportedCached(feature, browsers) { const key = JSON.stringify({ feature, browsers }); let result = isSupportedCache.get(key); if (!result) { result = isSupported(feature, /** @type {string[]} */ (browsers)); isSupportedCache.set(key, result); } return result; } /** * @param {string[]} selectors * @param{string[]=} browsers * @param{Map=} compatibilityCache * @return {boolean} */ function ensureCompatibility(selectors, browsers, compatibilityCache) { // Should not merge mixins if (selectors.some(isCssMixin)) { return false; } // Should not merge :host selector https://github.com/angular/angular-cli/issues/18672 if (selectors.some(isHostPseudoClass)) { return false; } return selectors.every((selector) => { if (simpleSelectorRe.test(selector)) { return true; } if (compatibilityCache && compatibilityCache.has(selector)) { return compatibilityCache.get(selector); } let compatible = true; selectorParser((ast) => { ast.walk((node) => { const { type, value } = node; if (type === 'pseudo') { const entry = pseudoElements[/** @type {keyof pseudoElements} */ (value)]; if (!entry && noVendor(value)) { compatible = false; } if (entry && compatible) { compatible = isSupportedCached(entry, browsers); } } if (type === 'combinator') { if (value.includes('~')) { compatible = isSupportedCached(cssSel3, browsers); } if (value.includes('>') || value.includes('+')) { compatible = isSupportedCached(cssSel2, browsers); } } if (type === 'attribute' && node.attribute) { // [foo] if (!node.operator) { compatible = isSupportedCached(cssSel2, browsers); } if (value) { // [foo="bar"], [foo~="bar"], [foo|="bar"] if (level2Sel.has(/** @type {string} */ (node.operator))) { compatible = isSupportedCached(cssSel2, browsers); } // [foo^="bar"], [foo$="bar"], [foo*="bar"] if (level3Sel.has(/** @type {string} */ (node.operator))) { compatible = isSupportedCached(cssSel3, browsers); } } // [foo="bar" i] if (node.insensitive) { compatible = isSupportedCached('css-case-insensitive', browsers); } } if (!compatible) { // If this node was not compatible, // break out early from walking the rest return false; } }); }).processSync(selector); if (compatibilityCache) { compatibilityCache.set(selector, compatible); } return compatible; }); } module.exports = { sameVendor, noVendor, pseudoElements, ensureCompatibility }; PK=[N@ww"types/lib/ensureCompatibility.d.tsnuW+A/** * @param {string[]} selectorsA * @param {string[]} selectorsB * @return {boolean} */ export function sameVendor(selectorsA: string[], selectorsB: string[]): boolean; /** * @param {string} selector * @return {boolean} */ export function noVendor(selector: string): boolean; export const pseudoElements: { ':active': string; ':after': string; ':any-link': string; ':before': string; ':checked': string; ':default': string; ':dir': string; ':disabled': string; ':empty': string; ':enabled': string; ':first-child': string; ':first-letter': string; ':first-line': string; ':first-of-type': string; ':focus': string; ':focus-within': string; ':focus-visible': string; ':has': string; ':hover': string; ':in-range': string; ':indeterminate': string; ':invalid': string; ':is': string; ':lang': string; ':last-child': string; ':last-of-type': string; ':link': string; ':matches': string; ':not': string; ':nth-child': string; ':nth-last-child': string; ':nth-last-of-type': string; ':nth-of-type': string; ':only-child': string; ':only-of-type': string; ':optional': string; ':out-of-range': string; ':placeholder-shown': string; ':required': string; ':root': string; ':target': string; '::after': string; '::backdrop': string; '::before': string; '::first-letter': string; '::first-line': string; '::marker': string; '::placeholder': string; '::selection': string; ':valid': string; ':visited': string; }; /** * @param {string[]} selectors * @param{string[]=} browsers * @param{Map=} compatibilityCache * @return {boolean} */ export function ensureCompatibility(selectors: string[], browsers?: string[] | undefined, compatibilityCache?: Map | undefined): boolean; PK=[m;Ӭtypes/index.d.tsnuW+Aexport = pluginCreator; /** * @type {import('postcss').PluginCreator} * @return {import('postcss').Plugin} */ declare function pluginCreator(): import('postcss').Plugin; declare namespace pluginCreator { const postcss: true; } PK=[͍DD LICENSE-MITnuW+ACopyright (c) Ben Briggs (http://beneb.info) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK=[C$ٲ package.jsonnuW+A{ "name": "postcss-merge-rules", "version": "5.1.4", "description": "Merge CSS rules with PostCSS.", "types": "types/index.d.ts", "main": "src/index.js", "files": [ "LICENSE-MIT", "src", "types" ], "keywords": [ "css", "optimise", "postcss", "postcss-plugin" ], "license": "MIT", "homepage": "https://github.com/cssnano/cssnano", "author": { "name": "Ben Briggs", "email": "beneb.info@gmail.com", "url": "http://beneb.info" }, "repository": "cssnano/cssnano", "dependencies": { "browserslist": "^4.21.4", "caniuse-api": "^3.0.0", "postcss-selector-parser": "^6.0.5", "cssnano-utils": "^3.1.0" }, "bugs": { "url": "https://github.com/cssnano/cssnano/issues" }, "engines": { "node": "^10 || ^12 || >=14.0" }, "devDependencies": { "@types/caniuse-api": "^3.0.2", "postcss": "^8.2.15", "postcss-discard-comments": "^5.1.2" }, "peerDependencies": { "postcss": "^8.2.15" } }PK=[1K  README.mdnuW+APK=[T7`,, src/index.jsnuW+APK=[crr1src/lib/ensureCompatibility.jsnuW+APK=[N@ww"Ktypes/lib/ensureCompatibility.d.tsnuW+APK=[m;ӬZStypes/index.d.tsnuW+APK=[͍DD TLICENSE-MITnuW+APK=[C$ٲ  Ypackage.jsonnuW+APK<)]