import { List, walk } from 'css-tree'; import { unsafeToSkipNode, isEqualSelectors, compareDeclarations, addSelectors } from './utils.js'; function calcSelectorLength(list) { return list.reduce((res, data) => res + data.id.length + 1, 0) - 1; } function calcDeclarationsLength(tokens) { let length = 0; for (const token of tokens) { length += token.length; } return ( length + // declarations tokens.length - 1 // delimeters ); } function processRule(node, item, list) { const avoidRulesMerge = this.block !== null ? this.block.avoidRulesMerge : false; const selectors = node.prelude.children; const block = node.block; const disallowDownMarkers = Object.create(null); let allowMergeUp = true; let allowMergeDown = true; list.prevUntil(item.prev, function(prev, prevItem) { const prevBlock = prev.block; const prevType = prev.type; if (prevType !== 'Rule') { const unsafe = unsafeToSkipNode.call(selectors, prev); if (!unsafe && prevType === 'Atrule' && prevBlock) { walk(prevBlock, { visit: 'Rule', enter(node) { node.prelude.children.forEach((data) => { disallowDownMarkers[data.compareMarker] = true; }); } }); } return unsafe; } if (node.pseudoSignature !== prev.pseudoSignature) { return true; } const prevSelectors = prev.prelude.children; allowMergeDown = !prevSelectors.some((selector) => selector.compareMarker in disallowDownMarkers ); // try prev ruleset if simpleselectors has no equal specifity and element selector if (!allowMergeDown && !allowMergeUp) { return true; } // try to join by selectors if (allowMergeUp && isEqualSelectors(prevSelectors, selectors)) { prevBlock.children.appendList(block.children); list.remove(item); return true; } // try to join by properties const diff = compareDeclarations(block.children, prevBlock.children); // console.log(diff.eq, diff.ne1, diff.ne2); if (diff.eq.length) { if (!diff.ne1.length && !diff.ne2.length) { // equal blocks if (allowMergeDown) { addSelectors(selectors, prevSelectors); list.remove(prevItem); } return true; } else if (!avoidRulesMerge) { /* probably we don't need to prevent those merges for @keyframes TODO: need to be checked */ if (diff.ne1.length && !diff.ne2.length) { // prevBlock is subset block const selectorLength = calcSelectorLength(selectors); const blockLength = calcDeclarationsLength(diff.eq); // declarations length if (allowMergeUp && selectorLength < blockLength) { addSelectors(prevSelectors, selectors); block.children.fromArray(diff.ne1); } } else if (!diff.ne1.length && diff.ne2.length) { // node is subset of prevBlock const selectorLength = calcSelectorLength(prevSelectors); const blockLength = calcDeclarationsLength(diff.eq); // declarations length if (allowMergeDown && selectorLength < blockLength) { addSelectors(selectors, prevSelectors); prevBlock.children.fromArray(diff.ne2); } } else { // diff.ne1.length && diff.ne2.length // extract equal block const newSelector = { type: 'SelectorList', loc: null, children: addSelectors(prevSelectors.copy(), selectors) }; const newBlockLength = calcSelectorLength(newSelector.children) + 2; // selectors length + curly braces length const blockLength = calcDeclarationsLength(diff.eq); // declarations length // create new ruleset if declarations length greater than // ruleset description overhead if (blockLength >= newBlockLength) { const newItem = list.createItem({ type: 'Rule', loc: null, prelude: newSelector, block: { type: 'Block', loc: null, children: new List().fromArray(diff.eq) }, pseudoSignature: node.pseudoSignature }); block.children.fromArray(diff.ne1); prevBlock.children.fromArray(diff.ne2overrided); if (allowMergeUp) { list.insert(newItem, prevItem); } else { list.insert(newItem, item); } return true; } } } } if (allowMergeUp) { // TODO: disallow up merge only if any property interception only (i.e. diff.ne2overrided.length > 0); // await property families to find property interception correctly allowMergeUp = !prevSelectors.some((prevSelector) => selectors.some((selector) => selector.compareMarker === prevSelector.compareMarker ) ); } prevSelectors.forEach((data) => { disallowDownMarkers[data.compareMarker] = true; }); }); } export default function restructRule(ast) { walk(ast, { visit: 'Rule', reverse: true, enter: processRule }); };