diff --git a/src/main/java/com/samskivert/mustache/Mustache.java b/src/main/java/com/samskivert/mustache/Mustache.java
index e79931f..1934515 100644
--- a/src/main/java/com/samskivert/mustache/Mustache.java
+++ b/src/main/java/com/samskivert/mustache/Mustache.java
@@ -1445,7 +1445,7 @@ protected SectionSegment (SectionSegment original, Template.Segment[] segs) {
}
} else if (value instanceof Lambda) {
try {
- ((Lambda)value).execute(tmpl.createFragment(_segs, ctx), out);
+ ((Lambda)value).execute(tmpl.createFragment(_segs, ctx, _line), out);
} catch (IOException ioe) {
throw new MustacheException(ioe);
}
@@ -1556,7 +1556,7 @@ protected InvertedSegment (InvertedSegment original, Template.Segment[] segs) {
}
} else if (value instanceof InvertibleLambda) {
try {
- ((InvertibleLambda)value).executeInverse(tmpl.createFragment(_segs, ctx), out);
+ ((InvertibleLambda)value).executeInverse(tmpl.createFragment(_segs, ctx, _line), out);
} catch (IOException ioe) {
throw new MustacheException(ioe);
}
diff --git a/src/main/java/com/samskivert/mustache/Template.java b/src/main/java/com/samskivert/mustache/Template.java
index acb5d4d..47b1c7a 100644
--- a/src/main/java/com/samskivert/mustache/Template.java
+++ b/src/main/java/com/samskivert/mustache/Template.java
@@ -80,12 +80,20 @@ public String execute (Object context) {
* to inspect it (be that a {@code Map} or a POJO or something else). */
public abstract Object context ();
- /** Like {@link #context()} btu returns the {@code n}th parent context object. {@code 0}
+ /** Like {@link #context()} but returns the {@code n}th parent context object. {@code 0}
* returns the same value as {@link #context()}, {@code 1} returns the parent context,
* {@code 2} returns the grandparent and so forth. Note that if you request a parent that
* does not exist an exception will be thrown. You should only use this method when you
* know your lambda is run consistently in a context with a particular lineage. */
public abstract Object context (int n);
+
+ /**
+ * Searches up the context stack for a matching name and returns the value associated.
+ * Names maybe dotted (also known as compound). Thisis equivalent to referencing a variable
+ * in a template like {{name}}
. If no value is found or the value found is
+ * null
then null
is returned.
+ */
+ public abstract /* @Nullable */ Object valueOrNull (String name);
/** Decompiles the template inside this lamdba and returns an approximation of
* the original template from which it was parsed. This is not the exact character for
@@ -197,7 +205,12 @@ protected void executeSegs (Context ctx, Writer out) throws MustacheException {
}
}
+ @Deprecated
protected Fragment createFragment (final Segment[] segs, final Context currentCtx) {
+ return createFragment(segs, currentCtx, 0);
+ }
+
+ protected Fragment createFragment (final Segment[] segs, final Context currentCtx, int line) {
return new Fragment() {
@Override public void execute (Writer out) {
execute(currentCtx, out);
@@ -214,6 +227,10 @@ protected Fragment createFragment (final Segment[] segs, final Context currentCt
@Override public Object context (int n) {
return context(currentCtx, n);
}
+ @Override
+ public /* @Nullable */ Object valueOrNull(String name) {
+ return Template.this.getValue(currentCtx, name, line, true);
+ }
@Override public StringBuilder decompile (StringBuilder into) {
for (Segment seg : segs) seg.decompile(_compiler.delims, into);
return into;
diff --git a/src/test/java/com/samskivert/mustache/LocaleLambdaTest.java b/src/test/java/com/samskivert/mustache/LocaleLambdaTest.java
new file mode 100644
index 0000000..fb4aaf4
--- /dev/null
+++ b/src/test/java/com/samskivert/mustache/LocaleLambdaTest.java
@@ -0,0 +1,140 @@
+package com.samskivert.mustache;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.math.BigDecimal;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.junit.Test;
+
+import com.samskivert.mustache.Template.Fragment;
+
+public class LocaleLambdaTest {
+
+ @Test
+ public void testLocaleSpike() {
+ Map model = new HashMap<>();
+ Map user = new HashMap<>();
+ user.put("name", "Michael");
+ user.put("balance", new BigDecimal("5.50"));
+ model.put("user", user);
+
+ /*
+ * This is our pretend resource bundle.
+ */
+ Map messages = new HashMap<>();
+ messages.put("hello.welcome", "Hello {0}, & btw you owe {1,number,currency}!");
+
+ /*
+ * Here is our magic i18n
+ */
+ MessageLambda ml = new MessageLambda(messages::get, Locale.US, Escapers.HTML);
+ model.put("@message", ml);
+ String template = "{{#@message}}hello.welcome(user.name,user.balance){{/@message}}";
+ String actual = Mustache.compiler().compile(template).execute(model);
+ String expected = "Hello Michael, & btw you owe $5.50!";
+ assertEquals(expected, actual);
+ }
+ static class MessageLambda implements Mustache.Lambda {
+
+ private final Function bundle;
+ private final Locale locale;
+ private final Mustache.Escaper escaper;
+
+ public MessageLambda(Function bundle, Locale locale, Mustache.Escaper escaper) {
+ super();
+ this.bundle = bundle;
+ this.locale = locale;
+ this.escaper = escaper;
+ }
+
+ @Override
+ public void execute(Fragment frag, Writer out) throws IOException {
+ String body = frag.decompile();
+ MessageFunction function = parseDSL(body);
+ String key = function.getKey();
+ String message = bundle.apply(key);
+ if (message == null) {
+ throw new RuntimeException("Bundle missing key: " + key);
+ }
+ MessageFormat mf = new MessageFormat(message, locale);
+ /*
+ * Replace the args with values from the context.
+ */
+ Object[] args = function.getParams().stream().map(k -> frag.valueOrNull(k)).toArray();
+ String response = mf.format(args);
+ escaper.escape(out, response);
+ }
+
+ }
+
+ /*
+ * Our format is
+ *
+ * key(param,...)
+ *
+ */
+ public static MessageFunction parseDSL(String input) {
+ if (! input.contains("(")) {
+ return new MessageFunction(input, Collections.emptyList());
+ }
+ // Chat GPT wrote this garbage but it looks good to me... well after I edited.
+ // Regular expression pattern to match the DSL syntax
+ Pattern pattern = Pattern.compile("^([a-zA-Z0-9\\.\\-_]+)\\((.*?)\\)$");
+ Matcher matcher = pattern.matcher(input);
+
+ if (matcher.matches()) {
+ String key = matcher.group(1);
+ String paramsStr = matcher.group(2);
+
+ List params = parseParameters(paramsStr);
+ return new MessageFunction(key, params);
+ }
+
+ return null; // Invalid DSL syntax
+ }
+
+ private static List parseParameters(String paramsStr) {
+ List params = new ArrayList<>();
+
+ // Split parameters by commas
+ String[] paramTokens = paramsStr.split(",");
+
+ // Add each parameter to the list
+ for (String param : paramTokens) {
+ params.add(param.trim()); // Remove any surrounding whitespace
+ }
+
+ return params;
+ }
+
+ static class MessageFunction {
+ private String key;
+ private List params;
+
+ public MessageFunction(String key, List params) {
+ this.key = key;
+ this.params = params;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public List getParams() {
+ return params;
+ }
+ }
+
+}