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

Qute: add arguments metadata for user-defined tags #35818

Merged
merged 1 commit into from
Sep 11, 2023
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
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,7 +1148,46 @@
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`
[source]
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());
}

}
Loading