Skip to content

Commit

Permalink
Merge pull request #22659 from mkouba/qute-fmt
Browse files Browse the repository at this point in the history
Introduce the convenient Qute.fmt() methods
  • Loading branch information
mkouba authored Jan 10, 2022
2 parents 2e9d5d5 + 13e276f commit e0bb150
Show file tree
Hide file tree
Showing 11 changed files with 599 additions and 35 deletions.
45 changes: 43 additions & 2 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,51 @@ In this guide, you will find an <<hello_world_example,introductory example>>, th

NOTE: Qute is primarily designed as a Quarkus extension. It is possible to use it as a "standalone" library too. However, in such case some of the features are not available. In general, any feature mentioned under the <<quarkus_integration>> section is missing. You can find more information about the limitations and possibilities in the <<standalone>> section.

[[the_simplest_example]]
== The Simplest Example

The easiest way to try Qute is to use the convenient `io.quarkus.qute.Qute` class and call one of its `fmt()` static methods that can be used to format simple messages:

[source,java]
----
import io.quarkus.qute.Qute;
Qute.fmt("Hello {}!", "Lucy"); <1>
// => Hello Lucy!
Qute.fmt("Hello {name} {surname ?: 'Default'}!", Map.of("name", "Andy")); <2>
// => Hello Andy Default!
Qute.fmt("<html>{header}</html>").contentType("text/html").data("header", "<h1>My header</h1>").render(); <3>
// <html>&lt;h1&gt;Header&lt;/h1&gt;</html> <4>
Qute.fmt("I am {#if ok}happy{#else}sad{/if}!", Map.of("ok", true)); <5>
// => I am happy!
----
<1> The empty expression `{}` is a placeholder that is replaced with an index-based array accessor, i.e. `{data[0]}`.
<2> You can provide a data map instead.
<3> A builder-like API is available for more complex formatting requirements.
<4> Note that for a "text/html" template the special chars are replaced with html entities by default.
<5> You can use any <<basic-building-blocks,building block>> in the template. In this case, the <<if_section>> is used to render the appropriate part of the message based on the input data.

TIP: In <<quarkus_integration,Quarkus>>, the engine used to format the messages is the same as the one injected by `@Inject Engine`. Therefore, you can make use of any Quarkus-specific integration feature such as <<template_extension_methods>>, <<injecting-beans-directly-in-templates>> or even <<type-safe-message-bundles>>.


The format object returned by the `Qute.fmt(String)` method can be evaluated lazily and used e.g. as a log message:

[source,java]
----
LOG.info(Qute.fmt("Hello {name}!").data("name", "Foo"));
// => Hello Foo! and the message template is only evaluated if the log level INFO is used for the specific logger
----

NOTE: Please read the javadoc of the `io.quarkus.qute.Qute` class for more details.

[[hello_world_example]]
== Hello World Example

In this example, we'd like to demonstrate the basic workflow when working with Qute templates.
Let's start with a simple hello world example.
In this example, we would like to demonstrate the _basic workflow_ when working with Qute templates.
Let's start with a simple "hello world" example.
We will always need some *template contents*:

.hello.html
Expand Down Expand Up @@ -84,6 +123,7 @@ TIP: The `Engine` is able to cache the template definitions so that it's not nec
[[core_features]]
== Core Features

[[basic-building-blocks]]
=== Basic Building Blocks

The dynamic parts of a template include comments, expressions, sections and unparsed character data.
Expand Down Expand Up @@ -1195,6 +1235,7 @@ class MyService {

NOTE: When using `quarkus-resteasy-qute` the content negotiation is performed automatically. See <<resteasy_integration>>.

[[injecting-beans-directly-in-templates]]
=== Injecting Beans Directly In Templates

A CDI bean annotated with `@Named` can be referenced in any template through `cdi` and/or `inject` namespaces:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

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

import java.util.Map;

import javax.inject.Inject;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.Qute;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateEnum;
import io.quarkus.test.QuarkusUnitTest;
Expand All @@ -28,6 +31,8 @@ public class TemplateEnumTest {
@Test
public void testTemplateData() {
assertEquals("OK::BAR::FOO", bar.data("tx", TransactionType.FOO).render());
// Test the convenient Qute class
assertEquals("OK", Qute.fmt("{#if tx == TransactionType:FOO}OK{#else}NOK{/if}", Map.of("tx", TransactionType.FOO)));
}

// namespace is TransactionType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.Engine;
import io.quarkus.qute.Qute;
import io.quarkus.qute.Template;
import io.quarkus.qute.i18n.Localized;
import io.quarkus.qute.i18n.MessageBundles;
Expand Down Expand Up @@ -82,6 +83,9 @@ public void testResolvers() {
assertEquals("There are 100 files on E.",
engine.parse("{msg:files(100,'E')}").render());

// Test the convenient Qute class
assertEquals("There are no files on C.", Qute.fmt("{msg:files(0,'C')}").render());
assertEquals("Hallo Welt!", Qute.fmt("{msg:hello}").attribute("locale", Locale.GERMAN).render());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.Qute;
import io.quarkus.qute.Template;
import io.quarkus.qute.deployment.Hello;
import io.quarkus.test.QuarkusUnitTest;
Expand All @@ -35,6 +36,12 @@ public class InjectNamespaceResolverTest {
public void testInjection() {
assertEquals("pong != simple and pong != simple", foo.render());
assertEquals(2, SimpleBean.DESTROYS.longValue());

// Test the convenient Qute class
// By default, the content type is plain text
assertEquals("pong::<br>", Qute.fmt("{cdi:hello.ping}::{}", "<br>"));
assertEquals("pong::&lt;br&gt;",
Qute.fmt("{cdi:hello.ping}::{newLine}").contentType("text/html").data("newLine", "<br>").render());
}

@Named("simple")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.quarkus.qute.EvalContext;
import io.quarkus.qute.HtmlEscaper;
import io.quarkus.qute.NamespaceResolver;
import io.quarkus.qute.Qute;
import io.quarkus.qute.ReflectionValueResolver;
import io.quarkus.qute.Resolver;
import io.quarkus.qute.Results;
Expand Down Expand Up @@ -175,13 +176,20 @@ public Object apply(EvalContext ctx) {
}
// Add locator
builder.addLocator(this::locate);

// Add a special parserk hook for Qute.fmt() methods
builder.addParserHook(new Qute.IndexedArgumentsParserHook());

engine = builder.build();

// Load discovered templates
for (String path : context.getTemplatePaths()) {
engine.getTemplate(path);
}
engineReady.fire(engine);

// Set the engine instance
Qute.setEngine(engine);
}

@Produces
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package io.quarkus.qute;

import io.quarkus.qute.Parser.StringReader;
import io.quarkus.qute.TemplateLocator.TemplateLocation;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import io.quarkus.qute.TemplateNode.Origin;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.nio.CharBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
Expand All @@ -22,6 +21,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Predicate;
Expand Down Expand Up @@ -76,6 +76,7 @@ class Parser implements Function<String, Expression>, ParserHelper {
private boolean ignoreContent;
private AtomicInteger expressionIdGenerator;
private final List<Function<String, String>> contentFilters;
private boolean hasLineSeparator;

public Parser(EngineImpl engine, Reader reader, String templateId, String generatedId, Optional<Variant> variant) {
this.engine = engine;
Expand Down Expand Up @@ -118,7 +119,7 @@ Template parse() {
.setEngine(engine)
.setHelperFactory(ROOT_SECTION_HELPER_FACTORY));

long start = System.currentTimeMillis();
long start = System.nanoTime();
Reader r = reader;

try {
Expand Down Expand Up @@ -164,8 +165,8 @@ Template parse() {
}
TemplateImpl template = new TemplateImpl(engine, root.build(), generatedId, variant);

Set<TemplateNode> nodesToRemove;
if (engine.removeStandaloneLines) {
Set<TemplateNode> nodesToRemove = Collections.emptySet();
if (hasLineSeparator && engine.removeStandaloneLines) {
nodesToRemove = new HashSet<>();
List<List<TemplateNode>> lines = readLines(template.root);
for (List<TemplateNode> line : lines) {
Expand All @@ -178,12 +179,13 @@ Template parse() {
}
}
}
} else {
nodesToRemove = Collections.emptySet();
if (nodesToRemove.isEmpty()) {
nodesToRemove = Collections.emptySet();
}
}
template.root.optimizeNodes(nodesToRemove);

LOGGER.tracef("Parsing finished in %s ms", System.currentTimeMillis() - start);
LOGGER.tracef("Parsing finished in %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start));
return template;

} catch (IOException e) {
Expand Down Expand Up @@ -256,6 +258,8 @@ private void lineSeparator(char character) {
state = State.TEXT;
processCharacter(character);
}
// Parsing of one-line templates can be optimized
hasLineSeparator = true;
}

private void comment(char character) {
Expand Down Expand Up @@ -743,15 +747,15 @@ static ExpressionImpl parseExpression(Supplier<Integer> idGenerator, String valu
}
String namespace = null;
int namespaceIdx = value.indexOf(NAMESPACE_SEPARATOR);
int spaceIdx = value.indexOf(' ');
int bracketIdx = value.indexOf('(');
int spaceIdx;
int bracketIdx;

List<String> strParts;
if (namespaceIdx != -1
// No space or colon before the space
&& (spaceIdx == -1 || namespaceIdx < spaceIdx)
&& ((spaceIdx = value.indexOf(' ')) == -1 || namespaceIdx < spaceIdx)
// No bracket or colon before the bracket
&& (bracketIdx == -1 || namespaceIdx < bracketIdx)
&& ((bracketIdx = value.indexOf('(')) == -1 || namespaceIdx < bracketIdx)
// No string literal
&& !LiteralSupport.isStringLiteralSeparator(value.charAt(0))) {
// Expression that starts with a namespace
Expand Down Expand Up @@ -943,6 +947,9 @@ private boolean isBlank(CharSequence val) {

private static String toString(Reader in)
throws IOException {
if (in instanceof StringReader) {
return ((StringReader) in).str;
}
StringBuilder out = new StringBuilder();
CharBuffer buffer = CharBuffer.allocate(8192);
while (in.read(buffer) != -1) {
Expand All @@ -953,6 +960,18 @@ private static String toString(Reader in)
return out.toString();
}

static class StringReader extends java.io.StringReader {

// make the underlying string accessible
final String str;

public StringReader(String s) {
super(s);
this.str = s;
}

}

static class OriginImpl implements Origin {

private final int line;
Expand Down
Loading

0 comments on commit e0bb150

Please sign in to comment.