/* * Copyright 2015 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.common.css.compiler.passes; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import com.google.common.css.compiler.ast.CssAtRuleNode; import com.google.common.css.compiler.ast.CssCompilerPass; import com.google.common.css.compiler.ast.CssForLoopRuleNode; import com.google.common.css.compiler.ast.CssLiteralNode; import com.google.common.css.compiler.ast.CssLoopVariableNode; import com.google.common.css.compiler.ast.CssNode; import com.google.common.css.compiler.ast.CssNumericNode; import com.google.common.css.compiler.ast.CssUnknownAtRuleNode; import com.google.common.css.compiler.ast.CssValueNode; import com.google.common.css.compiler.ast.DefaultTreeVisitor; import com.google.common.css.compiler.ast.ErrorManager; import com.google.common.css.compiler.ast.GssError; import com.google.common.css.compiler.ast.MutatingVisitController; import java.util.Stack; import java.util.regex.Pattern; /** * A compiler pass that replaces each {@code @for} with a {@link CssForLoopRuleNode}. */ public class CreateForLoopNodes extends DefaultTreeVisitor implements CssCompilerPass { @VisibleForTesting static final String SYNTAX_ERROR = "Invalid syntax for @for rule. Expected: " + "@for <IDENTIFIER> from <CONST|NUMBER> to <CONST|NUMBER>) [step <CONST|NUMBER>]"; @VisibleForTesting static final String ILLEGAL_VARIABLE_NAME = "Illegal variable name."; @VisibleForTesting static final String OVERRIDE_VARIABLE_NAME = "Overriding existing variable name."; private static final String FOR_NAME = CssAtRuleNode.Type.FOR.getCanonicalName(); private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\$[a-zA-Z_]\\w*"); private static final String FROM_KEYWORD = "from"; private static final String TO_KEYWORD = "to"; private static final String STEP_KEYWORD = "step"; private static final int VARIABLE_INDEX = 0; private static final int FROM_KEYWORD_INDEX = 1; private static final int FROM_VALUE_INDEX = 2; private static final int TO_KEYOWRD_INDEX = 3; private static final int TO_VALUE_INDEX = 4; private static final int STEP_KEYOWRD_INDEX = 5; private static final int STEP_VALUE_INDEX = 6; private static final int ARGUMENT_COUNT_WITHOUT_STEP = 5; private static final int ARGUMENT_COUNT_WITH_STEP = 7; private final MutatingVisitController visitController; private final ErrorManager errorManager; private final Stack<String> variables = new Stack<>(); private int uniqueLoopId = 0; public CreateForLoopNodes(MutatingVisitController visitController, ErrorManager errorManager) { this.visitController = visitController; this.errorManager = errorManager; } @Override public boolean enterUnknownAtRule(CssUnknownAtRuleNode node) { if (!node.getName().getValue().equals(FOR_NAME)) { return true; } if (!node.getType().hasBlock()) { reportError("@" + FOR_NAME + " with no block", node); return false; } if (node.getChildren().size() != ARGUMENT_COUNT_WITHOUT_STEP && node.getChildren().size() != ARGUMENT_COUNT_WITH_STEP) { reportError(SYNTAX_ERROR, node); return false; } if (!(node.getChildAt(VARIABLE_INDEX) instanceof CssLoopVariableNode) || !(node.getChildAt(FROM_KEYWORD_INDEX) instanceof CssLiteralNode) || !FROM_KEYWORD.equals(node.getChildAt(FROM_KEYWORD_INDEX).getValue()) || !isValidValueNode(node.getChildAt(FROM_VALUE_INDEX)) || !(node.getChildAt(TO_KEYOWRD_INDEX) instanceof CssLiteralNode) || !TO_KEYWORD.equals(node.getChildAt(TO_KEYOWRD_INDEX).getValue()) || !isValidValueNode(node.getChildAt(TO_VALUE_INDEX))) { reportError(SYNTAX_ERROR, node); return false; } String variableName = node.getChildAt(VARIABLE_INDEX).getValue(); if (!VARIABLE_PATTERN.matcher(variableName).matches()) { reportError(ILLEGAL_VARIABLE_NAME, node.getChildAt(VARIABLE_INDEX)); return false; } if (variables.contains(variableName)) { reportError(OVERRIDE_VARIABLE_NAME , node.getChildAt(VARIABLE_INDEX)); return false; } CssValueNode from = node.getChildAt(FROM_VALUE_INDEX); CssValueNode to = node.getChildAt(TO_VALUE_INDEX); CssValueNode step = new CssNumericNode("1", CssNumericNode.NO_UNITS); if (node.getChildren().size() == ARGUMENT_COUNT_WITH_STEP) { if (!(node.getChildAt(STEP_KEYOWRD_INDEX) instanceof CssLiteralNode) || !STEP_KEYWORD.equals(node.getChildAt(STEP_KEYOWRD_INDEX).getValue()) || !isValidValueNode(node.getChildAt(STEP_VALUE_INDEX))) { reportError(SYNTAX_ERROR, node); return false; } step = node.getChildAt(STEP_VALUE_INDEX); } CssForLoopRuleNode loopNode = new CssForLoopRuleNode(node.getName(), node.getBlock(), node.getComments(), from, to, step, variableName, nextLoopId(), node.getSourceCodeLocation()); loopNode.setParameters(node.getChildren()); visitController.replaceCurrentBlockChildWith(Lists.newArrayList(loopNode), true); variables.push(variableName); return true; } @Override public void leaveForLoop(CssForLoopRuleNode node) { variables.pop(); } private boolean isValidValueNode(CssValueNode node) { return node instanceof CssNumericNode || node instanceof CssLiteralNode; } private void reportError(String message, CssNode node) { errorManager.report(new GssError(message, node.getSourceCodeLocation())); visitController.removeCurrentNode(); } private int nextLoopId() { return uniqueLoopId++; } @Override public void runPass() { visitController.startVisit(this); } }