Skip to content

Commit

Permalink
Qute: add arguments metadata for user-defined tags
Browse files Browse the repository at this point in the history
- resolves #35765
  • Loading branch information
mkouba committed Sep 8, 2023
1 parent bc9e7ef commit e0b23ea
Show file tree
Hide file tree
Showing 11 changed files with 243 additions and 8 deletions.
39 changes: 39 additions & 0 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,45 @@ Qute executes the tag as an _isolated_ template, i.e. without access to the cont
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 /}`.

===== Arguments

Named arguments can be accessed directly in a tag template.
The first argument does not have to define a name but can be accessed using the `it` alias.

Check warning on line 1154 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": 1154, "column": 70}}}, "severity": "INFO"}
Furthermore, arguments metadata are accessible in a tag using the `_args` alias.

Check warning on line 1155 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": 1155, "column": 56}}}, "severity": "INFO"}

* `_args.size` - returns the actual number of arguments passed to a tag
* `_args.empty` - returns `true` if no arguments are passed
* `_args.get(String name)` - returns the argument value of the given name
* `_args.filter(String...)` - returns the arguments matching the given names
* `_args.skip(String...)` - returns only the arguments that do not match the given names
* `_args.asHtmlAttributes` - renders the arguments as HTML attributes; e.g. `foo="true" bar="false"` (the arguments are sorted by name in alphabetical order)

`_args` is also iterable: `{#each _args}{it.key}={it.value}{/each}`.

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

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Spelling] Use correct American English spelling. Did you really mean 'iterable'? Raw Output: {"message": "[Quarkus.Spelling] Use correct American English spelling. Did you really mean 'iterable'?", "location": {"path": "docs/src/main/asciidoc/qute-reference.adoc", "range": {"start": {"line": 1164, "column": 17}}}, "severity": "WARNING"}

For example, we can call the user tag defined below with `{#test 'Martin' readonly=true /}`.

.`tags/test.html`
[source]
----
{it} <1>
{readonly} <2>
{_args.filter('readonly').asHtmlAttributes} <3>
----
<1> `it` is replaced with the first unnamed parameter of the tag.
<2> `readonly` is a named parameter.
<3> `_args` represents arguments metadata.

The result would be:

[source]
----
Martin
true
readonly="true"
----

===== Inheritance

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

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in '3.5.8.2. Inheritance'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in '3.5.8.2. Inheritance'.", "location": {"path": "docs/src/main/asciidoc/qute-reference.adoc", "range": {"start": {"line": 1189, "column": 1}}}, "severity": "INFO"}
User tags can also make use of the template inheritance in the same way as regular `{#include}` sections do.

Check warning on line 1190 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 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/qute-reference.adoc", "range": {"start": {"line": 1190, "column": 73}}}, "severity": "INFO"}

.Tag `myTag`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@
import io.quarkus.qute.Expression;
import io.quarkus.qute.Expression.Part;
import io.quarkus.qute.Expressions;
import io.quarkus.qute.LoopSectionHelper;
import io.quarkus.qute.Namespaces;
import io.quarkus.qute.Resolver;
import io.quarkus.qute.SectionHelperFactory;
import io.quarkus.qute.deployment.QuteProcessor.JavaMemberLookupConfig;
import io.quarkus.qute.deployment.QuteProcessor.MatchResult;
import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis;
Expand Down Expand Up @@ -394,7 +394,7 @@ private void validateExpression(BuildProducer<IncorrectExpressionBuildItem> inco
String name = firstPart.getName();
String typeInfo = firstPart.getTypeInfo();
boolean isGlobal = globals.contains(name);
boolean isLoopMetadata = typeInfo != null && typeInfo.endsWith(LoopSectionHelper.Factory.HINT_METADATA);
boolean isLoopMetadata = typeInfo != null && typeInfo.endsWith(SectionHelperFactory.HINT_METADATA);
// Type info derived from a parent section, e.g "it<loop#3>" and "foo<set#3>"
boolean hasDerivedTypeInfo = typeInfo != null && !typeInfo.startsWith("" + Expressions.TYPE_INFO_SEPARATOR);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,11 @@ public void beforeParsing(ParserHelper parserHelper) {

addMethodParamsToParserHelper(parserHelper, pathToPathWithoutSuffix.get(templateId),
checkedTemplateIdToParamDecl);

if (templateId.startsWith(TemplatePathBuildItem.TAGS)) {
parserHelper.addParameter(UserTagSectionHelper.Factory.ARGS,
UserTagSectionHelper.Arguments.class.getName());
}
}

addMethodParamsToParserHelper(parserHelper, templateId, msgBundleTemplateIdToParamDecl);
Expand Down Expand Up @@ -827,7 +832,7 @@ void validateCheckedFragments(List<CheckedFragmentValidationBuildItem> validatio
continue;
}
String typeInfo = expression.getParts().get(0).getTypeInfo();
if (typeInfo == null || (typeInfo != null && typeInfo.endsWith(LoopSectionHelper.Factory.HINT_METADATA))) {
if (typeInfo == null || (typeInfo != null && typeInfo.endsWith(SectionHelperFactory.HINT_METADATA))) {
continue;
}
Info info = TypeInfos.create(expression, index, null).get(0);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.quarkus.qute.deployment.tag;

import static org.junit.jupiter.api.Assertions.assertEquals;

import jakarta.inject.Inject;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.Template;
import io.quarkus.test.QuarkusUnitTest;

public class UserTagArgumentsTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addAsResource(new StringAsset(
"{_args.size}::{_args.empty}::{_args.get('name')}::{_args.asHtmlAttributes}::{_args.skip('foo','baz').size}::{#each _args.filter('name')}{it.value}{/each}"),
"templates/tags/hello.txt")
.addAsResource(new StringAsset("{#hello name=val /}"), "templates/foo.txt"));

@Inject
Template foo;

@Test
public void testInjection() {
assertEquals("1::false::Lu::name=\"Lu\"::1::Lu", foo.data("val", "Lu").render());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.quarkus.qute.deployment.tag;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;

import org.assertj.core.util.Throwables;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.TemplateException;
import io.quarkus.test.QuarkusUnitTest;

public class UserTagArgumentsValidationTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addAsResource(new StringAsset(
"{_args.sizes}"),
"templates/tags/hello.txt")
.addAsResource(new StringAsset("{#hello name=val /}"), "templates/foo.txt"))
.assertException(t -> {
Throwable root = Throwables.getRootCause(t);
if (root == null) {
root = t;
}
assertThat(root)
.isInstanceOf(TemplateException.class)
.hasMessageContaining("Found incorrect expressions (1)").hasMessageContaining("{_args.sizes}");
});

@Test
public void test() {
fail();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,9 @@ protected boolean isSinglePart(String value) {
return Expressions.splitParts(value).size() == 1;
}

protected abstract boolean ignoreParameterInit(String key, String value);
protected boolean ignoreParameterInit(String key, String value) {
return key.equals(IGNORE_FRAGMENTS);
}

protected abstract T newHelper(Supplier<Template> template, Map<String, Expression> params,
Map<String, SectionBlock> extendingBlocks, Boolean isolatedValue, SectionInitContext context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ public static class Factory implements SectionHelperFactory<LoopSectionHelper> {
public static final String ITERATION_METADATA_PREFIX_NONE = "<none>";

public static final String HINT_ELEMENT = "<loop-element>";
public static final String HINT_METADATA = "<loop-metadata>";
public static final String HINT_PREFIX = "<loop#";
private static final String IN = "in";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
*/
public interface SectionHelperFactory<T extends SectionHelper> {

// The validation of expressions with the metadata hint may be relaxed in some cases
public static final String HINT_METADATA = "<metadata>";

String MAIN_BLOCK_NAME = "$main";

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package io.quarkus.qute;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;

public class UserTagSectionHelper extends IncludeSectionHelper implements SectionHelper {
Expand All @@ -23,6 +29,7 @@ protected boolean optimizeIfNoParams() {

@Override
protected void addAdditionalEvaluatedParams(SectionResolutionContext context, Map<String, Object> evaluatedParams) {
evaluatedParams.put(Factory.ARGS, new Arguments(evaluatedParams));
if (isNestedContentNeeded) {
// If needed then add the {nested-content} to the evaluated params
Expression nestedContent = ((TemplateImpl) template.get()).findExpression(this::isNestedContent);
Expand All @@ -41,6 +48,8 @@ private boolean isNestedContent(Expression expr) {

public static class Factory extends IncludeSectionHelper.AbstractIncludeFactory<UserTagSectionHelper> {

public static final String ARGS = "_args";

private static final String IT = "it";
// Unlike regular includes user tags are isolated by default
private static final String ISOLATED_DEFAULT_VALUE = "true";
Expand Down Expand Up @@ -78,13 +87,13 @@ public ParametersInfo getParameters() {
@Override
protected boolean ignoreParameterInit(String key, String value) {
// {#myTag _isolated=true /}
return key.equals(ISOLATED)
return super.ignoreParameterInit(key, value) || (key.equals(ISOLATED)
// {#myTag _isolated /}
|| value.equals(ISOLATED)
// {#myTag _unisolated /}
|| value.equals(UNISOLATED)
// IT with default value, e.g. {#myTag foo=bar /}
|| (key.equals(IT) && value.equals(IT));
|| (key.equals(IT) && value.equals(IT)));
}

@Override
Expand Down Expand Up @@ -117,4 +126,81 @@ protected void handleParamInit(String key, String value, SectionInitContext cont

}

public static class Arguments implements Iterable<Entry<String, Object>> {

private final List<Entry<String, Object>> args;

Arguments(Map<String, Object> map) {
this.args = new ArrayList<>(Objects.requireNonNull(map).size());
map.entrySet().forEach(args::add);
// sort by key
this.args.sort(Comparator.comparing(Entry::getKey));
}

private Arguments(List<Entry<String, Object>> args) {
this.args = args;
}

public boolean isEmpty() {
return args.isEmpty();
}

public int size() {
return args.size();
}

public Object get(String key) {
for (Entry<String, Object> e : args) {
if (e.getKey().equals(key)) {
return e.getValue();
}
}
return null;
}

@Override
public Iterator<Entry<String, Object>> iterator() {
return args.iterator();
}

public Arguments skip(String... keys) {
Set<String> keySet = Set.of(keys);
List<Entry<String, Object>> newArgs = new ArrayList<>(args.size());
for (Entry<String, Object> e : args) {
if (!keySet.contains(e.getKey())) {
newArgs.add(e);
}
}
return new Arguments(newArgs);
}

public Arguments filter(String... keys) {
Set<String> keySet = Set.of(keys);
List<Entry<String, Object>> newArgs = new ArrayList<>(args.size());
for (Entry<String, Object> e : args) {
if (keySet.contains(e.getKey())) {
newArgs.add(e);
}
}
return new Arguments(newArgs);
}

// foo="1" bar="true"
public String asHtmlAttributes() {
StringBuilder builder = new StringBuilder();
for (Iterator<Entry<String, Object>> it = args.iterator(); it.hasNext();) {
Entry<String, Object> e = it.next();
builder.append(e.getKey());
builder.append("=\"");
builder.append(e.getValue());
builder.append("\"");
if (it.hasNext()) {
builder.append(" ");
}
}
return builder.toString();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public void testTypeInfos() {
Expression machineStatusExpr = find(expressions, "machine.status");
assertExpr(expressions, "OK", 1, "OK<when#" + machineStatusExpr.getGeneratedId() + ">");

assertExpr(expressions, "it_hasNext", 1, "|java.lang.Boolean|<loop-metadata>");
assertExpr(expressions, "it_hasNext", 1, "|java.lang.Boolean|<metadata>");
assertExpr(expressions, "not_typesafe", 1, null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,35 @@ public void testIsolation() {
assertEquals("Dorka", engine.parse("{#myTag _unisolated /}").data("name", "Dorka").render());
}

@Test
public void testArguments() {
Engine engine = Engine.builder()
.addDefaults()
.addValueResolver(new ReflectionValueResolver())
.addSectionHelper(new UserTagSectionHelper.Factory("myTag1", "my-tag-1"))
.addSectionHelper(new UserTagSectionHelper.Factory("myTag2", "my-tag-2"))
.addSectionHelper(new UserTagSectionHelper.Factory("gravatar", "gravatar-tag"))
.strictRendering(false)
.build();
Template tag1 = engine.parse(
"{_args.size}::{_args.empty}::{_args.get('foo').or('bar')}::{_args.asHtmlAttributes}::{_args.skip('foo','baz').size}::{#each _args.filter('foo')}{it.value}{/each}");
engine.putTemplate("my-tag-1", tag1);
Template tag2 = engine.parse(
"{#each _args}{it.key}=\"{it.value}\"{#if it_hasNext} {/if}{/each}");
engine.putTemplate("my-tag-2", tag2);
engine.putTemplate("gravatar-tag", engine.parse(
"<img src=\"https://www.gravatar.com/avatar/{hash}{#if size}?s={size}{/if}\" {_args.skip('hash','size').asHtmlAttributes}/>"));

Template template = engine.parse("{#myTag1 /}");
assertEquals("0::true::bar::::0::", template.render());
assertEquals("2::false::1::bar=\"true\" foo=\"1\"::1::1", engine.parse("{#myTag1 foo=1 bar=true /}").render());

assertEquals("baz=\"false\" foo=\"1\"", engine.parse("{#myTag2 foo=1 baz=false /}").render());

assertEquals(
"<img src=\"https://www.gravatar.com/avatar/ia3andy\" alt=\"ia3andy\" class=\"rounded\" title=\"https://github.com/ia3andy\"/>",
engine.parse("{#gravatar hash='ia3andy' alt='ia3andy' title='https://github.com/ia3andy' class='rounded' /}")
.render());
}

}

0 comments on commit e0b23ea

Please sign in to comment.