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}