Repository: PebbleTemplates/pebble
Pull request #617 (2022-02-17 18:23:05)

added loader that works from memory
This attempts to solve the issue with StringLoader where inheritance is not supported.  This loader uses a flat hierarchy.

```
MemoryLoader memLoader = new MemoryLoader();
memLoader.addFile("home.html", "{% extends \"layout.html\" %}{% block title %} Home {% endblock %}\r\n"
			+ "\r\n"
			+ "{% block content %}\r\n"
			+ "	<h1> Home </h1>\r\n"
			+ "	<p> Welcome to my home page. My name is {{ name }}.</p>\r\n"
			+ "{% endblock %}");
memLoader.addFile("layout.html", "<!DOCTYPE html>\r\n"
		+ "<html>\r\n"
		+ "    <head>\r\n"
		+ "        <title>Hello Pebble</title>\r\n"
		+ "    </head>\r\n"
		+ "    <body>\r\n"
		+ "        {% block content %}{% endblock %}\r\n"
		+ "    </body>\r\n"
		+ "</html>");
PebbleEngine engine = new PebbleEngine.Builder().loader(memLoader).build();
PebbleTemplate compiledTemplate = engine.getTemplate("home.html");
Map<String, Object> context = new HashMap<String, Object>();
context.put("name", "Mitchell");

Writer writer = new StringWriter();
compiledTemplate.evaluate(writer, context);

String output = writer.toString();
System.out.println(output)
```;


Patch:
diff --git a/docs/src/orchid/resources/changelog/v3_2_0.md b/docs/src/orchid/resources/changelog/v3_2_0.md
index 3ab95a11..b0282615 100644
--- a/docs/src/orchid/resources/changelog/v3_2_0.md
+++ b/docs/src/orchid/resources/changelog/v3_2_0.md
@@ -3,7 +3,10 @@ version: '3.2.0'
 ---
 
 - Add support for spring framework 6 and spring-boot 3 (#630)
-- Bump minimum supported java version to 17 in pebble-spring6 and pebble-spring-boot-starter in order to work with spring (#630)
+- Bump minimum supported java version to 17 in pebble-spring6 and pebble-spring-boot-starter in order to work with
+  spring (#630)
+- Add a memory loader that supports inheritance and doesn't require a filesystem. This is useful for applications
+  that retrieve templates from a database for example (#617).
 - **BREAKING CHANGE**: Change default suffix to `.peb` instead of `.pebble` in spring boot autoconfiguration (#553)
 - **BREAKING CHANGE**: Rename method `getInstance` to `createInstance` in `BinaryOperator` interface (#521)
 - **BREAKING CHANGE**: Rename package from `com.mitchellbosecke` to `io.pebbletemplates` (#635)
\ No newline at end of file
diff --git a/docs/src/orchid/resources/wiki/guide/installation.md b/docs/src/orchid/resources/wiki/guide/installation.md
index 4a045807..ecfcd7b7 100644
--- a/docs/src/orchid/resources/wiki/guide/installation.md
+++ b/docs/src/orchid/resources/wiki/guide/installation.md
@@ -68,6 +68,8 @@ application server but is not enabled by default.
 - `Servlet5Loader`:  Same as `ServletLoader`, but for Jakarta Servlet 5.0 or newer.
 - `StringLoader`: Considers the name of the template to be the contents of the template.
 - `DelegatingLoader`: Delegates responsibility to a collection of children loaders.
+- `MemoryLoader`: Loader that supports inheritance and doesn't require a filesystem. This is useful for applications
+  that retrieve templates from a database for example.
 
 If you do not provide a custom Loader, Pebble will use an instance of the `DelegatingLoader` by default.
 This delegating loader will use a `ClasspathLoader` and a `FileLoader` behind the scenes to find your templates.
diff --git a/pebble/src/main/java/io/pebbletemplates/pebble/loader/MemoryLoader.java b/pebble/src/main/java/io/pebbletemplates/pebble/loader/MemoryLoader.java
new file mode 100644
index 00000000..be12a49e
--- /dev/null
+++ b/pebble/src/main/java/io/pebbletemplates/pebble/loader/MemoryLoader.java
@@ -0,0 +1,82 @@
+package io.pebbletemplates.pebble.loader;
+
+import io.pebbletemplates.pebble.error.LoaderException;
+
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MemoryLoader implements Loader<String> {
+    private final List<TemplateDefinition> templateDefinitions = new ArrayList<>();
+
+    @Override
+    public Reader getReader(String templateName) {
+        String content = "";
+        for (TemplateDefinition templateDefinition : this.templateDefinitions) {
+            if (templateDefinition.templateName.equals(templateName)) {
+                content = templateDefinition.content;
+                break;
+            }
+        }
+
+        if (content.isEmpty()) {
+            throw new LoaderException(null, "Could not find template \"" + templateName + "\"");
+        }
+
+        return new StringReader(content);
+    }
+
+    public void addTemplate(String templateName, String content) {
+        if (templateName == null) {
+            throw new IllegalArgumentException("templateName cannot be null");
+        }
+        if (content == null) {
+            throw new IllegalArgumentException("content cannot be null");
+        }
+        this.templateDefinitions.add(new TemplateDefinition(templateName, content));
+    }
+
+    @Override
+    public void setSuffix(String suffix) {
+    }
+
+    @Override
+    public void setPrefix(String prefix) {
+    }
+
+    @Override
+    public void setCharset(String charset) {
+    }
+
+    @Override
+    public String resolveRelativePath(String relativePath, String anchorPath) {
+        return relativePath; // hierarchy is flat
+    }
+
+    @Override
+    public String createCacheKey(String templateName) {
+        return templateName;
+    }
+
+    @Override
+    public boolean resourceExists(String templateName) {
+        for (TemplateDefinition templateDefinition : this.templateDefinitions) {
+            if (templateDefinition.templateName.equals(templateName)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static class TemplateDefinition {
+        public final String templateName;
+        public final String content;
+
+        public TemplateDefinition(String templateName,
+                                  String content) {
+            this.templateName = templateName;
+            this.content = content;
+        }
+    }
+}


Test patch:
diff --git a/pebble/src/test/java/io/pebbletemplates/pebble/LoaderTest.java b/pebble/src/test/java/io/pebbletemplates/pebble/LoaderTest.java
index cdbddb52..7394f4be 100644
--- a/pebble/src/test/java/io/pebbletemplates/pebble/LoaderTest.java
+++ b/pebble/src/test/java/io/pebbletemplates/pebble/LoaderTest.java
@@ -10,21 +10,11 @@ package io.pebbletemplates.pebble;
 
 import io.pebbletemplates.pebble.error.LoaderException;
 import io.pebbletemplates.pebble.error.PebbleException;
-import io.pebbletemplates.pebble.loader.ClasspathLoader;
-import io.pebbletemplates.pebble.loader.DelegatingLoader;
-import io.pebbletemplates.pebble.loader.FileLoader;
-import io.pebbletemplates.pebble.loader.Loader;
-import io.pebbletemplates.pebble.loader.StringLoader;
+import io.pebbletemplates.pebble.loader.*;
 import io.pebbletemplates.pebble.template.PebbleTemplate;
-
 import org.junit.jupiter.api.Test;
 
-import java.io.File;
-import java.io.IOException;
-import java.io.Reader;
-import java.io.StringReader;
-import java.io.StringWriter;
-import java.io.Writer;
+import java.io.*;
 import java.net.URISyntaxException;
 import java.net.URL;
 import java.net.URLClassLoader;
@@ -108,6 +98,36 @@ class LoaderTest {
     assertEquals("LOADER ONE", writer.toString());
   }
 
+  @Test
+  void testMemoryLoader() throws PebbleException, IOException {
+    MemoryLoader loader = new MemoryLoader();
+    PebbleEngine pebble = new PebbleEngine.Builder().loader(loader).strictVariables(false).build();
+
+    loader.addTemplate("home.html", "{% extends \"layout.html\" %}{% block title %} Home {% endblock %}"
+            + "{% block content %}"
+            + "<h1> Home </h1>"
+            + "<p> Welcome to my home page. My name is {{ name }}.</p>"
+            + "{% endblock %}");
+    loader.addTemplate("layout.html", "<html>"
+            + "<head>"
+            + "<title>Hello Pebble</title>"
+            + "</head>"
+            + "<body>"
+            + "{% block content %}{% endblock %}"
+            + "</body>"
+            + "</html>");
+
+    PebbleTemplate template = pebble.getTemplate("home.html");
+
+    Map<String, Object> context = new HashMap<>();
+    context.put("name", "Bob");
+
+    Writer writer = new StringWriter();
+    template.evaluate(writer, context);
+
+    assertEquals("<html><head><title>Hello Pebble</title></head><body><h1> Home </h1><p> Welcome to my home page. My name is Bob.</p></body></html>", writer.toString());
+  }
+
   @Test
   void testGetLiteralTemplate() throws IOException {
     PebbleEngine engine = new PebbleEngine.Builder().build();