diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/InsertSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/InsertSectionHelper.java index b018033f111ea..4987be8e5a6ee 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/InsertSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/InsertSectionHelper.java @@ -15,12 +15,39 @@ public InsertSectionHelper(String name, SectionBlock defaultBlock) { @Override public CompletionStage resolve(SectionResolutionContext context) { - SectionBlock extending = context.resolutionContext().getExtendingBlock(name); + // Note that {#insert} is evaluated on the current resolution context + // Therefore, we need to try to find the "correct" parent context to avoid stack + // overflow errors when using the same block names + ResolutionContext rc = findParentResolutionContext(context.resolutionContext()); + if (rc == null) { + // No parent context found - use the current + rc = context.resolutionContext(); + } + SectionBlock extending = rc.getExtendingBlock(name); if (extending != null) { - return context.execute(extending, context.resolutionContext()); + return context.execute(extending, rc); } else { - return context.execute(defaultBlock, context.resolutionContext()); + return context.execute(defaultBlock, rc); + } + } + + private ResolutionContext findParentResolutionContext(ResolutionContext context) { + if (context.getParent() == null) { + return null; } + // Let's iterate over all extending blocks and try to find the "correct" parent context + // The "correct" parent context is the parent of a context that contains this helper + // instance in any of its extending block + for (SectionBlock block : context.getExtendingBlocks()) { + if (block.findNode(this::containsThisHelperInstance) != null) { + return context.getParent(); + } + } + return findParentResolutionContext(context.getParent()); + } + + private boolean containsThisHelperInstance(TemplateNode node) { + return node.isSection() && node.asSection().helper == this; } public static class Factory implements SectionHelperFactory { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContext.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContext.java index ea293a2ca9ad8..5d14ec0493622 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContext.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContext.java @@ -4,7 +4,7 @@ import java.util.concurrent.CompletionStage; /** - * + * The resolution context holds the current context object. */ public interface ResolutionContext { @@ -52,6 +52,12 @@ public interface ResolutionContext { */ SectionBlock getExtendingBlock(String name); + /** + * + * @return the extending blocks + */ + Iterable getExtendingBlocks(); + /** * * @param key diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContextImpl.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContextImpl.java index b9eb6e794ecaf..69d45a4964bb9 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContextImpl.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContextImpl.java @@ -1,5 +1,6 @@ package io.quarkus.qute; +import java.util.Collections; import java.util.Map; import java.util.concurrent.CompletionStage; import java.util.function.Function; @@ -52,6 +53,11 @@ public SectionBlock getExtendingBlock(String name) { return null; } + @Override + public Iterable getExtendingBlocks() { + return extendingBlocks != null ? extendingBlocks.values() : Collections.emptyList(); + } + @Override public Object getAttribute(String key) { return attributeFun.apply(key); @@ -116,6 +122,11 @@ public SectionBlock getExtendingBlock(String name) { return null; } + @Override + public Iterable getExtendingBlocks() { + return extendingBlocks != null ? extendingBlocks.values() : Collections.emptyList(); + } + @Override public Object getAttribute(String key) { return parent.getAttribute(key); diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/IncludeTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/IncludeTest.java index 316e046642fa9..0482275186af2 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/IncludeTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/IncludeTest.java @@ -251,4 +251,100 @@ public void testIsolation() { assertEquals("NOT_FOUND", engine.parse("{#include foo _isolated /}").data("name", "Dorka").render()); } + @Test + public void testNestedMainBlocks() { + Engine engine = Engine.builder() + .addDefaults() + .build(); + + engine.putTemplate("root", engine.parse(""" + + {#insert /} + + """)); + engine.putTemplate("auth", engine.parse(""" + {#include root} +
+ {#insert /} +
+ {/include} + """)); + assertEquals("
LoginForm
" + + "", engine.parse(""" + {#include auth} +
Login Form
+ {/include} + """).render().replaceAll("\\s+", "")); + + engine.putTemplate("next", engine.parse(""" + {#include auth} + + {#insert /} + + {/include} + """)); + + // 1. top -> push child rc#1 with extending block $default$ + // 2. next -> push child rc#2 with extending block $default$ + // 3. auth -> push child rc#3 with extending block $default$ + // 4. root -> eval {#insert}, looks up $default$ in rc#3 + // 5. auth -> eval {#insert}, looks up $default$ in rc#2 + // 6. next -> eval {#insert}, looks up $default$ in rc#1 + assertEquals("
LoginForm
" + + "", engine.parse(""" + {#include next} +
Login Form
+ {/include} + """).render().replaceAll("\\s+", "")); + } + + @Test + public void testNestedBlocksWithSameName() { + Engine engine = Engine.builder() + .addDefaults() + .build(); + + engine.putTemplate("root", engine.parse(""" + + {#insert foo /} + + """)); + engine.putTemplate("auth", engine.parse(""" + {#include root} + {#foo} +
+ {#insert foo /} +
+ {/foo} + {/include} + """)); + assertEquals("
LoginForm
" + + "", engine.parse(""" + {#include auth} + {#foo} +
Login Form
+ {/foo} + {/include} + """).render().replaceAll("\\s+", "")); + + engine.putTemplate("next", engine.parse(""" + {#include auth} + {#foo} + + {#insert foo /} + + {/foo} + {/include} + """)); + + assertEquals("
LoginForm
" + + "", engine.parse(""" + {#include next} + {#foo} +
Login Form
+ {/foo} + {/include} + """).render().replaceAll("\\s+", "")); + } + }