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