/* * 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.Lists; import com.google.common.css.SourceCode; import com.google.common.css.compiler.ast.CssBooleanExpressionNode.Type; import com.google.common.css.compiler.ast.CssCompositeValueNode.Operator; import com.google.common.css.compiler.ast.CssFunctionNode.Function; import java.util.Arrays; import java.util.List; /** * Use this to build one {@link CssTree} object. * * @author oana@google.com (Oana Florescu) */ public class CssTreeBuilder implements CssParserEventHandler, CssParserEventHandler.ImportHandler, CssParserEventHandler.MediaHandler, CssParserEventHandler.ExpressionHandler, CssParserEventHandler.BooleanExpressionHandler { enum State { BEFORE_DOCUMENT_START, BEFORE_MAIN_BODY, INSIDE_IMPORT_RULE, INSIDE_MAIN_BODY, INSIDE_MEDIA_RULE, INSIDE_BLOCK, INSIDE_DECLARATION_BLOCK, INSIDE_PROPERTY_EXPRESSION, INSIDE_EXPRESSION_AFTER_OPERATOR, INSIDE_CONDITIONAL_BLOCK, BEFORE_BOOLEAN_EXPRESSION, INSIDE_BOOLEAN_EXPRESSION, INSIDE_DEFINITION, INSIDE_COMMENT, DONE_BUILDING; } private CssTree tree = null; private boolean treeIsConstructed = false; // TODO(user): Use Collections.asLifoQueue(new ArrayDeque()) for openBlocks private List<CssAbstractBlockNode> openBlocks = null; private List<CssConditionalBlockNode> openConditionalBlocks = null; private CssDeclarationBlockNode declarationBlock = null; private CssDeclarationNode declaration = null; private CssDefinitionNode definition = null; private List<CssCommentNode> comments = null; private CssRulesetNode ruleset = null; private CssImportRuleNode importRule = null; private CssMediaRuleNode mediaRule = null; private StateStack stateStack = new StateStack(State.BEFORE_DOCUMENT_START); public CssTreeBuilder() { } //TODO(oana): Maybe add a generic utility class for Stack than can be used in // DefaultVisitController too. @VisibleForTesting static class StateStack { private List<State> stack; StateStack(State initialState) { stack = Lists.newArrayList(initialState); } void push(State state) { stack.add(state); } void pop() { stack.remove(stack.size() - 1); } void transitionTo(State newState) { pop(); push(newState); } boolean isIn(State... states) { return Arrays.asList(states).contains( stack.get(stack.size() - 1)); } int size() { return stack.size(); } } private void startMainBody() { if (stateStack.isIn(State.BEFORE_MAIN_BODY)) { stateStack.transitionTo(State.INSIDE_MAIN_BODY); } } private CssAbstractBlockNode getEnclosingBlock() { return openBlocks.get(openBlocks.size() - 1); } private void pushEnclosingBlock(CssAbstractBlockNode block) { openBlocks.add(block); } private void popEnclosingBlock() { openBlocks.remove(openBlocks.size() - 1); } private void endConditionalRuleChain() { if (!stateStack.isIn(State.INSIDE_CONDITIONAL_BLOCK)) { return; } Preconditions.checkState(!openConditionalBlocks.isEmpty()); CssConditionalBlockNode conditionalBlock = openConditionalBlocks.remove( openConditionalBlocks.size() - 1); stateStack.pop(); Preconditions.checkState(stateStack.isIn( State.INSIDE_BLOCK, State.INSIDE_MAIN_BODY, State.INSIDE_MEDIA_RULE, State.INSIDE_CONDITIONAL_BLOCK)); getEnclosingBlock().addChildToBack(conditionalBlock); } private CssConditionalBlockNode getEnclosingConditonalBlock() { return openConditionalBlocks.get(openConditionalBlocks.size() - 1); } private void appendToCurrentExpression(CssValueNode node) { if (stateStack.isIn(State.INSIDE_DEFINITION)) { Preconditions.checkState(definition != null); definition.addChildToBack(node); } else if (stateStack.isIn(State.INSIDE_PROPERTY_EXPRESSION)) { Preconditions.checkState(declaration != null); declaration.getPropertyValue().addChildToBack(node); } else if (stateStack.isIn(State.INSIDE_EXPRESSION_AFTER_OPERATOR)) { stateStack.pop(); if (stateStack.isIn(State.INSIDE_DEFINITION)) { Preconditions.checkState(definition != null); CssCompositeValueNode compositeNode = (CssCompositeValueNode) definition.getLastChild(); compositeNode.addValue(node); } else if (stateStack.isIn(State.INSIDE_PROPERTY_EXPRESSION)) { Preconditions.checkState(declaration != null); CssCompositeValueNode compositeNode = (CssCompositeValueNode) declaration.getPropertyValue() .getLastChild(); compositeNode.addValue(node); } } } private CssFunctionNode getFunctionFromCurrentExpression() { if (stateStack.isIn(State.INSIDE_DEFINITION)) { Preconditions.checkState(definition != null); return (CssFunctionNode) definition.getLastChild(); } else if (stateStack.isIn(State.INSIDE_PROPERTY_EXPRESSION)) { Preconditions.checkState(declaration != null); int size = declaration.getPropertyValue().numChildren(); return ((CssFunctionNode) declaration.getPropertyValue() .getChildAt(size - 1)); } else { return null; } } @Override public void onDocumentStart(SourceCode sourceCode) { Preconditions.checkState(stateStack.isIn(State.BEFORE_DOCUMENT_START)); Preconditions.checkNotNull(sourceCode); Preconditions.checkState(tree == null); tree = new CssTree(sourceCode); Preconditions.checkState(openBlocks == null); openBlocks = Lists.newArrayList(); openBlocks.add(tree.getRoot().getBody()); Preconditions.checkState(openConditionalBlocks == null); openConditionalBlocks = Lists.newArrayList(); Preconditions.checkState(comments == null); comments = Lists.newArrayList(); stateStack.transitionTo(State.BEFORE_MAIN_BODY); } @Override public void onDocumentEnd() { startMainBody(); endConditionalRuleChain(); Preconditions.checkState(stateStack.isIn(State.INSIDE_MAIN_BODY)); Preconditions.checkState(openBlocks.size() == 1); Preconditions.checkState(openBlocks.get(0) == tree.getRoot().getBody()); openBlocks = null; Preconditions.checkState(openConditionalBlocks.size() == 0); openConditionalBlocks = null; treeIsConstructed = true; stateStack.transitionTo(State.DONE_BUILDING); Preconditions.checkState(stateStack.size() == 1); } @VisibleForTesting public CssTree getTree() { Preconditions.checkState(stateStack.isIn(State.DONE_BUILDING)); Preconditions.checkState(treeIsConstructed); return tree; } @Override public ImportHandler onImportRuleStart() { Preconditions.checkState(stateStack.isIn(State.BEFORE_MAIN_BODY)); Preconditions.checkState(importRule == null); stateStack.push(State.INSIDE_IMPORT_RULE); importRule = new CssImportRuleNode(comments); comments.clear(); return this; } @Override public void appendImportParameter(ParserToken parameter) { Preconditions.checkState(stateStack.isIn(State.INSIDE_IMPORT_RULE)); CssValueNode importParameter = new CssLiteralNode( parameter.getToken(), parameter.getSourceCodeLocation()); importRule.addChildToBack(importParameter); } @Override public void onImportRuleEnd() { Preconditions.checkState(stateStack.isIn(State.INSIDE_IMPORT_RULE)); stateStack.pop(); tree.getRoot().getImportRules().addChildToBack(importRule); Preconditions.checkState(importRule != null); importRule = null; } @Override public MediaHandler onMediaRuleStart() { startMainBody(); endConditionalRuleChain(); Preconditions.checkState(stateStack.isIn(State.INSIDE_MAIN_BODY)); Preconditions.checkState(mediaRule == null); stateStack.push(State.INSIDE_MEDIA_RULE); mediaRule = new CssMediaRuleNode(comments); comments.clear(); pushEnclosingBlock(mediaRule.getBlock()); return this; } @Override public void appendMediaParameter(ParserToken parameter) { Preconditions.checkState(stateStack.isIn(State.INSIDE_MEDIA_RULE)); CssValueNode mediaParameter = new CssLiteralNode( parameter.getToken(), parameter.getSourceCodeLocation()); mediaRule.addChildToBack(mediaParameter); } @Override public void onMediaRuleEnd() { Preconditions.checkState(stateStack.isIn(State.INSIDE_MEDIA_RULE)); stateStack.pop(); mediaRule.setBlock(getEnclosingBlock()); popEnclosingBlock(); getEnclosingBlock().addChildToBack(mediaRule); Preconditions.checkState(mediaRule != null); mediaRule = null; } @Override public ExpressionHandler onDefinitionStart(ParserToken definitionName) { startMainBody(); endConditionalRuleChain(); Preconditions.checkState( stateStack.isIn( State.INSIDE_MAIN_BODY, State.INSIDE_MEDIA_RULE, State.INSIDE_BLOCK)); Preconditions.checkState(definition == null); stateStack.push(State.INSIDE_DEFINITION); CssLiteralNode name = new CssLiteralNode( definitionName.getToken(), definitionName.getSourceCodeLocation()); definition = new CssDefinitionNode(name, comments); comments.clear(); return this; } @Override public void onDefinitionEnd() { Preconditions.checkState(stateStack.isIn(State.INSIDE_DEFINITION)); stateStack.pop(); Preconditions.checkState(definition != null); getEnclosingBlock().addChildToBack(definition); definition = null; } @Override public void onCommentStart(ParserToken commentToken) { // Comments can be anywhere in the file, so there is not requirement for the // state. stateStack.push(State.INSIDE_COMMENT); comments.add(new CssCommentNode(commentToken.getToken(), commentToken.getSourceCodeLocation())); } @Override public void onCommentEnd() { Preconditions.checkState(stateStack.isIn(State.INSIDE_COMMENT)); stateStack.pop(); } @Override public void onRulesetStart(CssSelectorListNode selectorList) { startMainBody(); endConditionalRuleChain(); Preconditions.checkState( stateStack.isIn(State.INSIDE_MAIN_BODY, State.INSIDE_BLOCK, State.INSIDE_MEDIA_RULE)); Preconditions.checkState(ruleset == null); ruleset = new CssRulesetNode(comments); ruleset.setSourceCodeLocation(selectorList.getSourceCodeLocation()); ruleset.setSelectors(selectorList); comments.clear(); Preconditions.checkState(declarationBlock == null); declarationBlock = ruleset.getDeclarations(); stateStack.push(State.INSIDE_DECLARATION_BLOCK); } @Override public void onRulesetEnd() { Preconditions.checkState(stateStack.isIn(State.INSIDE_DECLARATION_BLOCK)); Preconditions.checkState(declarationBlock != null); Preconditions.checkState(ruleset != null); Preconditions.checkState(declarationBlock == ruleset.getDeclarations()); declarationBlock = null; stateStack.pop(); Preconditions.checkState(stateStack.isIn( State.INSIDE_DECLARATION_BLOCK, State.INSIDE_MAIN_BODY, State.INSIDE_BLOCK, State.INSIDE_MEDIA_RULE)); getEnclosingBlock().addChildToBack(ruleset); ruleset = null; } @Override public ExpressionHandler onDeclarationStart(ParserToken propertyName, boolean hasStarHack) { Preconditions.checkState(stateStack.isIn(State.INSIDE_DECLARATION_BLOCK)); CssPropertyNode name = new CssPropertyNode( propertyName.getToken(), propertyName.getSourceCodeLocation()); Preconditions.checkState(declaration == null); declaration = new CssDeclarationNode(name, comments); declaration.setStarHack(hasStarHack); comments.clear(); stateStack.push(State.INSIDE_PROPERTY_EXPRESSION); return this; } @Override public void onDeclarationEnd() { stateStack.pop(); Preconditions.checkState(stateStack.isIn(State.INSIDE_DECLARATION_BLOCK)); Preconditions.checkState(declaration != null); declarationBlock.addChildToBack(declaration); declaration = null; } @Override public void onLiteral(ParserToken expressionToken) { Preconditions.checkState(stateStack.isIn(State.INSIDE_PROPERTY_EXPRESSION, State.INSIDE_DEFINITION, State.INSIDE_EXPRESSION_AFTER_OPERATOR)); CssLiteralNode expression = new CssLiteralNode( expressionToken.getToken(), expressionToken.getSourceCodeLocation()); appendToCurrentExpression(expression); } @Override public void onOperator(ParserToken expressionToken) { Preconditions.checkState(stateStack.isIn(State.INSIDE_PROPERTY_EXPRESSION, State.INSIDE_DEFINITION, State.INSIDE_EXPRESSION_AFTER_OPERATOR)); // Make sure that the string we are passing as operator actually has one char Preconditions.checkArgument(expressionToken.getToken().length() == 1); // We are going to change the state unless it's a space operator if (!" ".equals(expressionToken.getToken())) { // We may need to construct the corresponding composite node if the last // one in the list is not a composite node or if it is not based on the // same operator CssValueNode lastChild = null; if (stateStack.isIn(State.INSIDE_DEFINITION)) { Preconditions.checkState(definition != null); lastChild = definition.removeLastChild(); } else if (stateStack.isIn(State.INSIDE_PROPERTY_EXPRESSION)) { Preconditions.checkState(declaration != null); lastChild = declaration.getPropertyValue().removeLastChild(); } if (!(lastChild instanceof CssCompositeValueNode) || ((CssCompositeValueNode) lastChild).getOperator().toString().equals( expressionToken.getToken())) { CssCompositeValueNode node = new CssCompositeValueNode( Lists.newArrayList(lastChild), Operator.valueOf(expressionToken.getToken().charAt(0)), null); appendToCurrentExpression(node); } else if (lastChild != null) { appendToCurrentExpression(lastChild); } stateStack.push(State.INSIDE_EXPRESSION_AFTER_OPERATOR); } } @Override public void onPriority(ParserToken priority) { Preconditions.checkState(stateStack.isIn(State.INSIDE_PROPERTY_EXPRESSION, State.INSIDE_DEFINITION, State.INSIDE_EXPRESSION_AFTER_OPERATOR)); CssPriorityNode expressionPriority = new CssPriorityNode( CssPriorityNode.PriorityType.IMPORTANT, priority.getSourceCodeLocation()); appendToCurrentExpression(expressionPriority); } @Override public void onColor(ParserToken color) { Preconditions.checkState(stateStack.isIn(State.INSIDE_PROPERTY_EXPRESSION, State.INSIDE_DEFINITION, State.INSIDE_EXPRESSION_AFTER_OPERATOR)); CssHexColorNode expression = new CssHexColorNode( color.getToken(), color.getSourceCodeLocation()); appendToCurrentExpression(expression); } @Override public void onNumericValue(ParserToken numericValue, ParserToken unit) { Preconditions.checkState(stateStack.isIn(State.INSIDE_PROPERTY_EXPRESSION, State.INSIDE_DEFINITION, State.INSIDE_EXPRESSION_AFTER_OPERATOR)); CssValueNode expression = new CssNumericNode( numericValue.getToken(), unit != null ? unit.getToken() : CssNumericNode.NO_UNITS, numericValue.getSourceCodeLocation()); appendToCurrentExpression(expression); } @Override public void onReference(ParserToken reference) { Preconditions.checkState(stateStack.isIn(State.INSIDE_PROPERTY_EXPRESSION, State.INSIDE_DEFINITION, State.INSIDE_EXPRESSION_AFTER_OPERATOR)); CssConstantReferenceNode expression = new CssConstantReferenceNode( reference.getToken(), reference.getSourceCodeLocation()); appendToCurrentExpression(expression); } @Override public void onFunction(ParserToken constant) { Preconditions.checkState(stateStack.isIn(State.INSIDE_PROPERTY_EXPRESSION, State.INSIDE_DEFINITION, State.INSIDE_EXPRESSION_AFTER_OPERATOR)); Function f = Function.byName(constant.getToken()); CssFunctionNode expression; if (f != null) { expression = new CssFunctionNode(f, constant.getSourceCodeLocation()); } else { expression = new CssCustomFunctionNode( constant.getToken() /* gssFunctionName */, constant.getSourceCodeLocation()); } appendToCurrentExpression(expression); } @Override public void onFunctionArgument(ParserToken term) { Preconditions.checkState(stateStack.isIn(State.INSIDE_PROPERTY_EXPRESSION, State.INSIDE_DEFINITION, State.INSIDE_EXPRESSION_AFTER_OPERATOR)); CssValueNode expression = new CssLiteralNode( term.getToken(), term.getSourceCodeLocation()); getFunctionFromCurrentExpression().getArguments() .addChildToBack(expression); } @Override public void onReferenceFunctionArgument(ParserToken term) { Preconditions.checkState(stateStack.isIn(State.INSIDE_PROPERTY_EXPRESSION, State.INSIDE_DEFINITION, State.INSIDE_EXPRESSION_AFTER_OPERATOR)); CssConstantReferenceNode expression = new CssConstantReferenceNode( term.getToken(), term.getSourceCodeLocation()); getFunctionFromCurrentExpression().getArguments() .addChildToBack(expression); } @Override public BooleanExpressionHandler onConditionalRuleStart( CssAtRuleNode.Type type, ParserToken ruleName) { startMainBody(); if (type == CssAtRuleNode.Type.IF) { endConditionalRuleChain(); Preconditions.checkState(stateStack.isIn( State.INSIDE_MEDIA_RULE, State.INSIDE_MAIN_BODY, State.INSIDE_BLOCK)); } else { Preconditions.checkState(stateStack.isIn( State.INSIDE_MEDIA_RULE, State.INSIDE_MAIN_BODY, State.INSIDE_BLOCK, State.INSIDE_CONDITIONAL_BLOCK)); } if (stateStack.isIn(State.INSIDE_CONDITIONAL_BLOCK)) { Preconditions.checkState(!openConditionalBlocks.isEmpty()); } else { CssConditionalBlockNode conditionalBlock = new CssConditionalBlockNode(comments); comments.clear(); openConditionalBlocks.add(conditionalBlock); stateStack.push(State.INSIDE_CONDITIONAL_BLOCK); } CssLiteralNode name = new CssLiteralNode( ruleName.getToken(), ruleName.getSourceCodeLocation()); CssConditionalRuleNode conditionalRule = new CssConditionalRuleNode(type, name); pushEnclosingBlock(conditionalRule.getBlock()); if (type != CssAtRuleNode.Type.ELSE) { stateStack.push(State.BEFORE_BOOLEAN_EXPRESSION); return this; } else { stateStack.push(State.INSIDE_BLOCK); return null; } } @Override public void onConditionalRuleEnd() { endConditionalRuleChain(); Preconditions.checkState(stateStack.isIn(State.INSIDE_BLOCK)); CssConditionalRuleNode conditionalRule = (CssConditionalRuleNode) getEnclosingBlock().getParent(); getEnclosingConditonalBlock().addChildToBack(conditionalRule); CssAtRuleNode.Type type = conditionalRule.getType(); popEnclosingBlock(); stateStack.pop(); if (type == CssAtRuleNode.Type.ELSE) { endConditionalRuleChain(); } Preconditions.checkState(stateStack.isIn( State.INSIDE_BLOCK, State.INSIDE_MAIN_BODY, State.INSIDE_MEDIA_RULE, State.INSIDE_CONDITIONAL_BLOCK)); } @Override public void onBooleanExpressionStart() { Preconditions.checkState(stateStack.isIn(State.BEFORE_BOOLEAN_EXPRESSION)); stateStack.transitionTo(State.INSIDE_BOOLEAN_EXPRESSION); } @Override public Object onConstant(ParserToken constantName) { Preconditions.checkState(stateStack.isIn(State.INSIDE_BOOLEAN_EXPRESSION)); return new CssBooleanExpressionNode(CssBooleanExpressionNode.Type.CONSTANT, constantName.getToken(), constantName.getSourceCodeLocation()); } @Override public Object onUnaryOperator(Type operator, ParserToken operatorToken, Object operand) { Preconditions.checkState(stateStack.isIn(State.INSIDE_BOOLEAN_EXPRESSION)); return new CssBooleanExpressionNode(operator, operatorToken.getToken(), (CssBooleanExpressionNode) operand, operatorToken.getSourceCodeLocation()); } @Override public Object onBinaryOperator(Type operator, ParserToken operatorToken, Object leftOperand, Object rightOperand) { Preconditions.checkState(stateStack.isIn(State.INSIDE_BOOLEAN_EXPRESSION)); return new CssBooleanExpressionNode(operator, operatorToken.getToken(), (CssBooleanExpressionNode) leftOperand, (CssBooleanExpressionNode) rightOperand, operatorToken.getSourceCodeLocation()); } @Override public void onBooleanExpressionEnd(Object topOperand) { Preconditions.checkState(stateStack.isIn(State.INSIDE_BOOLEAN_EXPRESSION)); CssConditionalRuleNode conditionalRule = (CssConditionalRuleNode) getEnclosingBlock().getParent(); conditionalRule.setCondition((CssBooleanExpressionNode) topOperand); stateStack.transitionTo(State.INSIDE_BLOCK); } }