diff --git a/src/main/java/com/samskivert/mustache/Mustache.java b/src/main/java/com/samskivert/mustache/Mustache.java index 7804576..6399e94 100644 --- a/src/main/java/com/samskivert/mustache/Mustache.java +++ b/src/main/java/com/samskivert/mustache/Mustache.java @@ -557,6 +557,15 @@ public class Mustache return this; } + public void addTag (char prefix, String name, StringBuilder into) { + into.append(start1); + into.append(start2); + if (prefix != ' ') into.append(prefix); + into.append(name); + into.append(end1); + into.append(end2); + } + Delims copy () { Delims d = new Delims(); d.start1 = start1; @@ -708,6 +717,9 @@ public class Mustache @Override public void execute (Template tmpl, Template.Context ctx, Writer out) { write(out, _text); } + @Override public void decompile (Delims delims, StringBuilder into) { + into.append(_text); + } @Override public String toString () { return "Text(" + _text.replace("\r", "\\r").replace("\n", "\\n") + ")" + _leadBlank + "/" + _trailBlank; @@ -761,6 +773,9 @@ public class Mustache // would happen if we just called execute() with ctx.data _template.executeSegs(ctx, out); } + @Override public void decompile (Delims delims, StringBuilder into) { + delims.addTag('>', _name, into); + } protected final Compiler _comp; protected final String _name; protected Template _template; @@ -791,6 +806,9 @@ public class Mustache } write(out, _escaper.escape(_formatter.format(value))); } + @Override public void decompile (Delims delims, StringBuilder into) { + delims.addTag(' ', _name, into); + } @Override public String toString () { return "Var(" + _name + ":" + _line + ")"; } @@ -863,6 +881,11 @@ public class Mustache executeSegs(tmpl, ctx.nest(value, 0, false, false), out); } } + @Override public void decompile (Delims delims, StringBuilder into) { + delims.addTag('#', _name, into); + for (Template.Segment seg : _segs) seg.decompile(delims, into); + delims.addTag('/', _name, into); + } @Override public String toString () { return "Section(" + _name + ":" + _line + "): " + Arrays.toString(_segs); } @@ -896,6 +919,11 @@ public class Mustache executeSegs(tmpl, ctx, out); } // TODO: fail? } + @Override public void decompile (Delims delims, StringBuilder into) { + delims.addTag('^', _name, into); + for (Template.Segment seg : _segs) seg.decompile(delims, into); + delims.addTag('/', _name, into); + } @Override public String toString () { return "Inverted(" + _name + ":" + _line + "): " + Arrays.toString(_segs); } @@ -904,6 +932,7 @@ public class Mustache protected static class FauxSegment extends Template.Segment { @Override public void execute (Template tmpl, Template.Context ctx, Writer out) {} // nada + @Override public void decompile (Delims delims, StringBuilder into) {} // nada @Override public String toString () { return "Faux"; } } diff --git a/src/main/java/com/samskivert/mustache/Template.java b/src/main/java/com/samskivert/mustache/Template.java index 88eb29e..69892ee 100644 --- a/src/main/java/com/samskivert/mustache/Template.java +++ b/src/main/java/com/samskivert/mustache/Template.java @@ -67,6 +67,35 @@ public class Template execute(context, out); return out.toString(); } + + /** 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 + * character representation because the original text is not preserved because that would + * incur a huge memory penalty for all users of the library when the vast majority of + * them do not call decompile. + * + *
Limitations: + *
This feature is meant to enable use of lambdas for i18n such that you can recover + * the contents of a lambda (so long as they're simple) to use as the lookup key for a + * translation string. For example: {@code {{#i18n}}Hello {{user.name}}!{{/i18n}}} can be + * sent to an {@code i18n} lambda which can use {@code decompile} to recover the text + * {@code Hello {{user.name}}!} to be looked up in a translation dictionary. The + * translated fragment could then be compiled and cached and then executed in lieu of the + * original fragment using {@link Template.Fragment#context}. + */ + public String decompile () { + return decompile(new StringBuilder()).toString(); + } + + /** Decompiles this fragment into {@code into}. See {@link #decompile()}. + * @return {@code into} for call chaining. */ + public abstract StringBuilder decompile (StringBuilder into); } /** A sentinel object that can be returned by a {@link Mustache.Collector} to indicate that a @@ -126,6 +155,10 @@ public class Template @Override public void execute (Object context, Writer out) { execute(currentCtx.nest(context, 0, false, false), out); } + @Override public StringBuilder decompile (StringBuilder into) { + for (Segment seg : segs) seg.decompile(_compiler.delims, into); + return into; + } private void execute (Context ctx, Writer out) { for (Segment seg : segs) { seg.execute(Template.this, ctx, out); @@ -302,6 +335,8 @@ public class Template protected static abstract class Segment { abstract void execute (Template tmpl, Context ctx, Writer out); + abstract void decompile (Mustache.Delims delims, StringBuilder into); + protected static void write (Writer out, String data) { try { out.write(data); diff --git a/src/test/java/com/samskivert/mustache/SharedTests.java b/src/test/java/com/samskivert/mustache/SharedTests.java index 6106934..ca70583 100644 --- a/src/test/java/com/samskivert/mustache/SharedTests.java +++ b/src/test/java/com/samskivert/mustache/SharedTests.java @@ -594,6 +594,42 @@ public abstract class SharedTests extends GWTTestCase })); } + @Test public void testLambdaDecompile () { + test("Foo {{a}}, Bar {{a}}", "{{#lam}}Foo {{a}}{{/lam}}, {{#lam}}Bar {{a}}{{/lam}}", + context("lam", new Mustache.Lambda() { + public void execute (Template.Fragment frag, Writer out) throws IOException { + out.write(frag.decompile()); + } + })); + // whitespace inside tags is dropped! + test("Foo {{a}}, Bar {{a}}", "{{#lam}}Foo {{a}}{{/lam}}, {{#lam}}Bar {{ a }}{{/lam}}", + context("lam", new Mustache.Lambda() { + public void execute (Template.Fragment frag, Writer out) throws IOException { + out.write(frag.decompile()); + } + })); + // custom delimiters are ignored! + test("Foo {{a}}, Bar {{a}}", + "{{#lam}}Foo {{a}}{{/lam}}, {{=(( ))=}}((#lam))Bar ((a))((/lam))", + context("lam", new Mustache.Lambda() { + public void execute (Template.Fragment frag, Writer out) throws IOException { + out.write(frag.decompile()); + } + })); + // coalesced whitespace around section tags is preserved + test("{{#section}}\n" + + "{{a}}\n" + + "{{/section}}", + "{{#lam}}{{#section}}\n" + + "{{a}}\n" + + "{{/section}}{{/lam}}", + context("lam", new Mustache.Lambda() { + public void execute (Template.Fragment frag, Writer out) throws IOException { + out.write(frag.decompile()); + } + })); + } + @Test public void testNonStandardDefaultDelims () { test(Mustache.compiler().withDelims("<% %>"), "bar", "<%foo%>", context("foo", "bar")); }