/*
* 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);
}
}