/*
* Copyright 2008 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.parseinfo.passes;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CaseFormat;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.template.soy.base.BaseUtils;
import com.google.template.soy.base.IndentedLinesBuilder;
import com.google.template.soy.base.SoyFileKind;
import com.google.template.soy.base.SoySyntaxException;
import com.google.template.soy.internal.base.Pair;
import com.google.template.soy.parseinfo.SoyFileInfo.CssTagsPrefixPresence;
import com.google.template.soy.sharedpasses.FindIjParamsVisitor;
import com.google.template.soy.sharedpasses.FindIjParamsVisitor.IjParamsInfo;
import com.google.template.soy.sharedpasses.FindIndirectParamsVisitor;
import com.google.template.soy.sharedpasses.FindIndirectParamsVisitor.IndirectParamsInfo;
import com.google.template.soy.soytree.AbstractSoyNodeVisitor;
import com.google.template.soy.soytree.CssNode;
import com.google.template.soy.soytree.SoyFileNode;
import com.google.template.soy.soytree.SoyFileSetNode;
import com.google.template.soy.soytree.SoyNode;
import com.google.template.soy.soytree.SoyNode.ParentSoyNode;
import com.google.template.soy.soytree.SoySyntaxExceptionUtils;
import com.google.template.soy.soytree.TemplateBasicNode;
import com.google.template.soy.soytree.TemplateDelegateNode;
import com.google.template.soy.soytree.TemplateNode;
import com.google.template.soy.soytree.TemplateNode.SoyDocParam;
import com.google.template.soy.soytree.TemplateRegistry;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Visitor for generating Java classes containing the parse info.
*
* <p> Important: Do not use outside of Soy code (treat as superpackage-private).
*
* <p> {@link #exec} should be called on a full parse tree.
*
* <p> For an example Soy file and its corresponding generated code, see
* <pre>
* [tests_dir]/com/google/template/soy/test_data/AaaBbbCcc.soy
* [tests_dir]/com/google/template/soy/test_data/AaaBbbCccSoyInfo.java
* </pre>
*
* @author Kai Huang
*/
public class GenerateParseInfoVisitor extends AbstractSoyNodeVisitor<ImmutableMap<String, String>> {
/**
* Represents the source of the generated Java class names.
*/
@VisibleForTesting static enum JavaClassNameSource {
/** AaaBbb.soy or aaa_bbb.soy --> AaaBbbSoyInfo. */
SOY_FILE_NAME,
/** boo.foo.aaaBbb --> AaaBbbSoyInfo. */
SOY_NAMESPACE_LAST_PART,
/** File1SoyInfo, File2SoyInfo, etc. */
GENERIC;
/** Pattern for an all-upper-case word in a file name or identifier. */
private static final Pattern ALL_UPPER_WORD =
Pattern.compile("(?<= [^A-Za-z] | ^) [A-Z]+ (?= [^A-Za-z] | $)", Pattern.COMMENTS);
/** Pattern for an all-lower-case word in a file name or identifier. */
// Note: Char after an all-lower word can be an upper letter (e.g. first word of camel case).
private static final Pattern ALL_LOWER_WORD =
Pattern.compile("(?<= [^A-Za-z] | ^) [a-z]+ (?= [^a-z] | $)", Pattern.COMMENTS);
/** Pattern for a character that's not a letter nor a digit. */
private static final Pattern NON_LETTER_DIGIT = Pattern.compile("[^A-Za-z0-9]");
/**
* Generates the base Java class name for the given Soy file.
* @param soyFile The Soy file.
* @return The generated base Java class name (without any suffixes).
*/
@VisibleForTesting String generateBaseClassName(SoyFileNode soyFile) {
switch (this) {
case SOY_FILE_NAME:
String fileName = soyFile.getFileName();
if (fileName == null) {
throw new IllegalArgumentException(
"Trying to generate Java class name based on Soy file name, but Soy file name was" +
" not provided.");
}
if (fileName.toLowerCase().endsWith(".soy")) {
fileName = fileName.substring(0, fileName.length() - 4);
}
return makeUpperCamelCase(fileName);
case SOY_NAMESPACE_LAST_PART:
String namespace = soyFile.getNamespace();
assert namespace != null; // suppress warnings
String namespaceLastPart = namespace.substring(namespace.lastIndexOf('.') + 1);
return makeUpperCamelCase(namespaceLastPart);
case GENERIC:
return "File";
default:
throw new AssertionError();
}
}
/**
* Creates the upper camel case version of the given string (can be file name or identifier).
* @param str The string to turn into upper camel case.
* @return The upper camel case version of the string.
*/
private static String makeUpperCamelCase(String str) {
str = makeWordsCapitalized(str, ALL_UPPER_WORD);
str = makeWordsCapitalized(str, ALL_LOWER_WORD);
str = NON_LETTER_DIGIT.matcher(str).replaceAll("");
return str;
}
/**
* Makes all the words in the given string into capitalized format (first letter capital, rest
* lower case). Words are defined by the given regex pattern.
* @param str The string to process.
* @param wordPattern The regex pattern for matching a word.
* @return The resulting string with all words in capitalized format.
*/
private static String makeWordsCapitalized(String str, Pattern wordPattern) {
StringBuffer sb = new StringBuffer();
Matcher wordMatcher = wordPattern.matcher(str);
while (wordMatcher.find()) {
String oldWord = wordMatcher.group();
StringBuilder newWord = new StringBuilder();
for (int i = 0, n = oldWord.length(); i < n; i++) {
if (i == 0) {
newWord.append(Character.toUpperCase(oldWord.charAt(i)));
} else {
newWord.append(Character.toLowerCase(oldWord.charAt(i)));
}
}
wordMatcher.appendReplacement(sb, Matcher.quoteReplacement(newWord.toString()));
}
wordMatcher.appendTail(sb);
return sb.toString();
}
}
/** The package name of the generated files. */
private final String javaPackage;
/** The source of the generated Java class names. */
private final JavaClassNameSource javaClassNameSource;
/** Map from Soy file node to generated Java class name (built at start of pass). */
private Map<SoyFileNode, String> soyFileToJavaClassNameMap;
/** Registry of all templates in the Soy tree. */
private TemplateRegistry templateRegistry;
/** Cache for results of calls to {@code Utils.convertToUpperUnderscore()}. */
private final Map<String, String> convertedIdents = Maps.newHashMap();
/** The contents of the generated JS files. */
private LinkedHashMap<String, String> generatedFiles;
/** Builder for the generated code. */
private IndentedLinesBuilder ilb;
/**
* @param javaPackage The Java package for the generated classes.
* @param javaClassNameSource Source of the generated class names. Must be one of "filename",
* "namespace", or "generic".
*/
public GenerateParseInfoVisitor(String javaPackage, String javaClassNameSource) {
this.javaPackage = javaPackage;
if (javaClassNameSource.equals("filename")) {
this.javaClassNameSource = JavaClassNameSource.SOY_FILE_NAME;
} else if (javaClassNameSource.equals("namespace")) {
this.javaClassNameSource = JavaClassNameSource.SOY_NAMESPACE_LAST_PART;
} else if (javaClassNameSource.equals("generic")) {
this.javaClassNameSource = JavaClassNameSource.GENERIC;
} else {
throw new IllegalArgumentException(
"Invalid value for javaClassNameSource \"" + javaClassNameSource + "\"" +
" (valid values are \"filename\", \"namespace\", and \"generic\").");
}
}
@Override public ImmutableMap<String, String> exec(SoyNode node) {
generatedFiles = Maps.newLinkedHashMap();
ilb = null;
visit(node);
return ImmutableMap.copyOf(generatedFiles);
}
// -----------------------------------------------------------------------------------------------
// Implementations for specific nodes.
@Override protected void visitSoyFileSetNode(SoyFileSetNode node) {
// Figure out the generated class name for each Soy file, including adding number suffixes
// to resolve collisions, and then adding the common suffix "SoyInfo".
Multimap<String, SoyFileNode> baseGeneratedClassNameToSoyFilesMap = HashMultimap.create();
for (SoyFileNode soyFile : node.getChildren()) {
if (soyFile.getSoyFileKind() == SoyFileKind.SRC) {
baseGeneratedClassNameToSoyFilesMap.put(
javaClassNameSource.generateBaseClassName(soyFile), soyFile);
}
}
soyFileToJavaClassNameMap = Maps.newHashMap();
for (String baseClassName : baseGeneratedClassNameToSoyFilesMap.keySet()) {
Collection<SoyFileNode> soyFiles = baseGeneratedClassNameToSoyFilesMap.get(baseClassName);
if (soyFiles.size() == 1) {
for (SoyFileNode soyFile : soyFiles) {
soyFileToJavaClassNameMap.put(soyFile, baseClassName + "SoyInfo");
}
} else {
int numberSuffix = 1;
for (SoyFileNode soyFile : soyFiles) {
soyFileToJavaClassNameMap.put(soyFile, baseClassName + numberSuffix + "SoyInfo");
numberSuffix++;
}
}
}
// Build template registry.
templateRegistry = new TemplateRegistry(node);
// Run the pass.
for (SoyFileNode soyFile : node.getChildren()) {
try {
visit(soyFile);
} catch (SoySyntaxException sse) {
throw sse.associateMetaInfo(null, soyFile.getFilePath(), null);
}
}
}
@Override protected void visitSoyFileNode(SoyFileNode node) {
if (node.getSoyFileKind() == SoyFileKind.DEP) {
return; // don't generate code for deps
}
if (node.getFilePath() == null) {
throw SoySyntaxExceptionUtils.createWithNode(
"In order to generate parse info, all Soy files must have paths (file name is" +
" extracted from the path).",
node);
}
String javaClassName = soyFileToJavaClassNameMap.get(node);
// Collect the following:
// + all the public basic templates (non-private, non-delegate) in a map from the
// upper-underscore template name to the template's node,
// + all the param keys from all templates (including private),
// + for each param key, the list of templates that list it directly.
LinkedHashMap<String, TemplateNode> publicBasicTemplateMap = Maps.newLinkedHashMap();
Set<String> allParamKeys = Sets.newHashSet();
LinkedHashMultimap<String, TemplateNode> paramKeyToTemplatesMultimap =
LinkedHashMultimap.create();
for (TemplateNode template : node.getChildren()) {
if (!template.isPrivate() && template instanceof TemplateBasicNode) {
publicBasicTemplateMap.put(
convertToUpperUnderscore(template.getPartialTemplateName().substring(1)), template);
}
for (SoyDocParam param : template.getSoyDocParams()) {
allParamKeys.add(param.key);
paramKeyToTemplatesMultimap.put(param.key, template);
}
}
// allParamKeysMap is a map from upper-underscore key to original key.
SortedMap<String, String> allParamKeysMap = Maps.newTreeMap();
for (String key : allParamKeys) {
String upperUnderscoreKey = convertToUpperUnderscore(key);
if (allParamKeysMap.containsKey(upperUnderscoreKey)) {
throw SoySyntaxExceptionUtils.createWithNode(
"Cannot generate parse info because two param keys '" +
allParamKeysMap.get(upperUnderscoreKey) + "' and '" + key +
"' generate the same upper-underscore name '" + upperUnderscoreKey + "'.",
node);
}
allParamKeysMap.put(upperUnderscoreKey, key);
}
ilb = new IndentedLinesBuilder(2);
// ------ Header. ------
ilb.appendLine("// This file was automatically generated from ", node.getFileName(), ".");
ilb.appendLine("// Please don't edit this file by hand.");
ilb.appendLine();
ilb.appendLine("package ", javaPackage, ";");
ilb.appendLine();
ilb.appendLine("import com.google.common.collect.ImmutableList;");
ilb.appendLine("import com.google.common.collect.ImmutableMap;");
ilb.appendLine("import com.google.common.collect.ImmutableSortedSet;");
ilb.appendLine("import com.google.template.soy.parseinfo.SoyFileInfo;");
ilb.appendLine("import com.google.template.soy.parseinfo.SoyTemplateInfo;");
// ------ Class start. ------
ilb.appendLine();
ilb.appendLine();
appendJavadoc(ilb, "Soy parse info for " + node.getFileName() + ".", true, false);
ilb.appendLine("public class ", javaClassName, " extends SoyFileInfo {");
ilb.increaseIndent();
// ------ Constant for namespace. ------
ilb.appendLine();
ilb.appendLine();
ilb.appendLine("/** This Soy file's namespace. */");
ilb.appendLine("public static final String __NAMESPACE__ = \"", node.getNamespace(), "\";");
// ------ Template names. ------
ilb.appendLine();
ilb.appendLine();
ilb.appendLine("public static class TemplateName {");
ilb.increaseIndent();
ilb.appendLine("private TemplateName() {}");
ilb.appendLine();
for (Entry<String, TemplateNode> templateEntry : publicBasicTemplateMap.entrySet()) {
StringBuilder javadocSb = new StringBuilder();
javadocSb.append("The full template name of the ")
.append(templateEntry.getValue().getPartialTemplateName())
.append(" template.");
appendJavadoc(ilb, javadocSb.toString(), false, true);
ilb.appendLine("public static final String ", templateEntry.getKey(), " = \"",
templateEntry.getValue().getTemplateName(), "\";");
}
ilb.decreaseIndent();
ilb.appendLine("}");
// ------ Params. ------
ilb.appendLine();
ilb.appendLine();
ilb.appendLine("/**");
ilb.appendLine(" * Param names from all templates in this Soy file.");
ilb.appendLine(" */");
ilb.appendLine("public static class Param {");
ilb.increaseIndent();
ilb.appendLine("private Param() {}");
ilb.appendLine();
for (Map.Entry<String, String> paramEntry : allParamKeysMap.entrySet()) {
String upperUnderscoreKey = paramEntry.getKey();
String key = paramEntry.getValue();
StringBuilder javadocSb = new StringBuilder();
javadocSb.append("Listed by ");
boolean isFirst = true;
for (TemplateNode template : paramKeyToTemplatesMultimap.get(key)) {
if (isFirst) {
isFirst = false;
} else {
javadocSb.append(", ");
}
javadocSb.append(buildTemplateNameForJavadoc(node, template));
}
javadocSb.append('.');
appendJavadoc(ilb, javadocSb.toString(), false, true);
ilb.appendLine("public static final String ", upperUnderscoreKey, " = \"", key, "\";");
}
ilb.decreaseIndent();
ilb.appendLine("}");
// ------ Templates. ------
for (TemplateNode template : publicBasicTemplateMap.values()) {
try {
visit(template);
} catch (SoySyntaxException sse) {
throw sse.associateMetaInfo(null, null, template.getTemplateNameForUserMsgs());
}
}
// ------ Constructor. ------
ilb.appendLine();
ilb.appendLine();
ilb.appendLine("private ", javaClassName, "() {");
ilb.increaseIndent();
ilb.appendLine("super(");
ilb.increaseIndent(2);
ilb.appendLine("\"", node.getFileName(), "\",");
ilb.appendLine("\"", node.getNamespace(), "\",");
// Params from all templates.
List<String> itemSnippets = Lists.newArrayList();
for (String upperUnderscoreKey : allParamKeysMap.keySet()) {
itemSnippets.add("Param." + upperUnderscoreKey);
}
appendImmutableSortedSet(ilb, "<String>", itemSnippets);
ilb.appendLineEnd(",");
// Templates.
itemSnippets = Lists.newArrayList();
for (String upperUnderscoreTemplateName : publicBasicTemplateMap.keySet()) {
itemSnippets.add(upperUnderscoreTemplateName);
}
appendImmutableList(ilb, "<SoyTemplateInfo>", itemSnippets);
ilb.appendLineEnd(",");
// CSS names.
SortedMap<String, CssTagsPrefixPresence> cssNameMap = (new CollectCssNamesVisitor()).exec(node);
List<Pair<String, String>> entrySnippetPairs = Lists.newArrayList();
for (Map.Entry<String, CssTagsPrefixPresence> entry : cssNameMap.entrySet()) {
entrySnippetPairs.add(Pair.of(
"\"" + entry.getKey() + "\"",
"CssTagsPrefixPresence." + entry.getValue().name()));
}
appendImmutableMap(ilb, "<String, CssTagsPrefixPresence>", entrySnippetPairs);
ilb.appendLineEnd(");");
ilb.decreaseIndent(2);
ilb.decreaseIndent();
ilb.appendLine("}");
// ------ Singleton instance and its getter. ------
ilb.appendLine();
ilb.appendLine();
ilb.appendLine("private static final ", javaClassName, " __INSTANCE__ =");
ilb.increaseIndent(2);
ilb.appendLine("new ", javaClassName, "();");
ilb.decreaseIndent(2);
ilb.appendLine();
ilb.appendLine("public static ", javaClassName, " getInstance() {");
ilb.increaseIndent();
ilb.appendLine("return __INSTANCE__;");
ilb.decreaseIndent();
ilb.appendLine("}");
// ------ Class end. ------
ilb.appendLine();
ilb.decreaseIndent();
ilb.appendLine("}");
generatedFiles.put(javaClassName + ".java", ilb.toString());
ilb = null;
}
@Override protected void visitTemplateNode(TemplateNode node) {
// Don't generate anything for private or delegate templates.
if (node.isPrivate() || node instanceof TemplateDelegateNode) {
return;
}
// First build list of all transitive params (direct and indirect).
LinkedHashMap<String, SoyDocParam> transitiveParamMap = Maps.newLinkedHashMap();
// Direct params.
for (SoyDocParam param : node.getSoyDocParams()) {
transitiveParamMap.put(param.key, param);
}
// Indirect params.
IndirectParamsInfo indirectParamsInfo =
(new FindIndirectParamsVisitor(templateRegistry)).exec(node);
for (SoyDocParam param : indirectParamsInfo.indirectParams.values()) {
SoyDocParam existingParam = transitiveParamMap.get(param.key);
if (existingParam == null) {
// Note: We don't list the SoyDoc description for indirect params.
transitiveParamMap.put(param.key, new SoyDocParam(param.key, param.isRequired, null));
}
}
// Get info on injected params.
IjParamsInfo ijParamsInfo = (new FindIjParamsVisitor(templateRegistry)).exec(node);
String upperUnderscoreName =
convertToUpperUnderscore(node.getPartialTemplateName().substring(1));
String templateInfoClassName =
CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, upperUnderscoreName) +
"SoyTemplateInfo";
// ------ *SoyTemplateInfo class start. ------
ilb.appendLine();
ilb.appendLine();
appendJavadoc(ilb, node.getSoyDocDesc(), true, false);
ilb.appendLine("public static class ", templateInfoClassName, " extends SoyTemplateInfo {");
ilb.increaseIndent();
// ------ Constants for template name. ------
ilb.appendLine();
ilb.appendLine("/** This template's full name. */");
ilb.appendLine("public static final String __NAME__ = \"", node.getTemplateName(), "\";");
ilb.appendLine("/** This template's partial name. */");
ilb.appendLine("public static final String __PARTIAL_NAME__ = \"",
node.getPartialTemplateName(), "\";");
// ------ Param constants. ------
boolean hasSeenFirstDirectParam = false;
boolean hasSwitchedToIndirectParams = false;
for (SoyDocParam param : transitiveParamMap.values()) {
if (param.desc != null) {
// Direct param.
if (! hasSeenFirstDirectParam) {
ilb.appendLine();
hasSeenFirstDirectParam = true;
}
appendJavadoc(ilb, param.desc, false, false);
} else {
// Indirect param.
if (! hasSwitchedToIndirectParams) {
ilb.appendLine();
ilb.appendLine("// Indirect params.");
hasSwitchedToIndirectParams = true;
}
// Get the list of all transitive callee names as they will appear in the generated
// Javadoc (possibly containing both partial and full names) and sort them before
// generating the Javadoc.
SortedSet<String> sortedJavadocCalleeNames = Sets.newTreeSet();
for (TemplateNode transitiveCallee :
indirectParamsInfo.paramKeyToCalleesMultimap.get(param.key)) {
String javadocCalleeName =
buildTemplateNameForJavadoc(node.getParent(), transitiveCallee);
sortedJavadocCalleeNames.add(javadocCalleeName);
}
// Generate the Javadoc.
StringBuilder javadocSb = new StringBuilder();
javadocSb.append("Listed by ");
boolean isFirst = true;
for (String javadocCalleeName : sortedJavadocCalleeNames) {
if (isFirst) {
isFirst = false;
} else {
javadocSb.append(", ");
}
javadocSb.append(javadocCalleeName);
}
javadocSb.append('.');
appendJavadoc(ilb, javadocSb.toString(), false, true);
}
// The actual param field.
ilb.appendLine("public static final String ", convertToUpperUnderscore(param.key),
" = \"", param.key, "\";");
}
// ------ Constructor. ------
ilb.appendLine();
ilb.appendLine("private ", templateInfoClassName, "() {");
ilb.increaseIndent();
ilb.appendLine("super(");
ilb.increaseIndent(2);
ilb.appendLine("\"", node.getTemplateName(), "\",");
if (transitiveParamMap.size() > 0) {
List<Pair<String, String>> entrySnippetPairs = Lists.newArrayList();
for (SoyDocParam param : transitiveParamMap.values()) {
entrySnippetPairs.add(Pair.of(
"\"" + param.key + "\"",
param.isRequired ? "ParamRequisiteness.REQUIRED" : "ParamRequisiteness.OPTIONAL"));
}
appendImmutableMap(ilb, "<String, ParamRequisiteness>", entrySnippetPairs);
ilb.appendLineEnd(",");
} else {
ilb.appendLine("ImmutableMap.<String, ParamRequisiteness>of(),");
}
appendIjParamSet(ilb, ijParamsInfo);
ilb.appendLineEnd(",");
// TODO: We should really disallow external basic calls when GenerateParseInfoVisitor is used.
ilb.appendLine(ijParamsInfo.mayHaveIjParamsInExternalCalls, ",");
ilb.appendLineStart(ijParamsInfo.mayHaveIjParamsInExternalDelCalls);
ilb.appendLineEnd(");");
ilb.decreaseIndent(2);
ilb.decreaseIndent();
ilb.appendLine("}");
// ------ Singleton instance and its getter. ------
ilb.appendLine();
ilb.appendLine("private static final ", templateInfoClassName, " __INSTANCE__ =");
ilb.increaseIndent(2);
ilb.appendLine("new ", templateInfoClassName, "();");
ilb.decreaseIndent(2);
ilb.appendLine();
ilb.appendLine("public static ", templateInfoClassName, " getInstance() {");
ilb.increaseIndent();
ilb.appendLine("return __INSTANCE__;");
ilb.decreaseIndent();
ilb.appendLine("}");
// ------ *SoyTemplateInfo class end. ------
ilb.decreaseIndent();
ilb.appendLine("}");
// ------ Static field with instance of *SoyTemplateInfo class. ------
ilb.appendLine();
ilb.appendLine("/** Same as ", templateInfoClassName, ".getInstance(). */");
ilb.appendLine("public static final ", templateInfoClassName, " ", upperUnderscoreName, " =");
ilb.increaseIndent(2);
ilb.appendLine(templateInfoClassName, ".getInstance();");
ilb.decreaseIndent(2);
}
/**
* Private helper for visitSoyFileNode() and visitTemplateNode() to convert an identifier to upper
* underscore format.
*
* We simply dispatch to Utils.convertToUpperUnderscore() to do the actual conversion. The reason
* for the existence of this method is that we cache all results of previous invocations in this
* pass because this method is expected to be called for the same identifier multiple times.
*
* @param ident The identifier to convert.
* @return The identifier in upper underscore format.
*/
private String convertToUpperUnderscore(String ident) {
String result = convertedIdents.get(ident);
if (result == null) {
result = BaseUtils.convertToUpperUnderscore(ident);
convertedIdents.put(ident, result);
}
return result;
}
/**
* Private helper for visitSoyFileNode() and visitTemplateNode() to append a Javadoc comment to
* the code being built.
*
* @param ilb The builder for the code.
* @param doc The doc string to append as the content of a Javadoc comment. The Javadoc format
* will follow the usual conventions. Important: If the doc string is multiple lines, the
* line separator must be '\n'.
* @param forceMultiline If true, we always generate a multiline Javadoc comment even if the doc
* string only has one line. If false, we generate either a single line or multiline Javadoc
* comment, depending on the doc string.
* @param wrapAt100Chars If true, wrap at 100 chars.
*/
@VisibleForTesting
static void appendJavadoc(
IndentedLinesBuilder ilb, String doc, boolean forceMultiline, boolean wrapAt100Chars) {
if (wrapAt100Chars) {
// Actual wrap length is less because of indent and because of space used by Javadoc chars.
int wrapLen = 100 - ilb.getCurrIndentLen() - 7;
List<String> wrappedLines = Lists.newArrayList();
for (String line : Splitter.on('\n').split(doc)) {
while (line.length() > wrapLen) {
int spaceIndex = line.lastIndexOf(' ', wrapLen);
if (spaceIndex >= 0) {
wrappedLines.add(line.substring(0, spaceIndex));
line = line.substring(spaceIndex + 1); // add 1 to skip the space
} else {
// No spaces. Just wrap at wrapLen.
wrappedLines.add(line.substring(0, wrapLen));
line = line.substring(wrapLen);
}
}
wrappedLines.add(line);
}
doc = Joiner.on("\n").join(wrappedLines);
}
if (doc.contains("\n") || forceMultiline) {
// Multiline.
ilb.appendLine("/**");
for (String line : Splitter.on('\n').split(doc)) {
ilb.appendLine(" * ", line);
}
ilb.appendLine(" */");
} else {
// One line.
ilb.appendLine("/** ", doc, " */");
}
}
/**
* Private helper for visitTemplateNode() to append the set of injected params.
*
* @param ilb The builder for the code.
* @param ijParamsInfo Info on injected params for the template being processed.
*/
private void appendIjParamSet(IndentedLinesBuilder ilb, IjParamsInfo ijParamsInfo) {
List<String> itemSnippets = Lists.newArrayList();
for (String paramKey : ijParamsInfo.ijParamSet) {
itemSnippets.add("\"" + paramKey + "\"");
}
appendImmutableSortedSet(ilb, "<String>", itemSnippets);
}
// -----------------------------------------------------------------------------------------------
// General helpers.
/**
* Private helper to build the human-readable string for referring to a template in the generated
* code's javadoc.
* @param currSoyFile The current Soy file for which we're generating parse-info code.
* @param template The template that we want to refer to in the generated javadoc. Note that this
* template may not be in the current Soy file.
* @return The human-readable string for referring to the given template in the generated code's
* javadoc.
*/
private static String buildTemplateNameForJavadoc(
SoyFileNode currSoyFile, TemplateNode template) {
StringBuilder resultSb = new StringBuilder();
if (template.getParent() == currSoyFile && ! (template instanceof TemplateDelegateNode)) {
resultSb.append(template.getPartialTemplateName());
} else {
resultSb.append(template.getTemplateNameForUserMsgs());
}
if (template.isPrivate()) {
resultSb.append(" (private)");
}
if (template instanceof TemplateDelegateNode) {
resultSb.append(" (delegate)");
}
return resultSb.toString();
}
/**
* Private helper to append an ImmutableList to the code.
*
* @param ilb The builder for the code.
* @param typeParamSnippet The type parameter for the ImmutableList.
* @param itemSnippets Code snippets for the items to put into the ImmutableList.
*/
private static void appendImmutableList(
IndentedLinesBuilder ilb, String typeParamSnippet, Collection<String> itemSnippets) {
appendListOrSetHelper(
ilb, "ImmutableList." + typeParamSnippet + "of", itemSnippets);
}
/**
* Private helper to append an ImmutableSortedSet to the code.
*
* @param ilb The builder for the code.
* @param typeParamSnippet The type parameter for the ImmutableSortedSet.
* @param itemSnippets Code snippets for the items to put into the ImmutableSortedSet.
*/
private static void appendImmutableSortedSet(
IndentedLinesBuilder ilb, String typeParamSnippet, Collection<String> itemSnippets) {
appendListOrSetHelper(
ilb, "ImmutableSortedSet." + typeParamSnippet + "of", itemSnippets);
}
/**
* Private helper for appendImmutableList() and appendImmutableSortedSet().
*
* @param ilb The builder for the code.
* @param creationFunctionSnippet Code snippet for the qualified name of the list or set creation
* function (without trailing parentheses).
* @param itemSnippets Code snippets for the items to put into the list or set.
*/
private static void appendListOrSetHelper(
IndentedLinesBuilder ilb, String creationFunctionSnippet, Collection<String> itemSnippets) {
if (itemSnippets.size() == 0) {
ilb.appendLineStart(creationFunctionSnippet, "()");
} else {
ilb.appendLine(creationFunctionSnippet, "(");
boolean isFirst = true;
for (String item : itemSnippets) {
if (isFirst) {
isFirst = false;
} else {
ilb.appendLineEnd(",");
}
ilb.appendLineStart(" ", item);
}
ilb.append(")");
}
}
/**
* Private helper to append an ImmutableMap to the code.
*
* @param ilb The builder for the code.
* @param typeParamSnippet The type parameter for the ImmutableMap.
* @param entrySnippetPairs Pairs of (key, value) code snippets for the entries to put into the
* ImmutableMap.
*/
private static void appendImmutableMap(
IndentedLinesBuilder ilb, String typeParamSnippet,
Collection<Pair<String, String>> entrySnippetPairs) {
if (entrySnippetPairs.size() == 0) {
ilb.appendLineStart("ImmutableMap.", typeParamSnippet, "of()");
} else {
ilb.appendLine("ImmutableMap.", typeParamSnippet, "builder()");
for (Pair<String, String> entrySnippetPair : entrySnippetPairs) {
ilb.appendLine(" .put(", entrySnippetPair.first, ", ", entrySnippetPair.second, ")");
}
ilb.appendLineStart(" .build()");
}
}
// -----------------------------------------------------------------------------------------------
// Helper visitor to collect CSS names.
/**
* Private helper class for visitSoyFileNode() to collect all the CSS names appearing in a file.
*
* The return value of exec() is a map from each CSS name appearing in the given node's subtree to
* its CssTagsPrefixPresence state.
*/
private static class CollectCssNamesVisitor
extends AbstractSoyNodeVisitor<SortedMap<String, CssTagsPrefixPresence>> {
/** Map from each CSS name to its CssTagsPrefixPresence state. */
private SortedMap<String, CssTagsPrefixPresence> cssNamesMap;
public CollectCssNamesVisitor() {
cssNamesMap = Maps.newTreeMap();
}
@Override public SortedMap<String, CssTagsPrefixPresence> exec(SoyNode node) {
visit(node);
return cssNamesMap;
}
@Override protected void visitCssNode(CssNode node) {
String cssName = node.getSelectorText();
CssTagsPrefixPresence existingCssTagsPrefixPresence = cssNamesMap.get(cssName);
CssTagsPrefixPresence additionalCssTagsPrefixPresence =
(node.getComponentNameExpr() == null) ?
CssTagsPrefixPresence.NEVER : CssTagsPrefixPresence.ALWAYS;
if (existingCssTagsPrefixPresence == null) {
cssNamesMap.put(cssName, additionalCssTagsPrefixPresence);
} else if (existingCssTagsPrefixPresence != additionalCssTagsPrefixPresence) {
cssNamesMap.put(cssName, CssTagsPrefixPresence.SOMETIMES);
} else {
// Nothing to change.
}
}
@Override protected void visitSoyNode(SoyNode node) {
if (node instanceof ParentSoyNode<?>) {
visitChildren((ParentSoyNode<?>) node);
}
}
}
}