Skip to content

Commit

Permalink
Qute - prefix iteration metadata in a loop
Browse files Browse the repository at this point in the history
- the prefix is configurable (globally)
- also added "isFirst" and "isLast" properties for convenience
- resolves quarkusio#20671
  • Loading branch information
mkouba committed Nov 4, 2021
1 parent 7c22995 commit c7cb26b
Show file tree
Hide file tree
Showing 35 changed files with 425 additions and 140 deletions.
53 changes: 41 additions & 12 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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}<br>{/if} <2>
{/each}
----
<1> `it_count` represents one-based index.
<2> `<br>` 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}<br>{/if} <2>
{#for item in items}
{item_count}. {item.name} <1>
{#if item_hasNext}<br>{/if} <2>
{/each}
----
<1> `count` represents one-based index.
<1> `item_count` represents one-based index.
<2> `<br>` 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 `quarkus.qute.iteration-metadata-prefix` configuration property in a Quarkus application. Three special constants can be used:
1. `<alias_>` - the alias of an iterated element suffixed with an underscore is used (default)
2. `<alias?>` - the alias of an iterated element suffixed with a question mark is used
3. `<none>` - no prefix is used
====

The `for` statement also works with integers, starting from 1. In the example below, considering that `total = 3`:

[source]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
<tbody>
{#for bean in info:devBeanInfos.beans}
<tr>
<td>{count}.</td>
<td>{bean_count}.</td>
<td>
{#display-bean bean/}
</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
<tbody>
{#for interceptor in info:devBeanInfos.interceptors}
<tr>
<td>{count}.</td>
<td>{interceptor_count}.</td>
<td><span class="class-candidate">{interceptor.interceptorClass}</span></td>
<td><span class="badge rounded-pill bg-info text-light larger-badge" title="Priority: {interceptor.priority}">{interceptor.priority}</span></td>
<td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<tbody>
{#for observer in info:devBeanInfos.observers}
<tr>
<td>{count}.</td>
<td>{observer_count}.</td>
<td>
{#if observer.declaringClass}
<span class="class-candidate">{observer.declaringClass}</span>#{observer.methodName}()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<tbody>
{#for bean in info:devBeanInfos.removedBeans}
<tr>
<td>{count}.</td>
<td>{bean_count}.</td>
<td>
{#display-bean bean/}
</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
<tbody>
{#for service in info:grpcServices.infos}
<tr>
<td>{count}.</td>
<td>{service_count}.</td>
<td>
{#when service.status}
{#is SERVING}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ <h4><span class="badge">Persistence Unit</span> <i class="badge badge-info">{pu.
<tbody>
{#for entity in pu.managedEntities}
<tr>
<td>{count}.</td>
<td>{entity_count}.</td>
<td>{entity.className}</td>
<td>{entity.tableName}</td>
</tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ <h4><span class="badge">Persistence Unit</span> <i class="badge badge-info">{pu.
<tbody>
{#for query in pu.allNamedQueries}
<tr>
<td>{count}.</td>
<td>{query_count}.</td>
<td>{query.name}</td>
<td>{query.query}</td>
<td>{query.lockMode}</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,37 +32,37 @@
<thead class="thead-light">
<tr>
<th scope="col">
<a href="#" onclick="toggleExpanded(this, 'td-create-script-{count}'); return false;" class="script-heading">
<a href="#" onclick="toggleExpanded(this, 'td-create-script-{pu_count}'); return false;" class="script-heading">
<span class="fa fa-chevron-right icon"></span>
<span>Create Script</span>
</a>
<a href="#" onclick="copyToClipboard('create-script-{count}'); return false;" class="float-right badge">
<a href="#" onclick="copyToClipboard('create-script-{pu_count}'); return false;" class="float-right badge">
<span class="fa fa-clipboard"></span> Copy</a>
</th>
</tr>
</thead>
<tbody>
<tr>
<td id="td-create-script-{count}" class="hidden">
<pre id="create-script-{count}" class="ddl-script">{pu.createDDL}</pre>
<td id="td-create-script-{pu_count}" class="hidden">
<pre id="create-script-{pu_count}" class="ddl-script">{pu.createDDL}</pre>
</td>
</tr>
<thead class="thead-light">
<tr>
<th scope="col">
<a href="#" onclick="toggleExpanded(this, 'td-drop-script-{count}'); return false;" class="script-heading">
<a href="#" onclick="toggleExpanded(this, 'td-drop-script-{pu_count}'); return false;" class="script-heading">
<span class="fa fa-chevron-right icon"></span>
<span>Drop Script</span>
</a>
<a href="#" onclick="copyToClipboard('drop-script-{count}'); return false;" class="float-right badge">
<a href="#" onclick="copyToClipboard('drop-script-{pu_count}'); return false;" class="float-right badge">
<span class="fa fa-clipboard"></span> Copy</a>
</th>
</tr>
</thead>
<tbody>
<tr>
<td id="td-drop-script-{count}" class="hidden">
<pre id="drop-script-{count}" class="ddl-script">{pu.dropDDL}</pre>
<td id="td-drop-script-{pu_count}" class="hidden">
<pre id="drop-script-{pu_count}" class="ddl-script">{pu.dropDDL}</pre>
</td>
</tr>
</tbody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ public String apply(String id) {
for (Expression param : params) {
if (param.hasTypeInfo()) {
Map<String, Match> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ public class QuteProcessor {
private static final String CHECKED_TEMPLATE_BASE_PATH = "basePath";
private static final String BASE_PATH = "templates";

private static final Set<String> ITERATION_METADATA_KEYS = Set.of("count", "index", "indexParity", "hasNext", "odd",
"isOdd", "even", "isEven", "isLast", "isFirst");

private static final Function<FieldInfo, String> GETTER_FUN = new Function<FieldInfo, String>() {
@Override
public String apply(FieldInfo field) {
Expand Down Expand Up @@ -527,7 +530,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);
Expand Down Expand Up @@ -587,7 +591,8 @@ static String buildIgnorePattern(Iterable<String> names) {
return ignorePattern.toString();
}

static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassInfo rootClazz, Map<String, Match> results,
static Match validateNestedExpressions(QuteConfig config, TemplateAnalysis templateAnalysis, ClassInfo rootClazz,
Map<String, Match> results,
List<TypeCheckExcludeBuildItem> excludes, BuildProducer<IncorrectExpressionBuildItem> incorrectExpressions,
Expression expression, IndexView index,
Map<DotName, Set<String>> implicitClassToMembersUsed, Function<String, String> templateIdToPathFun,
Expand All @@ -606,7 +611,7 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI
continue;
}
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);
Expand Down Expand Up @@ -651,14 +656,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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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} += ',';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<tbody>
{#for template in info:devQuteInfos.templates}
<tr>
<td>{count}.</td>
<td>{template_count}.</td>
<td>
{template.path}
</td>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.quarkus.qute.deployment.loop;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

public class IterationMetadataPrefixAliasQuestionMarkTest extends IterationMetadataPrefixTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addAsResource(new StringAsset("{#for i in total}{i?count}:{i}{#if i?hasNext}::{/if}{/for}"),
"templates/loop.html"))
.overrideConfigKey("quarkus.qute.iteration-metadata-prefix", "<alias?>");

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.quarkus.qute.deployment.loop;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

public class IterationMetadataPrefixNoneTest extends IterationMetadataPrefixTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addAsResource(new StringAsset("{#for i in total}{count}:{i}{#if hasNext}::{/if}{/for}"),
"templates/loop.html"))
.overrideConfigKey("quarkus.qute.iteration-metadata-prefix", "<none>");

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkus.qute.deployment.loop;

import static org.junit.jupiter.api.Assertions.assertEquals;

import javax.inject.Inject;

import org.junit.jupiter.api.Test;

import io.quarkus.qute.Template;

public abstract class IterationMetadataPrefixTest {

@Inject
Template loop;

@Test
public void testIntegerIsIterable() {
assertEquals("1:1::2:2::3:3", loop.data("total", 3).render());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ public class CheckedTemplateRequireTypeSafeTest {
+ "{inject:fool.getJoke(null)} "
+ "{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 -> {
Expand All @@ -45,9 +48,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,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);

Expand Down
Loading

0 comments on commit c7cb26b

Please sign in to comment.