diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index ef07dd57687ba9..fb1d773c221f4b 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -557,26 +557,55 @@ The other form is using the `for` name and can specify the alias used to referen ---- <1> `item` is the alias used for the iteration element. -It's also possible to access the iteration metadata inside the loop: +It's also possible to access the iteration metadata inside the loop via the following keys: + +* `count` - 1-based index +* `index` - zero-based index +* `hasNext` - `true` if the iteration has more elements +* `isLast` - `true` if `hasNext == false` +* `isFirst` - `true` if `count == 1` +* `odd` - `true` if the zero-based index is odd +* `even` - `true` if the zero-based index is even +* `indexParity` - outputs `odd` or `even` based on the zero-based index value + +However, the keys cannot be used directly. +Instead, a prefix is used to avoid possible collisions with variables from the outer scope. +By default, the alias of an iterated element suffixed with an underscore is used as a prefix. +For example, the `hasNext` key must be prefixed with `it_` inside an `{#each}` section: `{it_hasNext}`. + +.`each` Iteration Metadata Example +[source] +---- +{#each items} + {it_count}. {it.name} <1> + {#if it_hasNext}
{/if} <2> +{/each} +---- +<1> `it_count` represents one-based index. +<2> `
` is only rendered if the iteration has more elements. -* `{count}` - 1-based index -* `{index}` - zero-based index -* `{hasNext}` - `true` if the iteration has more elements -* `{odd}` - `true` if the zero-based index is odd -* `{even}` - `true` if the zero-based index is even -* `{indexParity}` - outputs `odd` or `even` based on the zero-based index value +And must be used in a form of `{item_hasNext}` inside a `{#for}` section with the `item` element alias. -.Iteration Metadata Example +.`for` Iteration Metadata Example [source] ---- -{#each items} - {count}. {it.name} <1> - {#if hasNext}
{/if} <2> +{#for item in items} + {item_count}. {item.name} <1> + {#if item_hasNext}
{/if} <2> {/each} ---- -<1> `count` represents one-based index. +<1> `item_count` represents one-based index. <2> `
` is only rendered if the iteration has more elements. +[TIP] +==== +The iteration metadata prefix is configurable either via `EngineBuilder.iterationMetadataPrefix()` for standalone Qute or via the `io.quakurs.qute.iteration-metadata-prefix` configuration property in a Quarkus application. Three special constants can be used: + +1. `` - the alias of an iterated element suffixed with an underscore is used (default) +2. `` - the alias of an iterated element suffixed with a question mark is used +3. `` - no prefix is used +==== + The `for` statement also works with integers, starting from 1. In the example below, considering that `total = 3`: [source] diff --git a/extensions/arc/deployment/src/main/resources/dev-templates/beans.html b/extensions/arc/deployment/src/main/resources/dev-templates/beans.html index e4f6f7255beb4a..ed15e9272391b4 100644 --- a/extensions/arc/deployment/src/main/resources/dev-templates/beans.html +++ b/extensions/arc/deployment/src/main/resources/dev-templates/beans.html @@ -53,7 +53,7 @@ {#for bean in info:devBeanInfos.beans} - {count}. + {bean_count}. {#display-bean bean/} diff --git a/extensions/arc/deployment/src/main/resources/dev-templates/interceptors.html b/extensions/arc/deployment/src/main/resources/dev-templates/interceptors.html index 5397a433faccca..a4a2336c7c53d5 100644 --- a/extensions/arc/deployment/src/main/resources/dev-templates/interceptors.html +++ b/extensions/arc/deployment/src/main/resources/dev-templates/interceptors.html @@ -54,7 +54,7 @@ {#for interceptor in info:devBeanInfos.interceptors} - {count}. + {interceptor_count}. {interceptor.interceptorClass} {interceptor.priority} diff --git a/extensions/arc/deployment/src/main/resources/dev-templates/observers.html b/extensions/arc/deployment/src/main/resources/dev-templates/observers.html index 957d79d6752b3d..569d92a2de56d5 100644 --- a/extensions/arc/deployment/src/main/resources/dev-templates/observers.html +++ b/extensions/arc/deployment/src/main/resources/dev-templates/observers.html @@ -42,7 +42,7 @@ {#for observer in info:devBeanInfos.observers} - {count}. + {observer_count}. {#if observer.declaringClass} {observer.declaringClass}#{observer.methodName}() diff --git a/extensions/arc/deployment/src/main/resources/dev-templates/removed-beans.html b/extensions/arc/deployment/src/main/resources/dev-templates/removed-beans.html index 9cae8bf25ac302..e49173e4f94dbb 100644 --- a/extensions/arc/deployment/src/main/resources/dev-templates/removed-beans.html +++ b/extensions/arc/deployment/src/main/resources/dev-templates/removed-beans.html @@ -21,7 +21,7 @@ {#for bean in info:devBeanInfos.removedBeans} - {count}. + {bean_count}. {#display-bean bean/} diff --git a/extensions/grpc/deployment/src/main/resources/dev-templates/services.html b/extensions/grpc/deployment/src/main/resources/dev-templates/services.html index eed79393d2ec89..cff09223c3e14b 100644 --- a/extensions/grpc/deployment/src/main/resources/dev-templates/services.html +++ b/extensions/grpc/deployment/src/main/resources/dev-templates/services.html @@ -41,7 +41,7 @@ {#for service in info:grpcServices.infos} - {count}. + {service_count}. {#when service.status} {#is SERVING} diff --git a/extensions/hibernate-orm/deployment/src/main/resources/dev-templates/managed-entities.html b/extensions/hibernate-orm/deployment/src/main/resources/dev-templates/managed-entities.html index c2dcc5e8d7d611..b606c7932f755b 100644 --- a/extensions/hibernate-orm/deployment/src/main/resources/dev-templates/managed-entities.html +++ b/extensions/hibernate-orm/deployment/src/main/resources/dev-templates/managed-entities.html @@ -25,7 +25,7 @@

Persistence Unit {pu. {#for entity in pu.managedEntities} - {count}. + {entity_count}. {entity.className} {entity.tableName} diff --git a/extensions/hibernate-orm/deployment/src/main/resources/dev-templates/named-queries.html b/extensions/hibernate-orm/deployment/src/main/resources/dev-templates/named-queries.html index 53bfd00e69a8fb..14f1425ce31693 100644 --- a/extensions/hibernate-orm/deployment/src/main/resources/dev-templates/named-queries.html +++ b/extensions/hibernate-orm/deployment/src/main/resources/dev-templates/named-queries.html @@ -28,7 +28,7 @@

Persistence Unit {pu. {#for query in pu.allNamedQueries} - {count}. + {query_count}. {query.name} {query.query} {query.lockMode} diff --git a/extensions/hibernate-orm/deployment/src/main/resources/dev-templates/persistence-units.html b/extensions/hibernate-orm/deployment/src/main/resources/dev-templates/persistence-units.html index e624bd56570bf5..1349bb6c6e09da 100644 --- a/extensions/hibernate-orm/deployment/src/main/resources/dev-templates/persistence-units.html +++ b/extensions/hibernate-orm/deployment/src/main/resources/dev-templates/persistence-units.html @@ -32,37 +32,37 @@ - + Create Script - + Copy - -
{pu.createDDL}
+ +
{pu.createDDL}
- + Drop Script - + Copy - -
{pu.dropDDL}
+ +
{pu.dropDDL}
diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java index 9f4b2ad3779203..2aac1a962aceec 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java @@ -481,7 +481,7 @@ public String apply(String id) { for (Expression param : params) { if (param.hasTypeInfo()) { Map results = new HashMap<>(); - QuteProcessor.validateNestedExpressions(exprEntry.getKey(), defaultBundleInterface, + QuteProcessor.validateNestedExpressions(config, exprEntry.getKey(), defaultBundleInterface, results, excludes, incorrectExpressions, expression, index, implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData, diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index 6804ae0c9462bd..4e4b72d4a44ed7 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -130,6 +130,9 @@ public class QuteProcessor { private static final String CHECKED_TEMPLATE_REQUIRE_TYPE_SAFE = "requireTypeSafeExpressions"; private static final String CHECKED_TEMPLATE_BASE_PATH = "basePath"; + private static final Set ITERATION_METADATA_KEYS = Set.of("count", "index", "indexParity", "hasNext", "odd", + "isOdd", "even", "isEven", "isLast", "isFirst"); + private static final Function GETTER_FUN = new Function() { @Override public String apply(FieldInfo field) { @@ -521,7 +524,8 @@ public String apply(String id) { if (expression.isLiteral()) { continue; } - Match match = validateNestedExpressions(templateAnalysis, null, new HashMap<>(), excludes, incorrectExpressions, + Match match = validateNestedExpressions(config, templateAnalysis, null, new HashMap<>(), excludes, + incorrectExpressions, expression, index, implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData, regularExtensionMethods, namespaceExtensionMethods); @@ -581,7 +585,8 @@ static String buildIgnorePattern(Iterable names) { return ignorePattern.toString(); } - static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassInfo rootClazz, Map results, + static Match validateNestedExpressions(QuteConfig config, TemplateAnalysis templateAnalysis, ClassInfo rootClazz, + Map results, List excludes, BuildProducer incorrectExpressions, Expression expression, IndexView index, Map> implicitClassToMembersUsed, Function templateIdToPathFun, @@ -596,7 +601,7 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI if (part.isVirtualMethod()) { for (Expression param : part.asVirtualMethod().getParameters()) { if (!results.containsKey(param.toOriginalString())) { - validateNestedExpressions(templateAnalysis, null, results, excludes, + validateNestedExpressions(config, templateAnalysis, null, results, excludes, incorrectExpressions, param, index, implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches, checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData, regularExtensionMethods, namespaceExtensionMethods); @@ -641,14 +646,39 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI } if (checkedTemplate != null && checkedTemplate.requireTypeSafeExpressions && !expression.hasTypeInfo()) { - incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), - "Only type-safe expressions are allowed in the checked template defined via: " - + checkedTemplate.method.declaringClass().name() + "." - + checkedTemplate.method.name() - + "(); an expression must be based on a checked template parameter " - + checkedTemplate.bindings.keySet() - + ", or bound via a param declaration, or the requirement must be relaxed via @CheckedTemplate(requireTypeSafeExpressions = false)", - expression.getOrigin())); + if (!expression.hasNamespace() && expression.getParts().size() == 1 + && ITERATION_METADATA_KEYS.contains(expression.getParts().get(0).getName())) { + String prefixInfo; + if (config.iterationMetadataPrefix + .equals(LoopSectionHelper.Factory.ITERATION_METADATA_PREFIX_ALIAS_UNDERSCORE)) { + prefixInfo = String.format( + "based on the iteration alias, i.e. the correct key should be something like {it_%1$s} or {element_%1$s}", + expression.getParts().get(0).getName()); + } else if (config.iterationMetadataPrefix + .equals(LoopSectionHelper.Factory.ITERATION_METADATA_PREFIX_ALIAS_QM)) { + prefixInfo = String.format( + "based on the iteration alias, i.e. the correct key should be something like {it?%1$s} or {element?%1$s}", + expression.getParts().get(0).getName()); + } else { + prefixInfo = ": " + config.iterationMetadataPrefix + ", i.e. the correct key should be: " + + config.iterationMetadataPrefix + expression.getParts().get(0).getName(); + } + incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), + "An invalid iteration metadata key is probably used\n\t- The configured iteration metadata prefix is " + + prefixInfo + + "\n\t- You can configure the prefix via the io.quarkus.qute.iteration-metadata-prefix configuration property", + expression.getOrigin())); + } else { + incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), + "Only type-safe expressions are allowed in the checked template defined via: " + + checkedTemplate.method.declaringClass().name() + "." + + checkedTemplate.method.name() + + "(); an expression must be based on a checked template parameter " + + checkedTemplate.bindings.keySet() + + ", or bound via a param declaration, or the requirement must be relaxed via @CheckedTemplate(requireTypeSafeExpressions = false)", + expression.getOrigin())); + + } return putResult(match, results, expression); } diff --git a/extensions/qute/deployment/src/main/resources/dev-templates/preview.html b/extensions/qute/deployment/src/main/resources/dev-templates/preview.html index a0c2ae909fb7a5..47c59d988aa049 100644 --- a/extensions/qute/deployment/src/main/resources/dev-templates/preview.html +++ b/extensions/qute/deployment/src/main/resources/dev-templates/preview.html @@ -61,7 +61,7 @@ {#if template.parameters} testData{dataCount} = '{'; {#each template.parameters} - testData{dataCount} += '\n // Template parameter {count}: {it.value.raw}\n'; + testData{dataCount} += '\n // Template parameter {template_count}: {it.value.raw}\n'; testData{dataCount} += ' "{it.key}" : null'; {#if hasNext} testData{dataCount} += ','; diff --git a/extensions/qute/deployment/src/main/resources/dev-templates/templates.html b/extensions/qute/deployment/src/main/resources/dev-templates/templates.html index c26e52159f2aa9..5cdf5a88db0680 100644 --- a/extensions/qute/deployment/src/main/resources/dev-templates/templates.html +++ b/extensions/qute/deployment/src/main/resources/dev-templates/templates.html @@ -19,7 +19,7 @@ {#for template in info:devQuteInfos.templates} - {count}. + {template_count}. {template.path} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/CheckedTemplateRequireTypeSafeTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/CheckedTemplateRequireTypeSafeTest.java index 43247ae7b86bbe..6d7ecf8a20ec3d 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/CheckedTemplateRequireTypeSafeTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/CheckedTemplateRequireTypeSafeTest.java @@ -29,8 +29,11 @@ public class CheckedTemplateRequireTypeSafeTest { + "{any} " + "{inject:fool.getJoke(identifier)} " + "{#each name.chars.iterator}" - + "{! {index} is not considered an error because the binding is registered by the loop section !}" - + "{index}. {it}" + // {it_index} is not considered an error because the binding is registered by the loop section !} + + "{it_index}." + // however, {index} is an error + + "{index}" + + "{it}" + "{/each}"), "templates/CheckedTemplateRequireTypeSafeTest/hola.txt")) .assertException(t -> { @@ -44,9 +47,10 @@ public class CheckedTemplateRequireTypeSafeTest { e = e.getCause(); } assertNotNull(te); - assertTrue(te.getMessage().contains("Found template problems (2)"), te.getMessage()); + assertTrue(te.getMessage().contains("Found template problems (3)"), te.getMessage()); assertTrue(te.getMessage().contains("any"), te.getMessage()); assertTrue(te.getMessage().contains("identifier"), te.getMessage()); + assertTrue(te.getMessage().contains("index"), te.getMessage()); }); @Test diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ParamDeclarationTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ParamDeclarationTest.java index 82c81d4fd9d1c2..a298086b7bb8f1 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ParamDeclarationTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ParamDeclarationTest.java @@ -21,7 +21,7 @@ public class ParamDeclarationTest { .addAsResource(new StringAsset("{@io.quarkus.qute.deployment.typesafe.Movie movie}" + "{movie.mainCharacters.size}: {#for character in movie.mainCharacters}" + "{character}" - + "{#if hasNext}, {/}" + + "{#if character_hasNext}, {/}" + "{/}"), "templates/movie.html")); @Inject diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java index e154b2c1e25e91..6ade3b94b19e54 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java @@ -120,6 +120,9 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig // Remove standalone lines if desired builder.removeStandaloneLines(runtimeConfig.removeStandaloneLines); + // Iteration metadata prefix + builder.iterationMetadataPrefix(config.iterationMetadataPrefix); + // Allow anyone to customize the builder builderReady.fire(builder); diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java index f6a2d7c92159ce..370c0555c0402f 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java @@ -44,4 +44,21 @@ public class QuteConfig { @ConfigItem public Optional> typeCheckExcludes; + /** + * The prefix is used to access the iteration metadata inside a loop section. + *

+ * A valid prefix consists of alphanumeric characters and underscores. + * Three special constants can be used: + *

    + *
  • {@code } - the alias of an iterated element suffixed with an underscore is used, e.g. {@code item_hasNext} + * and {@code it_count}
  • + *
  • {@code } - the alias of an iterated element suffixed with a question mark is used, e.g. {@code item?hasNext} + * and {@code it?count}
  • + *
  • {@code } - no prefix is used, e.g. {@code hasNext} and {@code count}
  • + *
+ * By default, the {@code } constant is set. + */ + @ConfigItem(defaultValue = "") + public String iterationMetadataPrefix; + } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/resources/dev-templates/scores.html b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/resources/dev-templates/scores.html index 22e0b1d52ad2dc..0cba8e7850b777 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/resources/dev-templates/scores.html +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/resources/dev-templates/scores.html @@ -147,16 +147,16 @@
-
+
-
+
@@ -208,11 +208,11 @@
Filters:
-
+
{endpoint.requestFilterEntries.size}
-
+
{#for requestFilters in endpoint.requestFilterEntries} {requestFilters.getName}
diff --git a/extensions/scheduler/deployment/src/main/resources/dev-templates/schedules.html b/extensions/scheduler/deployment/src/main/resources/dev-templates/schedules.html index f28095a77a1fad..21a78c387fa1f4 100644 --- a/extensions/scheduler/deployment/src/main/resources/dev-templates/schedules.html +++ b/extensions/scheduler/deployment/src/main/resources/dev-templates/schedules.html @@ -51,7 +51,7 @@ {#for scheduledMethod in info:schedulerContext.scheduledMethods} - {count}. + {scheduledMethod_count}. {#if scheduledMethod.schedules.size > 1}
    diff --git a/extensions/smallrye-reactive-messaging/deployment/src/main/resources/dev-templates/channels.html b/extensions/smallrye-reactive-messaging/deployment/src/main/resources/dev-templates/channels.html index f611458dec230e..732c6661fd9e82 100644 --- a/extensions/smallrye-reactive-messaging/deployment/src/main/resources/dev-templates/channels.html +++ b/extensions/smallrye-reactive-messaging/deployment/src/main/resources/dev-templates/channels.html @@ -18,7 +18,7 @@ {#for channel in info:reactiveMessagingInfos.channels} - {count}. + {channel_count}. {channel.name} {#componentDescription channel.publisher /} diff --git a/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/config.html b/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/config.html index 43419922e74892..d9242a3a14ff4f 100644 --- a/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/config.html +++ b/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/config.html @@ -404,7 +404,7 @@ {#for configsource in info:config}
    - +
    diff --git a/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/tests.html b/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/tests.html index bf9981b60745ff..15f844e897309c 100644 --- a/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/tests.html +++ b/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/tests.html @@ -68,24 +68,22 @@
    {#for cn in info:tests.results.failing.orEmpty} - {#set parentCount=count} -
    - -
    +
      {#for r in cn.failing} {#if r.test}
    • - -
      +

      @@ -117,11 +115,11 @@ {#if r.test}
    • - -
      +
      {#if !r.logOutput.empty}
      @@ -141,12 +139,12 @@ {#if r.test}
    • - -
      +
      {#if !r.logOutput.empty}
      @@ -166,30 +164,27 @@
      - {/set} {/for} {#for cn in info:tests.results.passing.orEmpty} - {#set parentCount=count}
      - -
      +
        {#for r in cn.passing} {#if r.test}
      • - -
        +

        @@ -207,12 +202,12 @@ {#if r.test}
      • - -
        +

        @@ -230,31 +225,28 @@
        - {/set} {/for} {#for cn in info:tests.results.skipped.orEmpty} - {#set parentCount=count} -
        - -
        +
        {/body} diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineBuilder.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineBuilder.java index 638e1777996fe4..2cd5a63d2a4896 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineBuilder.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineBuilder.java @@ -22,6 +22,7 @@ public final class EngineBuilder { final List parserHooks; boolean removeStandaloneLines; boolean strictRendering; + String iterationMetadataPrefix; EngineBuilder() { this.sectionHelperFactories = new HashMap<>(); @@ -32,6 +33,7 @@ public final class EngineBuilder { this.parserHooks = new ArrayList<>(); this.strictRendering = true; this.removeStandaloneLines = true; + this.iterationMetadataPrefix = LoopSectionHelper.Factory.ITERATION_METADATA_PREFIX_ALIAS_UNDERSCORE; } public EngineBuilder addSectionHelper(SectionHelperFactory factory) { @@ -55,7 +57,7 @@ public EngineBuilder addSectionHelper(String name, SectionHelperFactory facto } public EngineBuilder addDefaultSectionHelpers() { - return addSectionHelpers(new IfSectionHelper.Factory(), new LoopSectionHelper.Factory(), + return addSectionHelpers(new IfSectionHelper.Factory(), new LoopSectionHelper.Factory(iterationMetadataPrefix), new WithSectionHelper.Factory(), new IncludeSectionHelper.Factory(), new InsertSectionHelper.Factory(), new SetSectionHelper.Factory(), new WhenSectionHelper.Factory(), new EvalSectionHelper.Factory()); } @@ -168,6 +170,26 @@ public EngineBuilder strictRendering(boolean value) { return this; } + /** + * This prefix is used to access the iteration metadata inside a loop section. + *

        + * A valid prefix consists of alphanumeric characters and underscores. + * + * @param prefix + * @return self + */ + public EngineBuilder iterationMetadataPrefix(String prefix) { + if (!LoopSectionHelper.Factory.ITERATION_METADATA_PREFIX_NONE.equals(prefix) + && !LoopSectionHelper.Factory.ITERATION_METADATA_PREFIX_ALIAS_UNDERSCORE.equals(prefix) + && !LoopSectionHelper.Factory.ITERATION_METADATA_PREFIX_ALIAS_QM.equals(prefix) + && !Namespaces.NAMESPACE_PATTERN.matcher(prefix).matches()) { + throw new TemplateException("[" + prefix + + "] is not a valid iteration metadata prefix. The value can only consist of alphanumeric characters and underscores."); + } + this.iterationMetadataPrefix = prefix; + return this; + } + /** * * @return a new engine instance diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java index b5db30221a7635..bf8eceac328299 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/LoopSectionHelper.java @@ -26,11 +26,13 @@ public class LoopSectionHelper implements SectionHelper { private static final String ITERABLE = "iterable"; private final String alias; + private final String metadataPrefix; private final Expression iterable; private final SectionBlock elseBlock; - LoopSectionHelper(SectionInitContext context) { + LoopSectionHelper(SectionInitContext context, String metadataPrefix) { this.alias = context.getParameterOrDefault(ALIAS, DEFAULT_ALIAS); + this.metadataPrefix = LoopSectionHelper.Factory.prefixValue(alias, metadataPrefix); this.iterable = Objects.requireNonNull(context.getExpression(ITERABLE)); this.elseBlock = context.getBlock(ELSE); } @@ -116,17 +118,48 @@ private Iterator extractIterator(Object it) { } CompletionStage nextElement(Object element, int index, boolean hasNext, SectionResolutionContext context) { - ResolutionContext child = context.resolutionContext().createChild(new IterationElement(alias, element, index, hasNext), + ResolutionContext child = context.resolutionContext().createChild( + new IterationElement(alias, metadataPrefix, element, index, hasNext), null); return context.execute(child); } public static class Factory implements SectionHelperFactory { + /** + * Constant value for iteration metadata prefix indicating that the alias suffixed with a question mark should be used. + */ + public static final String ITERATION_METADATA_PREFIX_ALIAS_QM = ""; + + /** + * Constant value for iteration metadata prefix indicating that the alias suffixed with an underscore should be used. + */ + public static final String ITERATION_METADATA_PREFIX_ALIAS_UNDERSCORE = ""; + + /** + * Constant value for iteration metadata prefix indicating that no prefix should be used. + */ + public static final String ITERATION_METADATA_PREFIX_NONE = ""; + public static final String HINT_ELEMENT = ""; public static final String HINT_PREFIX = " getDefaultAliases() { return ImmutableList.of("for", "each"); @@ -147,7 +180,7 @@ public List getBlockLabels() { @Override public LoopSectionHelper initialize(SectionInitContext context) { - return new LoopSectionHelper(context); + return new LoopSectionHelper(context, metadataPrefix); } @Override @@ -170,14 +203,17 @@ public Scope initializeBlock(Scope previousScope, BlockInfo block) { Scope newScope = new Scope(previousScope); newScope.putBinding(alias, alias + HINT_PREFIX + iterableExpr.getGeneratedId() + ">"); // Put bindings for iteration metadata - newScope.putBinding("count", Expressions.typeInfoFrom(Integer.class.getName())); - newScope.putBinding("index", Expressions.typeInfoFrom(Integer.class.getName())); - newScope.putBinding("indexParity", Expressions.typeInfoFrom(String.class.getName())); - newScope.putBinding("hasNext", Expressions.typeInfoFrom(Boolean.class.getName())); - newScope.putBinding("odd", Expressions.typeInfoFrom(Boolean.class.getName())); - newScope.putBinding("isOdd", Expressions.typeInfoFrom(Boolean.class.getName())); - newScope.putBinding("even", Expressions.typeInfoFrom(Boolean.class.getName())); - newScope.putBinding("isEven", Expressions.typeInfoFrom(Boolean.class.getName())); + String prefix = prefixValue(alias, metadataPrefix); + newScopeBinding(newScope, prefix, "count", Integer.class.getName()); + newScopeBinding(newScope, prefix, "index", Integer.class.getName()); + newScopeBinding(newScope, prefix, "indexParity", String.class.getName()); + newScopeBinding(newScope, prefix, "hasNext", Boolean.class.getName()); + newScopeBinding(newScope, prefix, "isLast", Boolean.class.getName()); + newScopeBinding(newScope, prefix, "isFirst", Boolean.class.getName()); + newScopeBinding(newScope, prefix, "odd", Boolean.class.getName()); + newScopeBinding(newScope, prefix, "isOdd", Boolean.class.getName()); + newScopeBinding(newScope, prefix, "even", Boolean.class.getName()); + newScopeBinding(newScope, prefix, "isEven", Boolean.class.getName()); return newScope; } else { // Make sure we do not try to validate against the parent context @@ -189,20 +225,38 @@ public Scope initializeBlock(Scope previousScope, BlockInfo block) { return previousScope; } } + + private void newScopeBinding(Scope scope, String prefix, String name, String typeName) { + scope.putBinding(prefix != null ? prefix + name : name, Expressions.typeInfoFrom(typeName)); + } + + static String prefixValue(String alias, String metadataPrefix) { + if (metadataPrefix == null || ITERATION_METADATA_PREFIX_NONE.equals(metadataPrefix)) { + return null; + } else if (ITERATION_METADATA_PREFIX_ALIAS_UNDERSCORE.equals(metadataPrefix)) { + return alias + "_"; + } else if (ITERATION_METADATA_PREFIX_ALIAS_QM.equals(metadataPrefix)) { + return alias + "?"; + } else { + return metadataPrefix; + } + } } static class IterationElement implements Mapper { static final CompletedStage EVEN = CompletedStage.of("even"); - static final CompletedStage ODD = CompletedStage.of("odd");; + static final CompletedStage ODD = CompletedStage.of("odd"); final String alias; + final String metadataPrefix; final CompletedStage element; final int index; final boolean hasNext; - public IterationElement(String alias, Object element, int index, boolean hasNext) { + public IterationElement(String alias, String metadataPrefix, Object element, int index, boolean hasNext) { this.alias = alias; + this.metadataPrefix = metadataPrefix; this.element = CompletedStage.of(element); this.index = index; this.hasNext = hasNext; @@ -213,6 +267,13 @@ public CompletionStage getAsync(String key) { if (alias.equals(key)) { return element; } + if (metadataPrefix != null) { + if (key.startsWith(metadataPrefix)) { + key = key.substring(metadataPrefix.length(), key.length()); + } else { + return Results.notFound(key); + } + } // Iteration metadata switch (key) { case "count": @@ -223,6 +284,10 @@ public CompletionStage getAsync(String key) { return index % 2 != 0 ? EVEN : ODD; case "hasNext": return hasNext ? Results.TRUE : Results.FALSE; + case "isLast": + return hasNext ? Results.FALSE : Results.TRUE; + case "isFirst": + return index == 0 ? Results.TRUE : Results.FALSE; case "isOdd": case "odd": return (index % 2 == 0) ? Results.TRUE : Results.FALSE; diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parameter.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parameter.java index a016ae24851fad..613c6d04add80e 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parameter.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parameter.java @@ -3,8 +3,10 @@ import io.quarkus.qute.SectionHelperFactory.ParametersInfo; /** + * Definition of a section parameter. * * @see ParametersInfo + * @see SectionHelperFactory#getParameters() */ public class Parameter { @@ -22,6 +24,22 @@ public Parameter(String name, String defaultValue, boolean optional) { this.optional = optional; } + public String getName() { + return name; + } + + public String getDefaultValue() { + return defaultValue; + } + + public boolean hasDefatulValue() { + return defaultValue != null; + } + + public boolean isOptional() { + return optional; + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); 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 b03a0c8aac8952..7e08b1f9d3af16 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 @@ -1,5 +1,7 @@ package io.quarkus.qute; +import static java.util.function.Predicate.not; + import io.quarkus.qute.Expression.Part; import io.quarkus.qute.SectionHelperFactory.ParametersInfo; import io.quarkus.qute.TemplateNode.Origin; @@ -515,20 +517,42 @@ private void processParams(String tag, String label, Iterator iter, Sect paramValues.add(val); } } - if (paramValues.size() > factoryParams.size()) { + + int actualSize = paramValues.size(); + if (actualSize > factoryParams.size()) { LOGGER.debugf("Too many params [label=%s, params=%s, factoryParams=%s]", label, paramValues, factoryParams); } - if (paramValues.size() < factoryParams.size()) { + + // Process named params first + for (Iterator it = paramValues.iterator(); it.hasNext();) { + String param = it.next(); + int equalsPosition = getFirstDeterminingEqualsCharPosition(param); + if (equalsPosition != -1) { + // Named param + params.put(param.substring(0, equalsPosition), param.substring(equalsPosition + 1, + param.length())); + it.remove(); + } + } + + // 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) { - int equalsPosition = getFirstDeterminingEqualsCharPosition(param); - if (equalsPosition != -1) { - // Named param - params.put(param.substring(0, equalsPosition), param.substring(equalsPosition + 1, - param.length())); - } else { - // Positional param - first non-default section param + 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 (factoryParam.defaultValue == null && !params.containsKey(factoryParam.name)) { + if (!params.containsKey(factoryParam.name)) { + found = factoryParam; params.put(factoryParam.name, param); break; } @@ -536,34 +560,34 @@ private void processParams(String tag, String label, Iterator iter, Sect } } } else { + // The number of actual params is greater or equals to factory params int generatedIdx = 0; 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())); - } else { - // Positional param - first non-default section param - Parameter found = null; - for (Parameter factoryParam : factoryParams) { - if (!params.containsKey(factoryParam.name)) { - found = factoryParam; - params.put(factoryParam.name, param); - break; - } - } - if (found == null) { - params.put("" + generatedIdx++, param); + // 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) { + params.put("" + generatedIdx++, param); + } } } - factoryParams.stream().filter(p -> p.defaultValue != null).forEach(p -> params.putIfAbsent(p.name, p.defaultValue)); + // Use the default values if needed + factoryParams.stream() + .filter(Parameter::hasDefatulValue) + .forEach(p -> params.putIfAbsent(p.name, p.defaultValue)); - // TODO validate params - List undeclaredParams = factoryParams.stream().filter(p -> !p.optional && !params.containsKey(p.name)) + // Find undeclared mandatory params + List undeclaredParams = factoryParams.stream() + .filter(not(Parameter::isOptional)) + .map(Parameter::getName) + .filter(not(params::containsKey)) .collect(Collectors.toList()); if (!undeclaredParams.isEmpty()) { throw parserError("mandatory section parameters not declared for " + tag + ": " + undeclaredParams); 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 b261e4deb109cd..394cf7c7b6bb75 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 @@ -196,8 +196,8 @@ private ParametersInfo(Map> parameters) { this.parameters = new HashMap<>(parameters); } - public List get(String sectionPart) { - return parameters.getOrDefault(sectionPart, Collections.emptyList()); + public List get(String blockLabel) { + return parameters.getOrDefault(blockLabel, Collections.emptyList()); } @Override diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/IncludeTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/IncludeTest.java index cc4ff4e4f048d1..0b6247b7073438 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/IncludeTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/IncludeTest.java @@ -65,7 +65,8 @@ public void testIncludeInLoop() { Engine engine = Engine.builder().addDefaults().build(); engine.putTemplate("foo", engine.parse("{#insert snippet}empty{/insert}")); assertEquals("1.2.3.4.5.", - engine.parse("{#for i in 5}{#include foo}{#snippet}{count}.{/snippet} this should be ingored {/include}{/for}") + engine.parse( + "{#for i in 5}{#include foo}{#snippet}{i_count}.{/snippet} this should be ingored {/include}{/for}") .render()); } diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/LoopSectionTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/LoopSectionTest.java index a88d655728f279..8cbb920754eaef 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/LoopSectionTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/LoopSectionTest.java @@ -26,19 +26,17 @@ public void tesLoop() { items.add(item); items.add(new HashMap<>()); - Engine engine = Engine.builder() - .addSectionHelper(new IfSectionHelper.Factory()) - .addSectionHelper(new LoopSectionHelper.Factory()).addDefaultValueResolvers() - .build(); + Engine engine = Engine.builder().addDefaults().build(); - Template template = engine.parse("{#for item in this}{count}.{item.name ?: 'NOT_FOUND'}{#if hasNext}\n{/if}{/for}"); + Template template = engine + .parse("{#for item in this}{item_count}.{item.name ?: 'NOT_FOUND'}{#if item_hasNext}\n{/if}{/for}"); assertEquals("1.Lu\n2.NOT_FOUND", template.render(items)); - template = engine.parse("{#each this}{count}.{it.name ?: 'NOT_FOUND'}{#if hasNext}\n{/if}{/each}"); + template = engine.parse("{#each this}{it_count}.{it.name ?: 'NOT_FOUND'}{#if it_hasNext}\n{/if}{/each}"); assertEquals("1.Lu\n2.NOT_FOUND", template.render(items)); - template = engine.parse("{#each this}{#if odd}odd{#else}even{/if}{/each}"); + template = engine.parse("{#each this}{#if it_odd}odd{#else}even{/if}{/each}"); assertEquals("oddeven", template.render(items)); } @@ -95,8 +93,8 @@ public CompletionStage resolve(EvalContext context) { .build(); String template = "{#for name in list}" - + "{count}.{name}: {#for char in name.chars}" - + "{name} {global} char at {index} = {char}{#if hasNext},{/}" + + "{name_count}.{name}: {#for char in name.chars}" + + "{name} {global} char at {char_index} = {char}{#if char_hasNext},{/}" + "{/}{/}"; assertEquals( @@ -179,4 +177,28 @@ public void testElseBlock() { engine.parse("{#for i in items}{item}{#else}No items.{/for}").data("items", Collections.emptyList()).render()); } + @Test + public void testIterationMetadata() { + String expected = "foo::0::1::false::true::true::odd::true::false"; + assertEquals(expected, + Engine.builder().iterationMetadataPrefix(LoopSectionHelper.Factory.ITERATION_METADATA_PREFIX_NONE) + .addDefaults().build().parse( + "{#each items}{it}::{index}::{count}::{hasNext}::{isLast}::{isFirst}::{indexParity}::{odd}::{even}{/each}") + .data("items", List.of("foo")).render()); + assertEquals(expected, + Engine.builder().iterationMetadataPrefix(LoopSectionHelper.Factory.ITERATION_METADATA_PREFIX_ALIAS_QM) + .addDefaults().build().parse( + "{#each items}{it}::{it?index}::{it?count}::{it?hasNext}::{it?isLast}::{it?isFirst}::{it?indexParity}::{it?odd}::{it?even}{/each}") + .data("items", List.of("foo")).render()); + assertEquals(expected, + Engine.builder().addDefaults().build().parse( + "{#each items}{it}::{it_index}::{it_count}::{it_hasNext}::{it_isLast}::{it_isFirst}::{it_indexParity}::{it_odd}::{it_even}{/each}") + .data("items", List.of("foo")).render()); + assertEquals(expected, + Engine.builder().iterationMetadataPrefix("meta_") + .addDefaults().build().parse( + "{#each items}{it}::{meta_index}::{meta_count}::{meta_hasNext}::{meta_isLast}::{meta_isFirst}::{meta_indexParity}::{meta_odd}::{meta_even}{/each}") + .data("items", List.of("foo")).render()); + } + } diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/MutinyTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/MutinyTest.java index 6512860b8e5caf..b1c5ae95be181a 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/MutinyTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/MutinyTest.java @@ -58,7 +58,7 @@ public String toString() { @Test public void testUniResolution() { Engine engine = Engine.builder().addDefaults().addValueResolver(new ReflectionValueResolver()).build(); - Template template = engine.parse("{foo.toLowerCase}::{#each items}{count}.{it}{#if hasNext},{/if}{/each}"); + Template template = engine.parse("{foo.toLowerCase}::{#each items}{it_count}.{it}{#if it_hasNext},{/if}{/each}"); Uni fooUni = Uni.createFrom().item("FOO"); Uni> itemsUni = Uni.createFrom().item(() -> Arrays.asList("foo", "bar", "baz")); assertEquals("foo::1.foo,2.bar,3.baz", template.data("foo", fooUni, "items", itemsUni).render()); diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java index cfc00ee9667a4c..aba4d4f4e6357f 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java @@ -168,7 +168,7 @@ public Optional getVariant() { assertThatExceptionOfType(TemplateException.class) .isThrownBy(() -> engine.getTemplate("foo.html")) .withMessage( - "Parser error in template [foo.html] on line 1: mandatory section parameters not declared for {#if}: [Parameter [name=condition, defaultValue=null, optional=false]]") + "Parser error in template [foo.html] on line 1: mandatory section parameters not declared for {#if}: [condition]") .hasFieldOrProperty("origin"); } @@ -223,7 +223,7 @@ public void testRemoveStandaloneLines() { + "\n" + " {! My comment !} \n" + " {#for i in 5}\n" // -> standalone - + "{index}:\n" + + "{i_index}:\n" + "{/} "; // -> standalone assertEquals("\n0:\n1:\n2:\n3:\n4:\n", engine.parse(content).render()); assertEquals("bar\n", engine.parse("{foo}\n").data("foo", "bar").render()); diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/SetSectionTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/SetSectionTest.java index 50161cecbf3ca3..af085dd976c956 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/SetSectionTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/SetSectionTest.java @@ -27,7 +27,7 @@ public void testLiterals() { assertEquals("1::4::Andy::false", engine.parse( "{#let foo=1 bar='qute' baz=name.or('Andy') alpha=name.ifTruthy('true').or('false')}" - + "{#for i in foo}{count}{/for}::{bar.length}::{baz}::{alpha}" + + "{#for i in foo}{i_count}{/for}::{bar.length}::{baz}::{alpha}" + "{/let}") .render()); }

    {configsource.key}