/*
* Copyright 2016 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.incrementaldomsrc;
import static com.google.template.soy.incrementaldomsrc.IncrementalDomRuntime.INCREMENTAL_DOM_TEXT;
import static com.google.template.soy.jssrc.dsl.CodeChunk.id;
import static com.google.template.soy.jssrc.internal.JsRuntime.GOOG_STRING_UNESCAPE_ENTITIES;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.template.soy.base.internal.BaseUtils;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.jssrc.SoyJsSrcOptions;
import com.google.template.soy.jssrc.dsl.CodeChunk;
import com.google.template.soy.jssrc.internal.GenCallCodeUtils;
import com.google.template.soy.jssrc.internal.GenJsCodeVisitorAssistantForMsgs;
import com.google.template.soy.jssrc.internal.GenJsExprsVisitor;
import com.google.template.soy.jssrc.internal.IsComputableAsJsExprsVisitor;
import com.google.template.soy.jssrc.internal.JsExprTranslator;
import com.google.template.soy.jssrc.internal.TemplateAliases;
import com.google.template.soy.jssrc.internal.TranslationContext;
import com.google.template.soy.soytree.HtmlContext;
import com.google.template.soy.soytree.MsgFallbackGroupNode;
import com.google.template.soy.soytree.MsgPlaceholderNode;
import java.util.Map;
/**
* Translates <code>{msg}</code> commands in HTML context into idom instructions.
*
* This class is not reusable.
*
* This will pass all interpolated values as special placeholder strings. It will then extract these
* placeholders from the translated message and execute the idom commands instead.
*/
final class AssistantForHtmlMsgs extends GenJsCodeVisitorAssistantForMsgs {
/**
* Maps dynamic nodes within the translated message to placeholder values to pass to goog.getMsg()
* and substitute for idom commands.
*/
private final Map<String, MsgPlaceholderNode> placeholderNames = Maps.newHashMap();
/**
* Wrapper character around placeholder placeholders. This is used to locate placeholder names in
* the translated result so we can instead run the idom instructions in their MsgPlaceholderNodes.
* The value is an arbitrary but short character that cannot appear in translated messages.
*/
private static final String PLACEHOLDER_WRAPPER = "\u0001";
/** A JS regex literal that matches our placeholder placeholders. */
private static final String PLACEHOLDER_REGEX = "/\\x01\\d+\\x01/g";
AssistantForHtmlMsgs(
GenIncrementalDomCodeVisitor master,
SoyJsSrcOptions jsSrcOptions,
JsExprTranslator jsExprTranslator,
GenCallCodeUtils genCallCodeUtils,
IsComputableAsJsExprsVisitor isComputableAsJsExprsVisitor,
TemplateAliases functionAliases,
GenJsExprsVisitor genJsExprsVisitor,
TranslationContext translationContext,
ErrorReporter errorReporter) {
super(
master,
jsSrcOptions,
jsExprTranslator,
genCallCodeUtils,
isComputableAsJsExprsVisitor,
functionAliases,
genJsExprsVisitor,
translationContext,
errorReporter);
}
@Override
public String generateMsgGroupVariable(MsgFallbackGroupNode node) {
throw new IllegalStateException(
"This class should only be used for via the new idom entry-point.");
}
/**
* Generates idom instructions that output the contents of a translated message as HTML. For
* example:
*
* <pre>
* {msg desc="Says hello to a person."}Hello {$name}!{/msg}
* </pre>
*
* compiles to
*
* <pre>
* /** @desc Says hello to a person. *{@literal /}
* var MSG_EXTERNAL_6936162475751860807 = goog.getMsg(
* 'Hello {$name}!',
* {'name': '\u00010\u0001'});
* var lastIndex_1153 = 0, partRe_1153 = /\x01\d+\x01/g, match_1153;
* do {
* match_1153 = partRe_1153.exec(MSG_EXTERNAL_6936162475751860807) || undefined;
* incrementalDom.text(goog.string.unescapeEntities(
* MSG_EXTERNAL_6936162475751860807.substring(
* lastIndex_1153, match_1153 && match_1153.index)));
* lastIndex_1153 = partRe_1153.lastIndex;
* switch (match_1153 && match_1153[0]) {
* case '\u00010\u0001':
* var dyn8 = opt_data.name;
* if (typeof dyn8 == 'function') dyn8();
* else if (dyn8 != null) incrementalDom.text(dyn8);
* break;
* }
* } while (match_1153);
* </pre>
*
* Each interpolated MsgPlaceholderNode (either for HTML tags or for print statements) compiles to
* a separate {@code case} statement.
*/
void generateMsgGroupCode(MsgFallbackGroupNode node) {
Preconditions.checkState(placeholderNames.isEmpty(), "This class is not reusable.");
// Non-HTML {msg}s should be extracted into LetContentNodes and handled by jssrc.
Preconditions.checkArgument(node.getHtmlContext() == HtmlContext.HTML_PCDATA,
"AssistantForHtmlMsgs is only for HTML {msg}s.");
// All of these helper variables must have uniquely-suffixed names because {msg}s can be nested.
// It'd be nice to move this codegen to a Soy template...
// The raw translated text, with placeholder placeholders.
String translationVar = super.generateMsgGroupVariable(node);
// If there are no placeholders, we don't need anything special (but we still need to unescape).
if (placeholderNames.isEmpty()) {
CodeChunk.WithValue unescape = GOOG_STRING_UNESCAPE_ENTITIES.call(id(translationVar));
jsCodeBuilder().append(INCREMENTAL_DOM_TEXT.call(unescape));
return;
}
// The mutable (tracking index of last match) regex to find the placeholder placeholders.
String regexVar = "partRe_" + node.getId();
// The current placeholder placeholder from the regex.
String matchVar = "match_" + node.getId();
// The index of the end of the previous placeholder, where the next raw text run starts.
String lastIndexVar = "lastIndex_" + node.getId();
// Declare everything.
jsCodeBuilder()
.appendLine(
"var ",
lastIndexVar,
" = 0, ",
regexVar,
" = ",
PLACEHOLDER_REGEX,
", ",
matchVar,
";");
// For each placeholder.
jsCodeBuilder().appendLine("do {");
jsCodeBuilder().increaseIndent();
// Find the placeholder.
jsCodeBuilder()
.appendLine(matchVar, " = ", regexVar, ".exec(", translationVar, ") || undefined;");
// Replace null with undefined. This is necessary to make substring() treat falsy as an omitted
// parameter, so that it goes until the end of the string. Otherwise, the non-numeric parameter
// would be coerced to zero.
// Emit the (possibly-empty) run of raw text since the last placeholder, until this placeholder,
// or until the end of the source string.
CodeChunk.WithValue endIndex =
id(matchVar).and(id(matchVar).dotAccess("index"), translationContext.codeGenerator());
CodeChunk.WithValue unescape =
GOOG_STRING_UNESCAPE_ENTITIES.call(
id(translationVar).dotAccess("substring").call(id(lastIndexVar), endIndex));
jsCodeBuilder().append(INCREMENTAL_DOM_TEXT.call(unescape));
jsCodeBuilder().appendLine(lastIndexVar, " = ", regexVar, ".lastIndex;");
// Handle the actual placeholder.
jsCodeBuilder().appendLine("switch (", matchVar, " && ", matchVar, "[0]) {");
jsCodeBuilder().increaseIndent();
for (Map.Entry<String, MsgPlaceholderNode> ph : placeholderNames.entrySet()) {
jsCodeBuilder().appendLine("case ", BaseUtils.escapeToSoyString(ph.getKey(), true), ":");
jsCodeBuilder().increaseIndent();
master.visitForUseByAssistants(ph.getValue());
jsCodeBuilder().appendLine("break;");
jsCodeBuilder().decreaseIndent();
}
jsCodeBuilder().decreaseIndent();
jsCodeBuilder().appendLine("}");
jsCodeBuilder().decreaseIndent();
jsCodeBuilder().appendLine("} while (", matchVar, ");");
}
@Override
protected CodeChunk.WithValue genGoogMsgPlaceholder(MsgPlaceholderNode msgPhNode) {
// Mark the node so we know what instructions to emit.
String name = PLACEHOLDER_WRAPPER + placeholderNames.size() + PLACEHOLDER_WRAPPER;
placeholderNames.put(name, msgPhNode);
// Return the marker string to insert into the translated text.
return CodeChunk.stringLiteral(name);
}
}