001/*
002 * Copyright (c) 2012-2018 Institut National des Sciences Appliquées de Lyon (INSA Lyon) and others
003 *
004 * This program and the accompanying materials are made available under the
005 * terms of the Eclipse Public License 2.0 which is available at
006 * http://www.eclipse.org/legal/epl-2.0.
007 *
008 * SPDX-License-Identifier: EPL-2.0
009 */
010
011package gololang;
012
013import java.util.regex.Matcher;
014import java.util.regex.Pattern;
015
016/**
017 * A simple template engine that turns text templates into Golo functions.
018 * <p>
019 * The template engine is similar to Ruby ERB or Java Server Pages. Golo code and directives can be embedded as follows:
020 * <ul>
021 * <li>{@code <% code %>} blocks contain any Golo code, and
022 * <li>{@code <%= expression %>} outputs the evaluation of {@code expression}, and
023 * <li>{@code <%@params foo, bar, baz %>} makes the template function take these parameter names, and
024 * <li>{@code <%@import foo.bar.Baz %>} is equivalent to a {@code import} in a Golo module.
025 * </ul>
026 *
027 * <p>
028 * Here is a template example:
029 * <pre class="listing"><code>
030 * <%@params persons %>
031 * <% foreach (person in persons) { %>
032 * Name: <%= person: name() %>
033 * Email: <%= person: email() orIfNull "n/a" %>
034 * <% } %>
035 * </code></pre>
036 *
037 * The resulting function would take a single parameter {@code persons}. When no {@code @params} clause is being
038 * specified, template functions are assumed to take a single {@code params} parameter.
039 * <p>
040 * It is important to note that this template engine performs no validation, either on the template itself or the
041 * generated function code. One may however catch the {@link org.eclipse.golo.compiler.GoloCompilationException}
042 * that {@link #compile(String)} may throw, and inspect the faulty code using
043 * {@link org.eclipse.golo.compiler.GoloCompilationException#getSourceCode()} and
044 * {@link org.eclipse.golo.compiler.GoloCompilationException#getProblems()}.
045 */
046public class TemplateEngine {
047
048  private final EvaluationEnvironment evaluationEnvironment = new EvaluationEnvironment();
049
050  private static final Pattern PATTERN = Pattern.compile("<%(.*?)%>", Pattern.DOTALL);
051
052  /**
053   * Compile a template into a function. The function takes parameters as specified using a {@code @params clause}, or
054   * a single {@code params} argument if none exists.
055   *
056   * @param template the template code.
057   * @return a compiled function that evaluates the template given parameters, and returns a {@link String}.
058   * @throws org.eclipse.golo.compiler.GoloCompilationException
059   *          if a compilation error occurs in the generated Golo code.
060   */
061  public FunctionReference compile(String template) {
062    evaluationEnvironment.clearImports();
063    String goloCode = templateToGolo(template);
064    return (FunctionReference) evaluationEnvironment.def(goloCode);
065  }
066
067  /**
068   * Generates the Golo code for a given template, but does not compile it.
069   *
070   * @param template the template code.
071   * @return the corresponding Golo source code which may or may not be valid.
072   */
073  public String templateToGolo(String template) {
074    StringBuilder builder = new StringBuilder();
075    String params = null;
076    builder.append("  let _$result = java.lang.StringBuilder()\n");
077    Matcher matcher = PATTERN.matcher(template);
078    int startIndex = 0;
079    while (matcher.find()) {
080      String text = template.substring(startIndex, matcher.start());
081      int lowerBound = 0;
082      int upperBound = text.length();
083      if (text.startsWith("\"")) {
084        lowerBound = 1;
085        builder.append("  _$result: append(\"\\\"\")\n");
086      }
087      if (text.endsWith("\"")) {
088        upperBound = text.length() - 1;
089      }
090      builder.append("  _$result: append(\"\"\"").append(text.substring(lowerBound, upperBound)).append("\"\"\")\n");
091      if (text.endsWith("\"")) {
092        builder.append("  _$result: append(\"\\\"\")\n");
093      }
094      String code = matcher.group();
095      code = code.substring(2, code.length() - 2);
096      if (code.startsWith("=")) {
097        builder.append("  _$result: append(").append(code.substring(1)).append(")\n");
098      } else if (code.startsWith("@params")) {
099        params = "|" + code.substring(7).trim() + "| {\n";
100      } else if (code.startsWith("@import")) {
101        evaluationEnvironment.imports(code.substring(7).trim());
102      } else {
103        builder.append(code);
104      }
105      startIndex = matcher.end();
106    }
107    builder
108        .append("\n  _$result: append(\"\"\"")
109        .append(template.substring(startIndex))
110        .append("\"\"\")\n")
111        .append("  return _$result: toString()\n")
112        .append("}\n");
113    if (params == null) {
114      params = "|params| {\n";
115    }
116    return params + builder.toString();
117  }
118}