Add reactive ScriptTemplateView

For now if sharedEngine is set to false, a new
ScriptEngine instance is created for each request.

Issue: SPR-15063
This commit is contained in:
Sebastien Deleuze
2017-01-09 19:32:42 +01:00
parent 74fa088c7e
commit b503e4679c
21 changed files with 1363 additions and 0 deletions

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* 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 org.springframework.web.reactive.result.view.script;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.http.MediaType;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.DefaultWebSessionManager;
import org.springframework.web.server.session.WebSessionManager;
/**
* Unit tests for ERB templates running on JRuby.
*
* @author Sebastien Deleuze
*/
@Ignore("JRuby not compatible with JDK 9 yet")
public class JRubyScriptTemplateTests {
private StaticApplicationContext context;
@Before
public void setup() {
this.context = new StaticApplicationContext();
}
@Test
public void renderTemplate() throws Exception {
Map<String, Object> model = new HashMap<>();
model.put("title", "Layout example");
model.put("body", "This is the body");
MockServerHttpResponse response = renderViewWithModel("org/springframework/web/reactive/result/view/script/jruby/template.erb", model);
assertEquals("<html><head><title>Layout example</title></head><body><p>This is the body</p></body></html>",
response.getBodyAsString().block());
}
private MockServerHttpResponse renderViewWithModel(String viewUrl, Map<String, Object> model) throws Exception {
ScriptTemplateView view = createViewWithUrl(viewUrl);
MockServerHttpRequest request = new MockServerHttpRequest();
MockServerHttpResponse response = new MockServerHttpResponse();
WebSessionManager manager = new DefaultWebSessionManager();
ServerWebExchange exchange = new DefaultServerWebExchange(request, response, manager);
view.renderInternal(model, MediaType.TEXT_HTML, exchange).block();
return response;
}
private ScriptTemplateView createViewWithUrl(String viewUrl) throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ScriptTemplatingConfiguration.class);
ctx.refresh();
ScriptTemplateView view = new ScriptTemplateView();
view.setApplicationContext(ctx);
view.setUrl(viewUrl);
view.afterPropertiesSet();
return view;
}
@Configuration
static class ScriptTemplatingConfiguration {
@Bean
public ScriptTemplateConfigurer jRubyConfigurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setScripts("org/springframework/web/reactive/result/view/script/jruby/render.rb");
configurer.setEngineName("jruby");
configurer.setRenderFunction("render");
return configurer;
}
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* 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 org.springframework.web.reactive.result.view.script;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.http.MediaType;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.DefaultWebSessionManager;
import org.springframework.web.server.session.WebSessionManager;
/**
* Unit tests for String templates running on Jython.
*
* @author Sebastien Deleuze
*/
public class JythonScriptTemplateTests {
private StaticApplicationContext context;
@Before
public void setup() {
this.context = new StaticApplicationContext();
}
@Test
public void renderTemplate() throws Exception {
Map<String, Object> model = new HashMap<>();
model.put("title", "Layout example");
model.put("body", "This is the body");
MockServerHttpResponse response = renderViewWithModel("org/springframework/web/reactive/result/view/script/jython/template.html", model);
assertEquals("<html><head><title>Layout example</title></head><body><p>This is the body</p></body></html>",
response.getBodyAsString().block());
}
private MockServerHttpResponse renderViewWithModel(String viewUrl, Map<String, Object> model) throws Exception {
ScriptTemplateView view = createViewWithUrl(viewUrl);
MockServerHttpRequest request = new MockServerHttpRequest();
MockServerHttpResponse response = new MockServerHttpResponse();
WebSessionManager manager = new DefaultWebSessionManager();
ServerWebExchange exchange = new DefaultServerWebExchange(request, response, manager);
view.renderInternal(model, MediaType.TEXT_HTML, exchange).block();
return response;
}
private ScriptTemplateView createViewWithUrl(String viewUrl) throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ScriptTemplatingConfiguration.class);
ctx.refresh();
ScriptTemplateView view = new ScriptTemplateView();
view.setApplicationContext(ctx);
view.setUrl(viewUrl);
view.afterPropertiesSet();
return view;
}
@Configuration
static class ScriptTemplatingConfiguration {
@Bean
public ScriptTemplateConfigurer jythonConfigurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setScripts("org/springframework/web/reactive/result/view/script/jython/render.py");
configurer.setEngineName("jython");
configurer.setRenderFunction("render");
return configurer;
}
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* 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 org.springframework.web.reactive.result.view.script;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.http.MediaType;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.DefaultWebSessionManager;
import org.springframework.web.server.session.WebSessionManager;
/**
* Unit tests for Kotlin script templates running on Kotlin JSR 223 support
*
* @author Sebastien Deleuze
*/
@Ignore // Temporary disabled since Kotlin 1.1-M04 generates bytecode not Kotlin 1.0 compliant, will be enable as soon as Kotlin 1.1-M05 is available
public class KotlinScriptTemplateTests {
private StaticApplicationContext context;
@Before
public void setup() {
this.context = new StaticApplicationContext();
}
@Test
public void renderTemplate() throws Exception {
Map<String, Object> model = new HashMap<>();
model.put("title", "Layout example");
model.put("body", "This is the body");
MockServerHttpResponse response = renderViewWithModel("org/springframework/web/reactive/result/view/script/kotlin/template.kts",
model, ScriptTemplatingConfiguration.class);
assertEquals("<html><head><title>Layout example</title></head><body><p>This is the body</p></body></html>",
response.getBodyAsString().block());
}
private MockServerHttpResponse renderViewWithModel(String viewUrl, Map<String, Object> model, Class<?> configuration) throws Exception {
ScriptTemplateView view = createViewWithUrl(viewUrl, configuration);
MockServerHttpRequest request = new MockServerHttpRequest();
MockServerHttpResponse response = new MockServerHttpResponse();
WebSessionManager manager = new DefaultWebSessionManager();
ServerWebExchange exchange = new DefaultServerWebExchange(request, response, manager);
view.renderInternal(model, MediaType.TEXT_HTML, exchange).block();
return response;
}
private ScriptTemplateView createViewWithUrl(String viewUrl, Class<?> configuration) throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(configuration);
ctx.refresh();
ScriptTemplateView view = new ScriptTemplateView();
view.setApplicationContext(ctx);
view.setUrl(viewUrl);
view.afterPropertiesSet();
return view;
}
@Configuration
static class ScriptTemplatingConfiguration {
@Bean
public ScriptTemplateConfigurer kotlinScriptConfigurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("kotlin");
configurer.setScripts("org/springframework/web/reactive/result/view/script/kotlin/render.kts");
configurer.setRenderFunction("render");
return configurer;
}
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* 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 org.springframework.web.reactive.result.view.script;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.http.MediaType;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.DefaultWebSessionManager;
import org.springframework.web.server.session.WebSessionManager;
/**
* Unit tests for pure Javascript templates running on Nashorn engine.
*
* @author Sebastien Deleuze
*/
public class NashornScriptTemplateTests {
private StaticApplicationContext context;
@Before
public void setup() {
this.context = new StaticApplicationContext();
}
@Test
public void renderTemplate() throws Exception {
Map<String, Object> model = new HashMap<>();
model.put("title", "Layout example");
model.put("body", "This is the body");
MockServerHttpResponse response = renderViewWithModel("org/springframework/web/reactive/result/view/script/nashorn/template.html",
model, ScriptTemplatingConfiguration.class);
assertEquals("<html><head><title>Layout example</title></head><body><p>This is the body</p></body></html>",
response.getBodyAsString().block());
}
@Test // SPR-13453
public void renderTemplateWithUrl() throws Exception {
MockServerHttpResponse response = renderViewWithModel("org/springframework/web/reactive/result/view/script/nashorn/template.html",
null, ScriptTemplatingWithUrlConfiguration.class);
assertEquals("<html><head><title>Check url parameter</title></head><body><p>org/springframework/web/reactive/result/view/script/nashorn/template.html</p></body></html>",
response.getBodyAsString().block());
}
private MockServerHttpResponse renderViewWithModel(String viewUrl, Map<String, Object> model, Class<?> configuration) throws Exception {
ScriptTemplateView view = createViewWithUrl(viewUrl, configuration);
MockServerHttpRequest request = new MockServerHttpRequest();
MockServerHttpResponse response = new MockServerHttpResponse();
WebSessionManager manager = new DefaultWebSessionManager();
ServerWebExchange exchange = new DefaultServerWebExchange(request, response, manager);
view.renderInternal(model, MediaType.TEXT_HTML, exchange).block();
return response;
}
private ScriptTemplateView createViewWithUrl(String viewUrl, Class<?> configuration) throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(configuration);
ctx.refresh();
ScriptTemplateView view = new ScriptTemplateView();
view.setApplicationContext(ctx);
view.setUrl(viewUrl);
view.afterPropertiesSet();
return view;
}
@Configuration
static class ScriptTemplatingConfiguration {
@Bean
public ScriptTemplateConfigurer nashornConfigurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("org/springframework/web/reactive/result/view/script/nashorn/render.js");
configurer.setRenderFunction("render");
return configurer;
}
}
@Configuration
static class ScriptTemplatingWithUrlConfiguration {
@Bean
public ScriptTemplateConfigurer nashornConfigurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("org/springframework/web/reactive/result/view/script/nashorn/render.js");
configurer.setRenderFunction("renderWithUrl");
return configurer;
}
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* 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 org.springframework.web.reactive.result.view.script;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.DirectFieldAccessor;
/**
* Unit tests for {@link ScriptTemplateViewResolver}.
*
* @author Sebastien Deleuze
*/
public class ScriptTemplateViewResolverTests {
@Test
public void viewClass() throws Exception {
ScriptTemplateViewResolver resolver = new ScriptTemplateViewResolver();
Assert.assertEquals(ScriptTemplateView.class, resolver.requiredViewClass());
DirectFieldAccessor viewAccessor = new DirectFieldAccessor(resolver);
Class viewClass = (Class) viewAccessor.getPropertyValue("viewClass");
Assert.assertEquals(ScriptTemplateView.class, viewClass);
}
}

View File

@@ -0,0 +1,190 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* 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 org.springframework.web.reactive.result.view.script;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import static org.mockito.BDDMockito.*;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.support.StaticApplicationContext;
/**
* Unit tests for {@link ScriptTemplateView}.
*
* @author Sebastien Deleuze
*/
public class ScriptTemplateViewTests {
private ScriptTemplateView view;
private ScriptTemplateConfigurer configurer;
private StaticApplicationContext context;
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Before
public void setup() {
this.configurer = new ScriptTemplateConfigurer();
this.context = new StaticApplicationContext();
this.context.getBeanFactory().registerSingleton("scriptTemplateConfigurer", this.configurer);
this.view = new ScriptTemplateView();
}
@Test
public void missingTemplate() throws Exception {
this.context.refresh();
this.view.setResourceLoaderPath("classpath:org/springframework/web/reactive/result/view/script/");
this.view.setUrl("missing.txt");
this.view.setEngine(mock(InvocableScriptEngine.class));
this.configurer.setRenderFunction("render");
this.view.setApplicationContext(this.context);
assertFalse(this.view.checkResourceExists(Locale.ENGLISH));
}
@Test
public void missingScriptTemplateConfig() throws Exception {
this.expectedException.expect(ApplicationContextException.class);
this.view.setApplicationContext(new StaticApplicationContext());
this.expectedException.expectMessage(contains("ScriptTemplateConfig"));
}
@Test
public void detectScriptTemplateConfigWithEngine() {
InvocableScriptEngine engine = mock(InvocableScriptEngine.class);
this.configurer.setEngine(engine);
this.configurer.setRenderObject("Template");
this.configurer.setRenderFunction("render");
this.configurer.setCharset(StandardCharsets.ISO_8859_1);
this.configurer.setSharedEngine(true);
DirectFieldAccessor accessor = new DirectFieldAccessor(this.view);
this.view.setApplicationContext(this.context);
assertEquals(engine, accessor.getPropertyValue("engine"));
assertEquals("Template", accessor.getPropertyValue("renderObject"));
assertEquals("render", accessor.getPropertyValue("renderFunction"));
assertEquals(StandardCharsets.ISO_8859_1, accessor.getPropertyValue("defaultCharset"));
assertEquals(true, accessor.getPropertyValue("sharedEngine"));
}
@Test
public void detectScriptTemplateConfigWithEngineName() {
this.configurer.setEngineName("nashorn");
this.configurer.setRenderObject("Template");
this.configurer.setRenderFunction("render");
DirectFieldAccessor accessor = new DirectFieldAccessor(this.view);
this.view.setApplicationContext(this.context);
assertEquals("nashorn", accessor.getPropertyValue("engineName"));
assertNotNull(accessor.getPropertyValue("engine"));
assertEquals("Template", accessor.getPropertyValue("renderObject"));
assertEquals("render", accessor.getPropertyValue("renderFunction"));
assertEquals(StandardCharsets.UTF_8, accessor.getPropertyValue("defaultCharset"));
}
@Test
public void customEngineAndRenderFunction() throws Exception {
ScriptEngine engine = mock(InvocableScriptEngine.class);
given(engine.get("key")).willReturn("value");
this.view.setEngine(engine);
this.view.setRenderFunction("render");
this.view.setApplicationContext(this.context);
engine = this.view.getEngine();
assertNotNull(engine);
assertEquals("value", engine.get("key"));
DirectFieldAccessor accessor = new DirectFieldAccessor(this.view);
assertNull(accessor.getPropertyValue("renderObject"));
assertEquals("render", accessor.getPropertyValue("renderFunction"));
assertEquals(StandardCharsets.UTF_8, accessor.getPropertyValue("defaultCharset"));
}
@Test
public void nonSharedEngine() throws Exception {
int iterations = 20;
this.view.setEngineName("nashorn");
this.view.setRenderFunction("render");
this.view.setSharedEngine(false);
this.view.setApplicationContext(this.context);
ExecutorService executor = Executors.newFixedThreadPool(4);
List<Future<Boolean>> results = new ArrayList<>();
for (int i = 0; i < iterations; i++) {
results.add(executor.submit(() -> view.getEngine() != null));
}
assertEquals(iterations, results.size());
for (int i = 0; i < iterations; i++) {
assertTrue(results.get(i).get());
}
executor.shutdown();
}
@Test
public void nonInvocableScriptEngine() throws Exception {
this.expectedException.expect(IllegalArgumentException.class);
this.view.setEngine(mock(ScriptEngine.class));
this.expectedException.expectMessage(contains("instance"));
}
@Test
public void noRenderFunctionDefined() {
this.view.setEngine(mock(InvocableScriptEngine.class));
this.expectedException.expect(IllegalArgumentException.class);
this.view.setApplicationContext(this.context);
this.expectedException.expectMessage(contains("renderFunction"));
}
@Test
public void engineAndEngineNameBothDefined() {
this.view.setEngine(mock(InvocableScriptEngine.class));
this.view.setEngineName("test");
this.view.setRenderFunction("render");
this.expectedException.expect(IllegalArgumentException.class);
this.view.setApplicationContext(this.context);
this.expectedException.expectMessage(contains("'engine' or 'engineName'"));
}
@Test
public void engineSetterAndNonSharedEngine() {
this.view.setEngine(mock(InvocableScriptEngine.class));
this.view.setRenderFunction("render");
this.view.setSharedEngine(false);
this.expectedException.expect(IllegalArgumentException.class);
this.view.setApplicationContext(this.context);
this.expectedException.expectMessage(contains("sharedEngine"));
}
private interface InvocableScriptEngine extends ScriptEngine, Invocable {
}
}

View File

@@ -0,0 +1,20 @@
require 'erb'
require 'ostruct'
require 'java'
# Renders an ERB template against a hashmap of variables.
def render(template, variables, url)
context = OpenStruct.new(variables).instance_eval do
variables.each do |k, v|
instance_variable_set(k, v) if k[0] == '@'
end
def partial(partial_name, options={})
new_variables = marshal_dump.merge(options[:locals] || {})
Java::Pavo::ERB.render(partial_name, new_variables)
end
binding
end
ERB.new(template).result(context);
end

View File

@@ -0,0 +1 @@
<html><head><title><%= title %></title></head><body><p><%= body %></p></body></html>

View File

@@ -0,0 +1,5 @@
from string import Template
def render(template, model, url):
s = Template(template)
return s.substitute(model)

View File

@@ -0,0 +1 @@
<html><head><title>$title</title></head><body><p>$body</p></body></html>

View File

@@ -0,0 +1,10 @@
import javax.script.*
// TODO Use engine.eval(String, Bindings) when https://youtrack.jetbrains.com/issue/KT-15450 will be fixed
fun render(template: String, model: Map<String, Any>, url: String): String {
val engine = ScriptEngineManager().getEngineByName("kotlin")
val bindings = SimpleBindings()
bindings.putAll(model)
engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE)
return engine.eval(template) as String
}

View File

@@ -0,0 +1 @@
"""<html><head><title>${bindings["title"]}</title></head><body><p>${bindings["body"]}</p></body></html>"""

View File

@@ -0,0 +1,7 @@
function render(template, model) {
return template.replace("{{title}}", model.title).replace("{{body}}", model.body);
}
function renderWithUrl(template, model, url) {
return template.replace("{{title}}", "Check url parameter").replace("{{body}}", url);
}

View File

@@ -0,0 +1 @@
<html><head><title>{{title}}</title></head><body><p>{{body}}</p></body></html>