Skip to content

Commit

Permalink
Qute: execute tag templates in isolation by default
Browse files Browse the repository at this point in the history
- see #22285
  • Loading branch information
mkouba committed Sep 6, 2023
1 parent 61eb419 commit 5ae4d25
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 38 deletions.
8 changes: 4 additions & 4 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1143,10 +1143,10 @@ Then, we can call the tag like this:
<1> `item` is resolved to an iteration element and can be referenced using the `it` key in the tag template.

Check warning on line 1143 in docs/src/main/asciidoc/qute-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'.", "location": {"path": "docs/src/main/asciidoc/qute-reference.adoc", "range": {"start": {"line": 1143, "column": 69}}}, "severity": "INFO"}
<2> Tag content injected using the `nested-content` key in the tag template.

Check warning on line 1144 in docs/src/main/asciidoc/qute-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'.", "location": {"path": "docs/src/main/asciidoc/qute-reference.adoc", "range": {"start": {"line": 1144, "column": 25}}}, "severity": "INFO"}

By default, the tag template can reference data from the parent context.
For example, the tag above could use the following expression `{items.size}`.
However, sometimes it might be useful to disable this behavior and execute the tag as an _isolated_ template, i.e. without access to the context of the template that calls the tag.
In this case, just add `_isolated` or `_isolated=true` argument to the call site, e.g. `{#itemDetail item showImage=true _isolated /}`.
By default, a tag template cannot reference the data from the parent context.
Qute executes the tag as an _isolated_ template, i.e. without access to the context of the template that calls the tag.

Check warning on line 1147 in docs/src/main/asciidoc/qute-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'that is' rather than 'i.e.' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'that is' rather than 'i.e.' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/qute-reference.adoc", "range": {"start": {"line": 1147, "column": 50}}}, "severity": "WARNING"}
However, sometimes it might be useful to change the default behavior and disable the isolation.
In this case, just add `_isolated=false` or `_unisolated` argument to the call site, for example `{#itemDetail item showImage=true _isolated=false /}` or `{#itemDetail item showImage=true _unisolated /}`.

User tags can also make use of the template inheritance in the same way as regular `{#include}` sections do.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Predicate;
import java.util.function.Supplier;

import io.quarkus.qute.Template.Fragment;
Expand Down Expand Up @@ -117,6 +116,8 @@ protected boolean ignoreParameterInit(String key, String value) {
|| key.equals(ISOLATED)
// {#include foo _isolated /}
|| value.equals(ISOLATED)
// {#include foo _unisolated /}
|| value.equals(UNISOLATED)
// {#include foo _ignoreFragments=true /}
|| key.equals(IGNORE_FRAGMENTS)
// {#include foo _ignoreFragments /}
Expand All @@ -134,41 +135,34 @@ protected String getTemplateId(SectionInitContext context) {

@Override
protected IncludeSectionHelper newHelper(Supplier<Template> template, Map<String, Expression> params,
Map<String, SectionBlock> extendingBlocks, boolean isolated, SectionInitContext context) {
return new IncludeSectionHelper(template, extendingBlocks, params, isolated);
Map<String, SectionBlock> extendingBlocks, Boolean isolatedValue, SectionInitContext context) {
return new IncludeSectionHelper(template, extendingBlocks, params, isolatedValue != null ? isolatedValue
: Boolean.parseBoolean(context.getParameterOrDefault(ISOLATED, Boolean.FALSE.toString())));
}

}

static abstract class AbstractIncludeFactory<T extends SectionHelper> implements SectionHelperFactory<T> {

static final String ISOLATED = "_isolated";
static final String UNISOLATED = "_unisolated";
static final String IGNORE_FRAGMENTS = "_ignoreFragments";
static final String ISOLATED_DEFAULT_VALUE = "false";

@Override
public boolean treatUnknownSectionsAsBlocks() {
return true;
}

String isolatedDefaultValue() {
return Boolean.FALSE.toString();
}

void addDefaultParams(ParametersInfo.Builder builder) {
builder
.addParameter(Parameter.builder(ISOLATED).defaultValue(ISOLATED_DEFAULT_VALUE).optional()
.valuePredicate(new Predicate<String>() {

@Override
public boolean test(String v) {
return ISOLATED.equals(v);
}
}).build())
.addParameter(Parameter.builder(IGNORE_FRAGMENTS).defaultValue(ISOLATED_DEFAULT_VALUE).optional()
.valuePredicate(new Predicate<String>() {

@Override
public boolean test(String v) {
return IGNORE_FRAGMENTS.equals(v);
}
}).build())
.addParameter(Parameter.builder(ISOLATED).defaultValue(isolatedDefaultValue()).optional()
.valuePredicate(ISOLATED::equals).build())
.addParameter(Parameter.builder(IGNORE_FRAGMENTS).defaultValue(Boolean.FALSE.toString()).optional()
.valuePredicate(IGNORE_FRAGMENTS::equals).build())
.build();
}

Expand All @@ -195,7 +189,7 @@ public Scope initializeBlock(Scope outerScope, BlockInfo block) {
@Override
public T initialize(SectionInitContext context) {
boolean isEmpty = context.getBlocks().size() == 1 && context.getBlocks().get(0).isEmpty();
boolean isolatedValue = false;
Boolean isolatedValue = null;
boolean ignoreFragments = false;
Map<String, SectionBlock> extendingBlocks;

Expand Down Expand Up @@ -226,6 +220,9 @@ public T initialize(SectionInitContext context) {
if (value.equals(ISOLATED)) {
isolatedValue = true;
continue;
} else if (value.equals(UNISOLATED)) {
isolatedValue = false;
continue;
}
if (value.equals(IGNORE_FRAGMENTS)) {
ignoreFragments = true;
Expand All @@ -242,7 +239,7 @@ public T initialize(SectionInitContext context) {

//
if (!ignoreFragments) {
ignoreFragments = Boolean.parseBoolean(context.getParameterOrDefault(IGNORE_FRAGMENTS, ISOLATED_DEFAULT_VALUE));
ignoreFragments = Boolean.parseBoolean(context.getParameterOrDefault(IGNORE_FRAGMENTS, "false"));
}
final String fragmentId = ignoreFragments ? null : getFragmentId(templateId, context);
Supplier<Template> currentTemplate;
Expand Down Expand Up @@ -292,9 +289,7 @@ public Template get() {
return template;
}
};

return newHelper(template, params, extendingBlocks, isolatedValue ? true
: Boolean.parseBoolean(context.getParameterOrDefault(ISOLATED, ISOLATED_DEFAULT_VALUE)), context);
return newHelper(template, params, extendingBlocks, isolatedValue, context);
}

protected abstract String getTemplateId(SectionInitContext context);
Expand Down Expand Up @@ -342,7 +337,7 @@ protected boolean isSinglePart(String value) {
protected abstract boolean ignoreParameterInit(String key, String value);

protected abstract T newHelper(Supplier<Template> template, Map<String, Expression> params,
Map<String, SectionBlock> extendingBlocks, boolean isolated, SectionInitContext context);
Map<String, SectionBlock> extendingBlocks, Boolean isolatedValue, SectionInitContext context);
}

enum Code implements ErrorCode {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ private boolean isNestedContent(Expression expr) {
public static class Factory extends IncludeSectionHelper.AbstractIncludeFactory<UserTagSectionHelper> {

private static final String IT = "it";
// Unlike regular includes user tags are isolated by default
private static final String ISOLATED_DEFAULT_VALUE = "true";

private final String name;
private final String templateId;
Expand All @@ -61,6 +63,11 @@ public List<String> getDefaultAliases() {
return ImmutableList.of(name);
}

@Override
String isolatedDefaultValue() {
return ISOLATED_DEFAULT_VALUE;
}

@Override
public ParametersInfo getParameters() {
ParametersInfo.Builder builder = ParametersInfo.builder().addParameter(Parameter.builder(IT).defaultValue(IT));
Expand All @@ -71,9 +78,11 @@ public ParametersInfo getParameters() {
@Override
protected boolean ignoreParameterInit(String key, String value) {
// {#myTag _isolated=true /}
return key.equals(IncludeSectionHelper.Factory.ISOLATED)
return key.equals(ISOLATED)
// {#myTag _isolated /}
|| value.equals(IncludeSectionHelper.Factory.ISOLATED)
|| value.equals(ISOLATED)
// {#myTag _unisolated /}
|| value.equals(UNISOLATED)
// IT with default value, e.g. {#myTag foo=bar /}
|| (key.equals(IT) && value.equals(IT));
}
Expand All @@ -85,9 +94,12 @@ protected String getTemplateId(SectionInitContext context) {

@Override
protected UserTagSectionHelper newHelper(Supplier<Template> template, Map<String, Expression> params,
Map<String, SectionBlock> extendingBlocks, boolean isolated, SectionInitContext context) {
Map<String, SectionBlock> extendingBlocks, Boolean isolatedValue, SectionInitContext context) {
boolean isNestedContentNeeded = !context.getBlock(SectionHelperFactory.MAIN_BLOCK_NAME).isEmpty();
return new UserTagSectionHelper(template, extendingBlocks, params, isolated, isNestedContentNeeded);
return new UserTagSectionHelper(template, extendingBlocks, params,
isolatedValue != null ? isolatedValue
: Boolean.parseBoolean(context.getParameterOrDefault(ISOLATED, ISOLATED_DEFAULT_VALUE)),
isNestedContentNeeded);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,18 @@ public void testOptionalEndTag() {
engine.parse("{#include super}{#let foo=1} {foo}").render());
}

@Test
public void testIsolation() {
Engine engine = Engine.builder()
.addDefaults()
.strictRendering(false)
.build();

Template foo = engine.parse("{name}");
engine.putTemplate("foo", foo);
assertEquals("Dorka", engine.parse("{#include foo /}").data("name", "Dorka").render());
assertEquals("Dorka", engine.parse("{#include foo _unisolated /}").data("name", "Dorka").render());
assertEquals("NOT_FOUND", engine.parse("{#include foo _isolated /}").data("name", "Dorka").render());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public void testUserTag() {
assertEquals("nope",
engine.parse("{#each this}{#myTag showImage=false /}{/each}").render(Collections.singletonMap("order", order)));
assertEquals("Herbert",
engine.parse("{#each this}{#myTag it showImage=true /}{/each}").render(Collections.singletonList(order)));
engine.parse("{#each this}{#myTag it showImage=true _isolated=false /}{/each}")
.render(Collections.singletonList(order)));
}

@Test
Expand All @@ -45,15 +46,16 @@ public void testUserTagWithNestedContent() {
Map<String, Object> order = new HashMap<>();
order.put("name", "Herbert");
assertEquals("<b>Herbert</b>",
engine.parse("{#myTag showImage=true}{order.name}{/}").render(Collections.singletonMap("order", order)));
engine.parse("{#myTag showImage=true _isolated=false}{order.name}{/}")
.render(Collections.singletonMap("order", order)));
assertEquals("nope", engine.parse("{#myTag}{order.name}{/}").render(Collections.singletonMap("order", order)));
assertEquals("nope",
engine.parse("{#myTag showImage=false}{order.name}{/}").render(Collections.singletonMap("order", order)));
assertEquals("nope",
engine.parse("{#each this}{#myTag showImage=false}{it.name}{/}{/each}")
.render(Collections.singletonMap("order", order)));
assertEquals("<b>Herbert</b>",
engine.parse("{#each this}{#myTag showImage=true}{it.name}{/}{/each}")
engine.parse("{#each this}{#myTag showImage=true _isolated=false}{it.name}{/}{/each}")
.render(Collections.singletonList(order)));
}

Expand Down Expand Up @@ -105,7 +107,7 @@ public void testEval() {
engine.putTemplate("my-tag-id", tag);

assertEquals("10 kg",
engine.parse("{#itemDetail itemId=1 myNestedContent=\"{item.quantity} {item.unit}\" /}")
engine.parse("{#itemDetail itemId=1 myNestedContent=\"{item.quantity} {item.unit}\" _isolated=false /}")
.data("items", Map.of(1, Map.of("quantity", 10, "unit", "kg"))).render());
}

Expand Down Expand Up @@ -140,4 +142,19 @@ public void testInsertSections() {
assertEquals("Baz!", engine.parse("{#myTag2}Baz!{/myTag2}").render());
}

@Test
public void testIsolation() {
Engine engine = Engine.builder()
.addDefaults()
.addSectionHelper(new UserTagSectionHelper.Factory("myTag", "my-tag-id"))
.strictRendering(false)
.build();

Template tag = engine.parse("{name}");
engine.putTemplate("my-tag-id", tag);
assertEquals("NOT_FOUND", engine.parse("{#myTag /}").data("name", "Dorka").render());
assertEquals("Dorka", engine.parse("{#myTag _isolated=false /}").data("name", "Dorka").render());
assertEquals("Dorka", engine.parse("{#myTag _unisolated /}").data("name", "Dorka").render());
}

}

0 comments on commit 5ae4d25

Please sign in to comment.