/* * 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 oana@google.com (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 <T> type of the children CSS nodes that can be used as a replacement * for currently visited block node */ @VisibleForTesting interface VisitState<T extends CssNode> { /** * 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.<p> * * <p>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. * * <p>NOTE(dgajda): This method probably does not need to be in VisitState. * * @param <S> type od replacement nodes * @param replacementNodes nodes used to replace current block * @param visitTheReplacementNodes whether new nodes should also be visited */ <S extends CssNode> void replaceCurrentBlockChildWithCalled( List<S> 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<T> replacementNodes, boolean visitTheReplacementNodes); } /** * Base implementation of AST visit state. * * @param <N> type of the children CSS nodes that can be used as a replacement * for currently visited block node */ @VisibleForTesting abstract class BaseVisitState<N extends CssNode> implements VisitState<N> { @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 <S extends CssNode> void replaceCurrentBlockChildWithCalled( List<S> replacementNodes, boolean visitTheReplacementNodes) { stateStack.pop(); @SuppressWarnings("unchecked") VisitState<S> topState = (VisitState<S>) stateStack.getTop(); topState.replaceCurrentBlockChildWith( replacementNodes, visitTheReplacementNodes); } @Override public void replaceCurrentBlockChildWith( List<N> replacementNodes, boolean visitTheReplacementNodes) { // Assume that by default this cannot happen. // assert false; } public VisitState<? extends CssNode> createFallbackState(N child) { return null; } } /** * Base class for VisitStates which control visits of {@link CssNodesListNode} * children. * * @param <T> type of the children CSS nodes that can be used as a replacement * for currently visited block node */ abstract class VisitChildrenState<T extends CssNode> extends BaseVisitState<CssNode> { private final CssNodesListNode<T> block; private int currentIndex = -1; VisitChildrenState(CssNodesListNode<T> 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<CssNode> 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 <T> type of the children CSS nodes that can be used as a replacement * for currently visited block node */ abstract class VisitReplaceChildrenState<T extends CssNode> extends BaseVisitState<T> { protected int currentIndex = -1; protected boolean doNotIncreaseIndex = false; protected final CssNodesListNode<T> node; VisitReplaceChildrenState(CssNodesListNode<T> node) { this.node = node; } @Override public void doVisit() { // Does nothing. } @Override public void removeCurrentChild() { node.removeChildAt(currentIndex); doNotIncreaseIndex = true; } @Override public void replaceCurrentBlockChildWith( List<T> 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<? extends CssNode> 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 <T> type of the children CSS nodes that can be used as a replacement * for currently visited block node */ abstract class VisitChildrenOptionalState<T extends CssNode> extends BaseVisitState<CssNode> { // TODO(user): move the common code here or delete this // useless complexity. } @VisibleForTesting class RootVisitBeforeChildrenState extends BaseVisitState<CssNode> { 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<CssNode> { 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<CssNode> { 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<CssNode> { 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<CssImportRuleNode> { VisitImportBlockChildrenState(CssImportBlockNode block) { super(block); } @Override VisitState<CssNode> getVisitState(CssImportRuleNode node) { return new VisitImportRuleState(node); } } @VisibleForTesting class VisitImportRuleState extends BaseVisitState<CssNode> { 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<CssNode> { 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<CssNode> { 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<CssNode> { 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<CssNode> { VisitBlockChildrenState(CssBlockNode block) { super(block); } } @VisibleForTesting class VisitDefinitionState extends VisitChildrenOptionalState<CssNode> { 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<CssValueNode> { VisitDefinitionParametersState(CssDefinitionNode def) { super(def); } @Override public VisitState<CssValueNode> createFallbackState(CssValueNode child) { return new VisitValueNodeState(child); } } @VisibleForTesting class VisitMediaRuleState extends VisitReplaceChildrenState<CssValueNode> { 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<CssValueNode> 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<? extends CssNode> getVisitState(CssValueNode node) { if (node instanceof CssCompositeValueNode) { return new VisitCompositeValueState((CssCompositeValueNode) node); } else { return new VisitValueNodeState(node); } } } private class VisitMediaTypeListDelimiterState extends BaseVisitState<CssNode> { private final CssNodesListNode<? extends CssNode> node; public VisitMediaTypeListDelimiterState( CssNodesListNode<? extends CssNode> 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<CssNode> { 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<CssNode> { 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<CssNode> { 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<CssNode> { 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<CssConditionalRuleNode> { VisitConditionalBlockChildrenState(CssConditionalBlockNode block) { super(block); } @Override VisitState<CssNode> getVisitState(CssConditionalRuleNode node) { return new VisitConditionalRuleState(node); } } @VisibleForTesting class VisitConditionalRuleState extends VisitChildrenOptionalState<CssNode> { 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<CssNode> { VisitConditionalRuleChildrenState(CssAbstractBlockNode block) { super(block); } } @VisibleForTesting class VisitRulesetState extends VisitChildrenOptionalState<CssNode> { 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<CssNode> { 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<CssNode> { 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<CssSelectorNode> { VisitSelectorBlockChildrenState(CssSelectorListNode block) { super(block); } @Override VisitState<CssNode> getVisitState(CssSelectorNode node) { return new VisitSelectorState(node); } } @VisibleForTesting class VisitKeyBlockState extends BaseVisitState<CssNode> { 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<CssKeyNode> { VisitKeyBlockChildrenState(CssKeyListNode block) { super(block); } @Override VisitState<CssNode> getVisitState(CssKeyNode node) { return new VisitKeyState(node); } } @VisibleForTesting class VisitSelectorState extends BaseVisitState<CssNode> { 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<CssNode> { 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<CssRefinerNode> { VisitRefinerListState(CssRefinerListNode node) { super(node); } } @VisibleForTesting class VisitRefinerNodeState extends BaseVisitState<CssNode> { 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<CssNode> { 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<CssNode> { 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<CssNode> 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<? extends CssNode> state = createVisitState(node.getChildAt(currentIndex), this); if (state != null) { stateStack.push(state); } } } @VisibleForTesting class VisitDeclarationState extends BaseVisitState<CssNode> { 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<CssNode> { 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<CssValueNode> { 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<CssValueNode> 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<? extends CssNode> state = createVisitState(node.getChildAt(currentIndex), this); if (state != null) { stateStack.push(state); } } @Override public VisitState<? extends CssNode> createFallbackState(CssValueNode child) { return new VisitValueNodeState(child); } } @VisibleForTesting class VisitValueNodeState extends BaseVisitState<CssValueNode> { 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<CssValueNode> { private final CssCompositeValueNode node; private final List<CssValueNode> 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<CssValueNode> 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<CssValueNode> 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<CssNode> { 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<CssNode> { 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. * * <p>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<CssValueNode> { VisitFunctionArgumentsNodeState(CssFunctionArgumentsNode node) { super(node); } @Override public void replaceCurrentBlockChildWith( List<CssValueNode> 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<CssNode> createFallbackState(CssValueNode child) { return new VisitFunctionArgumentNodeState(child); } } @VisibleForTesting class VisitFunctionArgumentNodeState extends BaseVisitState<CssNode> { 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<CssNode> { 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<CssNode> { VisitComponentChildrenState(CssBlockNode block) { super(block); } } @VisibleForTesting class VisitForLoopRuleState extends BaseVisitState<CssNode> { 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<CssNode> { 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<? extends CssNode> getVisitState(CssValueNode node) { if (node instanceof CssCompositeValueNode) { return new VisitCompositeValueState((CssCompositeValueNode) node); } else { return new VisitValueNodeState(node); } } } @VisibleForTesting class VisitUnknownAtRuleBlockState extends VisitChildrenOptionalState<CssNode> { 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<CssNode> { 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<CssNode> { 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<VisitState<? extends CssNode>> stack = Queues.newArrayDeque(); VisitState<? extends CssNode> getTop() { return stack.peek(); } void push(VisitState<? extends CssNode> state) { Preconditions.checkNotNull(state); stack.addFirst(state); } void pop() { stack.removeFirst(); } void transitionTo(VisitState<? extends CssNode> 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 <T extends CssNode> void replaceCurrentBlockChildWith( List<T> replacementNodes, boolean visitTheReplacementNodes) { Preconditions.checkState(allowMutating); @SuppressWarnings("unchecked") VisitState<T> stackTop = (VisitState<T>) 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 <T> 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 <T extends CssNode> VisitState<? extends CssNode> createVisitState( T child, BaseVisitState<T> fallbackStateSource) { VisitState<? extends CssNode> state = createVisitStateInternal(child); return (state == null) ? fallbackStateSource.createFallbackState(child) : state; } private VisitState<? extends CssNode> 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; } }