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}