Repository: PebbleTemplates/pebble
Pull request #376 (2018-07-26 17:15:05)

Global attribute access in macro (#242)


Patch:
diff --git a/pebble/src/main/java/com/mitchellbosecke/pebble/template/GlobalContext.java b/pebble/src/main/java/com/mitchellbosecke/pebble/template/GlobalContext.java
new file mode 100644
index 00000000..3abcd029
--- /dev/null
+++ b/pebble/src/main/java/com/mitchellbosecke/pebble/template/GlobalContext.java
@@ -0,0 +1,86 @@
+package com.mitchellbosecke.pebble.template;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class GlobalContext implements Map<String, Object> {
+
+  private final ScopeChain scopeChain;
+
+  public GlobalContext(ScopeChain scopeChain) {
+    this.scopeChain = scopeChain;
+  }
+
+  @Override
+  public Object get(Object key) {
+    List<Scope> globalScopes = this.scopeChain.getGlobalScopes();
+    String keyAsString = String.valueOf(key);
+    for (Scope scope : globalScopes) {
+      Object result = scope.get(keyAsString);
+      if (result != null) {
+        return result;
+      } else if (scope.containsKey(keyAsString)) {
+        return null;
+      }
+    }
+
+    return null;
+  }
+
+  @Override
+  public boolean isEmpty() {
+    return false;
+  }
+
+  @Override
+  public int size() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean containsKey(Object key) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean containsValue(Object value) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Object put(String key, Object value) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Object remove(Object key) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void putAll(Map<? extends String, ?> m) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void clear() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Set<String> keySet() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Collection<Object> values() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Set<Entry<String, Object>> entrySet() {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/pebble/src/main/java/com/mitchellbosecke/pebble/template/PebbleTemplateImpl.java b/pebble/src/main/java/com/mitchellbosecke/pebble/template/PebbleTemplateImpl.java
index d4f04a2c..01fac012 100644
--- a/pebble/src/main/java/com/mitchellbosecke/pebble/template/PebbleTemplateImpl.java
+++ b/pebble/src/main/java/com/mitchellbosecke/pebble/template/PebbleTemplateImpl.java
@@ -167,10 +167,13 @@ public class PebbleTemplateImpl implements PebbleTemplate {
     locale = locale == null ? this.engine.getDefaultLocale() : locale;
 
     // globals
+    ScopeChain scopeChain = new ScopeChain();
     Map<String, Object> globals = new HashMap<>();
     globals.put("locale", locale);
     globals.put("template", this);
-    ScopeChain scopeChain = new ScopeChain(globals);
+    globals.put("_context", new GlobalContext(scopeChain));
+
+    scopeChain.pushScope(globals);
 
     // global vars provided from extensions
     scopeChain.pushScope(this.engine.getExtensionRegistry().getGlobalVariables());
diff --git a/pebble/src/main/java/com/mitchellbosecke/pebble/template/Scope.java b/pebble/src/main/java/com/mitchellbosecke/pebble/template/Scope.java
index 890d1d2d..8b09a5d9 100644
--- a/pebble/src/main/java/com/mitchellbosecke/pebble/template/Scope.java
+++ b/pebble/src/main/java/com/mitchellbosecke/pebble/template/Scope.java
@@ -34,7 +34,7 @@ public class Scope {
    * Constructor
    *
    * @param backingMap The backing map of variables
-   * @param local    Whether this scope is local or not
+   * @param local Whether this scope is local or not
    */
   public Scope(Map<String, Object> backingMap, boolean local) {
     this.backingMap = backingMap == null ? new HashMap<>() : backingMap;
diff --git a/pebble/src/main/java/com/mitchellbosecke/pebble/template/ScopeChain.java b/pebble/src/main/java/com/mitchellbosecke/pebble/template/ScopeChain.java
index 1d4b0c9f..32563c40 100644
--- a/pebble/src/main/java/com/mitchellbosecke/pebble/template/ScopeChain.java
+++ b/pebble/src/main/java/com/mitchellbosecke/pebble/template/ScopeChain.java
@@ -8,9 +8,11 @@
  */
 package com.mitchellbosecke.pebble.template;
 
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedList;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -32,16 +34,6 @@ public class ScopeChain {
   public ScopeChain() {
   }
 
-  /**
-   * Constructs a new scope chain with one known scope.
-   *
-   * @param map The map of variables used to initialize a scope.
-   */
-  public ScopeChain(Map<String, Object> map) {
-    Scope scope = new Scope(new HashMap<>(map), false);
-    this.stack.push(scope);
-  }
-
   /**
    * Creates a deep copy of the ScopeChain. This is used for the parallel tag because every new
    * thread should have a "snapshot" of the scopes, i.e. if one thread adds a new object to a scope,
@@ -235,4 +227,19 @@ public class ScopeChain {
     // no existing variable, create a new one
     this.put(key, value);
   }
+
+  public List<Scope> getGlobalScopes() {
+    List<Scope> globalScopes = new ArrayList<>();
+    Iterator<Scope> iterator = this.stack.iterator();
+    while (iterator.hasNext()) {
+      Scope scope = iterator.next();
+      if (scope.isLocal()) {
+        globalScopes.clear();
+      } else {
+        globalScopes.add(scope);
+      }
+    }
+
+    return globalScopes;
+  }
 }


Test patch:
diff --git a/pebble/src/test/java/com/mitchellbosecke/pebble/MacroTest.java b/pebble/src/test/java/com/mitchellbosecke/pebble/MacroTest.java
index 6c28d057..e340c997 100644
--- a/pebble/src/test/java/com/mitchellbosecke/pebble/MacroTest.java
+++ b/pebble/src/test/java/com/mitchellbosecke/pebble/MacroTest.java
@@ -19,7 +19,7 @@ import org.junit.Test;
  */
 public class MacroTest {
 
-  public static final String LINE_SEPARATOR = System.lineSeparator();
+  private static final String LINE_SEPARATOR = System.lineSeparator();
 
   @Test
   public void testMacro() throws PebbleException, IOException {
@@ -181,4 +181,16 @@ public class MacroTest {
 
     assertEquals("tankinfantrytank", writer.toString());
   }
+
+  @Test
+  public void testMacroHasAccessToGlobalVariables() throws PebbleException, IOException {
+    PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader())
+        .strictVariables(false).build();
+    String templateContent = "{% set foo = 'bar' %}{{ test(_context) }}{% macro test(_context) %}{% set foo = 'foo' %}{{ _context.foo }}{% endmacro %}";
+    PebbleTemplate template = pebble.getTemplate(templateContent);
+
+    Writer writer = new StringWriter();
+    template.evaluate(writer);
+    assertEquals("bar", writer.toString());
+  }
 }