Simple support for decompiling fragments.

There are limitations! See the javadoc for Template.Fragment.decompile.

Closes #75.
This commit is contained in:
Michael Bayne
2016-04-14 08:24:54 -07:00
parent 59c45a2da7
commit 546f78318e
3 changed files with 100 additions and 0 deletions
@@ -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"; }
}
@@ -67,6 +67,35 @@ public class Template
execute(context, out);
return out.toString();
}
/** 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
* 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.
*
* <p>Limitations:
* <ul><li> Whitespace inside tags is not preserved: i.e. {@code {{ foo.bar }}} becomes
* {@code {{foo.bar}}}.
* <li> If the delimiters are changed by the template, those are not preserved.
* The delimiters configured on the {@link Compiler} are used for all decompilation.
* </ul>
*
* <p>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);
@@ -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"));
}