From 97e42a45ad877c5acb33ea6b59bdb1f4abd6fb7e Mon Sep 17 00:00:00 2001 From: Adam Gent Date: Thu, 18 Apr 2024 17:38:15 -0400 Subject: [PATCH] Add value name lookup for lambdas --- .../com/samskivert/mustache/Mustache.java | 4 +- .../com/samskivert/mustache/Template.java | 19 ++- .../samskivert/mustache/LocaleLambdaTest.java | 140 ++++++++++++++++++ 3 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/samskivert/mustache/LocaleLambdaTest.java 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; + } + } + +}