com.google.common.css.compiler.passes.AutoExpandBrowserPrefix Maven / Gradle / Ivy
Show all versions of closure-stylesheets Show documentation
/*
* Copyright 2015 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.collect.ImmutableList;
import com.google.common.css.compiler.ast.*;
import java.util.ArrayList;
import java.util.List;
/**
* A compiler pass that automatically detects certain properties that need additional
* browser specific property declarations, and adds them.
* The properties to be matched for expansion are provided by the {@link BrowserPrefixGenerator}.
*
* This mechanism is an alternative to using conventional mixins.
* Problems with conventional mixins:
* - developers have to always remember to use the mixin consistently
* - they have to go find the appropriate mixins
* - the framework has to to verify the code and ensure mixins were used correctly
* Automation addresses all of the above issues.
*
*
Currently three most common cases are handled:
* #1 Matching and replacing only the property name. Eg. flex-grow: VALUE;
* #2 Matching property name and value, replacing the value. Eg. display: flex;
* #3 Matching property name and value where value is a function, replacing the function name.
* Eg. background-image: linear-gradient(ARGS);
*/
public class AutoExpandBrowserPrefix extends DefaultTreeVisitor implements CssCompilerPass {
public static final String ALTERNATE = "/* @alternate */";
private final MutatingVisitController visitController;
private final ImmutableList expansionRules;
private boolean inDefMixinBlock;
public AutoExpandBrowserPrefix(MutatingVisitController visitController) {
this.visitController = visitController;
this.expansionRules = BrowserPrefixGenerator.getExpansionRules();
}
@Override
public boolean enterMixinDefinition(CssMixinDefinitionNode node) {
inDefMixinBlock = true;
return true;
}
@Override
public void leaveMixinDefinition(CssMixinDefinitionNode node) {
inDefMixinBlock = false;
}
@Override
public boolean enterDeclaration(CssDeclarationNode declaration) {
// Do not auto expand properties inside @defmixin blocks.
// To enable compatibility with existing mixin expansion, don't apply the rules to the
// mixin definitions. This leaves the mixin expansion unaffected.
if (inDefMixinBlock) {
return true;
}
ImmutableList.Builder expansionNodes = ImmutableList.builder();
for (BrowserPrefixRule rule : expansionRules) {
// If the name is present in the rule then it must match the declaration.
if (rule.getMatchPropertyName() != null
&& !rule.getMatchPropertyName().equals(declaration.getPropertyName().getPropertyName())) {
continue;
}
// Handle case #1 when no property value is available.
if (rule.getMatchPropertyValue() == null) {
for (CssDeclarationNode ruleExpansionNode : rule.getExpansionNodes()) {
CssDeclarationNode expansionNode = ruleExpansionNode.deepCopy();
expansionNode.setPropertyValue(declaration.getPropertyValue().deepCopy());
expansionNode.setSourceCodeLocation(declaration.getSourceCodeLocation());
expansionNode.setComments(declaration.getComments());
expansionNode.appendComment(new CssCommentNode(ALTERNATE, null));
expansionNodes.add(expansionNode);
}
} else if (!rule.isFunction()) {
// Handle case #2 where the property value is not a function.
expansionNodes.addAll(getNonFunctionValueMatches(rule, declaration));
} else if (hasMatchingValueOnlyFunction(declaration, rule)) {
// Handle case #3 where the property value is a function. Eg. linear-gradient().
// The rule is value-only and one of the declaration values matches.
expansionNodes.addAll(expandMatchingValueOnlyFunctions(declaration, rule));
} else {
// The rule is not value-only or did not match, check other rules.
expansionNodes.addAll(getOtherMatches(declaration, rule));
}
ImmutableList replacements = expansionNodes.build();
if (!replacements.isEmpty()) {
visitController.replaceCurrentBlockChildWith(
replacements, false /* visitTheReplacementNodes */);
break; // found a match, don't need to look for more
}
}
return true;
}
protected ImmutableList getNonFunctionValueMatches(
BrowserPrefixRule rule, CssDeclarationNode declaration) {
// Ensure that the property value matches exactly.
if (!(declaration.getPropertyValue().getChildren().size() == 1
&& rule.getMatchPropertyValue()
.equals(declaration.getPropertyValue().getChildAt(0).getValue()))) {
return ImmutableList.of();
}
ImmutableList.Builder replacements = ImmutableList.builder();
// TODO(user): Maybe support multiple values for non-function value-only expansions.
for (CssPropertyValueNode ruleValueNode : rule.getValueOnlyExpansionNodes()) {
// For valueOnlyExpansionNodes the property name comes from the declaration.
CssDeclarationNode expansionNode =
new CssDeclarationNode(
declaration.getPropertyName(),
ruleValueNode.deepCopy(),
declaration.getSourceCodeLocation());
expansionNode.appendComment(new CssCommentNode(ALTERNATE, null));
replacements.add(expansionNode);
}
for (CssDeclarationNode ruleExpansionNode : rule.getExpansionNodes()) {
CssDeclarationNode expansionNode = ruleExpansionNode.deepCopy();
expansionNode.setSourceCodeLocation(declaration.getSourceCodeLocation());
replacements.add(expansionNode);
}
return replacements.build();
}
private ImmutableList getOtherMatches(
CssDeclarationNode declaration, BrowserPrefixRule rule) {
CssValueNode matchValueNode = declaration.getPropertyValue().getChildAt(0);
if (!(matchValueNode instanceof CssFunctionNode)) {
return ImmutableList.of();
}
CssFunctionNode matchFunctionNode = (CssFunctionNode) matchValueNode;
if (!matchFunctionNode.getFunctionName().equals(rule.getMatchPropertyValue())) {
return ImmutableList.of();
}
ImmutableList.Builder replacements = ImmutableList.builder();
for (CssDeclarationNode ruleExpansionNode : rule.getExpansionNodes()) {
CssDeclarationNode expansionNode = ruleExpansionNode.deepCopy();
CssValueNode expandValueNode = expansionNode.getPropertyValue().getChildAt(0);
CssFunctionNode expandFunctionNode = (CssFunctionNode) expandValueNode;
expandFunctionNode.setArguments(matchFunctionNode.getArguments().deepCopy());
expansionNode.setSourceCodeLocation(declaration.getSourceCodeLocation());
replacements.add(expansionNode);
}
return replacements.build();
}
/**
* Returns true if the value node is a function and matches the rule.
*/
private static boolean matchesValueOnlyFunction(CssValueNode declarationValueNode,
BrowserPrefixRule rule) {
return (declarationValueNode instanceof CssFunctionNode)
&& ((CssFunctionNode) declarationValueNode).getFunctionName()
.equals(rule.getMatchPropertyValue());
}
/**
* Returns true if the rule is value-only and at least one function value in the declaration
* matches the rule.
*/
private static boolean hasMatchingValueOnlyFunction(CssDeclarationNode declaration,
BrowserPrefixRule rule) {
if (rule.getValueOnlyExpansionNodes().isEmpty()) {
return false;
}
for (CssValueNode declarationValueNode : declaration.getPropertyValue().getChildren()) {
if (matchesValueOnlyFunction(declarationValueNode, rule)) {
return true;
}
}
return false;
}
/**
* Returns the value-only function expansion for this declaration and rule. For each value-only
* expansion rule we can match 0 or more values.
* For example: margin: calc(X) calc(Y); -> margin: -webkit-calc(X) -webkit-calc(Y);
*/
private static ImmutableList expandMatchingValueOnlyFunctions(
CssDeclarationNode declaration, BrowserPrefixRule rule) {
ImmutableList.Builder expansionNodes = ImmutableList.builder();
for (CssPropertyValueNode ruleValueNode : rule.getValueOnlyExpansionNodes()) {
List expansionNodeValues =
new ArrayList<>(declaration.getPropertyValue().numChildren());
for (CssValueNode declarationValueNode : declaration.getPropertyValue().getChildren()) {
if (matchesValueOnlyFunction(declarationValueNode, rule)) {
CssFunctionNode declarationFunctionNode = (CssFunctionNode) declarationValueNode;
CssFunctionNode expansionFunctionNode =
(CssFunctionNode) ruleValueNode.getChildAt(0).deepCopy();
expansionFunctionNode.setArguments(declarationFunctionNode.getArguments().deepCopy());
expansionNodeValues.add(expansionFunctionNode);
} else {
expansionNodeValues.add(declarationValueNode.deepCopy());
}
}
// For valueOnlyExpansionNodes the property name comes from the declaration.
CssPropertyValueNode expansionValues = new CssPropertyValueNode(expansionNodeValues);
CssDeclarationNode expansionNode =
new CssDeclarationNode(
declaration.getPropertyName(), expansionValues, declaration.getComments(),
declaration.getSourceCodeLocation());
expansionNode.appendComment(new CssCommentNode(ALTERNATE, null));
expansionNodes.add(expansionNode);
}
return expansionNodes.build();
}
@Override
public void runPass() {
visitController.startVisit(this);
}
}