diff --git a/src/main/java/com/samskivert/mustache/Mustache.java b/src/main/java/com/samskivert/mustache/Mustache.java index 4e6d9bc..f551f45 100644 --- a/src/main/java/com/samskivert/mustache/Mustache.java +++ b/src/main/java/com/samskivert/mustache/Mustache.java @@ -297,6 +297,39 @@ public class Mustache { Map createFetcherCache (); } + /** Used to visit the tags in a template without executing it. */ + public interface Visitor { + + /** Visits a text segment. These are blocks of text that are normally just reproduced as + * is when executing a template. + * @param text the block of text. May contain newlines. + */ + void visitText (String text); + + /** Visits a variable tag. + * @param name the name of the variable. + */ + void visitVariable (String name); + + /** Visits an include (partial) tag. + * @param name the name of the partial template specified by the tag. + * @return true if the template should be processed and visited, false to skip it. + */ + boolean visitInclude (String name); + + /** Visits a section tag. + * @param name the name of the section. + * @return true if the contents of the section should be visited, false to skip. + */ + boolean visitSection (String name); + + /** Visits an inverted section tag. + * @param name the name of the inverted section. + * @return true if the contents of the section should be visited, false to skip. + */ + boolean visitInvertedSection (String name); + } + /** * Returns a compiler that escapes HTML by default and does not use standards mode. */ @@ -719,6 +752,9 @@ public class Mustache { @Override public void decompile (Delims delims, StringBuilder into) { into.append(_text); } + @Override public void visit (Visitor visitor) { + visitor.visitText(_text); + } @Override public String toString () { return "Text(" + _text.replace("\r", "\\r").replace("\n", "\\n") + ")" + _leadBlank + "/" + _trailBlank; @@ -741,12 +777,26 @@ public class Mustache { protected final int _leadBlank, _trailBlank; } + /** A segment that loads and executes a sub-template. */ protected static class IncludedTemplateSegment extends Template.Segment { public IncludedTemplateSegment (Compiler compiler, String name) { _comp = compiler; _name = name; } @Override public void execute (Template tmpl, Template.Context ctx, Writer out) { + // we must take care to preserve our context rather than creating a new one, which + // would happen if we just called execute() with ctx.data + getTemplate().executeSegs(ctx, out); + } + @Override public void decompile (Delims delims, StringBuilder into) { + delims.addTag('>', _name, into); + } + @Override public void visit (Visitor visitor) { + if (visitor.visitInclude(_name)) { + getTemplate().visit(visitor); + } + } + protected Template getTemplate () { // we compile our template lazily to avoid infinie recursion if a template includes // itself (see issue #13) if (_template == null) { @@ -768,16 +818,11 @@ public class Mustache { } } } - // we must take care to preserve our context rather than creating a new one, which - // 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); + return _template; } protected final Compiler _comp; protected final String _name; - protected Template _template; + private Template _template; } /** A helper class for named segments. */ @@ -808,6 +853,9 @@ public class Mustache { @Override public void decompile (Delims delims, StringBuilder into) { delims.addTag(' ', _name, into); } + @Override public void visit (Visitor visitor) { + visitor.visitVariable(_name); + } @Override public String toString () { return "Var(" + _name + ":" + _line + ")"; } @@ -885,6 +933,13 @@ public class Mustache { for (Template.Segment seg : _segs) seg.decompile(delims, into); delims.addTag('/', _name, into); } + @Override public void visit (Visitor visitor) { + if (visitor.visitSection(_name)) { + for (Template.Segment seg : _segs) { + seg.visit(visitor); + } + } + } @Override public String toString () { return "Section(" + _name + ":" + _line + "): " + Arrays.toString(_segs); } @@ -923,6 +978,13 @@ public class Mustache { for (Template.Segment seg : _segs) seg.decompile(delims, into); delims.addTag('/', _name, into); } + @Override public void visit (Visitor visitor) { + if (visitor.visitInvertedSection(_name)) { + for (Template.Segment seg : _segs) { + seg.visit(visitor); + } + } + } @Override public String toString () { return "Inverted(" + _name + ":" + _line + "): " + Arrays.toString(_segs); } @@ -932,6 +994,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 void visit (Visitor visit) {} @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 91f6cef..3f92402 100644 --- a/src/main/java/com/samskivert/mustache/Template.java +++ b/src/main/java/com/samskivert/mustache/Template.java @@ -149,6 +149,16 @@ public class Template { executeSegs(new Context(context, pctx, 0, false, false), out); } + /** + * Visits the tags in this template (via {@code visitor}) without executing it. + * @param visitor the visitor to be called back on each tag in the template. + */ + public void visit (Mustache.Visitor visitor) { + for (Segment seg : _segs) { + seg.visit(visitor); + } + } + protected Template (Segment[] segs, Mustache.Compiler compiler) { _segs = segs; _compiler = compiler; @@ -367,6 +377,8 @@ public class Template { abstract void decompile (Mustache.Delims delims, StringBuilder into); + abstract void visit (Mustache.Visitor visitor); + 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 a3d0209..8bd8a5b 100644 --- a/src/test/java/com/samskivert/mustache/SharedTests.java +++ b/src/test/java/com/samskivert/mustache/SharedTests.java @@ -8,11 +8,7 @@ import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.io.Writer; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import com.google.gwt.junit.client.GWTTestCase; @@ -348,6 +344,33 @@ public abstract class SharedTests extends GWTTestCase check("Text( \\r\\n)3/-1", str.trimTrailBlank().toString()); } + static class GetKeysVisitor implements Mustache.Visitor { + public final Set keys = new HashSet(); + public void visitText (String text) {} + public void visitVariable (String name) { keys.add(name); } + public boolean visitInclude (String name) { keys.add(name); return true; } + public boolean visitSection (String name) { keys.add(name); return true; } + public boolean visitInvertedSection (String name) { keys.add(name); return true; } + } + + @Test public void testVisit() { + String template = "{{#one}}1{{/one}} {{^two}}2{{three}}{{/two}}{{four}}"; + GetKeysVisitor viz = new GetKeysVisitor(); + Mustache.compiler().compile(template).visit(viz); + List expect = Arrays.asList("one", "two", "three", "four"); + assertEquals(new HashSet<>(expect), viz.keys); + } + + @Test public void testVisitNested() { + String template = "{{#one}}1{{/one}} {{^two}}" + + "2{{two_and_half}}{{#five}}{{three}}{{/five}}" + + "{{/two}} {{#one}}{{three}}{{/one}} {{four}}{{! ignore me }}"; + GetKeysVisitor viz = new GetKeysVisitor(); + Mustache.compiler().compile(template).visit(viz); + List expect = Arrays.asList("one", "two", "three", "four", "two_and_half", "five"); + assertEquals(new HashSet<>(expect), viz.keys); + } + protected void testNewlineSkipping (String sep) { String tmpl = "list:" + sep + "{{#items}}" + sep +