Skip to content

Commit

Permalink
Qute: improvements and fixes of UserTagSectionHelper.Arguments
Browse files Browse the repository at this point in the history
- always skip the artificial "it" arg
- strip quotes from the defaulted key for a single word string literal value-only argument; {#tag "foo"} becomes {#tag foo="foo"}
- add Arguments#skipIdenticalKeyValue(),  Arguments#filterIdenticalKeyValue() and Arguments#skipIt()
- resolves quarkusio#38280
  • Loading branch information
mkouba committed Jan 22, 2024
1 parent 18b26d5 commit 4bd0b0b
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 97 deletions.
22 changes: 15 additions & 7 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1152,18 +1152,26 @@ In this case, just add `_isolated=false` or `_unisolated` argument to the call s

===== 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.
Furthermore, arguments metadata are accessible in a tag using the `_args` alias.
Named arguments can be accessed directly in the tag template.
However, the first argument does not need to define a name and it can be accessed using the `it` alias.
Furthermore, if an argument does not have a name defined and the value is a single identifier, such as `foo`, then the name is defaulted to the value identifier, e.g. `{#myTag foo /}` becomes `{#myTag foo=foo /}`.
In other words, the argument value `foo` is resolved and can be accessed using `{foo}` in the tag template.

NOTE: If an argument does not have a name and the value is a single word string literal , such as `"foo"`, then the name is defaulted and quotation marks are removed, e.g. `{#myTag "foo" /}` becomes `{#myTag foo="foo" /}`.

`io.quarkus.qute.UserTagSectionHelper.Arguments` metadata are accessible in a tag using the `_args` alias.

* `_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.empty`/`_args.isEmpty` - returns `true` if no arguments are passed
* `_args.get(String name)` - returns the argument value of the given name or `null`
* `_args.filter(String...)` - returns the arguments matching the given names
* `_args.filterIdenticalKeyValue` - returns the arguments with the name equal to the value; typically `foo` from `{#test foo="foo" bar=true}` or `{#test "foo" bar=true /}`
* `_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.skipIdenticalKeyValue` - returns only the arguments with the name not equal to the value; typically `bar` from `{#test foo="foo" bar=true /}`
* `_args.skipIt` - returns all arguments except for the first unnamed argument; typically `bar` from `{#test foo bar=true /}`
* `_args.asHtmlAttributes` - renders the arguments as HTML attributes; e.g. `foo="true" readonly="readonly"`; the arguments are sorted by name in alphabetical order and the `'`, `"`, `<`, `>`, `&` characters are escaped

`_args` is also iterable: `{#each _args}{it.key}={it.value}{/each}`.
`_args` is also iterable of `java.util.Map.Entry`: `{#each _args}{it.key}={it.value}{/each}`.

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

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

import io.quarkus.qute.Template.Fragment;

Expand Down Expand Up @@ -110,7 +112,7 @@ public MissingEndTagStrategy missingEndTagStrategy() {
}

@Override
protected boolean ignoreParameterInit(String key, String value) {
protected boolean ignoreParameterInit(Supplier<String> firstParamSupplier, String key, String value) {
return key.equals(TEMPLATE)
// {#include foo _isolated=true /}
|| key.equals(ISOLATED)
Expand Down Expand Up @@ -147,6 +149,7 @@ static abstract class AbstractIncludeFactory<T extends SectionHelper> implements
static final String ISOLATED = "_isolated";
static final String UNISOLATED = "_unisolated";
static final String IGNORE_FRAGMENTS = "_ignoreFragments";
static final Pattern WHITESPACE = Pattern.compile("\\s");

@Override
public boolean treatUnknownSectionsAsBlocks() {
Expand All @@ -161,6 +164,7 @@ void addDefaultParams(ParametersInfo.Builder builder) {
builder
.addParameter(Parameter.builder(ISOLATED).defaultValue(isolatedDefaultValue()).optional()
.valuePredicate(ISOLATED::equals).build())
.addParameter(Parameter.builder(UNISOLATED).optional().valuePredicate(UNISOLATED::equals).build())
.addParameter(Parameter.builder(IGNORE_FRAGMENTS).defaultValue(Boolean.FALSE.toString()).optional()
.valuePredicate(IGNORE_FRAGMENTS::equals).build())
.build();
Expand All @@ -172,13 +176,7 @@ public Scope initializeBlock(Scope outerScope, BlockInfo block) {
for (Entry<String, String> entry : block.getParameters().entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (ignoreParameterInit(key, value)) {
continue;
} else if (useDefaultedKey(key, value)) {
// As "order" in {#include foo order /}
key = value;
}
block.addExpression(key, value);
handleParam(key, value, () -> block.getParameter(0), (k, v) -> block.addExpression(k, v));
}
return outerScope;
} else {
Expand Down Expand Up @@ -228,7 +226,7 @@ public T initialize(SectionInitContext context) {
ignoreFragments = true;
continue;
}
handleParamInit(key, value, context, params);
handleParam(key, value, () -> context.getParameter(0), (k, v) -> params.put(k, context.getExpression(k)));
}
}

Expand Down Expand Up @@ -312,13 +310,19 @@ protected String getFragmentId(String templateId, SectionInitContext context) {
return null;
}

protected void handleParamInit(String key, String value, SectionInitContext context, Map<String, Expression> params) {
if (ignoreParameterInit(key, value)) {
protected void handleParam(String key, String value, Supplier<String> firstParamSupplier,
BiConsumer<String, String> paramConsumer) {
if (ignoreParameterInit(firstParamSupplier, key, value)) {
return;
} else if (useDefaultedKey(key, value)) {
key = value;
if (LiteralSupport.isStringLiteral(value)) {
// {#include "foo" /} => {#include foo="foo" /}
key = value.substring(1, value.length() - 1);
} else {
key = value;
}
}
params.put(key, context.getExpression(key));
paramConsumer.accept(key, value);
}

protected boolean useDefaultedKey(String key, String value) {
Expand All @@ -331,10 +335,10 @@ protected boolean useDefaultedKey(String key, String value) {
}

protected boolean isSinglePart(String value) {
return Expressions.splitParts(value).size() == 1;
return Expressions.splitParts(value).size() == 1 && !WHITESPACE.matcher(value).find();
}

protected boolean ignoreParameterInit(String key, String value) {
protected boolean ignoreParameterInit(Supplier<String> firstParamSupplier, String key, String value) {
return key.equals(IGNORE_FRAGMENTS);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ static Object getLiteralValue(String literal) {
if (literal == null || literal.isEmpty()) {
return value;
}
char firstChar = literal.charAt(0);
if (isStringLiteralSeparator(firstChar) && literal.charAt(literal.length() - 1) == firstChar) {
if (isStringLiteral(literal)) {
value = literal.substring(1, literal.length() - 1);
} else if (literal.equals("true")) {
value = Boolean.TRUE;
Expand All @@ -33,6 +32,7 @@ static Object getLiteralValue(String literal) {
} else if (literal.equals("null")) {
value = null;
} else {
char firstChar = literal.charAt(0);
if (Character.isDigit(firstChar) || firstChar == '-' || firstChar == '+') {
if (INTEGER_LITERAL_PATTERN.matcher(literal).matches()) {
try {
Expand Down Expand Up @@ -77,4 +77,12 @@ static boolean isStringLiteralSeparator(char character) {
return character == '"' || character == '\'';
}

static boolean isStringLiteral(String value) {
if (value == null || value.isEmpty()) {
return false;
}
char firstChar = value.charAt(0);
return isStringLiteralSeparator(firstChar) && value.charAt(value.length() - 1) == firstChar;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -707,42 +707,24 @@ private void processParams(String tag, String label, Iterator<String> iter, Sect
LOGGER.debugf(builder.toString());
}

List<String> parametersPositions = new ArrayList<>(paramValues.size());

// Process named params first
for (Iterator<String> it = paramValues.iterator(); it.hasNext();) {
String param = it.next();
for (String param : paramValues) {
int equalsPosition = getFirstDeterminingEqualsCharPosition(param);
if (equalsPosition != -1) {
// Named param
params.put(param.substring(0, equalsPosition), param.substring(equalsPosition + 1,
param.length()));
it.remove();
String val = param.substring(equalsPosition + 1, param.length());
params.put(param.substring(0, equalsPosition), val);
parametersPositions.add(val);
} else {
parametersPositions.add(null);
}
}

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 = 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 = findFactoryParameter(param, factoryParams, included, false);
if (found != null) {
params.put(found.name, param);
} else {
params.put("" + generatedIdx++, param);
}
}
}
// When the number of actual params is less than factory params then we need to choose the best fit for positional params
processPositionalParams(paramValues, parametersPositions, factoryParams, params, actualSize < factoryParams.size());

// Use the default values if needed
for (Parameter param : factoryParams) {
Expand All @@ -767,8 +749,26 @@ private void processParams(String tag, String label, Iterator<String> iter, Sect
.build();
}

for (Entry<String, String> e : params.entrySet()) {
block.addParameter(e.getKey(), e.getValue());
params.entrySet().forEach(block::addParameter);
block.setParametersPositions(parametersPositions);
}

private void processPositionalParams(List<String> paramValues, List<String> parametersPositions,
List<Parameter> factoryParams, Map<String, String> params, boolean noDefaultValueTakesPrecedence) {
int generatedIdx = 0;
int idx = 0;
Predicate<String> included = params::containsKey;
for (String param : paramValues) {
if (parametersPositions.isEmpty() || parametersPositions.get(idx) == null) {
Parameter found = findFactoryParameter(param, factoryParams, included, noDefaultValueTakesPrecedence);
if (found != null) {
params.put(found.name, param);
} else {
params.put("" + generatedIdx++, param);
}
parametersPositions.set(idx, param);
}
idx++;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Predicate;

Expand All @@ -29,6 +30,10 @@ public final class SectionBlock implements WithOrigin, ErrorInitializer {
public final String label;
/**
* An unmodifiable ordered map of parsed parameters.
* <p>
* Note that the order does not necessary reflect the original positions of the parameters but the parsing order.
*
* @see SectionHelperFactory#getParameters()
*/
public final Map<String, String> parameters;

Expand All @@ -42,21 +47,33 @@ public final class SectionBlock implements WithOrigin, ErrorInitializer {
*/
List<TemplateNode> nodes;

private final List<String> positionalParameters;

public SectionBlock(Origin origin, String id, String label, Map<String, String> parameters,
Map<String, Expression> expressions,
List<TemplateNode> nodes) {
List<TemplateNode> nodes, List<String> positionalParameters) {
this.origin = origin;
this.id = id;
this.label = label;
this.parameters = parameters;
this.expressions = expressions;
this.nodes = ImmutableList.copyOf(nodes);
this.positionalParameters = positionalParameters;
}

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

/**
*
* @param position
* @return the parameter for the specified position, or {@code null} if no such parameter exists
*/
public String getParameter(int position) {
return positionalParameters.get(position);
}

List<Expression> getExpressions() {
List<Expression> expressions = new ArrayList<>();
expressions.addAll(this.expressions.values());
Expand Down Expand Up @@ -230,6 +247,7 @@ static class Builder implements BlockInfo {
private Origin origin;
private String label;
private Map<String, String> parameters;
private List<String> parametersPositions = List.of();
private final List<TemplateNode> nodes;
private Map<String, Expression> expressions;
private final Parser parser;
Expand Down Expand Up @@ -257,11 +275,16 @@ SectionBlock.Builder setLabel(String label) {
return this;
}

SectionBlock.Builder addParameter(String name, String value) {
SectionBlock.Builder addParameter(Entry<String, String> entry) {
if (parameters == null) {
parameters = new LinkedHashMap<>();
}
parameters.put(name, value);
parameters.put(entry.getKey(), entry.getValue());
return this;
}

SectionBlock.Builder setParametersPositions(List<String> parametersPositions) {
this.parametersPositions = Collections.unmodifiableList(parametersPositions);
return this;
}

Expand All @@ -279,6 +302,11 @@ public Map<String, String> getParameters() {
return parameters == null ? Collections.emptyMap() : Collections.unmodifiableMap(parameters);
}

@Override
public String getParameter(int position) {
return parametersPositions.get(position);
}

public String getLabel() {
return label;
}
Expand Down Expand Up @@ -310,7 +338,7 @@ SectionBlock build() {
} else {
expressions = Collections.unmodifiableMap(expressions);
}
return new SectionBlock(origin, id, label, parameters, expressions, nodes);
return new SectionBlock(origin, id, label, parameters, expressions, nodes, parametersPositions);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,12 @@ interface BlockInfo extends ParserDelegate, WithOrigin {
String getLabel();

/**
* Undeclared params with default values are included.
* An unmodifiable ordered map of parsed parameters.
* <p>
* Note that the order does not necessary reflect the original positions of the parameters but the parsing order.
*
* @return the map of parameters
* @see SectionHelperFactory#getParameters()
*/
Map<String, String> getParameters();

Expand All @@ -173,6 +176,14 @@ default boolean hasParameter(String name) {
return getParameters().containsKey(name);
}

/**
*
* @param position
* @return the parameter for the specified position, or {@code null} if no such parameter exists
* @see SectionBlock#getParameter(int)
*/
String getParameter(int position);

/**
* Parse and register an expression for the specified parameter.
* <p>
Expand All @@ -197,6 +208,7 @@ public interface SectionInitContext extends ParserDelegate {
/**
*
* @return the parameters of the main block
* @see SectionBlock#parameters
*/
default public Map<String, String> getParameters() {
return getBlocks().get(0).parameters;
Expand All @@ -219,6 +231,15 @@ default public String getParameter(String name) {
return getParameters().get(name);
}

/**
*
* @return the parameter for the specified position
* @see SectionBlock#getParameter(int)
*/
default public String getParameter(int position) {
return getBlocks().get(0).getParameter(position);
}

/**
*
* @param name
Expand Down
Loading

0 comments on commit 4bd0b0b

Please sign in to comment.