All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.common.css.compiler.passes.UnsafeMergeRulesetNodes Maven / Gradle / Ivy

Go to download

Closure Stylesheets is an extension to CSS that adds variables, functions, conditionals, and mixins to standard CSS. The tool also supports minification, linting, RTL flipping, and CSS class renaming.

There is a newer version: 1.8.0
Show newest version
/*
 * Copyright 2009 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.css.compiler.passes;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.*;
import com.google.common.css.compiler.ast.*;

import java.util.*;

// TODO(user): Consider adding an annotation of which classes
//     co-occur (or more generally, of which selectors with the same
//     specificity co-match) to prevent some breaking re-ordering.
//     For example, something like @conflict .FOO, .BAR should prevent
//     re-ordering conflicting declarations of the specified
//     selectors.

/**
 * Compiler pass that merges rulesets aggressively. The ordering of
 * the rulesets ensures that shorthand properties appear before their
 * detailed properties, and that the ordering of conflicting shorthand
 * properties is preserved. It assumes all other ordering is
 * irrelevant. When the {@link MarkRemovableRulesetNodes} pass is run
 * before this pass, the assumptions of this pass will be valid if no
 * DOM element matches two or more selectors of the same specificity
 * with conflicting declarations. For example, selectors {@code .FOO}
 * and {@code .BAR} might conflict if these classes co-occur on a DOM
 * element. As another example, {@code .FOO span} and {@code .BAR span}
 * might conflict say if a {@code span} DOM element has a parent with
 * a {@code FOO} class and a grandparent with a {@code BAR} class.
 */
public class UnsafeMergeRulesetNodes implements CssCompilerPass {

    /**
     * Partitions for which a fixed pre-determined ordering of properties won't do.
     */
    private static final ImmutableSet ORDER_DEPENDENT_PARTITIONS = ImmutableSet.of("border");

    private final CssTree tree;
    private final boolean byPartition;
    private final boolean skipping;

    /**
     * Constructor.
     *
     * 

This pass runs in two modes, depending on the value of * {@code byPartition}. If {@code byPartition} is {@code true}, all * declarations on a selector are first grouped by their partition (as defined * by {@link CssPropertyNode#getPartition}). Then, selectors which have the * same declarations for a partition are grouped together into a ruleset. If * {@code byPartition} is {@code false} (recommended), partitioning is only * performed on order-dependent partitions (as defined by * {@link #ORDER_DEPENDENT_PARTITIONS}. * * @param tree the tree to run the pass on * @param byPartition whether to group all declarations by partition * (and not just the ones such as border where order matters) * @param skipping whether to skip certain properties as defined by * {@link SkippingTreeVisitor} */ public UnsafeMergeRulesetNodes( CssTree tree, boolean byPartition, boolean skipping) { this.tree = tree; this.byPartition = byPartition; this.skipping = skipping; } @Override public void runPass() { replace(collect()); } /** * Performs the collect phase by traversing the tree, collecting all the * rules and placing a placeholder ruleset. */ private CollectPhaseTreeVisitor collect() { CollectPhaseTreeVisitor collectPhaseVisitor = new CollectPhaseTreeVisitor(tree.getMutatingVisitController(), byPartition, skipping); collectPhaseVisitor.runPass(); return collectPhaseVisitor; } /** * Performs the replace phase, by using the structures from the collect phase * to compute a new list of rulesets and traversing the tree to replace the * placeholder with the new list. */ private void replace(CollectPhaseTreeVisitor collectPhaseVisitor) { // If we don't have a placeholder, then nothing has been collected, and // nothing is to be done. if (collectPhaseVisitor.getPlaceholderRuleset() == null) { return; } List rulesets = Lists.newArrayList(); for (Map.Entry> entry : collectPhaseVisitor.getDecToSels()) { rulesets.add(makeOneDeclarationRuleset(entry.getKey(), entry.getValue())); } for (Map.Entry, Set> entry : collectPhaseVisitor.getPartitions()) { rulesets.add(makePartitionRuleset(entry.getKey(), entry.getValue())); } new ReplacePhaseTreeVisitor( tree.getMutatingVisitController(), rulesets, collectPhaseVisitor.getPlaceholderRuleset()) .runPass(); } /** * Makes a ruleset with many selectors and one declaration. */ private CssRulesetNode makeOneDeclarationRuleset( CssDeclarationNode dec, Iterable sels) { CssRulesetNode ruleset = new CssRulesetNode(new CssDeclarationBlockNode()); ruleset.addDeclaration(dec); for (CssSelectorNode sel : sels) { ruleset.addSelector(sel.deepCopy()); } return ruleset; } /** * Makes a ruleset with many selectors and many declarations, whose * properties are assumed to be from the same partition. */ private CssRulesetNode makePartitionRuleset( Iterable decs, Iterable sels) { CssRulesetNode ruleset = new CssRulesetNode(new CssDeclarationBlockNode()); for (CssDeclarationNode dec : decs) { ruleset.addDeclaration(dec.deepCopy()); } for (CssSelectorNode sel : sels) { ruleset.addSelector(sel.deepCopy()); } return ruleset; } /** * The tree visitor for the collect phase, which collects all the rules * replacing them with one placeholder rule. */ private static class CollectPhaseTreeVisitor extends SkippingTreeVisitor implements CssCompilerPass { private final MutatingVisitController visitController; private final boolean byPartition; private final Multimap decToSel = TreeMultimap.create(DECLARATION_COMPARATOR, TO_STRING_COMPARATOR); private final Partitioner partitioner = new Partitioner(); private CssRulesetNode placeholderRuleset = null; public CollectPhaseTreeVisitor( MutatingVisitController visitController, boolean byPartition, boolean skipping) { super(skipping); this.visitController = visitController; this.byPartition = byPartition; } /** * Processes a ruleset when allowed by collecting it and then deleting it * from the tree. If this is the first ruleset to be deleted, adds a * placeholder ruleset to be replaced in the {#replacePhase}. */ @Override public boolean enterRuleset(CssRulesetNode ruleset) { if (canModifyRuleset(ruleset)) { collectRuleset(ruleset); deleteRuleset(); } return true; } @Override public void runPass() { visitController.startVisit(this); } /** * Returns entries of declaration/selectors grouped by declarations. */ Iterable>> getDecToSels() { return decToSel.asMap().entrySet(); } /** * Returns entries of declarations/selectors grouped by partitions, then * selectors. */ Iterable, Set>> getPartitions() { return partitioner.getMap().entrySet(); } /** * Returns the placeholder ruleset, to be replaced. */ CssRulesetNode getPlaceholderRuleset() { return placeholderRuleset; } private void collectRuleset(CssRulesetNode ruleset) { for (CssNode abstractDecl : ruleset.getDeclarations().childIterable()) { Preconditions.checkState(abstractDecl instanceof CssDeclarationNode); CssDeclarationNode dec = (CssDeclarationNode) abstractDecl; boolean inPartition = byPartition || ORDER_DEPENDENT_PARTITIONS.contains(dec.getPropertyName().getPartition()); for (CssSelectorNode sel : ruleset.getSelectors().childIterable()) { if (inPartition) { partitioner.add(dec, sel); } else { decToSel.put(dec, sel); } } } } private void deleteRuleset() { if (placeholderRuleset == null) { // We don't have a place holder yet. Create one. placeholderRuleset = new CssRulesetNode(new CssDeclarationBlockNode()); visitController.replaceCurrentBlockChildWith( ImmutableList.of((CssNode) placeholderRuleset), false); } else { visitController.removeCurrentNode(); } } } /** * The tree visitor for the replace phase, which replaces the placeholder with * the new computed rulesets. */ private static class ReplacePhaseTreeVisitor extends DefaultTreeVisitor implements CssCompilerPass { private final MutatingVisitController visitController; private final List rulesets; private final CssRulesetNode placeholderRuleset; public ReplacePhaseTreeVisitor( MutatingVisitController visitController, List rulesets, CssRulesetNode placeholderRuleset) { this.visitController = visitController; this.rulesets = rulesets; this.placeholderRuleset = placeholderRuleset; } @Override public boolean enterRuleset(CssRulesetNode ruleset) { if (ruleset == placeholderRuleset) { visitController.replaceCurrentBlockChildWith(rulesets, false); } return true; } @Override public void runPass() { visitController.startVisit(this); } } /** * A partition organizes pairs of declaration/selector. */ private abstract static class Partition { @SuppressWarnings("unused") protected final String partition; protected Partition(String partition) { this.partition = partition; } /** * Creates a new partition. */ public static Partition newPartition(String partition) { return ORDER_DEPENDENT_PARTITIONS.contains(partition) ? new OrderDependentPartition(partition) : new OrderIndependentPartition(partition); } /** * Adds the given pair of declaration/selector to the partition. Assumes * that the selector belongs to this partition. */ public abstract void add(CssDeclarationNode declaration, CssSelectorNode selector); /** * Adds the pairs of this partition to the given map. */ public abstract void addTo(Map, Set> outMap); /** * Returns the selectors keyed by the given declarations in the given map. */ protected Set getSelectorsByDeclarations( Collection declarations, Map, Set> map) { return map.computeIfAbsent(declarations, k -> Sets.newTreeSet(TO_STRING_COMPARATOR)); } } /** * A partition whose declarations are order-independent. */ private static class OrderIndependentPartition extends Partition { private final Multimap inMap; private OrderIndependentPartition(String partition) { super(partition); this.inMap = TreeMultimap.create(TO_STRING_COMPARATOR, DECLARATION_COMPARATOR); } @Override public void add(CssDeclarationNode declaration, CssSelectorNode selector) { inMap.put(selector, declaration); } @Override public void addTo(Map, Set> outMap) { for (Map.Entry> entry : inMap.asMap().entrySet()) { getSelectorsByDeclarations(entry.getValue(), outMap).add(entry.getKey()); } } } /** * A partition whose declarations are order-dependent. */ private static class OrderDependentPartition extends Partition { private final Map> inMap; private OrderDependentPartition(String partition) { super(partition); this.inMap = Maps.newTreeMap(TO_STRING_COMPARATOR); } @Override public void add(CssDeclarationNode declaration, CssSelectorNode selector) { getDeclarations(selector).add(declaration); } @Override public void addTo(Map, Set> outMap) { for (Map.Entry> entry : inMap.entrySet()) { getSelectorsByDeclarations(entry.getValue(), outMap).add(entry.getKey()); } } private List getDeclarations(CssSelectorNode selector) { return inMap.computeIfAbsent(selector, k -> Lists.newLinkedList()); } } /** * A partitioner adds up pairs of declaration/selector to build a map from * groups of declarations in the same partition to selectors. */ private static class Partitioner { /** * This map stores all the declaration/selector pairs added to the * partitioner, grouped by partitions. */ private final Map partitions = Maps.newHashMap(); /** * Adds a declaration/selector pair to the partitioner. * * @param declaration the declaration of the pair to add * @param selector the selector of the pair to add */ public void add(CssDeclarationNode declaration, CssSelectorNode selector) { String partitionName = declaration.getPropertyName().getPartition(); getPartition(partitionName).add(declaration, selector); } /** * Computes a conveninent map from groups of declarations to sets of * selectors from all the additions to the partitioner. * * @return a map conveniently summarizing all the additions to the partitioner */ public Map, Set> getMap() { Map, Set> map = Maps.newTreeMap(TO_STRING_ITERABLE_COMPARATOR); for (Partition partition : partitions.values()) { partition.addTo(map); } return map; } private Partition getPartition(String partitionName) { return partitions.computeIfAbsent(partitionName, Partition::newPartition); } } /** * Compares objects by their string representation. */ @VisibleForTesting static final Comparator TO_STRING_COMPARATOR = Ordering.usingToString(); /** * Create a comparator for iterables that compares element pairwise using the * given element comparator. */ private static Comparator> createIterableComparator( final Comparator elementComparator) { return (o1, o2) -> { Iterator i1 = o1.iterator(); Iterator i2 = o2.iterator(); while (true) { if (i1.hasNext() && !i2.hasNext()) { return 1; } else if (!i1.hasNext() && i2.hasNext()) { return -1; } else if (!i1.hasNext()) { return 0; } else { int c = elementComparator.compare(i1.next(), i2.next()); if (c != 0) { return c; } } } }; } /** * Compares iterables by their extended string representation. */ @VisibleForTesting static final Comparator> TO_STRING_ITERABLE_COMPARATOR = createIterableComparator(TO_STRING_COMPARATOR); /** * Compare declarations, ensuring that shorthand properties appear before * their related properties and using their string representation otherwise. */ @VisibleForTesting static final Comparator DECLARATION_COMPARATOR = (o1, o2) -> { Property property1 = o1.getPropertyName().getProperty(); Property property2 = o2.getPropertyName().getProperty(); if (property1.getShorthands().contains(property2.getName())) { return 1; } else if (property2.getShorthands().contains(property1.getName())) { return -1; } else { return TO_STRING_COMPARATOR.compare(o1, o2); } }; }