/* * Copyright 2010 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 com.google.template.soy.shared.internal.AbstractGenerateSoyEscapingDirectiveCode; import com.google.template.soy.shared.internal.DirectiveDigest; import com.google.template.soy.shared.restricted.EscapingConventions; import com.google.template.soy.shared.restricted.EscapingConventions.EscapingLanguage; import com.google.template.soy.shared.restricted.Sanitizers; import com.google.template.soy.shared.restricted.TagWhitelist; import java.io.IOException; import java.util.regex.Pattern; import javax.annotation.ParametersAreNonnullByDefault; /** * Generates JavaScript code relied upon by soyutils.js and soyutils_use_goog.js. * * <p>This is an ant task and can be invoked as: <xmp> <taskdef name="gen.escape.directives" * classname="com.google.template.soy.jssrc.internal.GenerateSoyUtilsEscapingDirectiveCode"> * <classpath> * <!-- classpath to Soy classes and dependencies --> * </classpath> </taskdef> <gen.escape.directives> <input path="one or more JS files that use the * generated helpers"/> <output path="the output JS file"/> <libdefined pattern="goog.*"/> * <!-- enables closure alternatives --> * </gen.escape.directives> </xmp> * * <p>In the above, the first {@code <taskdef>} is an Ant builtin which links the element named * {@code <gen.escape.directives>} to this class. * * <p>That element contains zero or more {@code <input>}s which are JavaScript source files that may * use the helper functions generated by this task. * * <p>There must be exactly one {@code <output>} element which specifies where the output should be * written. That output contains the input sources and the generated helper functions. * * <p>There may be zero or more {@code <libdefined>} elements which specify which functions should * be available in the context in which {@code <output>} is run. * */ @ParametersAreNonnullByDefault public final class GenerateSoyUtilsEscapingDirectiveCode extends AbstractGenerateSoyEscapingDirectiveCode { @Override protected EscapingLanguage getLanguage() { return EscapingLanguage.JAVASCRIPT; } @Override protected String getLineCommentSyntax() { return "//"; } @Override protected String getLineEndSyntax() { return ";"; } @Override protected String getRegexStart() { return "/"; } @Override protected String getRegexEnd() { return "/g"; } @Override protected String escapeOutputString(String input) { return EscapingConventions.EscapeJsString.INSTANCE.escape(input); } @Override protected String convertFromJavaRegex(Pattern javaPattern) { String body = javaPattern .pattern() .replace("\r", "\\r") .replace("\n", "\\n") .replace("\t", "\\t") .replace("\u0000", "\\u0000") .replace("\u0020", "\\u0020") .replace("\u2028", "\\u2028") .replace("\u2029", "\\u2029") .replace("\\A", "^") .replace("\\z", "$") .replaceAll("(?<!\\\\)(?:\\\\{2})*/", "\\\\/"); // Some features supported by Java are not supported by JavaScript such as lookbehind, // DOTALL, and unicode character classes. if (body.contains("(?<")) { throw new IllegalArgumentException("Pattern " + javaPattern + " uses lookbehind."); } else if ((javaPattern.flags() & Pattern.DOTALL) != 0) { throw new IllegalArgumentException("Pattern " + javaPattern + " uses DOTALL."); } else if (NAMED_CLASS.matcher(body).find()) { throw new IllegalArgumentException( "Pattern " + javaPattern + " uses named characer classes."); } StringBuilder buffer = new StringBuilder(body.length() + 4); buffer.append('/').append(body).append('/'); if ((javaPattern.flags() & Pattern.CASE_INSENSITIVE) != 0) { buffer.append('i'); } if ((javaPattern.flags() & Pattern.MULTILINE) != 0) { buffer.append('m'); } return buffer.toString(); } @Override protected void generateCharacterMapSignature(StringBuilder outputCode, String mapName) { outputCode .append('\n') .append("/**\n") .append(" * Maps characters to the escaped versions for the named escape directives.\n") .append(" * @private {!Object<string, string>}\n") .append(" */\n") .append("soy.esc.$$ESCAPE_MAP_FOR_") .append(mapName) .append("_"); } @Override protected void generateMatcher(StringBuilder outputCode, String name, String matcher) { outputCode .append('\n') .append("/**\n") .append(" * Matches characters that need to be escaped for the named directives.\n") .append(" * @private {!RegExp}\n") .append(" */\n") .append("soy.esc.$$MATCHER_FOR_") .append(name) .append("_ = ") .append(matcher) .append(";\n"); } @Override protected void generateFilter(StringBuilder outputCode, String name, String filter) { outputCode .append('\n') .append("/**\n") .append(" * A pattern that vets values produced by the named directives.\n") .append(" * @private {!RegExp}\n") .append(" */\n") .append("soy.esc.$$FILTER_FOR_") .append(name) .append("_ = ") .append(filter) .append(";\n"); } @Override protected void generateCommonConstants(StringBuilder outputCode) { // Emit patterns and constants needed by escaping functions that are not part of any one // escaping convention. outputCode .append('\n') .append("/**\n") .append(" * Matches all tags, HTML comments, and DOCTYPEs in tag soup HTML.\n") .append(" * By removing these, and replacing any '<' or '>' characters with\n") .append(" * entities we guarantee that the result can be embedded into a\n") .append(" * an attribute without introducing a tag boundary.\n") .append(" *\n") .append(" * @private {!RegExp}\n") .append(" */\n") .append("soy.esc.$$HTML_TAG_REGEX_ = ") .append(convertFromJavaRegex(EscapingConventions.HTML_TAG_CONTENT)) .append("g;\n"); outputCode .append("\n") .append("/**\n") .append(" * Matches all occurrences of '<'.\n") .append(" *\n") .append(" * @private {!RegExp}\n") .append(" */\n") .append("soy.esc.$$LT_REGEX_ = /</g;\n"); outputCode .append('\n') .append("/**\n") .append(" * Maps lower-case names of innocuous tags to true.\n") .append(" *\n") .append(" * @private {!Object<string, boolean>}\n") .append(" */\n") .append("soy.esc.$$SAFE_TAG_WHITELIST_ = ") .append(toJsStringSet(TagWhitelist.FORMATTING.asSet())) .append(";\n"); outputCode .append('\n') .append("/**\n") .append(" * Pattern for matching attribute name and value, where value is single-quoted\n") .append(" * or double-quoted.\n") .append(" * See http://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#attributes-0\n") .append(" *\n") .append(" * @private {!RegExp}\n") .append(" */\n") .append("soy.esc.$$HTML_ATTRIBUTE_REGEX_ = ") .append(convertFromJavaRegex(Sanitizers.HTML_ATTRIBUTE_PATTERN)) .append("g;\n"); } @Override protected void generateReplacerFunction(StringBuilder outputCode, String mapName) { outputCode .append('\n') .append("/**\n") .append(" * A function that can be used with String.replace.\n") .append(" * @param {string} ch A single character matched by a compatible matcher.\n") .append(" * @return {string} A token in the output language.\n") .append(" * @private\n") .append(" */\n") .append("soy.esc.$$REPLACER_FOR_") .append(mapName) .append("_ = function(ch) {\n") .append(" return soy.esc.$$ESCAPE_MAP_FOR_") .append(mapName) .append("_[ch];\n") .append("};\n"); } @Override protected void useExistingLibraryFunction( StringBuilder outputCode, String identifier, String existingFunction) { outputCode .append('\n') .append("/**\n") .append(" * @type {function (*) : string}\n") .append(" */\n") .append("soy.esc.$$") .append(identifier) .append("Helper = function(v) {\n") .append(" return ") .append(existingFunction) .append("(String(v));\n") .append("};\n"); } @Override protected void generateHelperFunction(StringBuilder outputCode, DirectiveDigest digest) { String name = digest.getDirectiveName(); outputCode .append('\n') .append("/**\n") .append(" * A helper for the Soy directive |") .append(name) .append('\n') .append(" * @param {*} value Can be of any type but will be coerced to a string.\n") .append(" * @return {string} The escaped text.\n") .append(" */\n") .append("soy.esc.$$") .append(name) .append("Helper = function(value) {\n") .append(" var str = String(value);\n"); if (digest.getFilterName() != null) { String filterName = digest.getFilterName(); outputCode .append(" if (!soy.esc.$$FILTER_FOR_") .append(filterName) .append("_.test(str)) {\n"); if (availableIdentifiers.apply("goog.asserts.fail")) { outputCode .append(" goog.asserts.fail('Bad value `%s` for |") .append(name) .append("', [str]);\n"); } outputCode .append(" return '") .append(digest.getInnocuousOutput()) .append("';\n") .append(" }\n"); } if (digest.getNonAsciiPrefix() != null) { // TODO(msamuel): We can add a second replace of all non-ascii codepoints below. throw new UnsupportedOperationException("Non ASCII prefix escapers not implemented yet."); } if (digest.getEscapesName() != null) { String escapeMapName = digest.getEscapesName(); String matcherName = digest.getMatcherName(); outputCode .append(" return str.replace(\n") .append(" soy.esc.$$MATCHER_FOR_") .append(matcherName) .append("_,\n") .append(" soy.esc.$$REPLACER_FOR_") .append(escapeMapName) .append("_);\n"); } else { outputCode.append(" return str;\n"); } outputCode.append("};\n"); } /** ["foo", "bar"] -> '{"foo": true, "bar": true}' */ private String toJsStringSet(Iterable<String> strings) { StringBuilder sb = new StringBuilder(); boolean isFirst = true; sb.append('{'); for (String str : strings) { if (!isFirst) { sb.append(", "); } isFirst = false; writeStringLiteral(str, sb); sb.append(": true"); } sb.append('}'); return sb.toString(); } /** Matches named character classes in Java regular expressions. */ private static final Pattern NAMED_CLASS = Pattern.compile("(?<!\\\\)(\\\\{2})*\\\\p\\{"); /** A non Ant interface for this class. */ public static void main(String[] args) throws IOException { GenerateSoyUtilsEscapingDirectiveCode generator = new GenerateSoyUtilsEscapingDirectiveCode(); generator.configure(args); generator.execute(); } }