Skip to content

Commit

Permalink
Merge pull request #46258 from mkouba/eval-ns-resolver
Browse files Browse the repository at this point in the history
Qute: add some more built-in string extensions
  • Loading branch information
mkouba authored Feb 18, 2025
2 parents 4100928 + aae8420 commit be7e613
Show file tree
Hide file tree
Showing 17 changed files with 377 additions and 49 deletions.
28 changes: 25 additions & 3 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2367,12 +2367,34 @@ TIP: A list element can be accessed directly via an index: `{list.10}` or even `
* `fmt` or `format`: Formats the string instance via `java.lang.String.format()`
** `{myStr.fmt("arg1","arg2")}`
** `{myStr.format(locale,arg1)}`

* `+`: Infix notation for concatenation, works with `String` and `StringBuilder` base objects
** `{item.name + '_' + mySuffix}`
** `{name + 10}`

* `str:['<value>']`: Returns the string value, e.g. to easily concatenate another string value
** `{str:['/path/to/'] + fileName}`

* `str:fmt` or `str:format`: Formats the supplied string value via `java.lang.String.format()`
** `{str:format("Hello %s!",name)}`
** `{str:fmt(locale,'%tA',now)}`
* `+`: Concatenation
** `{item.name + '_' + mySuffix}`
** `{name + 10}`
** `{str:fmt('/path/to/%s', fileName)}`

* `str:concat`: Concatenates the string representations of the specified arguments.
** `{str:concat("Hello ",name,"!")}` yields `Hello Foo!` if `name` resolves to `Foo`
** `{str:concat('/path/to/', fileName)}`

* `str:join`: Joins the string representations of the specified arguments together with a delimiter.
** `{str:join('_','Qute','is','cool')}` yields `Qute_is_cool`

* `str:builder`: Returns a new string builder.
** `{str:builder('Qute').append("is").append("cool!")}` yields `Qute is cool!`
** `{str:builder('Qute') + "is" + whatisqute + "!"}` yields `Qute is cool!` if `whatisqute` resolves to `cool`

* `str:eval`: Evaluates the string representation of the first argument as a template in the <<current_context_object,current context>>.
** `{str:eval('Hello {name}!')` yields `Hello lovely!` if `name` resolves to `lovely`
** `{str:eval(myTemplate)}` yields `Hello lovely!` if `myTemplate` resolves to `Hello {name}!` and `name` resolves to `lovely`
** `{str:eval('/path/to/{fileName}')}` yields `/path/to/file.txt` if `fileName` resolves to `file.txt`

===== Config

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,43 @@ public void testTemplateExtensions() {
engine.parse("{foo + 'bar' + 1}")
.data("foo", "bar")
.render());
assertEquals("barbar1",
engine.parse("{str:concat(foo, 'bar', 1)}")
.data("foo", "bar")
.render());
assertEquals("barbar1",
engine.parse("{str:builder(foo).append('bar').append(1)}")
.data("foo", "bar")
.render());
assertEquals("barbar1",
engine.parse("{str:builder.append(foo).append('bar').append(1)}")
.data("foo", "bar")
.render());
assertEquals("barbar1",
engine.parse("{str:builder(foo) + 'bar' + 1}")
.data("foo", "bar")
.render());
assertEquals("barbar1",
engine.parse("{str:builder + foo + 'bar' + 1}")
.data("foo", "bar")
.render());
assertEquals("Qute-is-cool",
engine.parse("{str:join('-', 'Qute', 'is', foo)}")
.data("foo", "cool")
.render());
assertEquals("Qute is cool!",
engine.parse("{str:Qute + ' is ' + foo + '!'}")
.data("foo", "cool")
.render());
assertEquals("Qute is cool!",
engine.parse("{str:['Qute'] + ' is ' + foo + '!'}")
.data("foo", "cool")
.render());
// note that this is not implemented as a template extension but a ns resolver
assertEquals("Hello fool!",
engine.parse("{str:eval('Hello {name}!')}")
.data("name", "fool")
.render());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import io.quarkus.qute.Resolver;
import io.quarkus.qute.Results;
import io.quarkus.qute.SectionHelperFactory;
import io.quarkus.qute.StrEvalNamespaceResolver;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateGlobalProvider;
import io.quarkus.qute.TemplateInstance;
Expand Down Expand Up @@ -194,6 +195,9 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig
for (NamespaceResolver namespaceResolver : namespaceResolvers) {
builder.addNamespaceResolver(namespaceResolver);
}
// str:eval
StrEvalNamespaceResolver strEvalNamespaceResolver = new StrEvalNamespaceResolver();
builder.addNamespaceResolver(strEvalNamespaceResolver);

// Add generated resolvers
for (String resolverClass : context.getResolverClasses()) {
Expand Down Expand Up @@ -269,6 +273,9 @@ public void run() {

engine = builder.build();

// Init resolver for str:eval
strEvalNamespaceResolver.setEngine(engine);

// Load discovered template files
Map<String, List<Template>> discovered = new HashMap<>();
for (String path : context.getTemplatePaths()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io.quarkus.qute.runtime.extensions;

import static io.quarkus.qute.TemplateExtension.ANY;

import java.util.Locale;
import java.util.Objects;

import jakarta.enterprise.inject.Vetoed;

Expand Down Expand Up @@ -72,4 +75,64 @@ static String plus(String str, Object val) {
return str + val;
}

/**
* E.g. {@code str:concat("Hello ",name)}. The priority must be lower than {@link #fmt(String, String, Object...)}.
*
* @param args
*/
@TemplateExtension(namespace = STR, priority = 1)
static String concat(Object... args) {
StringBuilder b = new StringBuilder(args.length * 10);
for (Object obj : args) {
b.append(obj.toString());
}
return b.toString();
}

/**
* E.g. {@code str:join("_", "Hello",name)}. The priority must be lower than {@link #concat(Object...)}.
*
* @param delimiter
* @param args
*/
@TemplateExtension(namespace = STR, priority = 0)
static String join(String delimiter, Object... args) {
CharSequence[] elements = new CharSequence[args.length];
for (int i = 0; i < args.length; i++) {
elements[i] = args[i].toString();
}
return String.join(delimiter, elements);
}

/**
* E.g. {@code str:builder}. The priority must be lower than {@link #join(String, Object...)}.
*/
@TemplateExtension(namespace = STR, priority = -1)
static StringBuilder builder() {
return new StringBuilder();
}

/**
* E.g. {@code str:builder('Hello')}. The priority must be lower than {@link #builder()}.
*/
@TemplateExtension(namespace = STR, priority = -2)
static StringBuilder builder(Object val) {
return new StringBuilder(Objects.toString(val));
}

/**
* E.g. {@code str:['Foo and bar']}. The priority must be lower than any other {@code str:} resolver.
*
* @param name
*/
@TemplateExtension(namespace = STR, priority = -10, matchName = ANY)
static String self(String name) {
return name;
}

@TemplateExtension(matchName = "+")
static StringBuilder plus(StringBuilder builder, Object val) {
return builder.append(val);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,10 @@ public interface EvalContext {
*/
Object getAttribute(String key);

/**
*
* @return the current resolution context
*/
ResolutionContext resolutionContext();

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

public class EvalSectionHelper implements SectionHelper {

public static final String EVAL = "eval";
private static final String TEMPLATE = "template";

private final Map<String, Expression> parameters;
Expand All @@ -23,52 +24,69 @@ public EvalSectionHelper(Map<String, Expression> parameters, Engine engine) {

@Override
public CompletionStage<ResultNode> resolve(SectionResolutionContext context) {
CompletableFuture<ResultNode> result = new CompletableFuture<>();
context.evaluate(parameters).whenComplete((evaluatedParams, t1) -> {
if (t1 != null) {
result.completeExceptionally(t1);
} else {
try {
CompletableFuture<ResultNode> ret = new CompletableFuture<>();
if (parameters.size() > 1) {
context.evaluate(parameters).whenComplete((evaluatedParams, t1) -> {
if (t1 != null) {
ret.completeExceptionally(t1);
} else {
// Parse the template and execute with the params as the root context object
String templateStr = evaluatedParams.get(TEMPLATE).toString();
TemplateImpl template;
try {
template = (TemplateImpl) engine.parse(templateStr);
} catch (TemplateException e) {
Origin origin = parameters.get(TEMPLATE).getOrigin();
throw TemplateException.builder()
.message(
"Parser error in the evaluated template: {templateId} line {line}:\\n\\t{originalMessage}")
.code(Code.ERROR_IN_EVALUATED_TEMPLATE)
.argument("templateId",
origin.hasNonGeneratedTemplateId() ? " template [" + origin.getTemplateId() + "]"
: "")
.argument("line", origin.getLine())
.argument("originalMessage", e.getMessage())
.build();
}
template.root
.resolve(context.resolutionContext().createChild(Mapper.wrap(evaluatedParams), null))
.whenComplete((resultNode, t2) -> {
if (t2 != null) {
result.completeExceptionally(t2);
} else {
result.complete(resultNode);
}
});
} catch (Throwable e) {
result.completeExceptionally(e);
String contents = evaluatedParams.get(TEMPLATE).toString();
parseAndResolve(ret, contents,
context.resolutionContext().createChild(Mapper.wrap(evaluatedParams), null));
}
});
} else {
Expression contents = parameters.get(TEMPLATE);
if (contents.isLiteral()) {
parseAndResolve(ret, contents.getLiteral().toString(), context.resolutionContext());
} else {
context.evaluate(contents).whenComplete((r, t) -> {
if (t != null) {
ret.completeExceptionally(t);
} else {
parseAndResolve(ret, r.toString(), context.resolutionContext());
}
});
}
});
return result;
}

return ret;
}

private void parseAndResolve(CompletableFuture<ResultNode> ret, String contents, ResolutionContext resolutionContext) {
TemplateImpl template;
try {
template = (TemplateImpl) engine.parse(contents);
template.root
.resolve(resolutionContext)
.whenComplete((resultNode, t2) -> {
if (t2 != null) {
ret.completeExceptionally(t2);
} else {
ret.complete(resultNode);
}
});
} catch (TemplateException e) {
Origin origin = parameters.get(TEMPLATE).getOrigin();
ret.completeExceptionally(TemplateException.builder()
.message(
"Parser error in the evaluated template: {templateId} line {line}:\\n\\t{originalMessage}")
.code(Code.ERROR_IN_EVALUATED_TEMPLATE)
.argument("templateId",
origin.hasNonGeneratedTemplateId() ? " template [" + origin.getTemplateId() + "]"
: "")
.argument("line", origin.getLine())
.argument("originalMessage", e.getMessage())
.build());
}
}

public static class Factory implements SectionHelperFactory<EvalSectionHelper> {

@Override
public List<String> getDefaultAliases() {
return ImmutableList.of("eval");
return ImmutableList.of(EVAL);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,11 @@ public Object getAttribute(String key) {
return resolutionContext.getAttribute(key);
}

@Override
public ResolutionContext resolutionContext() {
return resolutionContext;
}

@Override
public String toString() {
StringBuilder builder = new StringBuilder();
Expand Down Expand Up @@ -405,6 +410,11 @@ boolean tryParent() {
return true;
}

@Override
public ResolutionContext resolutionContext() {
return resolutionContext;
}

@Override
public String toString() {
StringBuilder builder = new StringBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ static Object getLiteralValue(String literal) {
return value;
}
if (isStringLiteral(literal)) {
value = literal.substring(1, literal.length() - 1);
value = extractStringValue(literal);
} else if (literal.equals("true")) {
value = Boolean.TRUE;
} else if (literal.equals("false")) {
Expand Down Expand Up @@ -85,6 +85,10 @@ static boolean isStringLiteralSeparatorDouble(char character) {
return character == '"';
}

static String extractStringValue(String strLiteral) {
return strLiteral.substring(1, strLiteral.length() - 1);
}

static boolean isStringLiteral(String value) {
if (value == null || value.isEmpty()) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ public interface ResolutionContext {
*/
Object getAttribute(String key);

/**
* @return the current template
*/
Template getTemplate();

/**
*
* @return the evaluator
Expand Down
Loading

0 comments on commit be7e613

Please sign in to comment.