com.google.common.css.compiler.passes.UnsafeMergeRulesetNodes Maven / Gradle / Ivy
Show all versions of closure-stylesheets Show documentation
/*
* 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