Skip to content

Commit

Permalink
Add value name lookup for lambdas
Browse files Browse the repository at this point in the history
  • Loading branch information
agentgt committed Apr 18, 2024
1 parent 00be427 commit 97e42a4
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 3 deletions.
4 changes: 2 additions & 2 deletions src/main/java/com/samskivert/mustache/Mustache.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down
19 changes: 18 additions & 1 deletion src/main/java/com/samskivert/mustache/Template.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>{{name}}</code>. If no value is found or the value found is
* <code>null</code> then <code>null</code> is returned.
*/
public abstract /* @Nullable */ Object valueOrNull (String name);

/** Decompiles the template inside this lamdba and returns <em>an approximation</em> of
* the original template from which it was parsed. This is not the exact character for
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down
140 changes: 140 additions & 0 deletions src/test/java/com/samskivert/mustache/LocaleLambdaTest.java
Original file line number Diff line number Diff line change
@@ -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<String, Object> model = new HashMap<>();
Map<String, Object> 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<String,String> 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, &amp; btw you owe $5.50!";
assertEquals(expected, actual);
}
static class MessageLambda implements Mustache.Lambda {

private final Function<String,String> bundle;
private final Locale locale;
private final Mustache.Escaper escaper;

public MessageLambda(Function<String,String> 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<String> params = parseParameters(paramsStr);
return new MessageFunction(key, params);
}

return null; // Invalid DSL syntax
}

private static List<String> parseParameters(String paramsStr) {
List<String> 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<String> params;

public MessageFunction(String key, List<String> params) {
this.key = key;
this.params = params;
}

public String getKey() {
return key;
}

public List<String> getParams() {
return params;
}
}

}

0 comments on commit 97e42a4

Please sign in to comment.