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 user tags - use defaulted keys if appropriate #22003

Merged
merged 1 commit into from
Dec 17, 2021
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
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