Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement jinja2.ext.loopcontrols extensions #1219

Merged
merged 1 commit into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.hubspot.jinjava.interpret;

/**
* Exception thrown when `continue` or `break` is called outside of a loop
*/
public class NotInLoopException extends InterpretException {

public static final String MESSAGE_PREFIX = "`";
public static final String MESSAGE_SUFFIX = "` called while not in a for loop";

public NotInLoopException(String tagName) {
super(MESSAGE_PREFIX + tagName + MESSAGE_SUFFIX);
}
}
51 changes: 51 additions & 0 deletions src/main/java/com/hubspot/jinjava/lib/tag/BreakTag.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.hubspot.jinjava.lib.tag;

import com.hubspot.jinjava.doc.annotations.JinjavaDoc;
import com.hubspot.jinjava.doc.annotations.JinjavaTextMateSnippet;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.interpret.NotInLoopException;
import com.hubspot.jinjava.tree.TagNode;
import com.hubspot.jinjava.util.ForLoop;

/**
* Implements the common loopcontrol `continue`, as in the jinja2.ext.loopcontrols extension
* @author ccutrer
*/

@JinjavaDoc(
value = "Stops executing the current for loop, including any further iterations"
)
@JinjavaTextMateSnippet(
code = "{% for item in [1, 2, 3, 4] %}{% if item > 2 == 0 %}{% break %}{% endif %}{{ item }}{% endfor %}"
)
public class BreakTag implements Tag {

public static final String TAG_NAME = "break";

@Override
public String getName() {
return TAG_NAME;
}

@Override
public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) {
Object loop = interpreter.getContext().get(ForTag.LOOP);
if (loop instanceof ForLoop) {
ForLoop forLoop = (ForLoop) loop;
forLoop.doBreak();
} else {
throw new NotInLoopException(TAG_NAME);
}
return "";
}

@Override
public String getEndTagName() {
return null;
}

@Override
public boolean isRenderedInValidationMode() {
return true;
}
}
49 changes: 49 additions & 0 deletions src/main/java/com/hubspot/jinjava/lib/tag/ContinueTag.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.hubspot.jinjava.lib.tag;

import com.hubspot.jinjava.doc.annotations.JinjavaDoc;
import com.hubspot.jinjava.doc.annotations.JinjavaTextMateSnippet;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.interpret.NotInLoopException;
import com.hubspot.jinjava.tree.TagNode;
import com.hubspot.jinjava.util.ForLoop;

/**
* Implements the common loopcontrol `continue`, as in the jinja2.ext.loopcontrols extension
* @author ccutrer
*/

@JinjavaDoc(value = "Stops executing the current iteration of the current for loop")
@JinjavaTextMateSnippet(
code = "{% for item in [1, 2, 3, 4] %}{% if item % 2 == 0 %}{% continue %}{% endif %}{{ item }}{% endfor %}"
)
public class ContinueTag implements Tag {

public static final String TAG_NAME = "continue";

@Override
public String getName() {
return TAG_NAME;
}

@Override
public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) {
Object loop = interpreter.getContext().get(ForTag.LOOP);
if (loop instanceof ForLoop) {
ForLoop forLoop = (ForLoop) loop;
forLoop.doContinue();
} else {
throw new NotInLoopException(TAG_NAME);
}
return "";
}

@Override
public String getEndTagName() {
return null;
}

@Override
public boolean isRenderedInValidationMode() {
return true;
}
}
6 changes: 5 additions & 1 deletion src/main/java/com/hubspot/jinjava/lib/tag/ForTag.java
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ public String renderForCollection(
interpreter.addError(TemplateError.fromOutputTooBigException(e));
return checkLoopVariable(interpreter, buff);
}
// continue in the body of the loop; ignore the rest of the body
if (loop.isContinued()) {
break;
}
}
}
if (
Expand All @@ -297,7 +301,7 @@ private String checkLoopVariable(
JinjavaInterpreter interpreter,
LengthLimitingStringBuilder buff
) {
if (interpreter.getContext().get("loop") instanceof DeferredValue) {
if (interpreter.getContext().get(LOOP) instanceof DeferredValue) {
throw new DeferredValueException(
"loop variable deferred",
interpreter.getLineNumber(),
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/hubspot/jinjava/lib/tag/TagLibrary.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ protected void registerDefaults() {
registerClasses(
AutoEscapeTag.class,
BlockTag.class,
BreakTag.class,
CallTag.class,
ContinueTag.class,
CycleTag.class,
ElseTag.class,
ElseIfTag.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,8 @@ private EagerExecutionResult runLoopOnce(
) {
return EagerContextWatcher.executeInChildContext(
eagerInterpreter -> {
if (!(eagerInterpreter.getContext().get("loop") instanceof DeferredValue)) {
eagerInterpreter.getContext().put("loop", DeferredValue.instance());
if (!(eagerInterpreter.getContext().get(ForTag.LOOP) instanceof DeferredValue)) {
eagerInterpreter.getContext().put(ForTag.LOOP, DeferredValue.instance());
}
List<String> loopVars = getTag()
.getLoopVarsAndExpression((TagToken) tagNode.getMaster())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.hubspot.jinjava.interpret.MetaContextVariables;
import com.hubspot.jinjava.interpret.OneTimeReconstructible;
import com.hubspot.jinjava.interpret.RevertibleObject;
import com.hubspot.jinjava.lib.tag.ForTag;
import com.hubspot.jinjava.lib.tag.eager.EagerExecutionResult;
import com.hubspot.jinjava.objects.collections.PyList;
import com.hubspot.jinjava.objects.collections.PyMap;
Expand Down Expand Up @@ -211,7 +212,7 @@ private static Map<String, Object> getBasicSpeculativeBindings(
)
)
.filter(entry -> !ignoredKeys.contains(entry.getKey()))
.filter(entry -> !"loop".equals(entry.getKey()))
.filter(entry -> !ForTag.LOOP.equals(entry.getKey()))
.map(entry -> {
if (
eagerExecutionResult.getResult().isFullyResolved() ||
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/com/hubspot/jinjava/util/ForLoop.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public class ForLoop implements Iterator<Object> {
private int length = NULL_VALUE;
private boolean first = true;
private boolean last;
private boolean continued;
private boolean broken;

private int depth;

Expand All @@ -45,6 +47,8 @@ public ForLoop(Iterator<?> ite, int len) {
last = false;
}
it = ite;
continued = false;
broken = false;
}

public ForLoop(Iterator<?> ite) {
Expand All @@ -57,10 +61,16 @@ public ForLoop(Iterator<?> ite) {
revcounter = 2;
last = true;
}
continued = false;
broken = false;
}

@Override
public Object next() {
if (broken) {
return null;
}
continued = false;
Object res;
if (it.hasNext()) {
index++;
Expand Down Expand Up @@ -129,8 +139,24 @@ public boolean isLast() {
return last;
}

public boolean isContinued() {
return continued;
}

public void doContinue() {
continued = true;
}

public void doBreak() {
continued = true;
broken = true;
}

@Override
public boolean hasNext() {
if (broken) {
return false;
}
return it.hasNext();
}

Expand Down
51 changes: 51 additions & 0 deletions src/test/java/com/hubspot/jinjava/lib/tag/BreakTagTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.hubspot.jinjava.lib.tag;

import static org.assertj.core.api.Assertions.assertThat;

import com.hubspot.jinjava.BaseInterpretingTest;
import com.hubspot.jinjava.interpret.RenderResult;
import com.hubspot.jinjava.interpret.TemplateError;
import org.junit.Test;

public class BreakTagTest extends BaseInterpretingTest {

@Test
public void testBreak() {
String template =
"{% for item in [1, 2, 3, 4] %}{% if item > 2 %}{% break %}{% endif %}{{ item }}{% endfor %}";

RenderResult rendered = jinjava.renderForResult(template, context);
assertThat(rendered.getOutput()).isEqualTo("12");
}

@Test
public void testNestedBreak() {
String template =
"{% for item in [1, 2, 3, 4] %}{% for item2 in [5, 6, 7] %}{% break %}{{ item2 }}{% endfor %}{{ item }}{% endfor %}";

RenderResult rendered = jinjava.renderForResult(template, context);
assertThat(rendered.getOutput()).isEqualTo("1234");
}

@Test
public void testBreakWithEarlierContent() {
String template =
"{% for item in [1, 2, 3, 4] %}{{ item }}{% if item > 2 %}{% break %}{% endif %}{{ item }}{% endfor %}";

RenderResult rendered = jinjava.renderForResult(template, context);
assertThat(rendered.getOutput()).isEqualTo("11223");
}

@Test
public void testBreakOutOfContext() {
String template = "{% break %}";

RenderResult rendered = jinjava.renderForResult(template, context);
assertThat(rendered.getOutput()).isEqualTo("");
assertThat(rendered.getErrors()).hasSize(1);
assertThat(rendered.getErrors().get(0).getSeverity())
.isEqualTo(TemplateError.ErrorType.FATAL);
assertThat(rendered.getErrors().get(0).getMessage())
.contains("NotInLoopException: `break` called while not in a for loop");
}
}
51 changes: 51 additions & 0 deletions src/test/java/com/hubspot/jinjava/lib/tag/ContinueTagTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.hubspot.jinjava.lib.tag;

import static org.assertj.core.api.Assertions.assertThat;

import com.hubspot.jinjava.BaseInterpretingTest;
import com.hubspot.jinjava.interpret.RenderResult;
import com.hubspot.jinjava.interpret.TemplateError;
import org.junit.Test;

public class ContinueTagTest extends BaseInterpretingTest {

@Test
public void testContinue() {
String template =
"{% for item in [1, 2, 3, 4] %}{% if item % 2 == 0 %}{% continue %}{% endif %}{{ item }}{% endfor %}";

RenderResult rendered = jinjava.renderForResult(template, context);
assertThat(rendered.getOutput()).isEqualTo("13");
}

@Test
public void testNestedContinue() {
String template =
"{% for item in [1, 2, 3, 4] %}{% for item2 in [5, 6, 7] %}{% continue %}{{ item2 }}{% endfor %}{{ item }}{% endfor %}";

RenderResult rendered = jinjava.renderForResult(template, context);
assertThat(rendered.getOutput()).isEqualTo("1234");
}

@Test
public void testContinueWithEarlierContent() {
String template =
"{% for item in [1, 2, 3, 4] %}{{ item }}{% if item % 2 == 0 %}{% continue %}{% endif %}{{ item }}{% endfor %}";

RenderResult rendered = jinjava.renderForResult(template, context);
assertThat(rendered.getOutput()).isEqualTo("112334");
}

@Test
public void testContinueOutOfContext() {
String template = "{% continue %}";

RenderResult rendered = jinjava.renderForResult(template, context);
assertThat(rendered.getOutput()).isEqualTo("");
assertThat(rendered.getErrors()).hasSize(1);
assertThat(rendered.getErrors().get(0).getSeverity())
.isEqualTo(TemplateError.ErrorType.FATAL);
assertThat(rendered.getErrors().get(0).getMessage())
.contains("NotInLoopException: `continue` called while not in a for loop");
}
}
Loading