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

com.google.common.css.compiler.passes.ProcessComponents 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: 20160212
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.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); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy