export default function collapseDuplicateDeclarations() { return (root) => { root.walkRules((node) => { let seen = new Map() let droppable = new Set([]) let byProperty = new Map() node.walkDecls((decl) => { // This could happen if we have nested selectors. In that case the // parent will loop over all its declarations but also the declarations // of nested rules. With this we ensure that we are shallowly checking // declarations. if (decl.parent !== node) { return } if (seen.has(decl.prop)) { // Exact same value as what we have seen so far if (seen.get(decl.prop).value === decl.value) { // Keep the last one, drop the one we've seen so far droppable.add(seen.get(decl.prop)) // Override the existing one with the new value. This is necessary // so that if we happen to have more than one declaration with the // same value, that we keep removing the previous one. Otherwise we // will only remove the *first* one. seen.set(decl.prop, decl) return } // Not the same value, so we need to check if we can merge it so // let's collect it first. if (!byProperty.has(decl.prop)) { byProperty.set(decl.prop, new Set()) } byProperty.get(decl.prop).add(seen.get(decl.prop)) byProperty.get(decl.prop).add(decl) } seen.set(decl.prop, decl) }) // Drop all the duplicate declarations with the exact same value we've // already seen so far. for (let decl of droppable) { decl.remove() } // Analyze the declarations based on its unit, drop all the declarations // with the same unit but the last one in the list. for (let declarations of byProperty.values()) { let byUnit = new Map() for (let decl of declarations) { let unit = resolveUnit(decl.value) if (unit === null) { // We don't have a unit, so should never try and collapse this // value. This is because we can't know how to do it in a correct // way (e.g.: overrides for older browsers) continue } if (!byUnit.has(unit)) { byUnit.set(unit, new Set()) } byUnit.get(unit).add(decl) } for (let declarations of byUnit.values()) { // Get all but the last one let removableDeclarations = Array.from(declarations).slice(0, -1) for (let decl of removableDeclarations) { decl.remove() } } } }) } } let UNITLESS_NUMBER = Symbol('unitless-number') function resolveUnit(input) { let result = /^-?\d*.?\d+([\w%]+)?$/g.exec(input) if (result) { return result[1] ?? UNITLESS_NUMBER } return null }