Repository: PebbleTemplates/pebble
Pull request #269 (2017-05-22 14:50:33)

Extend PebbleTemplate to allow rendering of single blocks
I have recently adopted Pebble for rendering html views in an enterprise application, and thought it could be useful for emails as well. It seems sensible to describe subject, text and html body in the same template, but this would require rendering them as separate blocks.

This PR extends `PebbleTemplate` and `PebbleTemplateImpl` with a new `evaluateBlock()` method that allows rendering of single blocks, similar to the Twig [renderBlock()](https://twig.sensiolabs.org/doc/2.x/api.html#rendering-templates) method.

Happy to improve this if I have missed an easier way!


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3aa85e7a..5d0211b4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@
 - Output filename and line number of non-existent macro
 - Fix for null pointer and index out of bounds exceptions when invalid or no endif/endfor tags are used in template (#266)
 - Add DynamicAttributeProvider interface. When implemented by an object, tells the expression parser that this object is able to provide attributes dynamically, given their names and the potential arguments(#230)
+- Add rendering of single blocks, similar to the Twig renderBlock() method.
 
 ## v2.3.0 (2016-11-13)
 - Upgrade SLF4J from 1.6.1 to 1.7.21
diff --git a/src/main/java/com/mitchellbosecke/pebble/template/PebbleTemplate.java b/src/main/java/com/mitchellbosecke/pebble/template/PebbleTemplate.java
index 8ffeec1f..b3c6006f 100644
--- a/src/main/java/com/mitchellbosecke/pebble/template/PebbleTemplate.java
+++ b/src/main/java/com/mitchellbosecke/pebble/template/PebbleTemplate.java
@@ -63,6 +63,50 @@ public interface PebbleTemplate {
      */
     void evaluate(Writer writer, Map<String, Object> context, Locale locale) throws PebbleException, IOException;
 
+    /**
+     * Evaluate the template but only render the contents of a specific block.
+     *
+     * @param blockName The name of the template block to return.
+     * @param writer    The results of the evaluation are written to this writer.
+     * @throws PebbleException An exception with the evaluation of the template
+     * @throws IOException     An IO exception during the evaluation
+     */
+    void evaluateBlock(String blockName, Writer writer) throws PebbleException, IOException;
+
+    /**
+     * Evaluate the template but only render the contents of a specific block.
+     *
+     * @param blockName The name of the template block to return.
+     * @param writer    The results of the evaluation are written to this writer.
+     * @param locale    The locale used during the evaluation of the template.
+     * @throws PebbleException An exception with the evaluation of the template
+     * @throws IOException     An IO exception during the evaluation
+     */
+    void evaluateBlock(String blockName, Writer writer, Locale locale) throws PebbleException, IOException;
+
+    /**
+     * Evaluate the template but only render the contents of a specific block.
+     *
+     * @param blockName The name of the template block to return.
+     * @param writer    The results of the evaluation are written to this writer.
+     * @param context   The variables used during the evaluation of the template.
+     * @throws PebbleException An exception with the evaluation of the template
+     * @throws IOException     An IO exception during the evaluation
+     */
+    void evaluateBlock(String blockName, Writer writer, Map<String, Object> context) throws PebbleException, IOException;
+
+    /**
+     * Evaluate the template but only render the contents of a specific block.
+     *
+     * @param blockName	The name of the template block to return.
+     * @param writer	The results of the evaluation are written to this writer.
+     * @param context	The variables used during the evaluation of the template.
+     * @param locale  	The locale used during the evaluation of the template.
+     * @throws PebbleException An exception with the evaluation of the template
+     * @throws IOException     An IO exception during the evaluation
+     */
+    void evaluateBlock(String blockName, Writer writer, Map<String, Object> context, Locale locale) throws PebbleException, IOException;
+
     /**
      * Returns the name of the template
      *
diff --git a/src/main/java/com/mitchellbosecke/pebble/template/PebbleTemplateImpl.java b/src/main/java/com/mitchellbosecke/pebble/template/PebbleTemplateImpl.java
index b07a4609..acc0a2dd 100644
--- a/src/main/java/com/mitchellbosecke/pebble/template/PebbleTemplateImpl.java
+++ b/src/main/java/com/mitchellbosecke/pebble/template/PebbleTemplateImpl.java
@@ -92,6 +92,40 @@ public class PebbleTemplateImpl implements PebbleTemplate {
         this.evaluate(writer, context);
     }
 
+    public void evaluateBlock(String blockName, Writer writer) throws PebbleException, IOException {
+        EvaluationContext context = this.initContext(null);
+        this.evaluate(new NoopWriter(), context);
+
+        this.block(writer, context, blockName, false);
+        writer.flush();
+    }
+
+    public void evaluateBlock(String blockName, Writer writer, Locale locale) throws PebbleException, IOException {
+        EvaluationContext context = this.initContext(locale);
+        this.evaluate(new NoopWriter(), context);
+
+        this.block(writer, context, blockName, false);
+        writer.flush();
+    }
+
+    public void evaluateBlock(String blockName, Writer writer, Map<String, Object> map) throws PebbleException, IOException {
+        EvaluationContext context = this.initContext(null);
+        context.getScopeChain().pushScope(map);
+        this.evaluate(new NoopWriter(), context);
+
+        this.block(writer, context, blockName, false);
+        writer.flush();
+    }
+
+    public void evaluateBlock(String blockName, Writer writer, Map<String, Object> map, Locale locale) throws PebbleException, IOException {
+        EvaluationContext context = this.initContext(locale);
+        context.getScopeChain().pushScope(map);
+        this.evaluate(new NoopWriter(), context);
+
+        this.block(writer, context, blockName, false);
+        writer.flush();
+    }
+
     /**
      * This is the authoritative evaluate method. It will evaluate the template
      * starting at the root node.
@@ -139,10 +173,9 @@ public class PebbleTemplateImpl implements PebbleTemplate {
         // global vars provided from extensions
         scopeChain.pushScope(this.engine.getExtensionRegistry().getGlobalVariables());
 
-        EvaluationContext context = new EvaluationContext(this, this.engine.isStrictVariables(), locale,
+        return new EvaluationContext(this, this.engine.isStrictVariables(), locale,
                 this.engine.getExtensionRegistry(), this.engine.getTagCache(), this.engine.getExecutorService(),
                 new ArrayList<PebbleTemplateImpl>(), scopeChain, null);
-        return context;
     }
 
     /**
@@ -353,4 +386,14 @@ public class PebbleTemplateImpl implements PebbleTemplate {
         return this.name;
     }
 
+    private static class NoopWriter extends Writer {
+        public void write(char[] cbuf, int off, int len) throws IOException {
+        }
+
+        public void flush() throws IOException {
+        }
+
+        public void close() throws IOException {
+        }
+    }
 }


Test patch:
diff --git a/src/test/java/com/mitchellbosecke/pebble/RenderSingleBlockTest.java b/src/test/java/com/mitchellbosecke/pebble/RenderSingleBlockTest.java
new file mode 100644
index 00000000..c86e5c5d
--- /dev/null
+++ b/src/test/java/com/mitchellbosecke/pebble/RenderSingleBlockTest.java
@@ -0,0 +1,106 @@
+package com.mitchellbosecke.pebble;
+
+import com.mitchellbosecke.pebble.error.PebbleException;
+import com.mitchellbosecke.pebble.loader.StringLoader;
+import com.mitchellbosecke.pebble.template.PebbleTemplate;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.Map;
+
+import static java.util.Locale.CANADA;
+import static org.junit.Assert.assertEquals;
+
+public class RenderSingleBlockTest extends AbstractTest {
+
+    @Test
+    public void testRenderSingleBlock() throws PebbleException, IOException {
+        PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(false).build();
+
+        String source = "Prefix {% block block_a %}Block A{% endblock %}{% block block_b %}Block B{% endblock %} Postfix";
+        PebbleTemplate template = pebble.getTemplate(source);
+
+        Writer writer_a = new StringWriter();
+        template.evaluateBlock("block_a", writer_a);
+        assertEquals("Block A", writer_a.toString());
+
+        Writer writer_b = new StringWriter();
+        template.evaluateBlock("block_b", writer_b);
+        assertEquals("Block B", writer_b.toString());
+    }
+
+    @Test
+    public void testRenderSingleBlockWithLocale() throws PebbleException, IOException {
+        PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(false).build();
+
+        String source = "Prefix {% block block_a %}Block A{% endblock %}{% block block_b %}Block B{% endblock %} Postfix";
+        PebbleTemplate template = pebble.getTemplate(source);
+
+        Writer writer_a = new StringWriter();
+        template.evaluateBlock("block_a", writer_a, CANADA);
+        assertEquals("Block A", writer_a.toString());
+
+        Writer writer_b = new StringWriter();
+        template.evaluateBlock("block_b", writer_b, CANADA);
+        assertEquals("Block B", writer_b.toString());
+    }
+
+    @Test
+    public void testRenderSingleBlockWithContext() throws PebbleException, IOException {
+        PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(false).build();
+
+        String source = "Prefix {% block block_a %}{{vara}}{% endblock %}{% block block_b %}{{varb}}{% endblock %} Postfix";
+        PebbleTemplate template = pebble.getTemplate(source);
+
+        Map<String, Object> context = new HashMap<>();
+        context.put("vara", "FOO");
+        context.put("varb", "BAR");
+
+        Writer writer_a = new StringWriter();
+        template.evaluateBlock("block_a", writer_a, context, CANADA);
+        assertEquals("FOO", writer_a.toString());
+
+        Writer writer_b = new StringWriter();
+        template.evaluateBlock("block_b", writer_b, context, CANADA);
+        assertEquals("BAR", writer_b.toString());
+    }
+
+    @Test
+    public void testRenderSingleBlockWithContextAndLocale() throws PebbleException, IOException {
+        PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(false).build();
+
+        String source = "Prefix {% block block_a %}{{vara}}{% endblock %}{% block block_b %}{{varb}}{% endblock %} Postfix";
+        PebbleTemplate template = pebble.getTemplate(source);
+
+        Map<String, Object> context = new HashMap<>();
+        context.put("vara", "FOO");
+        context.put("varb", "BAR");
+
+        Writer writer_a = new StringWriter();
+        template.evaluateBlock("block_a", writer_a, context);
+        assertEquals("FOO", writer_a.toString());
+
+        Writer writer_b = new StringWriter();
+        template.evaluateBlock("block_b", writer_b, context);
+        assertEquals("BAR", writer_b.toString());
+    }
+
+    @Test
+    public void testRenderSingleExtendedBlock() throws PebbleException, IOException {
+        PebbleEngine pebble = new PebbleEngine.Builder().strictVariables(true).build();
+        PebbleTemplate template = pebble.getTemplate("templates/single-block/template.renderextendedblock1.peb");
+
+        Writer writer_a = new StringWriter();
+        template.evaluateBlock("container_a", writer_a);
+        assertEquals("Block A extended", writer_a.toString());
+
+        Writer writer_b = new StringWriter();
+        template.evaluateBlock("container_b", writer_b);
+        assertEquals("Block B extended", writer_b.toString());
+    }
+
+}
diff --git a/src/test/resources/templates/single-block/template.renderextendedblock1.peb b/src/test/resources/templates/single-block/template.renderextendedblock1.peb
new file mode 100644
index 00000000..1795da75
--- /dev/null
+++ b/src/test/resources/templates/single-block/template.renderextendedblock1.peb
@@ -0,0 +1,7 @@
+{# used by RenderSingleBlockTest.testRenderSingleExtendedBlock #}
+{% extends "./template.renderextendedblock2.peb" %}
+
+Text in the template that should not appear in either block
+
+{% block content_a %}Block A{% endblock %}
+{% block content_b %}Block B{% endblock %}
diff --git a/src/test/resources/templates/single-block/template.renderextendedblock2.peb b/src/test/resources/templates/single-block/template.renderextendedblock2.peb
new file mode 100644
index 00000000..4c84a708
--- /dev/null
+++ b/src/test/resources/templates/single-block/template.renderextendedblock2.peb
@@ -0,0 +1,6 @@
+{# used by RenderSingleBlockTest.testRenderSingleExtendedBlock #}
+
+Text in the parent template that should not appear in either block
+
+{% block container_a %}{% block content_a %}{% endblock %} extended{% endblock %}
+{% block container_b %}{% block content_b %}{% endblock %} extended{% endblock %}