/* * Copyright 2009 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.template.soy.soytree; import com.google.common.collect.ImmutableList; import com.google.template.soy.base.SourceLocation; import com.google.template.soy.base.internal.BaseUtils; import com.google.template.soy.basetree.CopyState; import com.google.template.soy.error.SoyErrorKind; import com.google.template.soy.exprparse.ExpressionParser; import com.google.template.soy.exprparse.SoyParsingContext; import com.google.template.soy.exprtree.ExprRootNode; import com.google.template.soy.internal.base.Pair; import com.google.template.soy.shared.SoyCssRenamingMap; import com.google.template.soy.soytree.SoyNode.ExprHolderNode; import com.google.template.soy.soytree.SoyNode.StandaloneNode; import com.google.template.soy.soytree.SoyNode.StatementNode; import java.util.regex.Pattern; import javax.annotation.Nullable; /** * Node representing a 'css' statement. * * <p>Important: Do not use outside of Soy code (treat as superpackage-private). * */ // TODO: Figure out why the CSS @component syntax doesn't support // injected data ($ij.foo). It looks like Soy is not checking CssNodes for // injected data. public final class CssNode extends AbstractCommandNode implements StandaloneNode, StatementNode, ExprHolderNode { /** Regular expression for a CSS class name. */ private static final String CSS_CLASS_NAME_RE = "(-|%)?[a-zA-Z_]+[a-zA-Z0-9_-]*"; /** Pattern for valid selectorText in a 'css' tag. */ private static final Pattern SELECTOR_TEXT_PATTERN = Pattern.compile("^(" + CSS_CLASS_NAME_RE + "|" + "[$]?" + BaseUtils.DOTTED_IDENT_RE + ")$"); private static final SoyErrorKind INVALID_CSS_ARGUMENT = SoyErrorKind.of( "Invalid argument to CSS command. Argument must be a valid CSS class name or" + " identifier."); /** * Component name expression of a CSS command. Null if CSS command has no expression. In the * example <code>{css $componentName, SUFFIX}</code>, this would be $componentName. */ @Nullable private final ExprRootNode componentNameExpr; /** * The selector text. Either the entire command text of the CSS command, or the suffix if you are * using @component soy syntax: <code>{css $componentName, SUFFIX}</code> */ private final String selectorText; /** * This pair keeps a mapping to the last used map and the calculated value, so that we don't have * lookup the value again if the same renaming map is used. Note that you need to make sure that * the number of actually occuring maps is very low and should really be at max 2 (one for * obfuscated and one for unobfuscated renaming). Also in production only one of the maps should * really be used, so that cache hit rate approaches 100%. */ Pair<SoyCssRenamingMap, String> renameCache; private CssNode( int id, String commandText, @Nullable ExprRootNode componentNameExpr, String selectorText, SourceLocation sourceLocation) { super(id, sourceLocation, "css", commandText); this.componentNameExpr = componentNameExpr; this.selectorText = selectorText; } /** * Copy constructor. * * @param orig The node to copy. */ private CssNode(CssNode orig, CopyState copyState) { super(orig, copyState); //noinspection ConstantConditions IntelliJ this.componentNameExpr = (orig.componentNameExpr != null) ? orig.componentNameExpr.copy(copyState) : null; this.selectorText = orig.selectorText; } /** * Transform constructor - creates a copy but with different selector text. * * @param orig The node to copy. */ public CssNode(CssNode orig, String newSelectorText, CopyState copyState) { super(orig, copyState); //noinspection ConstantConditions IntelliJ this.componentNameExpr = (orig.componentNameExpr != null) ? orig.componentNameExpr.copy(copyState) : null; this.selectorText = newSelectorText; } @Override public Kind getKind() { return Kind.CSS_NODE; } /** Returns the parsed component name expression, or null if this node has no expression. */ @Nullable public ExprRootNode getComponentNameExpr() { return componentNameExpr; } /** Returns the selector text from this command. */ public String getSelectorText() { return selectorText; } public String getRenamedSelectorText(SoyCssRenamingMap cssRenamingMap) { // Copy the property to a local here as it may be written to in a separate thread. // The cached value is a pair that keeps a reference to the map that was used for renaming it. // If the same map is passed to this call, we use the cached value, otherwise we rename // again and store the a new pair in the cache. For thread safety reasons this must be a Pair // over 2 independent instance variables. Pair<SoyCssRenamingMap, String> cache = renameCache; if (cache != null && cache.first == cssRenamingMap) { return cache.second; } if (cssRenamingMap != null) { String mappedText = cssRenamingMap.get(selectorText); if (mappedText != null) { renameCache = Pair.of(cssRenamingMap, mappedText); return mappedText; } } return selectorText; } @Override public ImmutableList<ExprRootNode> getExprList() { return (componentNameExpr != null) ? ImmutableList.of(componentNameExpr) : ImmutableList.<ExprRootNode>of(); } @SuppressWarnings("unchecked") @Override public ParentSoyNode<StandaloneNode> getParent() { return (ParentSoyNode<StandaloneNode>) super.getParent(); } @Override public CssNode copy(CopyState copyState) { return new CssNode(this, copyState); } /** Builder for {@link CssNode}. */ public static final class Builder { private final int id; private final String commandText; private final SourceLocation sourceLocation; /** * @param id The node's id. * @param commandText The node's command text. * @param sourceLocation The node's source location. */ public Builder(int id, String commandText, SourceLocation sourceLocation) { this.id = id; this.commandText = commandText; this.sourceLocation = sourceLocation; } /** * Returns a new {@link CssNode} built from the builder's state, reporting syntax errors to the * given {@link ErrorReporter}. */ public CssNode build(SoyParsingContext context) { int delimPos = commandText.lastIndexOf(','); ExprRootNode componentNameExpr = null; String selectorText = commandText; if (delimPos != -1) { String componentNameText = commandText.substring(0, delimPos).trim(); componentNameExpr = new ExprRootNode( new ExpressionParser(componentNameText, sourceLocation, context).parseExpression()); selectorText = commandText.substring(delimPos + 1).trim(); } if (!SELECTOR_TEXT_PATTERN.matcher(selectorText).matches()) { context.report(sourceLocation, INVALID_CSS_ARGUMENT); } return new CssNode(id, commandText, componentNameExpr, selectorText, sourceLocation); } } }