diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 4c129ba581ba3..ba56d9283048c 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -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 /}`. diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java index 84042ece5b1c8..583fe35aadbe7 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java @@ -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; @@ -110,7 +112,7 @@ public MissingEndTagStrategy missingEndTagStrategy() { } @Override - protected boolean ignoreParameterInit(String key, String value) { + protected boolean ignoreParameterInit(Supplier firstParamSupplier, String key, String value) { return key.equals(TEMPLATE) // {#include foo _isolated=true /} || key.equals(ISOLATED) @@ -147,6 +149,7 @@ static abstract class AbstractIncludeFactory 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() { @@ -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(); @@ -172,13 +176,7 @@ public Scope initializeBlock(Scope outerScope, BlockInfo block) { for (Entry 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 { @@ -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))); } } @@ -312,13 +310,19 @@ protected String getFragmentId(String templateId, SectionInitContext context) { return null; } - protected void handleParamInit(String key, String value, SectionInitContext context, Map params) { - if (ignoreParameterInit(key, value)) { + protected void handleParam(String key, String value, Supplier firstParamSupplier, + BiConsumer 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) { @@ -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 firstParamSupplier, String key, String value) { return key.equals(IGNORE_FRAGMENTS); } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/LiteralSupport.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/LiteralSupport.java index 65afaf5eb21a0..e2bd413a21a50 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/LiteralSupport.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/LiteralSupport.java @@ -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; @@ -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 { @@ -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; + } + } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java index b6eba251f7d9b..3b43d28e9cb81 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java @@ -707,42 +707,24 @@ private void processParams(String tag, String label, Iterator iter, Sect LOGGER.debugf(builder.toString()); } + List parametersPositions = new ArrayList<>(paramValues.size()); + // Process named params first - for (Iterator 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 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) { @@ -767,8 +749,26 @@ private void processParams(String tag, String label, Iterator iter, Sect .build(); } - for (Entry e : params.entrySet()) { - block.addParameter(e.getKey(), e.getValue()); + params.entrySet().forEach(block::addParameter); + block.setParametersPositions(parametersPositions); + } + + private void processPositionalParams(List paramValues, List parametersPositions, + List factoryParams, Map params, boolean noDefaultValueTakesPrecedence) { + int generatedIdx = 0; + int idx = 0; + Predicate 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++; } } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionBlock.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionBlock.java index f3cfe26a22a29..a584e9be57fe3 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionBlock.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionBlock.java @@ -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; @@ -29,6 +30,10 @@ public final class SectionBlock implements WithOrigin, ErrorInitializer { public final String label; /** * An unmodifiable ordered map of parsed parameters. + *

+ * Note that the order does not necessary reflect the original positions of the parameters but the parsing order. + * + * @see SectionHelperFactory#getParameters() */ public final Map parameters; @@ -42,21 +47,33 @@ public final class SectionBlock implements WithOrigin, ErrorInitializer { */ List nodes; + private final List positionalParameters; + public SectionBlock(Origin origin, String id, String label, Map parameters, Map expressions, - List nodes) { + List nodes, List 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 getExpressions() { List expressions = new ArrayList<>(); expressions.addAll(this.expressions.values()); @@ -230,6 +247,7 @@ static class Builder implements BlockInfo { private Origin origin; private String label; private Map parameters; + private List parametersPositions = List.of(); private final List nodes; private Map expressions; private final Parser parser; @@ -257,11 +275,16 @@ SectionBlock.Builder setLabel(String label) { return this; } - SectionBlock.Builder addParameter(String name, String value) { + SectionBlock.Builder addParameter(Entry entry) { if (parameters == null) { parameters = new LinkedHashMap<>(); } - parameters.put(name, value); + parameters.put(entry.getKey(), entry.getValue()); + return this; + } + + SectionBlock.Builder setParametersPositions(List parametersPositions) { + this.parametersPositions = Collections.unmodifiableList(parametersPositions); return this; } @@ -279,6 +302,11 @@ public Map getParameters() { return parameters == null ? Collections.emptyMap() : Collections.unmodifiableMap(parameters); } + @Override + public String getParameter(int position) { + return parametersPositions.get(position); + } + public String getLabel() { return label; } @@ -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); } } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java index 4b542c527db18..bee4473a4d500 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java @@ -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. + *

+ * 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 getParameters(); @@ -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. *

@@ -197,6 +208,7 @@ public interface SectionInitContext extends ParserDelegate { /** * * @return the parameters of the main block + * @see SectionBlock#parameters */ default public Map getParameters() { return getBlocks().get(0).parameters; @@ -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 diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/UserTagSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/UserTagSectionHelper.java index e0f3d9ef1e8c8..7754638e9f476 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/UserTagSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/UserTagSectionHelper.java @@ -8,6 +8,8 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Predicate; import java.util.function.Supplier; public class UserTagSectionHelper extends IncludeSectionHelper implements SectionHelper { @@ -15,14 +17,16 @@ public class UserTagSectionHelper extends IncludeSectionHelper implements Sectio private static final String NESTED_CONTENT = "nested-content"; protected final boolean isNestedContentNeeded; - private final HtmlEscaper htmlEscaper; + private final String itKey; UserTagSectionHelper(Supplier