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

com.google.common.css.compiler.ast.DefaultVisitController 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.

The newest version!
/*
 * Copyright 2008 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.ast;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.Queues;
import com.google.common.css.compiler.ast.CssCompositeValueNode.Operator;

import java.util.Deque;
import java.util.List;

/**
 * Default implementation of the MutatingVisitController. The controller is
 * mutating or not depending on a flag passed as a parameter to the constructor.
 *
 * @author [email protected] (Oana Florescu)
 */
class DefaultVisitController implements MutatingVisitController {

  /** The (sub)tree to be visited. */
  private final CssNode subtree;

  /** Whether mutations of the tree are allowed or not. */
  private final boolean allowMutating;

  /** The visitor of the tree. */
  @VisibleForTesting
  CssTreeVisitor visitor;

  /** The stack of states for the controller. */
  private final StateStack stateStack = new StateStack();

  /** Whether the visit was required to stop. */
  @SuppressWarnings("unused")
  private boolean stopVisitCalled = false;

  @SuppressWarnings("serial")
  private static class StopVisitRequestedException extends RuntimeException {}

  /**
   * Interface for CSS AST visit states. Visit states are used to track which
   * node and what type of node is currently visited, perform tree modifications
   * and take care of visit state transitions. Visit states should only allow
   * valid state transitions - this can be used as AST structure validation.
   *
   * @param  type of the children CSS nodes that can be used as a replacement
   *     for currently visited block node
   */
  @VisibleForTesting
  interface VisitState {
    /**
     * Performs the visit by calling appropriate methods of the visitor
     * (enter and leave).
     */
    void doVisit();

    /**
     * Transitions to next state by putting a new one onto the stack or popping
     * the old one off the state stack. Some implementations can handle children
     * nodes changes and omit visits of children nodes (effectively a subtree).
     */
    void transitionToNextState();

    /**
     * Notifies the state, that the stop tree visit has been requested.

* *

NOTE(dgajda): Practically unused, remove? */ void stopVisitCalled(); /** * Notifies the state that removal of current node is requested. * Performs the removal by passing the control to the state * below current one. */ void removeCurrentNodeCalled(); /** Removes currently visited child node. */ void removeCurrentChild(); /** * Used to notify current state and allow the state below of the top state * remove the current node with a list of replacement nodes. * *

NOTE(dgajda): This method probably does not need to be in VisitState. * * @param type od replacement nodes * @param replacementNodes nodes used to replace current block * @param visitTheReplacementNodes whether new nodes should also be visited */ void replaceCurrentBlockChildWithCalled( List replacementNodes, boolean visitTheReplacementNodes); /** * Replaces current node with given replacement nodes. * Called by {@link #replaceCurrentBlockChildWithCalled(List, boolean)}. * * @param replacementNodes nodes used to replace current block * @param visitTheReplacementNodes whether new nodes should also be visited */ void replaceCurrentBlockChildWith(List replacementNodes, boolean visitTheReplacementNodes); } /** * Base implementation of AST visit state. * * @param type of the children CSS nodes that can be used as a replacement * for currently visited block node */ @VisibleForTesting abstract class BaseVisitState implements VisitState { @Override public void stopVisitCalled() {} @Override public void removeCurrentChild() { // Assume that by default this cannot happen. throw new AssertionError("Current child removal is not supported by " + this.getClass().getName() + " VisitState class."); } @Override public void removeCurrentNodeCalled() { stateStack.pop(); stateStack.getTop().removeCurrentChild(); } @Override public void replaceCurrentBlockChildWithCalled( List replacementNodes, boolean visitTheReplacementNodes) { stateStack.pop(); @SuppressWarnings("unchecked") VisitState topState = (VisitState) stateStack.getTop(); topState.replaceCurrentBlockChildWith( replacementNodes, visitTheReplacementNodes); } @Override public void replaceCurrentBlockChildWith( List replacementNodes, boolean visitTheReplacementNodes) { // Assume that by default this cannot happen. // assert false; } public VisitState createFallbackState(N child) { return null; } } /** * Base class for VisitStates which control visits of {@link CssNodesListNode} * children. * * @param type of the children CSS nodes that can be used as a replacement * for currently visited block node */ abstract class VisitChildrenState extends BaseVisitState { private final CssNodesListNode block; private int currentIndex = -1; VisitChildrenState(CssNodesListNode block) { this.block = block; } @Override public void transitionToNextState() { if (currentIndex == block.numChildren() - 1) { stateStack.pop(); return; } // Remain in this state to finish visiting all the children currentIndex++; stateStack.push(getVisitState(block.getChildAt(currentIndex))); return; } /** * Returns a visit state for a given child node. * * @param node child node to create visit state for * @return new visit state */ abstract VisitState getVisitState(T node); /** {@inheritDoc} */ @Override public void doVisit() { // Does nothing. } } /** * Base class for VisitStates which control visits of {@link CssNodesListNode} * children and can replace currently visited node with replacement nodes. * * @param type of the children CSS nodes that can be used as a replacement * for currently visited block node */ abstract class VisitReplaceChildrenState extends BaseVisitState { protected int currentIndex = -1; protected boolean doNotIncreaseIndex = false; protected final CssNodesListNode node; VisitReplaceChildrenState(CssNodesListNode node) { this.node = node; } @Override public void doVisit() { // Does nothing. } @Override public void removeCurrentChild() { node.removeChildAt(currentIndex); doNotIncreaseIndex = true; } @Override public void replaceCurrentBlockChildWith( List replacementNodes, boolean visitTheReplacementNodes) { node.replaceChildAt(currentIndex, replacementNodes); if (visitTheReplacementNodes) { doNotIncreaseIndex = true; } else { currentIndex += replacementNodes.size() - 1; } } @Override public void transitionToNextState() { // We get out of this state if we are on the last child and we are allowed // to increment the index, which means we arrived here with no special // case, or we have just removed the last element. if ((currentIndex == node.numChildren() - 1 && !doNotIncreaseIndex) || currentIndex == node.numChildren()) { stateStack.pop(); return; } // Remain in this state to finish visiting all the children. if (!doNotIncreaseIndex) { currentIndex++; } else { doNotIncreaseIndex = false; } VisitState state = createVisitState(node.getChildAt(currentIndex), this); if (state != null) { stateStack.push(state); } } } /** * Unfinished base class for VisitStates which share the same code to * optionally visit child nodes. * * @param type of the children CSS nodes that can be used as a replacement * for currently visited block node */ abstract class VisitChildrenOptionalState extends BaseVisitState { // TODO(user): move the common code here or delete this // useless complexity. } @VisibleForTesting class RootVisitBeforeChildrenState extends BaseVisitState { private final CssRootNode root; RootVisitBeforeChildrenState(CssRootNode root) { Preconditions.checkNotNull(root); this.root = root; } @Override public void doVisit() { visitor.enterTree(root); } @Override public void transitionToNextState() { stateStack.transitionTo( new RootVisitCharsetState(root, root.getCharsetRule())); } } @VisibleForTesting class RootVisitAfterChildrenState extends BaseVisitState { private final CssRootNode root; RootVisitAfterChildrenState(CssRootNode root) { Preconditions.checkNotNull(root); this.root = root; } @Override public void doVisit() { visitor.leaveTree(root); } @Override public void transitionToNextState() { stateStack.pop(); // assert stateStack.isEmpty(); } } @VisibleForTesting class RootVisitCharsetState extends BaseVisitState { private final CssRootNode root; private final CssAtRuleNode charsetRule; RootVisitCharsetState(CssRootNode root, CssAtRuleNode charsetRule) { this.root = root; this.charsetRule = charsetRule; } @Override public void doVisit() { if (charsetRule == null) { // Nothing left to do. return; } } @Override public void removeCurrentNodeCalled() { root.setCharsetRule(null); } @Override public void transitionToNextState() { stateStack.transitionTo(new RootVisitImportBlockState(root, root.getImportRules())); } } @VisibleForTesting class RootVisitImportBlockState extends BaseVisitState { private final CssRootNode root; private final CssImportBlockNode block; private boolean visitedChildren = false; private boolean shouldVisitChildren = true; RootVisitImportBlockState(CssRootNode root, CssImportBlockNode block) { this.root = root; this.block = block; } @Override public void doVisit() { if (!visitedChildren) { shouldVisitChildren = visitor.enterImportBlock(block); } else { visitor.leaveImportBlock(block); } } @Override public void transitionToNextState() { if (!visitedChildren && shouldVisitChildren) { stateStack.push(new VisitImportBlockChildrenState(block)); visitedChildren = true; } else { stateStack.transitionTo( new RootVisitBodyState(root, root.getBody())); } } } @VisibleForTesting class VisitImportBlockChildrenState extends VisitChildrenState { VisitImportBlockChildrenState(CssImportBlockNode block) { super(block); } @Override VisitState getVisitState(CssImportRuleNode node) { return new VisitImportRuleState(node); } } @VisibleForTesting class VisitImportRuleState extends BaseVisitState { private final CssImportRuleNode node; VisitImportRuleState(CssImportRuleNode node) { this.node = node; } @Override public void doVisit() { if (visitor.enterImportRule(node)) { visitor.leaveImportRule(node); } } @Override public void transitionToNextState() { stateStack.pop(); } } @VisibleForTesting class VisitProvideState extends BaseVisitState { private final CssProvideNode node; VisitProvideState(CssProvideNode node) { this.node = node; } @Override public void doVisit() { visitor.enterProvideNode(node); visitor.leaveProvideNode(node); } @Override public void transitionToNextState() { stateStack.pop(); } } @VisibleForTesting class VisitRequireState extends BaseVisitState { private final CssRequireNode node; VisitRequireState(CssRequireNode node) { this.node = node; } @Override public void doVisit() { visitor.enterRequireNode(node); visitor.leaveRequireNode(node); } @Override public void transitionToNextState() { stateStack.pop(); } } @VisibleForTesting class RootVisitBodyState extends VisitChildrenOptionalState { private final CssRootNode root; private final CssBlockNode body; private boolean visitedChildren = false; private boolean shouldVisitChildren = true; RootVisitBodyState(CssRootNode root, CssBlockNode body) { this.root = root; this.body = body; } @Override public void doVisit() { if (!visitedChildren) { shouldVisitChildren = visitor.enterBlock(body); } else { visitor.leaveBlock(body); } } @Override public void transitionToNextState() { if (!visitedChildren && shouldVisitChildren) { stateStack.push(new VisitBlockChildrenState(body)); visitedChildren = true; } else { stateStack.transitionTo(new RootVisitAfterChildrenState(root)); } } } @VisibleForTesting class VisitBlockChildrenState extends VisitReplaceChildrenState { VisitBlockChildrenState(CssBlockNode block) { super(block); } } @VisibleForTesting class VisitDefinitionState extends VisitChildrenOptionalState { private final CssDefinitionNode node; private boolean visitedChildren = false; private boolean shouldVisitChildren = true; VisitDefinitionState(CssDefinitionNode node) { this.node = node; } @Override public void doVisit() { if (!visitedChildren) { shouldVisitChildren = visitor.enterDefinition(node); } else { visitor.leaveDefinition(node); } } @Override public void transitionToNextState() { if (!visitedChildren && shouldVisitChildren) { stateStack.push(new VisitDefinitionParametersState(node)); visitedChildren = true; } else { stateStack.pop(); } } } @VisibleForTesting class VisitDefinitionParametersState extends VisitReplaceChildrenState { VisitDefinitionParametersState(CssDefinitionNode def) { super(def); } @Override public VisitState createFallbackState(CssValueNode child) { return new VisitValueNodeState(child); } } @VisibleForTesting class VisitMediaRuleState extends VisitReplaceChildrenState { private final CssMediaRuleNode node; private boolean visitedChildren = false; private boolean shouldVisitChildren = true; VisitMediaRuleState(CssMediaRuleNode node) { super(node); this.node = node; } @Override public void doVisit() { if (!visitedChildren && currentIndex == -1) { shouldVisitChildren = visitor.enterMediaRule(node); } else if (visitedChildren) { visitor.leaveMediaRule(node); } } @Override public void replaceCurrentBlockChildWith( List replacementNodes, boolean visitTheReplacementNodes) { // If we're replacing the current property with a composite value // separated by the space, we really just want to graft those nodes at // the current child's location. if (replacementNodes.size() == 1 && replacementNodes.get(0) instanceof CssCompositeValueNode) { CssCompositeValueNode compositeValueNode = (CssCompositeValueNode) replacementNodes.get(0); if (compositeValueNode.getOperator() == Operator.SPACE) { replacementNodes = compositeValueNode.getValues(); } } super.replaceCurrentBlockChildWith( replacementNodes, visitTheReplacementNodes); } @Override public void transitionToNextState() { if (visitedChildren || !shouldVisitChildren) { stateStack.pop(); return; } if (!doNotIncreaseIndex) { currentIndex++; } else { doNotIncreaseIndex = false; } final int parametersCount = node.getParameters().size(); if (currentIndex < parametersCount) { if (currentIndex < parametersCount - 1) { stateStack.push(new VisitMediaTypeListDelimiterState(node)); } stateStack.push(getVisitState(node.getParameters().get(currentIndex))); } else { if (node.getType().hasBlock()) { stateStack.push(new VisitUnknownAtRuleBlockState(node.getBlock())); } visitedChildren = true; } } public VisitState getVisitState(CssValueNode node) { if (node instanceof CssCompositeValueNode) { return new VisitCompositeValueState((CssCompositeValueNode) node); } else { return new VisitValueNodeState(node); } } } private class VisitMediaTypeListDelimiterState extends BaseVisitState { private final CssNodesListNode node; public VisitMediaTypeListDelimiterState( CssNodesListNode node) { this.node = node; } @Override public void doVisit() { visitor.enterMediaTypeListDelimiter(node); visitor.leaveMediaTypeListDelimiter(node); } @Override public void transitionToNextState() { stateStack.pop(); } } @VisibleForTesting class VisitPageRuleState extends VisitChildrenOptionalState { private final CssPageRuleNode node; private boolean visitedChildren = false; private boolean shouldVisitChildren = true; VisitPageRuleState(CssPageRuleNode node) { this.node = node; } @Override public void doVisit() { if (!visitedChildren) { shouldVisitChildren = visitor.enterPageRule(node); } else { visitor.leavePageRule(node); } } @Override public void transitionToNextState() { if (!visitedChildren && shouldVisitChildren) { stateStack.push(new VisitUnknownAtRuleBlockState(node.getBlock())); visitedChildren = true; } else { stateStack.pop(); } } } @VisibleForTesting class VisitPageSelectorState extends VisitChildrenOptionalState { private final CssPageSelectorNode node; private boolean visitedChildren = false; private boolean shouldVisitChildren = true; VisitPageSelectorState(CssPageSelectorNode node) { this.node = node; } @Override public void doVisit() { if (!visitedChildren) { shouldVisitChildren = visitor.enterPageSelector(node); } else { visitor.leavePageSelector(node); } } @Override public void transitionToNextState() { if (!visitedChildren && shouldVisitChildren) { stateStack.push(new VisitUnknownAtRuleBlockState(node.getBlock())); visitedChildren = true; } else { stateStack.pop(); } } } @VisibleForTesting class VisitFontFaceState extends VisitChildrenOptionalState { private final CssFontFaceNode node; private boolean visitedChildren = false; private boolean shouldVisitChildren = true; VisitFontFaceState(CssFontFaceNode node) { this.node = node; } @Override public void doVisit() { if (!visitedChildren) { shouldVisitChildren = visitor.enterFontFace(node); } else { visitor.leaveFontFace(node); } } @Override public void transitionToNextState() { if (!visitedChildren && shouldVisitChildren) { stateStack.push(new VisitUnknownAtRuleBlockState(node.getBlock())); visitedChildren = true; } else { stateStack.pop(); } } } @VisibleForTesting class VisitConditionalBlockState extends BaseVisitState { private final CssConditionalBlockNode block; private boolean visitedChildren = false; VisitConditionalBlockState(CssConditionalBlockNode block) { this.block = block; } @Override public void doVisit() { if (!visitedChildren) { visitor.enterConditionalBlock(block); } else { visitor.leaveConditionalBlock(block); } } @Override public void transitionToNextState() { if (!visitedChildren) { stateStack.push( new VisitConditionalBlockChildrenState(block)); visitedChildren = true; } else { stateStack.pop(); } } } @VisibleForTesting class VisitConditionalBlockChildrenState extends VisitChildrenState { VisitConditionalBlockChildrenState(CssConditionalBlockNode block) { super(block); } @Override VisitState getVisitState(CssConditionalRuleNode node) { return new VisitConditionalRuleState(node); } } @VisibleForTesting class VisitConditionalRuleState extends VisitChildrenOptionalState { private final CssConditionalRuleNode node; private boolean visitedChildren = false; private boolean shouldVisitChildren = true; VisitConditionalRuleState(CssConditionalRuleNode node) { this.node = node; } @Override public void doVisit() { if (!visitedChildren) { shouldVisitChildren = visitor.enterConditionalRule(node); } else { visitor.leaveConditionalRule(node); } } @Override public void transitionToNextState() { if (!visitedChildren && shouldVisitChildren) { stateStack.push( new VisitConditionalRuleChildrenState(node.getBlock())); visitedChildren = true; } else { stateStack.pop(); } } } @VisibleForTesting class VisitConditionalRuleChildrenState extends VisitReplaceChildrenState { VisitConditionalRuleChildrenState(CssBlockNode block) { super(block); } } @VisibleForTesting class VisitRulesetState extends VisitChildrenOptionalState { private final CssRulesetNode node; private boolean visitedChildren = false; private boolean shouldVisitChildren = true; VisitRulesetState(CssRulesetNode node) { this.node = node; } @Override public void doVisit() { if (!visitedChildren) { shouldVisitChildren = visitor.enterRuleset(node); } else { visitor.leaveRuleset(node); } } @Override public void transitionToNextState() { if (!visitedChildren && shouldVisitChildren) { stateStack.push(new VisitSelectorBlockState(node, node.getSelectors())); visitedChildren = true; } else { stateStack.pop(); } } } @VisibleForTesting class VisitKeyframeRulesetState extends VisitChildrenOptionalState { private final CssKeyframeRulesetNode node; private boolean visitedChildren = false; private boolean shouldVisitChildren = true; VisitKeyframeRulesetState(CssKeyframeRulesetNode node) { this.node = node; } @Override public void doVisit() { if (!visitedChildren) { shouldVisitChildren = visitor.enterKeyframeRuleset(node); } else { visitor.leaveKeyframeRuleset(node); } } @Override public void transitionToNextState() { if (!visitedChildren && shouldVisitChildren) { stateStack.push(new VisitKeyBlockState(node, node.getKeys())); visitedChildren = true; } else { stateStack.pop(); } } } @VisibleForTesting class VisitSelectorBlockState extends BaseVisitState { private final CssSelectorListNode block; private final CssRulesetNode ruleset; private boolean visitedChildren = false; VisitSelectorBlockState(CssRulesetNode ruleset, CssSelectorListNode block) { this.ruleset = ruleset; this.block = block; } @Override public void doVisit() { if (!visitedChildren) { visitor.enterSelectorBlock(block); } else { visitor.leaveSelectorBlock(block); } } @Override public void transitionToNextState() { if (!visitedChildren) { stateStack.push( new VisitSelectorBlockChildrenState(block)); visitedChildren = true; } else { stateStack.transitionTo( new VisitDeclarationBlockState(ruleset.getDeclarations())); } } } @VisibleForTesting class VisitSelectorBlockChildrenState extends VisitChildrenState { VisitSelectorBlockChildrenState(CssSelectorListNode block) { super(block); } @Override VisitState getVisitState(CssSelectorNode node) { return new VisitSelectorState(node); } } @VisibleForTesting class VisitKeyBlockState extends BaseVisitState { private final CssKeyListNode block; private final CssKeyframeRulesetNode ruleset; private boolean visitedChildren = false; VisitKeyBlockState(CssKeyframeRulesetNode ruleset, CssKeyListNode block) { this.ruleset = ruleset; this.block = block; } @Override public void doVisit() { if (!visitedChildren) { visitor.enterKeyBlock(block); } else { visitor.leaveKeyBlock(block); } } @Override public void transitionToNextState() { if (!visitedChildren) { stateStack.push( new VisitKeyBlockChildrenState(block)); visitedChildren = true; } else { stateStack.transitionTo( new VisitDeclarationBlockState(ruleset.getDeclarations())); } } } @VisibleForTesting class VisitKeyBlockChildrenState extends VisitChildrenState { VisitKeyBlockChildrenState(CssKeyListNode block) { super(block); } @Override VisitState getVisitState(CssKeyNode node) { return new VisitKeyState(node); } } @VisibleForTesting class VisitSelectorState extends BaseVisitState { private final CssSelectorNode node; private boolean visitedChildren = false; VisitSelectorState(CssSelectorNode node) { this.node = node; } @Override public void doVisit() { if (!visitedChildren) { visitor.enterSelector(node); } else { visitor.leaveSelector(node); } } @Override public void transitionToNextState() { if (!visitedChildren) { // We need to prepare the stack such that the refiners are visited first // and then the combinator if there is one. if (node.getCombinator() != null) { stateStack.push(new VisitCombinatorState(node.getCombinator())); } stateStack.push(new VisitRefinerListState(node.getRefiners())); visitedChildren = true; } else { stateStack.pop(); } } } @VisibleForTesting class VisitKeyState extends BaseVisitState { private final CssKeyNode node; private boolean visitedChildren = false; VisitKeyState(CssKeyNode node) { this.node = node; } @Override public void doVisit() { if (!visitedChildren) { visitor.enterKey(node); } else { visitor.leaveKey(node); } } @Override public void transitionToNextState() { if (!visitedChildren) { visitedChildren = true; } else { stateStack.pop(); } } } @VisibleForTesting class VisitRefinerListState extends VisitReplaceChildrenState { VisitRefinerListState(CssRefinerListNode node) { super(node); } } @VisibleForTesting class VisitRefinerNodeState extends BaseVisitState { private final CssRefinerNode node; private boolean visitedChildren = false; VisitRefinerNodeState(CssRefinerNode node) { this.node = node; } @Override public void doVisit() { // TODO(fbenz): Actually each of these nodes should have its own state // here but this adds a bunch of similar code that is not really // necessary. The problem is the design of the visit controller. I'm // going to refactor it so that it doesn't make sense to add all the // states. if (!visitedChildren) { if (node instanceof CssClassSelectorNode) { visitor.enterClassSelector((CssClassSelectorNode) node); } else if (node instanceof CssIdSelectorNode) { visitor.enterIdSelector((CssIdSelectorNode) node); } else if (node instanceof CssPseudoClassNode) { visitor.enterPseudoClass((CssPseudoClassNode) node); } else if (node instanceof CssPseudoElementNode) { visitor.enterPseudoElement((CssPseudoElementNode) node); } else if (node instanceof CssAttributeSelectorNode) { visitor.enterAttributeSelector((CssAttributeSelectorNode) node); } } else { if (node instanceof CssClassSelectorNode) { visitor.leaveClassSelector((CssClassSelectorNode) node); } else if (node instanceof CssIdSelectorNode) { visitor.leaveIdSelector((CssIdSelectorNode) node); } else if (node instanceof CssPseudoClassNode) { visitor.leavePseudoClass((CssPseudoClassNode) node); } else if (node instanceof CssPseudoElementNode) { visitor.leavePseudoElement((CssPseudoElementNode) node); } else if (node instanceof CssAttributeSelectorNode) { visitor.leaveAttributeSelector((CssAttributeSelectorNode) node); } } } @Override public void transitionToNextState() { if (!visitedChildren) { if (node instanceof CssPseudoClassNode) { CssPseudoClassNode pseudoClass = (CssPseudoClassNode) node; if (pseudoClass.getNotSelector() != null) { stateStack.push(new VisitSelectorState( pseudoClass.getNotSelector())); } } visitedChildren = true; } else { stateStack.pop(); } } } @VisibleForTesting class VisitCombinatorState extends BaseVisitState { private final CssCombinatorNode node; private boolean visitedChildren = false; VisitCombinatorState(CssCombinatorNode node) { this.node = node; } @Override public void doVisit() { if (!visitedChildren) { visitor.enterCombinator(node); } else { visitor.leaveCombinator(node); } } @Override public void transitionToNextState() { if (!visitedChildren) { stateStack.push(new VisitSelectorState(node.getSelector())); visitedChildren = true; } else { stateStack.pop(); } } } @VisibleForTesting class VisitDeclarationBlockState extends BaseVisitState { private final CssDeclarationBlockNode node; private boolean startedVisitingChildren = false; private boolean finishedVisitingChildren = false; @VisibleForTesting int currentIndex = -1; private boolean doNotIncreaseIndex = false; VisitDeclarationBlockState(CssDeclarationBlockNode block) { this.node = block; } @Override public void doVisit() { if (!startedVisitingChildren) { visitor.enterDeclarationBlock(node); startedVisitingChildren = true; } else if (finishedVisitingChildren) { visitor.leaveDeclarationBlock(node); } } @Override public void removeCurrentChild() { node.removeChildAt(currentIndex); doNotIncreaseIndex = true; } @Override public void replaceCurrentBlockChildWith( List replacementNodes, boolean visitTheReplacementNodes) { node.replaceChildAt(currentIndex, replacementNodes); if (visitTheReplacementNodes) { doNotIncreaseIndex = true; } else { currentIndex += replacementNodes.size() - 1; } } @Override public void transitionToNextState() { if (finishedVisitingChildren) { stateStack.pop(); return; } // We finish visiting this state if we are on the last child and we are // allowed to increment the index, which means we arrived here with no // special case, or we have just removed the last element. if ((currentIndex == node.numChildren() - 1 && !doNotIncreaseIndex) || currentIndex == node.numChildren()) { finishedVisitingChildren = true; return; } // Remain in this state to finish visiting all the children. if (!doNotIncreaseIndex) { currentIndex++; } else { doNotIncreaseIndex = false; } VisitState state = createVisitState(node.getChildAt(currentIndex), this); if (state != null) { stateStack.push(state); } } } @VisibleForTesting class VisitDeclarationState extends BaseVisitState { private final CssDeclarationNode node; private boolean visitedChildren = false; VisitDeclarationState(CssDeclarationNode node) { this.node = node; } @Override public void doVisit() { if (!visitedChildren) { visitor.enterDeclaration(node); } else { visitor.leaveDeclaration(node); } } @Override public void transitionToNextState() { if (!visitedChildren) { stateStack.push(new VisitPropertyValueState(node.getPropertyValue())); visitedChildren = true; } else { stateStack.pop(); } } @Override public void removeCurrentChild() { stateStack.pop(); stateStack.getTop().removeCurrentChild(); } } @VisibleForTesting class VisitMixinState extends BaseVisitState { private final CssMixinNode node; private boolean visitedChildren = false; VisitMixinState(CssMixinNode node) { this.node = node; } @Override public void doVisit() { if (!visitedChildren) { visitor.enterMixin(node); } else { visitor.leaveMixin(node); } } @Override public void transitionToNextState() { if (!visitedChildren) { stateStack.push( new VisitFunctionArgumentsNodeState(node.getArguments())); visitedChildren = true; } else { stateStack.pop(); } } } @VisibleForTesting class VisitPropertyValueState extends BaseVisitState { private final CssPropertyValueNode node; private boolean visitedChildren = false; private boolean visitingChildren = false; private int currentIndex = -1; private boolean doNotIncreaseIndex = false; VisitPropertyValueState(CssPropertyValueNode node) { this.node = node; } @Override public void doVisit() { if (!visitingChildren) { if (!visitedChildren) { visitor.enterPropertyValue(node); } else { visitor.leavePropertyValue(node); } } } @Override public void removeCurrentChild() { node.removeChildAt(currentIndex); doNotIncreaseIndex = true; } @Override public void replaceCurrentBlockChildWith( List replacementNodes, boolean visitTheReplacementNodes) { // If we're replacing the current property with a composite value // separated by spaces, we really just want to graft those nodes at the // current child's location. if (replacementNodes.size() == 1 && replacementNodes.get(0) instanceof CssCompositeValueNode) { CssCompositeValueNode compositeValueNode = (CssCompositeValueNode) replacementNodes.get(0); if (compositeValueNode.getOperator() == Operator.SPACE) { replacementNodes = compositeValueNode.getValues(); } } node.replaceChildAt(currentIndex, replacementNodes); if (visitTheReplacementNodes) { doNotIncreaseIndex = true; } else { currentIndex += replacementNodes.size() - 1; } } @Override public void transitionToNextState() { // We get out of this state if we are on the last child and we are allowed // to increment the index, which means we arrived here with no special // case, or we have just removed the last element. if ((currentIndex == node.numChildren() - 1 && !doNotIncreaseIndex) || currentIndex == node.numChildren()) { if (visitedChildren) { stateStack.pop(); } else { visitingChildren = false; visitedChildren = true; } return; } // Remain in this state to finish visiting all the children. visitingChildren = true; if (!doNotIncreaseIndex) { currentIndex++; } else { doNotIncreaseIndex = false; } VisitState state = createVisitState(node.getChildAt(currentIndex), this); if (state != null) { stateStack.push(state); } } @Override public VisitState createFallbackState(CssValueNode child) { return new VisitValueNodeState(child); } } @VisibleForTesting class VisitValueNodeState extends BaseVisitState { private final CssValueNode node; VisitValueNodeState(CssValueNode node) { this.node = node; } @Override public void doVisit() { visitor.enterValueNode(node); visitor.leaveValueNode(node); } @Override public void transitionToNextState() { stateStack.pop(); } } @VisibleForTesting class VisitCompositeValueState extends BaseVisitState { private final CssCompositeValueNode node; private final List children; private int currentIndex = -1; private boolean doNotIncreaseIndex = false; private boolean visitChildren = true; private boolean intervalueStateIsNext = false; VisitCompositeValueState(CssCompositeValueNode node) { this.node = node; this.children = node.getValues(); } @Override public void transitionToNextState() { if (currentIndex == children.size() - 1) { stateStack.pop(); return; } if (visitChildren == false) { currentIndex = children.size() - 1; return; } // Remain in this state to finish visiting all the children if (intervalueStateIsNext) { stateStack.push(new IntervalueState(node)); intervalueStateIsNext = false; return; } if (!doNotIncreaseIndex) { currentIndex++; } else { doNotIncreaseIndex = false; } stateStack.push(createVisitState(children.get(currentIndex), this)); intervalueStateIsNext = true; return; } @Override public void removeCurrentChild() { children.remove(currentIndex); intervalueStateIsNext = false; doNotIncreaseIndex = true; if (currentIndex == children.size()) { stateStack.pop(); } } @Override public VisitState createFallbackState(CssValueNode child) { return new VisitValueNodeState(child); } /** {@inheritDoc} */ @Override public void doVisit() { if (currentIndex < 0) { visitChildren = visitor.enterCompositeValueNode(node); } else if (currentIndex == children.size() - 1) { visitor.leaveCompositeValueNode(node); } } @Override public void replaceCurrentBlockChildWith( List replacementNodes, boolean visitTheReplacementNodes) { children.remove(currentIndex); // If we're replacing the current property with a composite value // separated by the same operator, we really just want to graft those // nodes at the current child's location. if (replacementNodes.size() == 1 && replacementNodes.get(0) instanceof CssCompositeValueNode) { CssCompositeValueNode compositeValueNode = (CssCompositeValueNode) replacementNodes.get(0); if (compositeValueNode.getOperator() == node.getOperator()) { replacementNodes = compositeValueNode.getValues(); } } children.addAll(currentIndex, replacementNodes); if (!visitTheReplacementNodes) { currentIndex += replacementNodes.size() - 1; } else { doNotIncreaseIndex = true; } } } class IntervalueState extends BaseVisitState { private final CssCompositeValueNode parent; IntervalueState(CssCompositeValueNode parent) { this.parent = parent; } @Override public void doVisit() { visitor.enterCompositeValueNodeOperator(parent); visitor.leaveCompositeValueNodeOperator(parent); } @Override public void transitionToNextState() { stateStack.pop(); } } @VisibleForTesting class VisitFunctionNodeState extends BaseVisitState { private final CssFunctionNode node; private boolean visitedChildren = false; VisitFunctionNodeState(CssFunctionNode node) { this.node = node; } @Override public void doVisit() { if (!visitedChildren) { visitor.enterFunctionNode(node); } else { visitor.leaveFunctionNode(node); } } @Override public void transitionToNextState() { if (!visitedChildren) { stateStack.push( new VisitFunctionArgumentsNodeState(node.getArguments())); visitedChildren = true; } else { stateStack.pop(); } } @Override public void removeCurrentChild() { node.setArguments(new CssFunctionArgumentsNode()); } @Override @SuppressWarnings("unchecked") public void removeCurrentNodeCalled() { // If the function is a singleton, remove the nearest declaration that // contains it. popToNonDegenerateState(); stateStack.getTop().removeCurrentChild(); } /** * Exit states until we reach the nearest ancestor that will not * be made degenerate by the removal of its current child. * *

E.g., if this node's parent's role is to represent a collection of * children, and this node has no siblings, then we want to remove * the parent, and the transitive closure. So for example if we have * div { * background: url('http://google.com/logo') * } * and we remove the url function node, then we should not leave * div { * background: * } * but rather should remove background as well. */ private void popToNonDegenerateState() { for (CssNode child = node; true; child = child.getParent()) { // will removing the child leave the tree in a bad state? boolean otherSiblingsExist; if (child instanceof CssDeclarationNode) { // it's just too hard, so stop removing ancestors if we // get this high. otherSiblingsExist = true; } else if (child instanceof CssNodesListNode) { otherSiblingsExist = ((CssNodesListNode) child).numChildren() > 1; } else if (child instanceof CssCompositeValueNode) { otherSiblingsExist = ((CssCompositeValueNode) child).getValues().size() > 1; } else if (child instanceof CssDeclarationNode) { // there's always just one CssPropertyValueNode otherSiblingsExist = false; } else if (child instanceof CssDeclarationNode) { otherSiblingsExist = false; } else if (child instanceof CssFunctionNode) { otherSiblingsExist = false; } else { break; } if (otherSiblingsExist) { break; } // TODO(user): refactor the preceding giant conditional branch to // use dynamic dispatch. Maybe each node state should just have a // locally-sane predicate to verify that its own properties are in // good shape. Then we could just remove and pop our way up to // sanity. // TODO(user): verify that the stateStack.getTop() corresponds to // node. I think the only VisitState implementation whose ctor does // not demand a corresponding CssNode is the IntervalueState, so we // can add a method to get the node and then bail on this loop if // the result is either null or inconsistent with the current child. stateStack.pop(); } } } @VisibleForTesting class VisitFunctionArgumentsNodeState extends VisitReplaceChildrenState { VisitFunctionArgumentsNodeState(CssFunctionArgumentsNode node) { super(node); } @Override public void replaceCurrentBlockChildWith( List replacementNodes, boolean visitTheReplacementNodes) { // If we're replacing the current property with a composite value // separated by the space, we really just want to graft those nodes at // the current child's location. if (replacementNodes.size() == 1 && replacementNodes.get(0) instanceof CssCompositeValueNode) { CssCompositeValueNode compositeValueNode = (CssCompositeValueNode) replacementNodes.get(0); if (compositeValueNode.getOperator() == Operator.SPACE) { replacementNodes = compositeValueNode.getValues(); } } super.replaceCurrentBlockChildWith( replacementNodes, visitTheReplacementNodes); } @Override public VisitState createFallbackState(CssValueNode child) { return new VisitFunctionArgumentNodeState(child); } } @VisibleForTesting class VisitFunctionArgumentNodeState extends BaseVisitState { private final CssValueNode node; VisitFunctionArgumentNodeState(CssValueNode node) { this.node = node; } @Override public void doVisit() { visitor.enterArgumentNode(node); visitor.leaveArgumentNode(node); } @Override public void transitionToNextState() { stateStack.pop(); } } @VisibleForTesting class VisitComponentState extends VisitChildrenOptionalState { private final CssComponentNode node; private boolean visitedChildren = false; private boolean shouldVisitChildren = true; VisitComponentState(CssComponentNode node) { this.node = node; } @Override public void doVisit() { if (!visitedChildren) { shouldVisitChildren = visitor.enterComponent(node); } else { visitor.leaveComponent(node); } } @Override public void transitionToNextState() { if (!visitedChildren && shouldVisitChildren) { stateStack.push(new VisitComponentChildrenState(node.getBlock())); visitedChildren = true; } else { stateStack.pop(); } } } @VisibleForTesting class VisitComponentChildrenState extends VisitReplaceChildrenState { VisitComponentChildrenState(CssBlockNode block) { super(block); } } @VisibleForTesting class VisitForLoopRuleState extends BaseVisitState { private final CssForLoopRuleNode node; private boolean visitedChildren = false; private boolean shouldVisitChildren = true; VisitForLoopRuleState(CssForLoopRuleNode node) { this.node = node; } @Override public void doVisit() { if (!visitedChildren) { shouldVisitChildren = visitor.enterForLoop(node); } else { visitor.leaveForLoop(node); } } @Override public void transitionToNextState() { if (!visitedChildren && shouldVisitChildren) { stateStack.push(new VisitBlockChildrenState(node.getBlock())); visitedChildren = true; } else { stateStack.pop(); } } } @VisibleForTesting class VisitUnknownAtRuleState extends VisitChildrenOptionalState { private final CssUnknownAtRuleNode node; private boolean visitedChildren = false; private boolean shouldVisitChildren = true; private int currentIndex = -1; VisitUnknownAtRuleState(CssUnknownAtRuleNode node) { this.node = node; } @Override public void doVisit() { if (!visitedChildren && currentIndex == -1) { shouldVisitChildren = visitor.enterUnknownAtRule(node); } else if (visitedChildren) { visitor.leaveUnknownAtRule(node); } } @Override public void transitionToNextState() { if (visitedChildren || !shouldVisitChildren) { stateStack.pop(); return; } ++currentIndex; final int parametersCount = node.getParameters().size(); if (currentIndex < parametersCount) { if (currentIndex < parametersCount - 1) { stateStack.push(new VisitMediaTypeListDelimiterState(node)); } stateStack.push(getVisitState(node.getParameters().get(currentIndex))); } else { if (node.getType().hasBlock()) { stateStack.push(new VisitUnknownAtRuleBlockState(node.getBlock())); } visitedChildren = true; } } /** * Returns a visit state for a given child node. * * @param node child node for which to create visit state * @return new visit state */ public VisitState getVisitState(CssValueNode node) { if (node instanceof CssCompositeValueNode) { return new VisitCompositeValueState((CssCompositeValueNode) node); } else { return new VisitValueNodeState(node); } } } @VisibleForTesting class VisitUnknownAtRuleBlockState extends VisitChildrenOptionalState { private final CssAbstractBlockNode body; private boolean visitedChildren = false; private boolean shouldVisitChildren = true; VisitUnknownAtRuleBlockState(CssAbstractBlockNode body) { this.body = body; } @Override public void doVisit() { if (!visitedChildren) { if (body instanceof CssBlockNode) { shouldVisitChildren = visitor.enterBlock((CssBlockNode) body); } } else { if (body instanceof CssBlockNode) { visitor.leaveBlock((CssBlockNode) body); } } } @Override public void transitionToNextState() { if (!visitedChildren && shouldVisitChildren) { if (body instanceof CssBlockNode) { stateStack.push(new VisitBlockChildrenState((CssBlockNode) body)); } else if (body instanceof CssDeclarationBlockNode) { stateStack.push( new VisitDeclarationBlockState((CssDeclarationBlockNode) body)); } visitedChildren = true; } else { stateStack.pop(); } } } @VisibleForTesting class VisitWebkitKeyframesState extends VisitChildrenOptionalState { private final CssKeyframesNode node; private boolean visitedChildren = false; private boolean shouldVisitChildren = true; VisitWebkitKeyframesState(CssKeyframesNode node) { this.node = node; } @Override public void doVisit() { if (!visitedChildren) { shouldVisitChildren = visitor.enterKeyframesRule(node); } else { visitor.leaveKeyframesRule(node); } } @Override public void transitionToNextState() { if (!visitedChildren && shouldVisitChildren) { if (node.getType().hasBlock()) { stateStack.push(new VisitUnknownAtRuleBlockState(node.getBlock())); } visitedChildren = true; } else { stateStack.pop(); } } } @VisibleForTesting class VisitMixinDefinitionState extends BaseVisitState { private final CssMixinDefinitionNode node; private boolean visitedChildren = false; VisitMixinDefinitionState(CssMixinDefinitionNode node) { this.node = node; } @Override public void doVisit() { if (!visitedChildren) { visitor.enterMixinDefinition(node); } else { visitor.leaveMixinDefinition(node); } } @Override public void transitionToNextState() { if (!visitedChildren) { stateStack.push(new VisitDeclarationBlockState(node.getBlock())); visitedChildren = true; } else { stateStack.pop(); } } } // TODO(oana): Maybe add a generic utility class for Stack than can be used in // CssTreeBuilder too. @VisibleForTesting static class StateStack { Deque> stack = Queues.newArrayDeque(); VisitState getTop() { return stack.peek(); } void push(VisitState state) { Preconditions.checkNotNull(state); stack.addFirst(state); } void pop() { stack.removeFirst(); } void transitionTo(VisitState state) { pop(); push(state); } int size() { return stack.size(); } boolean isEmpty() { return stack.isEmpty(); } } public DefaultVisitController(CssNode subtree, boolean allowMutating) { Preconditions.checkNotNull(subtree); this.subtree = subtree; this.allowMutating = allowMutating; } public DefaultVisitController(CssTree tree, boolean allowMutating) { this(tree.getRoot(), allowMutating); } public StateStack getStateStack() { return stateStack; } @Override public void removeCurrentNode() { Preconditions.checkState(allowMutating); stateStack.getTop().removeCurrentNodeCalled(); } @Override public void replaceCurrentBlockChildWith( List replacementNodes, boolean visitTheReplacementNodes) { Preconditions.checkState(allowMutating); @SuppressWarnings("unchecked") VisitState stackTop = (VisitState) stateStack.getTop(); stackTop.replaceCurrentBlockChildWithCalled( replacementNodes, visitTheReplacementNodes); } @Override public void startVisit(CssTreeVisitor treeVisitor) { Preconditions.checkNotNull(treeVisitor); this.visitor = treeVisitor; stateStack.push(createVisitStateInternal(subtree)); while (!stateStack.isEmpty()) { try { stateStack.getTop().doVisit(); stateStack.getTop().transitionToNextState(); } catch (StopVisitRequestedException e) { // We stop visiting. // assert stopVisitCalled; break; } } } @Override public void stopVisit() { stopVisitCalled = true; stateStack.getTop().stopVisitCalled(); throw new StopVisitRequestedException(); } /** * Factory method to create visit state for a child node. Class of the visit * state depends on the child node class. * * @param child node type * @param child node for which the visit state is created * @param fallbackStateSource object which will create the fallback visit * state if the factory cannot create the state for a given child node * @return created visit state or {@code null} if state cannot be created by * the factory or the fallback factory */ private VisitState createVisitState( T child, BaseVisitState fallbackStateSource) { VisitState state = createVisitStateInternal(child); return (state == null) ? fallbackStateSource.createFallbackState(child) : state; } private VisitState createVisitStateInternal(CssNode child) { // VisitProvideState if (child instanceof CssProvideNode) { return new VisitProvideState((CssProvideNode) child); } // VisitRequireState if (child instanceof CssRequireNode) { return new VisitRequireState((CssRequireNode) child); } // VisitUnknownAtRuleBlockState if (child instanceof CssMediaRuleNode) { return new VisitMediaRuleState((CssMediaRuleNode) child); } // VisitUnknownAtRuleBlockState if (child instanceof CssPageRuleNode) { return new VisitPageRuleState((CssPageRuleNode) child); } // VisitUnknownAtRuleBlockState if (child instanceof CssPageSelectorNode) { return new VisitPageSelectorState((CssPageSelectorNode) child); } // VisitUnknownAtRuleBlockState if (child instanceof CssFontFaceNode) { return new VisitFontFaceState((CssFontFaceNode) child); } // VisitUnknownAtRuleBlockState if (child instanceof CssImportRuleNode) { return new VisitImportRuleState((CssImportRuleNode) child); } if (child instanceof CssComponentNode) { return new VisitComponentState((CssComponentNode) child); } // VisitRefinerListState if (child instanceof CssRefinerNode) { return new VisitRefinerNodeState((CssRefinerNode) child); } // VisitDeclarationBlockState if (child instanceof CssDeclarationNode) { return new VisitDeclarationState((CssDeclarationNode) child); } // VisitDeclarationBlockState if (child instanceof CssMixinNode) { return new VisitMixinState((CssMixinNode) child); } if (child instanceof CssForLoopRuleNode) { return new VisitForLoopRuleState((CssForLoopRuleNode) child); } // VisitBlockChildrenState // VisitComponentChildrenState, VisitUnknownAtRuleChildrenState if (child instanceof CssUnknownAtRuleNode) { return new VisitUnknownAtRuleState((CssUnknownAtRuleNode) child); } // VisitUnknownAtRuleBlockState if (child instanceof CssKeyframesNode) { return new VisitWebkitKeyframesState((CssKeyframesNode) child); } // VisitKeyBlockState if (child instanceof CssKeyframeRulesetNode) { return new VisitKeyframeRulesetState((CssKeyframeRulesetNode) child); } // VisitComponentChildrenState, VisitUnknownAtRuleChildrenState // VisitBlockChildrenState, VisitConditionalRuleChildrenState if (child instanceof CssConditionalBlockNode) { return new VisitConditionalBlockState((CssConditionalBlockNode) child); } if (child instanceof CssRulesetNode) { return new VisitRulesetState((CssRulesetNode) child); } if (child instanceof CssDefinitionNode) { return new VisitDefinitionState((CssDefinitionNode) child); } // VisitDefinitionParametersState, VisitPropertyValueState, VisitFunctionArgumentsNodeState if (child instanceof CssFunctionNode) { return new VisitFunctionNodeState((CssFunctionNode) child); } if (child instanceof CssMixinDefinitionNode) { return new VisitMixinDefinitionState((CssMixinDefinitionNode) child); } if (child instanceof CssCompositeValueNode) { return new VisitCompositeValueState((CssCompositeValueNode) child); } if (child instanceof CssPropertyValueNode) { return new VisitPropertyValueState((CssPropertyValueNode) child); } if (child instanceof CssRootNode) { return new RootVisitBeforeChildrenState((CssRootNode) child); } return null; } }