Java tutorial
/* * 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.template.soy.jssrc.internal; import static com.google.template.soy.jssrc.dsl.CodeChunk.declare; import static com.google.template.soy.jssrc.dsl.CodeChunk.id; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.template.soy.jssrc.dsl.CodeChunk; import com.google.template.soy.jssrc.dsl.CodeChunk.RequiresCollector; import com.google.template.soy.jssrc.dsl.CodeChunkUtils; import com.google.template.soy.jssrc.dsl.GoogRequire; import java.util.ArrayDeque; import java.util.Deque; import java.util.List; import java.util.Map; import java.util.TreeMap; import javax.annotation.Nullable; /** * A JavaScript implementation of the CodeBuilder class. * * <p>Usage example that demonstrates most of the methods: * <pre> * JsCodeBuilder jcb = new JsCodeBuilder(CodeStyle.STRINGBUILDER); * jcb.appendLine("story.title = function(opt_data) {"); * jcb.increaseIndent(); * jcb.pushOutputVar("output"); * jcb.initOutputVarIfNecessary(); * jcb.pushOutputVar("temp"); * jcb.addToOutputVar(Lists.newArrayList( * new JsExpr("'Snow White and the '", Integer.MAX_VALUE), * new JsExpr("opt_data.numDwarfs", Integer.MAX_VALUE)); * jcb.popOutputVar(); * jcb.addToOutputVar(Lists.newArrayList( * new JsExpr("temp", Integer.MAX_VALUE), * new JsExpr("' Dwarfs'", Integer.MAX_VALUE)); * jcb.appendLineStart("return ").appendOutputVarName().appendLineEnd(".toString();"); * jcb.popOutputVar(); * jcb.decreaseIndent(); * String THE_END = "the end"; * jcb.appendLine("} // ", THE_END); * </pre> * * <p>The above example builds the following JS code: * <pre> * story.title = function(opt_data) { * var output = new soy.StringBuilder(); * var temp = new soy.StringBuilder('Snow White and the ', opt_data.numDwarfs); * output.append(temp, ' Dwarfs'); * return output.toString(); * } // the end * </pre> * */ public class JsCodeBuilder { private static final class OutputVar { final CodeChunk.WithValue name; final boolean initialized; OutputVar(CodeChunk.WithValue name, boolean initialized) { this.name = name; this.initialized = initialized; } } /** The size of a single indent level. */ private static final int INDENT_SIZE = 2; /** A buffer to accumulate the generated code. */ private final StringBuilder code; /** The current stack of output variables. */ private final Deque<OutputVar> outputVars; /** The current indent (some even number of spaces). */ private String indent; private final CodeChunk.RequiresCollector requireCollector = new CodeChunk.RequiresCollector() { @Override public void add(GoogRequire require) { addGoogRequire(require); } }; // the set of symbols to require, indexed by symbol name to detect conflicting imports private final Map<String, GoogRequire> googRequires = new TreeMap<>(); /** * The current output variable. * * <p>TODO(user): this is always an {@link CodeChunk#id}. Consider exposing a subclass of * CodeChunk so we can enforce this invariant at compile time. */ @Nullable protected CodeChunk.WithValue currOutputVar; /** Whether the current output variable is initialized. */ private boolean currOutputVarIsInited; protected JsCodeBuilder() { code = new StringBuilder(); indent = ""; outputVars = new ArrayDeque<>(); currOutputVar = null; currOutputVarIsInited = false; } protected JsCodeBuilder(JsCodeBuilder parent) { code = new StringBuilder(); indent = parent.indent; outputVars = parent.outputVars; currOutputVar = parent.currOutputVar; currOutputVarIsInited = parent.currOutputVarIsInited; } Iterable<GoogRequire> googRequires() { return googRequires.values(); } public void initOutputVarIfNecessary() { if (currOutputVarIsInited) { // Nothing to do since it's already initialized. return; } // var output = ''; appendLine("var ", // Don't tell the chunk about the current indent level. // We're in the middle of a line! currOutputVar.assertExpr().getText(), " = '';"); setOutputVarInited(); } /** Appends the given code chunk to the current output variable. */ public JsCodeBuilder addChunkToOutputVar(CodeChunk.WithValue chunk) { return addChunksToOutputVar(ImmutableList.of(chunk)); } /** * Appends one or more lines representing the concatenation of the values of the given code chunks * saved to the current output variable. */ public JsCodeBuilder addChunksToOutputVar(List<? extends CodeChunk.WithValue> codeChunks) { if (currOutputVarIsInited) { CodeChunk.WithValue rhs = CodeChunkUtils.concatChunks(codeChunks); rhs.collectRequires(requireCollector); appendLine(currOutputVar.plusEquals(rhs).getCode()); } else { CodeChunk.WithValue rhs = CodeChunkUtils.concatChunksForceString(codeChunks); rhs.collectRequires(requireCollector); append(declare(currOutputVar.singleExprOrName().getText(), rhs)); setOutputVarInited(); } return this; } /** Increases the current indent. */ public final JsCodeBuilder increaseIndent() { return changeIndentHelper(1); } /** Increases the current indent twice. */ public final JsCodeBuilder increaseIndentTwice() { return changeIndentHelper(2); } /** Decreases the current indent. */ public final JsCodeBuilder decreaseIndent() { return changeIndentHelper(-1); } /** Decreases the current indent twice. */ public final JsCodeBuilder decreaseIndentTwice() { return changeIndentHelper(-2); } /** * Helper for the various indent methods. * @param chg The number of indent levels to change. */ private JsCodeBuilder changeIndentHelper(int chg) { int newIndentDepth = indent.length() + chg * INDENT_SIZE; Preconditions.checkState(newIndentDepth >= 0); indent = Strings.repeat(" ", newIndentDepth); return this; } void setIndent(int indentCt) { this.indent = Strings.repeat(" ", indentCt); } int getIndent() { return this.indent.length(); } /** * Pushes on a new current output variable. * @param outputVarName The new output variable name. */ public JsCodeBuilder pushOutputVar(String outputVarName) { currOutputVar = id(outputVarName); outputVars.push(new OutputVar(currOutputVar, false)); currOutputVarIsInited = false; return this; } /** * Pops off the current output variable. The previous output variable again becomes the current. */ public JsCodeBuilder popOutputVar() { outputVars.pop(); OutputVar top = outputVars.peek(); // null if outputVars is now empty if (top != null) { currOutputVar = top.name; currOutputVarIsInited = top.initialized; } else { currOutputVar = null; currOutputVarIsInited = false; } return this; } /** * Tells this CodeBuilder that the current output variable has already been initialized. This * causes {@code initOutputVarIfNecessary} and {@code addToOutputVar} to not add initialization * code even on the first use of the variable. */ public JsCodeBuilder setOutputVarInited() { outputVars.pop(); outputVars.push(new OutputVar(currOutputVar, true /* isInitialized */)); currOutputVarIsInited = true; return this; } /** * Serializes the given {@link CodeChunk} into the code builder, respecting the code builder's * current indentation level. */ public JsCodeBuilder append(CodeChunk codeChunk) { codeChunk.collectRequires(requireCollector); return append(codeChunk.getStatementsForInsertingIntoForeignCodeAtIndent(indent.length())); } /** * Appends one or more strings to the generated code. * @param codeFragments The code string(s) to append. * @return This CodeBuilder (for stringing together operations). */ public JsCodeBuilder append(String... codeFragments) { for (String codeFragment : codeFragments) { code.append(codeFragment); } return this; } /** * Appends the current indent, then the given strings, then a newline. * @param codeFragments The code string(s) to append. * @return This CodeBuilder (for stringing together operations). */ public JsCodeBuilder appendLine(String... codeFragments) { code.append(indent); append(codeFragments); code.append("\n"); return this; } /** * Appends the current indent, then the given strings. * * @param codeFragments The code string(s) to append. * @return This CodeBuilder (for stringing together operations). */ public JsCodeBuilder appendLineStart(String... codeFragments) { code.append(indent); append(codeFragments); return this; } /** * Appends the given strings, then a newline. * * @param codeFragments The code string(s) to append. * @return This CodeBuilder (for stringing together operations). */ public JsCodeBuilder appendLineEnd(String... codeFragments) { append(codeFragments); code.append("\n"); return this; } public RequiresCollector getRequiresCollector() { return requireCollector; } /** * Adds a {@code goog.require} * * @param require The namespace being required */ public void addGoogRequire(GoogRequire require) { GoogRequire oldRequire = googRequires.put(require.symbol(), require); if (oldRequire != null && !oldRequire.equals(require)) { throw new IllegalArgumentException( "Found the same namespace added as a require in multiple incompatible ways: " + oldRequire + " vs. " + require); } } /** Should only be used by {@link GenJsCodeVisitor#visitSoyFileNode}. */ void appendGoogRequires(StringBuilder sb) { for (GoogRequire require : googRequires.values()) { // TODO(lukes): we need some namespace management here... though really we need namespace // management with all declarations... The problem is that a require could introduce a name // alias that conflicts with a symbol defined elsewhere in the file. require.writeTo(sb); } } /** * @return The generated code. */ public String getCode() { return code.toString(); } /** Appends the code accumulated in this builder to the given {@link StringBuilder}. */ void appendCode(StringBuilder sb) { sb.append(code); } }