/*
* 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.pysrc.internal;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.template.soy.internal.base.Pair;
import com.google.template.soy.pysrc.restricted.PyExpr;
import com.google.template.soy.pysrc.restricted.PyExprUtils;
import com.google.template.soy.pysrc.restricted.PyListExpr;
import com.google.template.soy.pysrc.restricted.PyStringExpr;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
/**
* A Python implementation of the CodeBuilder class.
*
* <p>Usage example that demonstrates most of the methods:
*
* <pre>
* PyCodeBuilder pcb = new PyCodeBuilder();
* pcb.appendLine("def title(data):");
* pcb.increaseIndent();
* pcb.pushOutputVar("output");
* pcb.initOutputVarIfNecessary();
* pcb.pushOutputVar("temp");
* pcb.addToOutputVar(Lists.newArrayList(
* new PyExpr("'Snow White and the '", Integer.MAX_VALUE),
* new PyExpr("data['numDwarfs']", Integer.MAX_VALUE));
* pcb.popOutputVar();
* pcb.addToOutputVar(Lists.newArrayList(
* new PyExpr("temp", Integer.MAX_VALUE),
* new PyExpr("' Dwarfs'", Integer.MAX_VALUE));
* pcb.appendLineStart("return ").appendOutputVarName().appendLineEnd();
* pcb.popOutputVar();
* pcb.decreaseIndent();
* String THE_END = "the end";
* pcb.appendLine("# ", THE_END);
* </pre>
*
* The above example builds the following Python code:
*
* <pre>
* def title(data):
* output = ''
* temp = ''.join(['Snow White and the ', str(data['numDwarfs'])])
* output += ''.join([temp, ' Dwarfs'])
* return output
* # the end
* </pre>
*
*/
final class PyCodeBuilder {
/** 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 indent (some even number of spaces). */
private String indent;
/** The current stack of output variables. */
private final Deque<Pair<String, Boolean>> outputVars;
/** The current output variable name. */
private String currOutputVarName;
/** Whether the current output variable is initialized. */
private boolean currOutputVarIsInited;
PyCodeBuilder() {
code = new StringBuilder();
indent = "";
outputVars = new ArrayDeque<>();
currOutputVarName = null;
currOutputVarIsInited = false;
}
void initOutputVarIfNecessary() {
if (getOutputVarIsInited()) {
// Nothing to do since it's already initialized.
return;
}
// output = ''
appendLine(getOutputVarName(), " = []");
setOutputVarInited();
}
void addToOutputVar(List<? extends PyExpr> pyExprs) {
addToOutputVar(PyExprUtils.concatPyExprs(pyExprs));
}
/** Add a single PyExpr object to the output variable. */
void addToOutputVar(PyExpr pyExpr) {
boolean isList = pyExpr instanceof PyListExpr;
if (isList && !getOutputVarIsInited()) {
appendLine(getOutputVarName(), " = ", pyExpr.getText());
} else {
initOutputVarIfNecessary();
String function = isList ? ".extend(" : ".append(";
appendLine(getOutputVarName(), function, pyExpr.getText(), ")");
}
setOutputVarInited();
}
/**
* Provide the output object as a string. Since we store all data in the output variables as a
* list for concatenation performance, this step does the joining to convert the output into a
* String.
*
* @return A PyExpr object of the output joined into a String.
*/
PyStringExpr getOutputAsString() {
Preconditions.checkState(getOutputVarName() != null);
initOutputVarIfNecessary();
return new PyListExpr(getOutputVarName(), Integer.MAX_VALUE).toPyString();
}
/** Increases the current indent. */
void increaseIndent() {
changeIndentHelper(1);
}
/** Increases the current indent twice. */
void increaseIndentTwice() {
changeIndentHelper(2);
}
/** Decreases the current indent. */
public void decreaseIndent() {
changeIndentHelper(-1);
}
/** Decreases the current indent twice. */
void decreaseIndentTwice() {
changeIndentHelper(-2);
}
/**
* Private helper for increaseIndent(), increaseIndentTwice(), decreaseIndent(), and
* decreaseIndentTwice().
*
* @param chg The number of indent levels to change.
*/
private void changeIndentHelper(int chg) {
int newIndentDepth = indent.length() + chg * INDENT_SIZE;
Preconditions.checkState(newIndentDepth >= 0);
indent = Strings.repeat(" ", newIndentDepth);
}
/**
* Pushes on a new current output variable.
*
* @param outputVarName The new output variable name.
*/
public void pushOutputVar(String outputVarName) {
outputVars.push(Pair.of(outputVarName, false));
currOutputVarName = outputVarName;
currOutputVarIsInited = false;
}
/**
* Pops off the current output variable. The previous output variable again becomes the current.
*/
void popOutputVar() {
outputVars.pop();
Pair<String, Boolean> topPair = outputVars.peek(); // null if outputVars is now empty
if (topPair != null) {
currOutputVarName = topPair.first;
currOutputVarIsInited = topPair.second;
} else {
currOutputVarName = null;
currOutputVarIsInited = false;
}
}
/**
* 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.
*/
void setOutputVarInited() {
outputVars.pop();
outputVars.push(Pair.of(currOutputVarName, true));
currOutputVarIsInited = true;
}
/**
* Gets the current output variable name.
*
* @return The current output variable name.
*/
String getOutputVarName() {
return currOutputVarName;
}
/**
* 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 PyCodeBuilder 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 PyCodeBuilder 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 PyCodeBuilder 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 PyCodeBuilder appendLineEnd(String... codeFragments) {
append(codeFragments);
code.append("\n");
return this;
}
/**
* Appends the name of the current output variable.
*
* @return This CodeBuilder (for stringing together operations).
*/
PyCodeBuilder appendOutputVarName() {
code.append(currOutputVarName);
return this;
}
/** @return The generated code. */
public String getCode() {
return code.toString();
}
/**
* Gets the current output variable initialization status.
*
* @return The current output variable initialization status.
*/
boolean getOutputVarIsInited() {
return currOutputVarIsInited;
}
}