Skip to content

Commit

Permalink
Qute user tags - use defaulted keys if appropriate
Browse files Browse the repository at this point in the history
- i.e. for params that define no key and contain only single part
- also use Mapper instead of Map for the data passed to a template
- Mapper value resolver has priority 15, i.e. is called before a
generated value resolver
- resolves quarkusio#21855
- related to quarkusio#21861
  • Loading branch information
mkouba committed Dec 15, 2021
1 parent 8bc2149 commit f0a79ec
Show file tree
Hide file tree
Showing 16 changed files with 270 additions and 64 deletions.
19 changes: 12 additions & 7 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -962,8 +962,8 @@ NOTE: The evaluated template is parsed and evaluated every time the section is e
[[user_tags]]
==== User-defined Tags

User-defined tags can be used to include a template and optionally pass some parameters.
Let's suppose we have a template called `itemDetail.html`:
User-defined tags can be used to include a tag template and optionally pass some parameters.
Let's suppose we have a tag template called `itemDetail.html`:

[source]
----
Expand All @@ -973,21 +973,21 @@ Let's suppose we have a template called `itemDetail.html`:
{/if}
----
<1> `showImage` is a named parameter.
<2> `it` is a special key that is replaced with the first unnamed param of the tag.
<2> `it` is a special key that is replaced with the first unnamed parameter of the tag.
<3> (optional) `nested-content` is a special key that will be replaced by the content of the tag.

Now if we register this template under the name `itemDetail.html` and if we add a `UserTagSectionHelper` to the engine:
In Quarkus, all files from the `src/main/resources/templates/tags` are registered and monitored automatically.
For Qute standalone, you need to put the parsed template under the name `itemDetail.html` and register a relevant `UserTagSectionHelper` to the engine:

[source,java]
----
Engine engine = Engine.builder()
.addSectionHelper(new UserTagSectionHelper.Factory("itemDetail","itemDetail.html"))
.build();
engine.putTemplate("itemDetail.html", engine.parse("..."));
----

NOTE: In Quarkus, all files from the `src/main/resources/templates/tags` are registered and monitored automatically!

We can include the tag like this:
Then, we can call the tag like this:

[source,html]
----
Expand All @@ -1004,6 +1004,11 @@ We can include the tag like this:
<1> `item` is resolved to an iteration element and can be referenced using the `it` key in the tag template.
<2> Tag content injected using the `nested-content` key in the tag template.

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 /}`.

=== Rendering Output

`TemplateInstance` provides several ways to trigger the rendering and consume the result.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ public ParametersInfo getParameters() {
return ParametersInfo.builder()
.addParameter(ALIAS, EMPTY)
.addParameter(IN, EMPTY)
.addParameter(new Parameter(ITERABLE, null, true))
.addParameter(Parameter.builder(ITERABLE).optional())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package io.quarkus.qute;

import io.quarkus.qute.SectionHelperFactory.ParametersInfo;
import java.util.Objects;
import java.util.function.Predicate;

/**
* Definition of a section parameter.
* Definition of a section factory parameter.
*
* @see ParametersInfo
* @see SectionHelperFactory#getParameters()
*/
public class Parameter {
public final class Parameter {

public static Builder builder(String name) {
return new Builder(name);
}

public static final String EMPTY = "$empty$";

Expand All @@ -18,10 +24,19 @@ public class Parameter {

public final boolean optional;

public final Predicate<String> valuePredicate;

private static final Predicate<String> ALWAYS_TRUE = v -> true;

public Parameter(String name, String defaultValue, boolean optional) {
this.name = name;
this(name, defaultValue, optional, ALWAYS_TRUE);
}

private Parameter(String name, String defaultValue, boolean optional, Predicate<String> valuePredicate) {
this.name = Objects.requireNonNull(name);
this.defaultValue = defaultValue;
this.optional = optional;
this.valuePredicate = valuePredicate != null ? valuePredicate : ALWAYS_TRUE;
}

public String getName() {
Expand All @@ -32,14 +47,24 @@ public String getDefaultValue() {
return defaultValue;
}

public boolean hasDefatulValue() {
public boolean hasDefaultValue() {
return defaultValue != null;
}

public boolean isOptional() {
return optional;
}

/**
* Allows a factory parameter to refuse a value of an unnamed actual parameter.
*
* @param value
* @return {@code true} if the value is acceptable, {@code false} otherwise
*/
public boolean accepts(String value) {
return valuePredicate.test(value);
}

@Override
public String toString() {
StringBuilder builder = new StringBuilder();
Expand All @@ -48,4 +73,37 @@ public String toString() {
return builder.toString();
}

public static class Builder {

private final String name;
private String defaultValue;
private boolean optional;
private Predicate<String> valuePredicate;

public Builder(String name) {
this.name = name;
this.optional = false;
}

public Builder defaultValue(String defaultValue) {
this.defaultValue = defaultValue;
return this;
}

public Builder optional() {
this.optional = true;
return this;
}

public Builder valuePredicate(Predicate<String> valuePredicate) {
this.valuePredicate = valuePredicate;
return this;
}

public Parameter build() {
return new Parameter(name, defaultValue, optional, valuePredicate);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.concurrent.CompletionStage;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;
Expand Down Expand Up @@ -548,52 +549,34 @@ private void processParams(String tag, String label, Iterator<String> iter, Sect
}
}

Predicate<String> included = params::containsKey;
// Then process positional params
if (actualSize < factoryParams.size()) {
// The number of actual params is less than factory params
// We need to choose the best fit for positional params
for (String param : paramValues) {
Parameter found = null;
for (Parameter factoryParam : factoryParams) {
// Prefer params with no default value
if (factoryParam.defaultValue == null && !params.containsKey(factoryParam.name)) {
found = factoryParam;
params.put(factoryParam.name, param);
break;
}
}
if (found == null) {
for (Parameter factoryParam : factoryParams) {
if (!params.containsKey(factoryParam.name)) {
found = factoryParam;
params.put(factoryParam.name, param);
break;
}
}
Parameter found = findFactoryParameter(param, factoryParams, included, true);
if (found != null) {
params.put(found.name, param);
}
}
} else {
// The number of actual params is greater or equals to factory params
int generatedIdx = 0;
for (String param : paramValues) {
// Positional param
Parameter found = null;
for (Parameter factoryParam : factoryParams) {
if (!params.containsKey(factoryParam.name)) {
found = factoryParam;
params.put(factoryParam.name, param);
break;
}
}
if (found == null) {
Parameter found = findFactoryParameter(param, factoryParams, included, false);
if (found != null) {
params.put(found.name, param);
} else {
params.put("" + generatedIdx++, param);
}
}
}

// Use the default values if needed
factoryParams.stream()
.filter(Parameter::hasDefatulValue)
.filter(Parameter::hasDefaultValue)
.forEach(p -> params.putIfAbsent(p.name, p.defaultValue));

// Find undeclared mandatory params
Expand All @@ -609,6 +592,24 @@ private void processParams(String tag, String label, Iterator<String> iter, Sect
params.forEach(block::addParameter);
}

private Parameter findFactoryParameter(String paramValue, List<Parameter> factoryParams, Predicate<String> included,
boolean noDefaultValueTakesPrecedence) {
if (noDefaultValueTakesPrecedence) {
for (Parameter param : factoryParams) {
// Params with no default value take precedence
if (param.accepts(paramValue) && !param.hasDefaultValue() && !included.test(param.name)) {
return param;
}
}
}
for (Parameter param : factoryParams) {
if (param.accepts(paramValue) && !included.test(param.name)) {
return param;
}
}
return null;
}

/**
*
* @param part
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@

import java.util.Map;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;

class ResolutionContextImpl implements ResolutionContext {

private final Object data;
private final Evaluator evaluator;
private final Map<String, SectionBlock> extendingBlocks;
private final TemplateInstance templateInstance;
private final Function<String, Object> attributeFun;

ResolutionContextImpl(Object data,
Evaluator evaluator, Map<String, SectionBlock> extendingBlocks, TemplateInstance templateInstance) {
Evaluator evaluator, Map<String, SectionBlock> extendingBlocks, Function<String, Object> attributeFun) {
this.data = data;
this.evaluator = evaluator;
this.extendingBlocks = extendingBlocks;
this.templateInstance = templateInstance;
this.attributeFun = attributeFun;
}

@Override
Expand Down Expand Up @@ -53,7 +54,7 @@ public SectionBlock getExtendingBlock(String name) {

@Override
public Object getAttribute(String key) {
return templateInstance.getAttribute(key);
return attributeFun.apply(key);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ public String asMessage() {
if (name != null) {
Object base = getBase().orElse(null);
List<Expression> params = getParams();
boolean isDataMap = (base instanceof Map) && ((Map<?, ?>) base).containsKey(TemplateInstanceBase.DATA_MAP_KEY);
boolean isDataMap = isDataMap(base);
// Entry "foo" not found in the data map
// Property "foo" not found on base object "org.acme.Bar"
// Method "getDiscount(value)" not found on base object "org.acme.Item"
Expand Down Expand Up @@ -190,6 +190,15 @@ public String asMessage() {
}
}

private boolean isDataMap(Object base) {
if (base instanceof Map) {
return ((Map<?, ?>) base).containsKey(TemplateInstanceBase.DATA_MAP_KEY);
} else if (base instanceof Mapper) {
return ((Mapper) base).get(TemplateInstanceBase.DATA_MAP_KEY) != null;
}
return false;
}

@Override
public String toString() {
return "NOT_FOUND";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.qute;

import java.util.Map;
import java.util.concurrent.CompletionStage;

/**
Expand Down Expand Up @@ -28,6 +29,14 @@ public interface SectionResolutionContext {
*/
ResolutionContext resolutionContext();

/**
*
* @param data
* @param extendingBlocks
* @return a new resolution context
*/
ResolutionContext newResolutionContext(Object data, Map<String, SectionBlock> extendingBlocks);

/**
* Execute the main block with the current resolution context.
*
Expand Down
Loading

0 comments on commit f0a79ec

Please sign in to comment.