/* * Copyright 2014 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.gwt.resources.gss; import com.google.gwt.resources.gss.ast.CssJavaExpressionNode; import com.google.gwt.resources.gss.ast.CssRuntimeConditionalRuleNode; import com.google.gwt.thirdparty.common.css.compiler.ast.CssAtRuleNode.Type; import com.google.gwt.thirdparty.common.css.compiler.ast.CssBooleanExpressionNode; import com.google.gwt.thirdparty.common.css.compiler.ast.CssCompilerPass; import com.google.gwt.thirdparty.common.css.compiler.ast.CssConditionalBlockNode; import com.google.gwt.thirdparty.common.css.compiler.ast.CssConditionalRuleNode; import com.google.gwt.thirdparty.common.css.compiler.ast.CssValueNode; import com.google.gwt.thirdparty.common.css.compiler.ast.DefaultTreeVisitor; import com.google.gwt.thirdparty.common.css.compiler.ast.MutatingVisitController; import com.google.gwt.thirdparty.guava.common.collect.Lists; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Visitor that handles conditional nodes with conditions that need to be evaluated at runtime. * * <p>The corresponding GSS handled by this pass looks like: * * <pre> * @if(eval("com.foo.BAR")) { * } * @elseif(eval("com.foo.bar()")) { * } * } * </pre> */ public class CreateRuntimeConditionalNodes extends DefaultTreeVisitor implements CssCompilerPass { // TODO(jdr): valid input like eval('foo(")\')")') will break this regex private static final Pattern EVAL_FUNCTION = Pattern.compile("^eval\\(([\"'])(((?!\\1).)*)\\1\\)$"); private final MutatingVisitController visitController; public CreateRuntimeConditionalNodes(MutatingVisitController visitController) { this.visitController = visitController; } @Override public boolean enterConditionalBlock(CssConditionalBlockNode block) { // We have to visit all the CssConditionalRuleNode when we visit the CssConditionalBlockNode // parent node because we are going to replace CssConditionalRuleNode by another node and // unfortunately the visitController doesn't support to replace a CssConditionalRuleNode and // we have to do it manually. That implies that the new nodes won't be visited by the // visitor if we do that during the visit of the CssConditionalRuleNodes and they can contain // other CssConditionalBlockNodes that won't be visited. // Once MutatingVisitController supports replacement of CssConditionalRuleNode, // we will be able to visit CssConditionalRuleNode directly. // Make a copy in order to avoid ConcurrentModificationException List<CssConditionalRuleNode> children = Lists.newArrayList(block.getChildren()); for (CssConditionalRuleNode ruleNode : children) { visitConditionalRule(ruleNode, block); } return true; } private void visitConditionalRule(CssConditionalRuleNode node, CssConditionalBlockNode parent) { if (node.getType() != Type.ELSE) { CssBooleanExpressionNode nodeCondition = node.getCondition(); String condition = extractRuntimeCondition(nodeCondition); if (condition != null) { CssJavaExpressionNode newNode = new CssJavaExpressionNode(condition, nodeCondition.getSourceCodeLocation()); CssRuntimeConditionalRuleNode newRuleNode = new CssRuntimeConditionalRuleNode(node, newNode); // Unfortunately visitController.replaceCurrentBlockChildWith doesn't work with // CssConditionnalRuleNode int index = parent.getChildren().indexOf(node); parent.replaceChildAt(index, Lists.newArrayList(newRuleNode)); } } } private String extractRuntimeCondition(CssValueNode node) { Matcher m = EVAL_FUNCTION.matcher(node.getValue()); return m.matches() ? m.group(2) : null; } @Override public void runPass() { visitController.startVisit(this); } }