com.google.common.css.compiler.passes.ProcessComponents 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.base.CaseFormat;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.css.SourceCode;
import com.google.common.css.SourceCodeLocation;
import com.google.common.css.compiler.ast.CssAtRuleNode;
import com.google.common.css.compiler.ast.CssBlockNode;
import com.google.common.css.compiler.ast.CssClassSelectorNode;
import com.google.common.css.compiler.ast.CssClassSelectorNode.ComponentScoping;
import com.google.common.css.compiler.ast.CssCombinatorNode;
import com.google.common.css.compiler.ast.CssCompilerPass;
import com.google.common.css.compiler.ast.CssComponentNode;
import com.google.common.css.compiler.ast.CssConstantReferenceNode;
import com.google.common.css.compiler.ast.CssDefinitionNode;
import com.google.common.css.compiler.ast.CssFunctionNode;
import com.google.common.css.compiler.ast.CssLiteralNode;
import com.google.common.css.compiler.ast.CssNode;
import com.google.common.css.compiler.ast.CssProvideNode;
import com.google.common.css.compiler.ast.CssPseudoClassNode;
import com.google.common.css.compiler.ast.CssRootNode;
import com.google.common.css.compiler.ast.CssRulesetNode;
import com.google.common.css.compiler.ast.CssSelectorNode;
import com.google.common.css.compiler.ast.CssTree;
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.GssError;
import com.google.common.css.compiler.ast.MutatingVisitController;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
public class ProcessComponents extends DefaultTreeVisitor
implements CssCompilerPass {
private static final String CLASS_SEP = "-";
private static final String DEF_SEP = "__";
private final Map components = Maps.newHashMap();
private final MutatingVisitController visitController;
private final ErrorManager errorManager;
private final Map fileToChunk;
private final List provideNodes = Lists.newArrayList();
private SourceCode lastFile = null;
/**
* Creates a new pass to process components for the given visit
* controller, using the given error manager, while ignoring chunks.
*/
public ProcessComponents(MutatingVisitController visitController, ErrorManager errorManager) {
this(visitController, errorManager, null);
}
/**
* Creates a new pass to process components for the given visit
* controller, using the given error manager, while maintaining the
* chunk ids on the nodes created in the process according to the
* given map from files to chunks.
*/
public ProcessComponents(
MutatingVisitController visitController, ErrorManager errorManager,
@Nullable Map fileToChunk) {
this.visitController = visitController;
this.errorManager = errorManager;
this.fileToChunk = fileToChunk;
}
@Override
public boolean enterProvideNode(CssProvideNode node) {
// Often this pass is called on a bunch of GSS files which have been concatenated
// together, meaning that there will be multiple @provide declarations. We are only
// interested in @provide nodes which are in the same source file as the @component.
SourceCode sourceCode = node.getSourceCodeLocation().getSourceCode();
if (sourceCode != lastFile) {
provideNodes.clear();
lastFile = sourceCode;
}
provideNodes.add(node);
return false;
}
@Override
public boolean enterComponent(CssComponentNode node) {
SourceCode sourceCode = node.getSourceCodeLocation().getSourceCode();
if (sourceCode != lastFile) {
provideNodes.clear();
lastFile = sourceCode;
}
String name = node.getName().getValue();
if (node.isImplicitlyNamed()) {
// together before compiling, which can result in multiple @component nodes in the same file.
// So in the unnamed @component case, having multiple @provide is okay (use the last) but not
// having any is still not allowed.
if (provideNodes.size() < 1) {
reportError("implicitly-named @components require a prior @provide declaration ", node);
return false;
}
name = Iterables.getLast(provideNodes).getProvide();
}
if (components.containsKey(name)) {
reportError("cannot redefine component in chunk ", node);
return false;
}
CssLiteralNode parentName = node.getParentName();
if ((parentName != null) && !components.containsKey(parentName.getValue())) {
reportError("parent component is undefined in chunk ", node);
return false;
}
visitController.replaceCurrentBlockChildWith(transformAllNodes(node), false);
components.put(name, node);
return false;
}
@Override
public boolean enterClassSelector(CssClassSelectorNode node) {
// Note that this works because enterComponent, above, returns false -
// this visitor never sees class selectors inside components (the other
// visitor does).
if (node.getScoping() == ComponentScoping.FORCE_SCOPED) {
reportError("'%' prefix for class selectors may only be used in the scope of an @component",
node);
return false;
}
if (node.getScoping() == ComponentScoping.FORCE_UNSCOPED) {
reportError("'^' prefix for class selectors may only be used in the scope of an @component",
node);
return false;
}
return true;
}
private void reportError(String message, CssNode node) {
if (fileToChunk != null) {
message += String.valueOf(
MapChunkAwareNodesToChunk.getChunk(node, fileToChunk));
}
errorManager.report(new GssError(message, node.getSourceCodeLocation()));
visitController.removeCurrentNode();
}
private List transformAllNodes(CssComponentNode current) {
Set constants = Sets.newHashSet();
List nodes = Lists.newLinkedList();
transformAllParentNodes(nodes, constants, current, current.getParentName());
nodes.addAll(transformNodes(constants, current, current));
return nodes;
}
/**
* Recursively goes up the component inheritance hierarchy and copies the
* ancestor component contents.
*
* @param nodes the list of copied child nodes collected from ancestor
* components
* @param constants the set of names of constants defined in the ancestor
* components, used to differentiate local constant names from global
* constant names
* @param current the component for which the nodes are collected
* @param parentLiteralNode the node which contains the name of the ancestor
* node to process, may be {@code null} if we reached the root of the
* inheritance tree
*/
private void transformAllParentNodes(List nodes, Set constants,
CssComponentNode current, @Nullable CssLiteralNode parentLiteralNode) {
if (parentLiteralNode == null) {
return;
}
String parentName = parentLiteralNode.getValue();
CssComponentNode parentComponent = components.get(parentName);
transformAllParentNodes(nodes, constants, current, parentComponent.getParentName());
nodes.addAll(transformNodes(constants, current, parentComponent));
}
/**
* Copies and transforms the contents of the source component block for
* inclusion in the expanded version of the target component.
*
* The transformation of the source component block is basically a renaming
* of the local constant references to their global equivalent. Their names
* are prefixed with the expanded component name. Additionally ancestor
* component contents are also emitted with appropriate renaming, although the
* {@code @def} values are replaced with a reference to the ancestor
* component. For examples look at {@link ProcessComponentsTest}.
*
* @param constants the set of names of constants defined in the ancestor
* components, used to differentiate local constant names from global
* constant names
* @param target the component for which the block contents are copied
* @param source the component from which the block contents are taked
* @return the list of transformed nodes
*/
private List transformNodes(
Set constants, CssComponentNode target, CssComponentNode source) {
CssBlockNode sourceBlock = source.getBlock();
CssBlockNode copyBlock = new CssBlockNode(false, sourceBlock.deepCopy().getChildren());
copyBlock.setSourceCodeLocation(source.getBlock().getSourceCodeLocation());
CssTree tree = new CssTree(
target.getSourceCodeLocation().getSourceCode(), new CssRootNode(copyBlock));
new TransformNodes(constants, target, target != source,
tree.getMutatingVisitController(), errorManager, provideNodes).runPass();
if (fileToChunk != null) {
T chunk = MapChunkAwareNodesToChunk.getChunk(target, fileToChunk);
new SetChunk(tree, chunk).runPass();
}
return tree.getRoot().getBody().getChildren();
}
private static class SetChunk extends DefaultTreeVisitor
implements CssCompilerPass {
private final CssTree tree;
private final Object chunk;
public SetChunk(CssTree tree, Object chunk) {
this.tree = tree;
this.chunk = chunk;
}
@Override
public boolean enterDefinition(CssDefinitionNode definition) {
definition.setChunk(chunk);
return false;
}
@Override
public boolean enterSelector(CssSelectorNode selector) {
selector.setChunk(chunk);
return true;
}
@Override
public boolean enterFunctionNode(CssFunctionNode function) {
function.setChunk(chunk);
return super.enterFunctionNode(function);
}
@Override
public void runPass() {
tree.getVisitController().startVisit(this);
}
}
private static class TransformNodes extends DefaultTreeVisitor
implements CssCompilerPass {
private final boolean inAncestorBlock;
private final MutatingVisitController visitController;
private final ErrorManager errorManager;
private final Set renamedDefinitions = Sets.newHashSet();
private final Set componentConstants;
private final boolean isAbstract;
private final String classPrefix;
private final String defPrefix;
private final String parentName;
private final SourceCodeLocation sourceCodeLocation;
private boolean firstClassSelector;
/** If non-zero, we won't process the first classname in the current selector. */
private int nestedSelectorDepth;
public TransformNodes(Set constants, CssComponentNode current, boolean inAncestorBlock,
MutatingVisitController visitController, ErrorManager errorManager,
List provideNodes) {
this.componentConstants = constants;
this.inAncestorBlock = inAncestorBlock;
this.visitController = visitController;
this.errorManager = errorManager;
String currentName = current.getName().getValue();
if (current.isImplicitlyNamed()) {
currentName = Iterables.getLast(provideNodes).getProvide();
}
this.isAbstract = current.isAbstract();
if (current.getPrefixStyle() == CssComponentNode.PrefixStyle.CASE_CONVERT) {
this.classPrefix = getClassPrefixFromDottedName(currentName);
this.defPrefix = getDefPrefixFromDottedName(currentName);
} else {
this.classPrefix = currentName + CLASS_SEP;
this.defPrefix = currentName + DEF_SEP;
}
this.parentName = inAncestorBlock ? current.getParentName().getValue() : null;
this.sourceCodeLocation = current.getSourceCodeLocation();
}
@Override
public boolean enterComponent(CssComponentNode node) {
if (!inAncestorBlock) {
errorManager.report(
new GssError("nested components are not allowed", node.getSourceCodeLocation()));
}
visitController.removeCurrentNode();
return false;
}
@Override
public boolean enterRuleset(CssRulesetNode node) {
if (isAbstract) {
visitController.removeCurrentNode();
}
return !isAbstract;
}
@Override
public boolean enterCombinator(CssCombinatorNode combinator) {
nestedSelectorDepth++;
return true;
}
@Override
public void leaveCombinator(CssCombinatorNode combinator) {
nestedSelectorDepth--;
}
@Override
public boolean enterSelector(CssSelectorNode selector) {
// Only reset the 'first selector' flag if we're not in a combinator.
// Otherwise, keep the same flag value (which may or may not have been set
// depending on whether we saw a class selector in an earlier refiner list.)
if (nestedSelectorDepth == 0) {
firstClassSelector = true;
}
return true;
}
@Override
public void leaveSelector(CssSelectorNode selector) {
firstClassSelector = false;
}
// Don't reset firstClassSelector for classes in :not().
@Override
public boolean enterPseudoClass(CssPseudoClassNode pseudoClass) {
nestedSelectorDepth++;
return true;
}
@Override
public void leavePseudoClass(CssPseudoClassNode pseudoClass) {
nestedSelectorDepth--;
}
@Override
public boolean enterClassSelector(CssClassSelectorNode node) {
Preconditions.checkState(!isAbstract);
if (!firstClassSelector && node.getScoping() == ComponentScoping.FORCE_UNSCOPED) {
errorManager.report(new GssError(
"'^' prefix may only be used on the first classname in a selector.",
node.getSourceCodeLocation()));
}
if (firstClassSelector && node.getScoping() != ComponentScoping.FORCE_UNSCOPED
|| node.getScoping() == ComponentScoping.FORCE_SCOPED) {
CssClassSelectorNode newNode = new CssClassSelectorNode(
classPrefix + node.getRefinerName(),
inAncestorBlock ? sourceCodeLocation : node.getSourceCodeLocation());
visitController.replaceCurrentBlockChildWith(ImmutableList.of(newNode), false);
}
firstClassSelector = false;
return true;
}
@Override
public boolean enterDefinition(CssDefinitionNode node) {
// Do not modify the renamed node created below, but descend and modify
// its children.
if (renamedDefinitions.contains(node)) {
return true;
}
String defName = node.getName().getValue();
CssLiteralNode newDefLit =
new CssLiteralNode(defPrefix + defName,
inAncestorBlock ? sourceCodeLocation : node.getSourceCodeLocation());
CssDefinitionNode newNode;
// When copying the ancestor block, we want to replace definition values
// with a reference to the constant emitted when the parent component was
// transformed. This makes it possible to actually inherit values from
// the parent component (parent component definitions changes will
// propagate to descendant components).
if (inAncestorBlock) {
String parentRefPrefix = parentName + DEF_SEP;
// Hack to avoid breaking hacked components with http://b/3213779
// workarounds. Can be removed when all workarounds are removed.
String parentRefName = defName.startsWith(parentRefPrefix)
? defName : parentRefPrefix + defName;
CssConstantReferenceNode parentRefNode =
new CssConstantReferenceNode(parentRefName, sourceCodeLocation);
newNode = new CssDefinitionNode(ImmutableList.of(parentRefNode),
newDefLit, sourceCodeLocation);
} else {
newNode = new CssDefinitionNode(CssAtRuleNode.copyNodes(node.getParameters()),
newDefLit, sourceCodeLocation);
}
componentConstants.add(defName);
renamedDefinitions.add(newNode);
visitController.replaceCurrentBlockChildWith(ImmutableList.of(newNode), true);
return false;
}
@Override
public boolean enterValueNode(CssValueNode node) {
if (node instanceof CssConstantReferenceNode
// Avoid renaming constant references for constants not defined in the
// component tree.
&& componentConstants.contains(node.getValue())) {
CssConstantReferenceNode newNode =
new CssConstantReferenceNode(defPrefix + node.getValue(),
inAncestorBlock ? sourceCodeLocation : node.getSourceCodeLocation());
visitController.replaceCurrentBlockChildWith(ImmutableList.of(newNode), false);
}
return true;
}
@Override
public boolean enterArgumentNode(CssValueNode node) {
return enterValueNode(node);
}
@Override
public void runPass() {
visitController.startVisit(this);
}
/**
* Compute the name of the class prefix from the package name. This converts
* the dot-separated package name to camel case, so foo.bar becomes fooBar.
*
* @param packageName the @provide package name
* @return the converted class prefix
*/
private String getClassPrefixFromDottedName(String packageName) {
// CaseFormat doesn't have a format for names separated by dots, so we transform
// the dots into dashes. Then we can use the regular CaseFormat transformation
// to camel case instead of having to write our own.
String packageNameWithDashes = packageName.replace('.', '-');
return CaseFormat.LOWER_HYPHEN.to(CaseFormat.LOWER_CAMEL, packageNameWithDashes);
}
/**
* Compute the name of the def prefix from the package name. This converts the dot-separated
* package name to uppercase with underscores, so foo.bar becomes FOO_BAR_.
*
* @param packageName the @provide package name
* @return the converted def prefix
*/
private String getDefPrefixFromDottedName(String packageName) {
return packageName.replace('.', '_').toUpperCase() + "_";
}
}
@Override
public void runPass() {
visitController.startVisit(this);
}
}