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.collect.Lists;
import com.google.common.css.compiler.ast.CssCommentNode;
import com.google.common.css.compiler.ast.CssCompilerPass;
import com.google.common.css.compiler.ast.CssDeclarationNode;
import com.google.common.css.compiler.ast.CssFunctionNode;
import com.google.common.css.compiler.ast.CssMixinDefinitionNode;
import com.google.common.css.compiler.ast.CssPropertyValueNode;
import com.google.common.css.compiler.ast.CssValueNode;
import com.google.common.css.compiler.ast.DefaultTreeVisitor;
import com.google.common.css.compiler.ast.ErrorManager;
import com.google.common.css.compiler.ast.MutatingVisitController;
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 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 final class AutoExpandBrowserPrefix extends DefaultTreeVisitor
implements CssCompilerPass {
private final MutatingVisitController visitController;
private final ErrorManager errorManager;
private final ImmutableList expansionRules;
private boolean inDefMixinBlock;
public AutoExpandBrowserPrefix(
MutatingVisitController visitController, ErrorManager errorManager) {
this.visitController = visitController;
this.errorManager = errorManager;
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;
}
List expansionNodes = Lists.newArrayList();
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());
expansionNodes.add(expansionNode);
}
} else {
if (!rule.isFunction()) {
// Handle case #2 where the property value is not a function.
// Ensure that the property value matches exactly.
if (!(declaration.getPropertyValue().getChildren().size() == 1
&& rule.getMatchPropertyValue().equals(
declaration.getPropertyValue().getChildAt(0).getValue()))) {
continue;
}
// 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());
expansionNode.setSourceCodeLocation(declaration.getSourceCodeLocation());
expansionNode.appendComment(new CssCommentNode("/* @alternate */", null));
expansionNodes.add(expansionNode);
}
for (CssDeclarationNode ruleExpansionNode : rule.getExpansionNodes()) {
CssDeclarationNode expansionNode = ruleExpansionNode.deepCopy();
expansionNode.setSourceCodeLocation(declaration.getSourceCodeLocation());
expansionNodes.add(expansionNode);
}
} else {
// Handle case #3 where the property value is a function. Eg. linear-gradient().
if (hasMatchingValueOnlyFunction(declaration, rule)) {
// 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.
CssValueNode matchValueNode = declaration.getPropertyValue().getChildAt(0);
if (matchValueNode instanceof CssFunctionNode) {
CssFunctionNode matchFunctionNode = (CssFunctionNode) matchValueNode;
if (matchFunctionNode.getFunctionName().equals(rule.getMatchPropertyValue())) {
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());
expansionNodes.add(expansionNode);
}
}
}
}
}
}
if (!expansionNodes.isEmpty()) {
visitController.replaceCurrentBlockChildWith(expansionNodes, false);
return true;
}
}
return true;
}
/**
* 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 List expandMatchingValueOnlyFunctions(
CssDeclarationNode declaration, BrowserPrefixRule rule) {
List expansionNodes = Lists.newArrayList();
for (CssPropertyValueNode ruleValueNode : rule.getValueOnlyExpansionNodes()) {
List expansionNodeValues =
Lists.newArrayListWithCapacity(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);
expansionNode.setSourceCodeLocation(declaration.getSourceCodeLocation());
expansionNode.appendComment(new CssCommentNode("/* @alternate */", null));
expansionNodes.add(expansionNode);
}
return expansionNodes;
}
@Override
public void runPass() {
visitController.startVisit(this);
}
}