Skip to content

Commit

Permalink
Merge pull request #47 from bkiers/bug/43-nested-for-tag
Browse files Browse the repository at this point in the history
Added nested contexts
  • Loading branch information
bkiers authored Feb 22, 2017
2 parents 2bdd94a + 7d7ea78 commit e843b2d
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 19 deletions.
14 changes: 14 additions & 0 deletions src/main/java/liqp/ProtectionSettings.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package liqp;

import liqp.exceptions.ExceededMaxIterationsException;

public class ProtectionSettings {

public final int maxIterations;
public final int maxSizeRenderedString;
public final long maxRenderTimeMillis;
public final long maxTemplateSizeBytes;

// A global counter that keeps track of the amount of iterations
private int iterations = 0;

public static class Builder {

private int maxIterations;
Expand Down Expand Up @@ -52,4 +57,13 @@ private ProtectionSettings(int maxIterations, int maxSizeRenderedString, long ma
this.maxRenderTimeMillis = maxRenderTimeMillis;
this.maxTemplateSizeBytes = maxTemplateSizeBytes;
}

public void incrementIterations() {

this.iterations++;

if (this.iterations > this.maxIterations) {
throw new ExceededMaxIterationsException(this.maxIterations);
}
}
}
73 changes: 60 additions & 13 deletions src/main/java/liqp/TemplateContext.java
Original file line number Diff line number Diff line change
@@ -1,52 +1,99 @@
package liqp;

import liqp.exceptions.ExceededMaxIterationsException;
import liqp.parser.Flavor;

import java.util.LinkedHashMap;
import java.util.Map;

public class TemplateContext {

protected TemplateContext parent;
public final ProtectionSettings protectionSettings;
public final Flavor flavor;
private final Map<String, Object> variables;
private int iterations;
private Map<String, Object> variables;

public TemplateContext() {
this(new ProtectionSettings.Builder().build(), Flavor.LIQUID, new LinkedHashMap<String, Object>());
}

public TemplateContext(ProtectionSettings protectionSettings, Flavor flavor, Map<String, Object> variables) {
this.parent = null;
this.protectionSettings = protectionSettings;
this.flavor = flavor;
this.variables = variables;
this.iterations = 0;
}

public TemplateContext(TemplateContext parent) {
this.parent = parent;
this.protectionSettings = parent.protectionSettings;
this.flavor = parent.flavor;
this.variables = new LinkedHashMap<String, Object>();
}

public void incrementIterations() {
this.protectionSettings.incrementIterations();
}

this.iterations++;
public boolean containsKey(String key) {

if (this.iterations > this.protectionSettings.maxIterations) {
throw new ExceededMaxIterationsException(this.protectionSettings.maxIterations);
if (this.containsKey(key)) {
return true;
}
}

public boolean containsKey(String key) {
return this.variables.containsKey(key);
if (parent != null) {
return parent.containsKey(key);
}

return false;
}

public Object get(String key) {
return this.variables.get(key);

// First try to retrieve the key from the local context
Object value = this.variables.get(key);

if (value != null) {
return value;
}

if (parent != null) {
// Not available locally, try the parent context
return parent.get(key);
}

// Not available
return null;
}

public Object put(String key, Object value) {
return this.variables.put(key, value);
return this.put(key, value, false);
}

public Object put(String key, Object value, boolean putInRootContext) {

if (!putInRootContext || parent == null) {
// Either store it in the local context, or this context is the root context
return this.variables.put(key, value);
}

// Else put in the parent context
return parent.put(key, value, putInRootContext);
}

public Object remove(String key) {
return this.variables.remove(key);

if (this.variables.containsKey(key)) {
// Remove the key from the local context
return this.variables.remove(key);
}

if (parent != null) {
// Not available in the local context, try the parent
return parent.remove(key);
}

// Key was not present
return null;
}

public Map<String,Object> getVariables() {
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/liqp/tags/Assign.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public Object render(TemplateContext context, LNode... nodes) {
value = filter.apply(value, context);
}

context.put(id, value);
// Assign causes variable to be saved "globally"
context.put(id, value, true);

return "";
}
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/liqp/tags/Capture.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public Object render(TemplateContext context, LNode... nodes) {

LNode block = nodes[1];

context.put(id, block.render(context));
// Capture causes variable to be saved "globally"
context.put(id, block.render(context), true);

return null;
}
Expand Down
9 changes: 5 additions & 4 deletions src/main/java/liqp/tags/For.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,12 @@ public Object render(TemplateContext context, LNode... nodes) {

String id = super.asString(nodes[1].render(context));

context.put(FORLOOP, new HashMap<String, Object>());
// Each for tag has its own context that keeps track of its own variables (scope)
TemplateContext nestedContext = new TemplateContext(context);

Object rendered = array ? renderArray(id, context, nodes) : renderRange(id, context, nodes);
nestedContext.put(FORLOOP, new HashMap<String, Object>());

context.remove(FORLOOP);
Object rendered = array ? renderArray(id, nestedContext, nodes) : renderRange(id, nestedContext, nodes);

return rendered;
}
Expand Down Expand Up @@ -140,7 +141,7 @@ private Object renderArray(String id, TemplateContext context, LNode... tokens)
}
}

context.put(CONTINUE, continueIndex + 1);
context.put(CONTINUE, continueIndex + 1, true);

return builder.toString();
}
Expand Down
68 changes: 68 additions & 0 deletions src/test/java/liqp/tags/ForTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import org.antlr.runtime.RecognitionException;
import org.junit.Test;

import java.util.HashMap;
import java.util.Map;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

Expand Down Expand Up @@ -631,4 +634,69 @@ public void blankStringNotIterableTest() throws RecognitionException {

assertThat(Template.parse("{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}").render(), is(""));
}

/*
* Verified with the following Ruby code:
*
* require 'liquid'
*
* template = <<-HEREDOC
* `{% for c1 in chars %}
* {{ forloop.index }}
* {% for c2 in chars %}
* {{ forloop.index }} {{ c1 }} {{ c2 }}
* {% endfor %}
* {% endfor %}`
* HEREDOC
*
* @template = Liquid::Template.parse(template)
* rendered = @template.render('chars' => %w[a b c])
*
* puts(rendered)
*/
@Test
public void nestedTest() {

Map<String, Object> variables = new HashMap<String, Object>();

variables.put("chars", new String[]{"a", "b", "c"});

String template = "`{% for c1 in chars %}\n" +
" {{ forloop.index }}\n" +
" {% for c2 in chars %}\n" +
" {{ forloop.index }} {{ c1 }} {{ c2 }}\n" +
" {% endfor %}\n" +
"{% endfor %}`";

String expected = "`\n" +
" 1\n" +
" \n" +
" 1 a a\n" +
" \n" +
" 2 a b\n" +
" \n" +
" 3 a c\n" +
" \n" +
"\n" +
" 2\n" +
" \n" +
" 1 b a\n" +
" \n" +
" 2 b b\n" +
" \n" +
" 3 b c\n" +
" \n" +
"\n" +
" 3\n" +
" \n" +
" 1 c a\n" +
" \n" +
" 2 c b\n" +
" \n" +
" 3 c c\n" +
" \n" +
"`";

assertThat(Template.parse(template).render(variables), is(expected));
}
}

0 comments on commit e843b2d

Please sign in to comment.