Split text parsing and rendering into two filters
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 25 Jul 2015 08:51:31 +0000 (10:51 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 25 Jul 2015 08:56:41 +0000 (10:56 +0200)
14 files changed:
src/main/java/net/pterodactylus/sone/template/ParserFilter.java
src/main/java/net/pterodactylus/sone/template/RenderFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java
src/main/resources/templates/imageBrowser.html
src/main/resources/templates/include/browseAlbums.html
src/main/resources/templates/include/viewPost.html
src/main/resources/templates/include/viewReply.html
src/main/resources/templates/invalid.html
src/main/resources/templates/notify/newVersionNotification.html
src/main/resources/templates/notify/soneInsertNotification.html
src/main/resources/templates/viewSone.html
src/test/java/net/pterodactylus/sone/template/ParserFilterTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/template/RenderFilterTest.java [new file with mode: 0644]

index 2aafd1f..2625441 100644 (file)
 package net.pterodactylus.sone.template;
 
 import static java.lang.String.valueOf;
-import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
 
 import java.io.IOException;
 import java.io.StringReader;
-import java.io.StringWriter;
-import java.io.UnsupportedEncodingException;
-import java.io.Writer;
-import java.net.URLEncoder;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Collections;
 import java.util.Map;
 
 import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.text.AlbumPart;
-import net.pterodactylus.sone.text.FreenetLinkPart;
-import net.pterodactylus.sone.text.LinkPart;
 import net.pterodactylus.sone.text.Part;
-import net.pterodactylus.sone.text.PlainTextPart;
-import net.pterodactylus.sone.text.PostPart;
-import net.pterodactylus.sone.text.SonePart;
 import net.pterodactylus.sone.text.SoneTextParser;
 import net.pterodactylus.sone.text.SoneTextParserContext;
 import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Filter;
-import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.template.TemplateContextFactory;
-import net.pterodactylus.util.template.TemplateParser;
 
 /**
- * Filter that filters a given text through a {@link SoneTextParser}.
+ * Filter that filters a given text through a {@link SoneTextParser} and returns the parsed {@link Part}s.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
 public class ParserFilter implements Filter {
 
-       /** The core. */
        private final Core core;
-
-       /** The link parser. */
        private final SoneTextParser soneTextParser;
 
-       /** The template context factory. */
-       private final TemplateContextFactory templateContextFactory;
-
-       /** The template for {@link PlainTextPart}s. */
-       private static final Template plainTextTemplate = TemplateParser.parse(new StringReader("<%text|html>"));
-
-       /** The template for {@link FreenetLinkPart}s. */
-       private static final Template linkTemplate = TemplateParser.parse(new StringReader("<a class=\"<%cssClass|html>\" href=\"<%link|html>\" title=\"<%title|html>\"><%text|html></a>"));
-
-       /**
-        * Creates a new filter that runs its input through a {@link SoneTextParser}
-        * .
-        *
-        * @param core
-        *            The core
-        * @param templateContextFactory
-        *            The context factory for rendering the parts
-        * @param soneTextParser
-        *            The Sone text parser
-        */
-       public ParserFilter(Core core, TemplateContextFactory templateContextFactory, SoneTextParser soneTextParser) {
+       public ParserFilter(Core core, SoneTextParser soneTextParser) {
                this.core = core;
-               this.templateContextFactory = templateContextFactory;
                this.soneTextParser = soneTextParser;
        }
 
@@ -94,217 +54,18 @@ public class ParserFilter implements Filter {
        @Override
        public Object format(TemplateContext templateContext, Object data, Map<String, Object> parameters) {
                String text = valueOf(data);
-               int length = parseInt(valueOf(parameters.get("length")), -1);
-               int cutOffLength = parseInt(valueOf(parameters.get("cut-off-length")), length);
                Object sone = parameters.get("sone");
                if (sone instanceof String) {
                        sone = core.getSone((String) sone).orNull();
                }
                FreenetRequest request = (FreenetRequest) templateContext.get("request");
                SoneTextParserContext context = new SoneTextParserContext(request, (Sone) sone);
-               StringWriter parsedTextWriter = new StringWriter();
                try {
-                       Iterable<Part> parts = soneTextParser.parse(context, new StringReader(text));
-                       if (length > -1) {
-                               int allPartsLength = 0;
-                               List<Part> shortenedParts = new ArrayList<Part>();
-                               for (Part part : parts) {
-                                       if (part instanceof PlainTextPart) {
-                                               String longText = ((PlainTextPart) part).getText();
-                                               if (allPartsLength < cutOffLength) {
-                                                       if ((allPartsLength + longText.length()) > cutOffLength) {
-                                                               shortenedParts.add(new PlainTextPart(longText.substring(0, cutOffLength - allPartsLength) + "…"));
-                                                       } else {
-                                                               shortenedParts.add(part);
-                                                       }
-                                               }
-                                               allPartsLength += longText.length();
-                                       } else if (part instanceof LinkPart) {
-                                               if (allPartsLength < cutOffLength) {
-                                                       shortenedParts.add(part);
-                                               }
-                                               allPartsLength += ((LinkPart) part).getText().length();
-                                       } else {
-                                               if (allPartsLength < cutOffLength) {
-                                                       shortenedParts.add(part);
-                                               }
-                                       }
-                               }
-                               if (allPartsLength >= length) {
-                                       parts = shortenedParts;
-                               }
-                       }
-                       render(parsedTextWriter, parts);
+                       return soneTextParser.parse(context, new StringReader(text));
                } catch (IOException ioe1) {
-                       /* no exceptions in a StringReader or StringWriter, ignore. */
+                       /* no exceptions in a StringReader, ignore. */
+                       return Collections.<Part>emptyList();
                }
-               return parsedTextWriter.toString();
-       }
-
-       //
-       // PRIVATE METHODS
-       //
-
-       /**
-        * Renders the given parts.
-        *
-        * @param writer
-        *            The writer to render the parts to
-        * @param parts
-        *            The parts to render
-        */
-       private void render(Writer writer, Iterable<Part> parts) {
-               for (Part part : parts) {
-                       render(writer, part);
-               }
-       }
-
-       /**
-        * Renders the given part.
-        *
-        * @param writer
-        *            The writer to render the part to
-        * @param part
-        *            The part to render
-        */
-       @SuppressWarnings("unchecked")
-       private void render(Writer writer, Part part) {
-               if (part instanceof PlainTextPart) {
-                       render(writer, (PlainTextPart) part);
-               } else if (part instanceof FreenetLinkPart) {
-                       render(writer, (FreenetLinkPart) part);
-               } else if (part instanceof LinkPart) {
-                       render(writer, (LinkPart) part);
-               } else if (part instanceof SonePart) {
-                       render(writer, (SonePart) part);
-               } else if (part instanceof PostPart) {
-                       render(writer, (PostPart) part);
-               } else if (part instanceof AlbumPart) {
-                       render(writer, (AlbumPart) part);
-               } else if (part instanceof Iterable<?>) {
-                       render(writer, (Iterable<Part>) part);
-               }
-       }
-
-       /**
-        * Renders the given plain-text part.
-        *
-        * @param writer
-        *            The writer to render the part to
-        * @param plainTextPart
-        *            The part to render
-        */
-       private void render(Writer writer, PlainTextPart plainTextPart) {
-               TemplateContext templateContext = templateContextFactory.createTemplateContext();
-               templateContext.set("text", plainTextPart.getText());
-               plainTextTemplate.render(templateContext, writer);
-       }
-
-       /**
-        * Renders the given freenet link part.
-        *
-        * @param writer
-        *            The writer to render the part to
-        * @param freenetLinkPart
-        *            The part to render
-        */
-       private void render(Writer writer, FreenetLinkPart freenetLinkPart) {
-               renderLink(writer, "/" + freenetLinkPart.getLink(), freenetLinkPart.getText(), freenetLinkPart.getTitle(), freenetLinkPart.isTrusted() ? "freenet-trusted" : "freenet");
-       }
-
-       /**
-        * Renders the given link part.
-        *
-        * @param writer
-        *            The writer to render the part to
-        * @param linkPart
-        *            The part to render
-        */
-       private void render(Writer writer, LinkPart linkPart) {
-               try {
-                       renderLink(writer, "/external-link/?_CHECKED_HTTP_=" + URLEncoder.encode(linkPart.getLink(), "UTF-8"), linkPart.getText(), linkPart.getTitle(), "internet");
-               } catch (UnsupportedEncodingException uee1) {
-                       /* not possible for UTF-8. */
-                       throw new RuntimeException("The JVM does not support UTF-8 encoding!", uee1);
-               }
-       }
-
-       /**
-        * Renders the given Sone part.
-        *
-        * @param writer
-        *            The writer to render the part to
-        * @param sonePart
-        *            The part to render
-        */
-       private void render(Writer writer, SonePart sonePart) {
-               if ((sonePart.getSone() != null) && (sonePart.getSone().getName() != null)) {
-                       renderLink(writer, "viewSone.html?sone=" + sonePart.getSone().getId(), SoneAccessor.getNiceName(sonePart.getSone()), SoneAccessor.getNiceName(sonePart.getSone()), "in-sone");
-               } else {
-                       renderLink(writer, "/WebOfTrust/ShowIdentity?id=" + sonePart.getSone().getId(), sonePart.getSone().getId(), sonePart.getSone().getId(), "in-sone");
-               }
-       }
-
-       /**
-        * Renders the given post part.
-        *
-        * @param writer
-        *            The writer to render the part to
-        * @param postPart
-        *            The part to render
-        */
-       private void render(Writer writer, PostPart postPart) {
-               SoneTextParser parser = new SoneTextParser(core, core, core);
-               SoneTextParserContext parserContext = new SoneTextParserContext(null, postPart.getPost().getSone());
-               try {
-                       Iterable<Part> parts = parser.parse(parserContext, new StringReader(postPart.getPost().getText()));
-                       StringBuilder excerpt = new StringBuilder();
-                       for (Part part : parts) {
-                               excerpt.append(part.getText());
-                               if (excerpt.length() > 20) {
-                                       int lastSpace = excerpt.lastIndexOf(" ", 20);
-                                       if (lastSpace > -1) {
-                                               excerpt.setLength(lastSpace);
-                                       } else {
-                                               excerpt.setLength(20);
-                                       }
-                                       excerpt.append("…");
-                                       break;
-                               }
-                       }
-                       renderLink(writer, "viewPost.html?post=" + postPart.getPost().getId(), excerpt.toString(),
-                               SoneAccessor.getNiceName(postPart.getPost().getSone()), postPart.usesDeprecatedLink() ? "internet" : "in-sone");
-               } catch (IOException ioe1) {
-                       /* StringReader shouldn’t throw. */
-               }
-       }
-
-       private void render(Writer writer, AlbumPart albumPart) {
-               Album album = albumPart.getAlbum();
-               renderLink(writer, String.format("imageBrowser.html?album=%s", album.getId()), album.getTitle(), album.getDescription(), "in-sone");
-       }
-
-       /**
-        * Renders the given link.
-        *
-        * @param writer
-        *            The writer to render the link to
-        * @param link
-        *            The link to render
-        * @param text
-        *            The text of the link
-        * @param title
-        *            The title of the link
-        * @param cssClass
-        *            The CSS class of the link
-        */
-       private void renderLink(Writer writer, String link, String text, String title, String cssClass) {
-               TemplateContext templateContext = templateContextFactory.createTemplateContext();
-               templateContext.set("cssClass", cssClass);
-               templateContext.set("link", link);
-               templateContext.set("text", text);
-               templateContext.set("title", title);
-               linkTemplate.render(templateContext, writer);
        }
 
 }
diff --git a/src/main/java/net/pterodactylus/sone/template/RenderFilter.java b/src/main/java/net/pterodactylus/sone/template/RenderFilter.java
new file mode 100644 (file)
index 0000000..1ab0d9e
--- /dev/null
@@ -0,0 +1,190 @@
+package net.pterodactylus.sone.template;
+
+import static java.lang.String.valueOf;
+import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.text.AlbumPart;
+import net.pterodactylus.sone.text.FreenetLinkPart;
+import net.pterodactylus.sone.text.LinkPart;
+import net.pterodactylus.sone.text.Part;
+import net.pterodactylus.sone.text.PartContainer;
+import net.pterodactylus.sone.text.PlainTextPart;
+import net.pterodactylus.sone.text.PostPart;
+import net.pterodactylus.sone.text.SonePart;
+import net.pterodactylus.sone.text.SoneTextParser;
+import net.pterodactylus.sone.text.SoneTextParserContext;
+import net.pterodactylus.util.template.Filter;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.template.TemplateContextFactory;
+import net.pterodactylus.util.template.TemplateParser;
+
+/**
+ * {@link Filter} implementation that renders an {@link Iterable} (such as a {@link PartContainer}) of {@link Part}s to HTML.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class RenderFilter implements Filter {
+
+       private static final Template plainTextTemplate = TemplateParser.parse(new StringReader("<%text|html>"));
+       private static final Template linkTemplate =
+                       TemplateParser.parse(new StringReader("<a class=\"<%cssClass|html>\" href=\"<%link|html>\" title=\"<%title|html>\"><%text|html></a>"));
+       private final Core core;
+       private final TemplateContextFactory templateContextFactory;
+
+       public RenderFilter(Core core, TemplateContextFactory templateContextFactory) {
+               this.core = core;
+               this.templateContextFactory = templateContextFactory;
+       }
+
+       @Override
+       public Object format(TemplateContext templateContext, Object data, Map<String, Object> parameters) {
+               if (!(data instanceof Iterable<?>)) {
+                       return null;
+               }
+               Iterable<Part> parts = (Iterable<Part>) data;
+               int length = parseInt(valueOf(parameters.get("length")), -1);
+               int cutOffLength = parseInt(valueOf(parameters.get("cut-off-length")), length);
+               StringWriter parsedTextWriter = new StringWriter();
+               if (length > -1) {
+                       int allPartsLength = 0;
+                       List<Part> shortenedParts = new ArrayList<Part>();
+                       for (Part part : parts) {
+                               if (part instanceof PlainTextPart) {
+                                       String longText = part.getText();
+                                       if (allPartsLength < cutOffLength) {
+                                               if ((allPartsLength + longText.length()) > cutOffLength) {
+                                                       shortenedParts.add(new PlainTextPart(longText.substring(0, cutOffLength - allPartsLength) + "…"));
+                                               } else {
+                                                       shortenedParts.add(part);
+                                               }
+                                       }
+                                       allPartsLength += longText.length();
+                               } else if (part instanceof LinkPart) {
+                                       if (allPartsLength < cutOffLength) {
+                                               shortenedParts.add(part);
+                                       }
+                                       allPartsLength += part.getText().length();
+                               } else {
+                                       if (allPartsLength < cutOffLength) {
+                                               shortenedParts.add(part);
+                                       }
+                               }
+                       }
+                       if (allPartsLength >= length) {
+                               parts = shortenedParts;
+                       }
+               }
+               render(parsedTextWriter, parts);
+               return parsedTextWriter.toString();
+       }
+
+       private void render(Writer writer, Iterable<Part> parts) {
+               for (Part part : parts) {
+                       render(writer, part);
+               }
+       }
+
+       private void render(Writer writer, Part part) {
+               if (part instanceof PlainTextPart) {
+                       render(writer, (PlainTextPart) part);
+               } else if (part instanceof FreenetLinkPart) {
+                       render(writer, (FreenetLinkPart) part);
+               } else if (part instanceof LinkPart) {
+                       render(writer, (LinkPart) part);
+               } else if (part instanceof SonePart) {
+                       render(writer, (SonePart) part);
+               } else if (part instanceof PostPart) {
+                       render(writer, (PostPart) part);
+               } else if (part instanceof AlbumPart) {
+                       render(writer, (AlbumPart) part);
+               } else if (part instanceof Iterable<?>) {
+                       render(writer, (Iterable<Part>) part);
+               }
+       }
+
+       private void render(Writer writer, PlainTextPart plainTextPart) {
+               TemplateContext templateContext = templateContextFactory.createTemplateContext();
+               templateContext.set("text", plainTextPart.getText());
+               plainTextTemplate.render(templateContext, writer);
+       }
+
+       private void render(Writer writer, FreenetLinkPart freenetLinkPart) {
+               renderLink(writer, "/" + freenetLinkPart.getLink(), freenetLinkPart.getText(), freenetLinkPart.getTitle(),
+                               freenetLinkPart.isTrusted() ? "freenet-trusted" : "freenet");
+       }
+
+       private void render(Writer writer, LinkPart linkPart) {
+               try {
+                       renderLink(writer, "/external-link/?_CHECKED_HTTP_=" + URLEncoder.encode(linkPart.getLink(), "UTF-8"), linkPart.getText(),
+                                       linkPart.getTitle(), "internet");
+               } catch (UnsupportedEncodingException uee1) {
+                       /* not possible for UTF-8. */
+                       throw new RuntimeException("The JVM does not support UTF-8 encoding!", uee1);
+               }
+       }
+
+       private void render(Writer writer, SonePart sonePart) {
+               Sone sone = sonePart.getSone();
+               if ((sone != null) && (sone.getName() != null)) {
+                       String niceName = SoneAccessor.getNiceName(sone);
+                       renderLink(writer, "viewSone.html?sone=" + sone.getId(), niceName, niceName, "in-sone");
+               } else {
+                       renderLink(writer, "/WebOfTrust/ShowIdentity?id=" + sone.getId(), sone.getId(), sone.getId(), "in-sone");
+               }
+       }
+
+       private void render(Writer writer, PostPart postPart) {
+               SoneTextParser parser = new SoneTextParser(core, core, core);
+               SoneTextParserContext parserContext = new SoneTextParserContext(null, postPart.getPost().getSone());
+               try {
+                       Iterable<Part> parts = parser.parse(parserContext, new StringReader(postPart.getPost().getText()));
+                       StringBuilder excerpt = new StringBuilder();
+                       for (Part part : parts) {
+                               excerpt.append(part.getText());
+                               if (excerpt.length() > 20) {
+                                       int lastSpace = excerpt.lastIndexOf(" ", 20);
+                                       if (lastSpace > -1) {
+                                               excerpt.setLength(lastSpace);
+                                       } else {
+                                               excerpt.setLength(20);
+                                       }
+                                       excerpt.append("…");
+                                       break;
+                               }
+                       }
+                       renderLink(writer, "viewPost.html?post=" + postPart.getPost().getId(), excerpt.toString(),
+                                       SoneAccessor.getNiceName(postPart.getPost().getSone()), postPart.usesDeprecatedLink() ? "internet" : "in-sone");
+               } catch (IOException ioe1) {
+                       /* StringReader shouldn’t throw. */
+               }
+       }
+
+       private void render(Writer writer, AlbumPart albumPart) {
+               Album album = albumPart.getAlbum();
+               renderLink(writer, String.format("imageBrowser.html?album=%s", album.getId()), album.getTitle(), album.getDescription(), "in-sone");
+       }
+
+       private void renderLink(Writer writer, String link, String text, String title, String cssClass) {
+               TemplateContext templateContext = templateContextFactory.createTemplateContext();
+               templateContext.set("cssClass", cssClass);
+               templateContext.set("link", link);
+               templateContext.set("text", text);
+               templateContext.set("title", title);
+               linkTemplate.render(templateContext, writer);
+       }
+
+}
index 0b7ec52..c1fae80 100644 (file)
@@ -87,6 +87,7 @@ import net.pterodactylus.sone.template.JavascriptFilter;
 import net.pterodactylus.sone.template.ParserFilter;
 import net.pterodactylus.sone.template.PostAccessor;
 import net.pterodactylus.sone.template.ProfileAccessor;
+import net.pterodactylus.sone.template.RenderFilter;
 import net.pterodactylus.sone.template.ReplyAccessor;
 import net.pterodactylus.sone.template.ReplyGroupFilter;
 import net.pterodactylus.sone.template.RequestChangeFilter;
@@ -197,6 +198,7 @@ public class WebInterface {
 
        /** The parser filter. */
        private final ParserFilter parserFilter;
+       private final RenderFilter renderFilter;
 
        /** The “new Sone” notification. */
        private final ListNotification<Sone> newSoneNotification;
@@ -275,8 +277,9 @@ public class WebInterface {
                templateContextFactory.addFilter("match", new MatchFilter());
                templateContextFactory.addFilter("css", new CssClassNameFilter());
                templateContextFactory.addFilter("js", new JavascriptFilter());
-               templateContextFactory.addFilter("parse", parserFilter = new ParserFilter(getCore(), templateContextFactory, soneTextParser));
+               templateContextFactory.addFilter("parse", parserFilter = new ParserFilter(getCore(), soneTextParser));
                templateContextFactory.addFilter("reparse", new ReparseFilter());
+               templateContextFactory.addFilter("render", renderFilter = new RenderFilter(getCore(), templateContextFactory));
                templateContextFactory.addFilter("unknown", new UnknownDateFilter(getL10n(), "View.Sone.Text.UnknownDate"));
                templateContextFactory.addFilter("format", new FormatFilter());
                templateContextFactory.addFilter("sort", new CollectionSortFilter());
@@ -724,7 +727,7 @@ public class WebInterface {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new FollowSoneAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnfollowSoneAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new EditAlbumAjaxPage(this)));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new EditImageAjaxPage(this, parserFilter)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new EditImageAjaxPage(this, parserFilter, renderFilter)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new TrustAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DistrustAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UntrustAjaxPage(this)));
index 79de200..a40a955 100644 (file)
 
 package net.pterodactylus.sone.web.ajax;
 
+import java.util.Collections;
+
 import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.template.ParserFilter;
+import net.pterodactylus.sone.template.RenderFilter;
+import net.pterodactylus.sone.text.Part;
 import net.pterodactylus.sone.text.TextFilter;
 import net.pterodactylus.sone.web.WebInterface;
 import net.pterodactylus.sone.web.page.FreenetRequest;
@@ -33,8 +37,8 @@ import com.google.common.collect.ImmutableMap;
  */
 public class EditImageAjaxPage extends JsonPage {
 
-       /** Parser for image descriptions. */
        private final ParserFilter parserFilter;
+       private final RenderFilter renderFilter;
 
        /**
         * Creates a new edit image AJAX page.
@@ -44,9 +48,10 @@ public class EditImageAjaxPage extends JsonPage {
         * @param parserFilter
         *            The parser filter for image descriptions
         */
-       public EditImageAjaxPage(WebInterface webInterface, ParserFilter parserFilter) {
+       public EditImageAjaxPage(WebInterface webInterface, ParserFilter parserFilter, RenderFilter renderFilter) {
                super("editImage.ajax", webInterface);
                this.parserFilter = parserFilter;
+               this.renderFilter = renderFilter;
        }
 
        //
@@ -83,7 +88,16 @@ public class EditImageAjaxPage extends JsonPage {
                String description = request.getHttpRequest().getParam("description").trim();
                image.modify().setTitle(title).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).update();
                webInterface.getCore().touchConfiguration();
-               return createSuccessJsonObject().put("imageId", image.getId()).put("title", image.getTitle()).put("description", image.getDescription()).put("parsedDescription", (String) parserFilter.format(new TemplateContext(), image.getDescription(), ImmutableMap.<String, Object>builder().put("sone", image.getSone()).build()));
+               return createSuccessJsonObject().put("imageId", image.getId())
+                               .put("title", image.getTitle())
+                               .put("description", image.getDescription())
+                               .put("parsedDescription", parseDescription(image));
+       }
+
+       private String parseDescription(Image image) {
+               Iterable<Part> parts = (Iterable<Part>) parserFilter.format(new TemplateContext(), image.getDescription(),
+                               ImmutableMap.<String, Object>builder().put("sone", image.getSone()).build());
+               return (String) renderFilter.format(new TemplateContext(), parts, Collections.<String, Object>emptyMap());
        }
 
 }
index 7dd4c29..d399862 100644 (file)
                                <%/foreach>
                        </div>
 
-                       <p id="description"><% album.description|parse sone=album.sone></p>
+                       <p id="description"><% album.description|parse sone=album.sone|render></p>
 
                        <%if album.sone.local>
                                <div class="show-edit-album hidden toggle-link"><a class="small-link">» <%= Page.ImageBrowser.Album.Edit.Title|l10n|html></a></div>
                                        </div>
                                        <div class="show-data">
                                                <div class="image-title"><% image.title|html></div>
-                                               <div class="image-description"><% image.description|parse sone=image.sone></div>
+                                               <div class="image-description"><% image.description|parse sone=image.sone|render></div>
                                        </div>
                                        <%if album.sone.local>
                                                <form class="edit-image" action="editImage.html" method="post">
                                <%/if>
                        </div>
 
-                       <p class="parsed"><%image.description|parse sone=image.sone></p>
+                       <p class="parsed"><%image.description|parse sone=image.sone|render></p>
 
                        <%if image.sone.local>
 
                                <div class="show-data">
                                        <div class="album-sone"><a href="imageBrowser.html?sone=<%album.sone.id|html>"><%album.sone.niceName|html></a></div>
                                        <div class="album-title"><% album.title|html> (<%= View.Sone.Stats.Images|l10n 0=album.images.size>)</div>
-                                       <div class="album-description"><% album.description|parse sone=album.sone></div>
+                                       <div class="album-description"><% album.description|parse sone=album.sone|render></div>
                                </div>
                        </div>
                        <%= false|store key==endRow>
index 9aacad3..7c8c490 100644 (file)
@@ -14,7 +14,7 @@
                </div>
                <div class="show-data">
                        <div class="album-title"><% album.title|html> (<%= View.Sone.Stats.Images|l10n 0=album.images.size>)</div>
-                       <div class="album-description"><% album.description|parse sone=album.sone></div>
+                       <div class="album-description"><% album.description|parse sone=album.sone|render></div>
                </div>
                <%if album.sone.local>
                        <form class="edit-album" action="editAlbum.html" method="post">
index 9a24671..f899e36 100644 (file)
@@ -27,8 +27,9 @@
                                <%/if>
                        <%/if>
                        <% post.text|html|store key==originalText text==true>
-                       <% post.text|parse sone=post.sone|store key==parsedText text==true>
-                       <% post.text|parse sone=post.sone length=core.preferences.charactersPerPost cut-off-length=core.preferences.postCutOffLength|store key==shortText text==true>
+                       <% post.text|parse sone=post.sone|store key==parts>
+                       <% parts|render|store key==parsedText text==true>
+                       <% parts|render length=core.preferences.charactersPerPost cut-off-length=core.preferences.postCutOffLength|store key==shortText text==true>
                        <div class="post-text raw-text<%if !raw> hidden<%/if>"><% originalText></div>
                        <div class="post-text text<%if raw> hidden<%/if><%if !shortText|match key=parsedText> hidden<%/if>"><% parsedText></div>
                        <div class="post-text short-text<%if raw> hidden<%/if><%if shortText|match key=parsedText> hidden<%/if>"><% shortText></div>
index 2e56275..ec8b447 100644 (file)
@@ -15,8 +15,9 @@
                <div>
                        <div class="author profile-link"><a href="viewSone.html?sone=<% reply.sone.id|html>"><% reply.sone.niceName|html></a></div>
                        <% reply.text|html|store key==originalText text==true>
-                       <% reply.text|parse sone=reply.sone|store key==parsedText text==true>
-                       <% reply.text|parse sone=reply.sone length=core.preferences.charactersPerPost cut-off-length=core.preferences.postCutOffLength|store key==shortText text==true>
+                       <% reply.text|parse sone=reply.sone|store key==parts>
+                       <% parts|render|store key==parsedText text==true>
+                       <% parts|render length=core.preferences.charactersPerPost cut-off-length=core.preferences.postCutOffLength|store key==shortText text==true>
                        <div class="reply-text raw-text<%if !raw> hidden<%/if>"><% originalText></div>
                        <div class="reply-text text<%if raw> hidden<%/if><%if !shortText|match key=parsedText> hidden<%/if>"><% parsedText></div>
                        <div class="reply-text short-text<%if raw> hidden<%/if><%if shortText|match key=parsedText> hidden<%/if>"><% shortText></div>
index 3ac37cf..922829c 100644 (file)
@@ -4,9 +4,9 @@
 
        <%foreach messages message>
                <%if message|substring start==0 length==1|match value=='!'>
-                       <p class="error"><% message|substring start==1|parse></p>
+                       <p class="error"><% message|substring start==1|parse|render></p>
                <%else>
-                       <p><% message|parse></p>
+                       <p><% message|parse|render></p>
                <%/if>
        <%foreachelse>
                <p><%= Page.Invalid.Text|l10n|html|replace needle=="{link}" replacement=='<a href="index.html">'|replace needle=="{/link}" replacement=='</a>'></p>
index 599a7e6..4cee9ce 100644 (file)
@@ -1 +1 @@
-<div class="text"><%= Notification.NewVersion.Text|l10n|replace needle=="{version}" replacement=latestVersion|replace needle=="{edition}" replacement=latestEdition|parse sone=="nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI"></div>
+<div class="text"><%= Notification.NewVersion.Text|l10n|replace needle=="{version}" replacement=latestVersion|replace needle=="{edition}" replacement=latestEdition|parse sone=="nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI"|render></div>
index c15864f..8965e36 100644 (file)
@@ -1,7 +1,7 @@
 <%if soneStatus|match value=="inserting">
-       <%= Notification.SoneIsInserting.Text|l10n 0=insertSone.id|parse>
+       <%= Notification.SoneIsInserting.Text|l10n 0=insertSone.id|parse|render>
 <%elseif soneStatus|match value=="inserted">
-       <%= Notification.SoneIsInserted.Text|l10n 0=insertSone.id 1=insertDuration|parse>
+       <%= Notification.SoneIsInserted.Text|l10n 0=insertSone.id 1=insertDuration|parse|render>
 <%elseif soneStatus|match value=="insert-aborted">
-       <%= Notification.SoneInsertAborted.Text|l10n 0=insertSone.id|parse>
+       <%= Notification.SoneInsertAborted.Text|l10n 0=insertSone.id|parse|render>
 <%/if>
index 4b88474..dd994ea 100644 (file)
@@ -52,7 +52,7 @@
                        <%foreach sone.profile.fields field>
                                <div class="profile-field">
                                        <div class="name"><% field.name|html></div>
-                                       <div class="value"><% field.value|parse sone=sone></div>
+                                       <div class="value"><% field.value|parse sone=sone|render></div>
                                </div>
                        <%/foreach>
 
diff --git a/src/test/java/net/pterodactylus/sone/template/ParserFilterTest.java b/src/test/java/net/pterodactylus/sone/template/ParserFilterTest.java
new file mode 100644 (file)
index 0000000..268a3fa
--- /dev/null
@@ -0,0 +1,83 @@
+package net.pterodactylus.sone.template;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.text.Part;
+import net.pterodactylus.sone.text.PlainTextPart;
+import net.pterodactylus.sone.text.SoneTextParser;
+import net.pterodactylus.sone.text.SoneTextParserContext;
+import net.pterodactylus.util.template.TemplateContext;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableMap;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+/**
+ * Unit test for {@link ParserFilter}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ParserFilterTest {
+
+       private final Core core = mock(Core.class);
+       private final SoneTextParser soneTextParser = mock(SoneTextParser.class);
+       private final ParserFilter parserFilter = new ParserFilter(core, soneTextParser);
+       private final TemplateContext templateContext = new TemplateContext();
+       private final Sone sone = mock(Sone.class);
+
+       @Test
+       public void filterReturnsPartsReturnedByParser() throws IOException {
+               List<Part> parts = setupSoneTextParser();
+               assertThat(parserFilter.format(templateContext, "Text", Collections.<String, Object>emptyMap()), is((Object) parts));
+       }
+
+       private List<Part> setupSoneTextParser() throws IOException {
+               List<Part> parts = Arrays.<Part>asList(new PlainTextPart("Text"));
+               when(soneTextParser.parse(any(SoneTextParserContext.class), any(StringReader.class))).thenReturn(parts);
+               return parts;
+       }
+
+       @Test
+       public void filterUsesGivenSone() throws IOException {
+               List<Part> parts = setupSoneTextParser();
+               assertThat(parserFilter.format(templateContext, "Text", ImmutableMap.<String, Object>of("sone", sone)), is((Object) parts));
+               verifyThatContextContainsCorrectSone();
+       }
+
+       @Test
+       public void filterGetsCorrectSoneFromCore() throws IOException {
+               when(core.getSone("sone-id")).thenReturn(Optional.of(sone));
+               List<Part> parts = setupSoneTextParser();
+               assertThat(parserFilter.format(templateContext, "Text", ImmutableMap.<String, Object>of("sone", "sone-id")), is((Object) parts));
+               verifyThatContextContainsCorrectSone();
+       }
+
+       private void verifyThatContextContainsCorrectSone() throws IOException {
+               ArgumentCaptor<SoneTextParserContext> contextArgumentCaptor = ArgumentCaptor.forClass(SoneTextParserContext.class);
+               verify(soneTextParser).parse(contextArgumentCaptor.capture(), any(StringReader.class));
+               assertThat(contextArgumentCaptor.getValue().getPostingSone(), is(sone));
+       }
+
+       @Test
+       public void filterReturnsEmptyCollectionOnExceptionInParser() throws IOException {
+               when(soneTextParser.parse(any(SoneTextParserContext.class), any(StringReader.class))).thenThrow(IOException.class);
+               assertThat((Collection<Part>) parserFilter.format(templateContext, "Text", Collections.<String, Object>emptyMap()), empty());
+       }
+
+}
diff --git a/src/test/java/net/pterodactylus/sone/template/RenderFilterTest.java b/src/test/java/net/pterodactylus/sone/template/RenderFilterTest.java
new file mode 100644 (file)
index 0000000..a600cdd
--- /dev/null
@@ -0,0 +1,172 @@
+package net.pterodactylus.sone.template;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.text.AlbumPart;
+import net.pterodactylus.sone.text.FreenetLinkPart;
+import net.pterodactylus.sone.text.LinkPart;
+import net.pterodactylus.sone.text.Part;
+import net.pterodactylus.sone.text.PlainTextPart;
+import net.pterodactylus.sone.text.PostPart;
+import net.pterodactylus.sone.text.SonePart;
+import net.pterodactylus.util.template.HtmlFilter;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.template.TemplateContextFactory;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit test for {@link RenderFilter}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class RenderFilterTest {
+
+       private static final Map<String, Object> EMPTY_MAP = Collections.emptyMap();
+       private final Core core = mock(Core.class);
+       private final TemplateContextFactory templateContextFactory = new TemplateContextFactory();
+       private final RenderFilter renderFilter = new RenderFilter(core, templateContextFactory);
+       private final TemplateContext templateContext = new TemplateContext();
+       private final Sone sone = mock(Sone.class);
+       private final Post post = mock(Post.class);
+
+       @Before
+       public void setupTemplateContextFactory() {
+               templateContextFactory.addFilter("html", new HtmlFilter());
+       }
+
+       @Before
+       public void setupSone() {
+               when(sone.getId()).thenReturn("sone-id");
+               when(sone.getName()).thenReturn("SoneName");
+               when(sone.getProfile()).thenReturn(new Profile(sone));
+       }
+
+       @Before
+       public void setupPost() {
+               when(post.getId()).thenReturn("post-id");
+               when(post.getSone()).thenReturn(sone);
+       }
+
+       @Test
+       public void filterCanRenderPlainText() {
+               List<Part> parts = Arrays.<Part>asList(new PlainTextPart("<Text>"));
+               assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP), is((Object) "&lt;Text&gt;"));
+       }
+
+       @Test
+       public void filterCanRenderMultiplePlainTextParts() {
+               List<Part> parts = Arrays.<Part>asList(new PlainTextPart("<Text>"), new PlainTextPart("<Foo>"));
+               assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP), is((Object) "&lt;Text&gt;&lt;Foo&gt;"));
+       }
+
+       @Test
+       public void filterCanRenderUntrustedFreenetLinks() {
+               List<Part> parts = Arrays.<Part>asList(new FreenetLinkPart("SSK@foo,bar/baz", "foo/baz", false));
+               assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP),
+                               is((Object) "<a class=\"freenet\" href=\"/SSK@foo,bar/baz\" title=\"foo/baz\">foo/baz</a>"));
+       }
+
+       @Test
+       public void filterCanRenderTrustedFreenetLinks() {
+               List<Part> parts = Arrays.<Part>asList(new FreenetLinkPart("SSK@foo,bar/baz", "foo/baz", true));
+               assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP),
+                               is((Object) "<a class=\"freenet-trusted\" href=\"/SSK@foo,bar/baz\" title=\"foo/baz\">foo/baz</a>"));
+       }
+
+       @Test
+       public void filterCanRenderInternetLinks() {
+               List<Part> parts = Arrays.<Part>asList(new LinkPart("http://link.sone", "link.sone"));
+               assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP),
+                               is((Object) "<a class=\"internet\" href=\"/external-link/?_CHECKED_HTTP_=http%3A%2F%2Flink.sone\" title=\"link.sone\">link.sone</a>"));
+       }
+
+       @Test
+       public void filterCanRenderSonePartsForUnknownSones() {
+               Sone unknownSone = mock(Sone.class);
+               when(unknownSone.getId()).thenReturn("sone-id");
+               List<Part> parts = Arrays.<Part>asList(new SonePart(unknownSone));
+               assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP),
+                               is((Object) "<a class=\"in-sone\" href=\"/WebOfTrust/ShowIdentity?id=sone-id\" title=\"sone-id\">sone-id</a>"));
+       }
+
+       @Test
+       public void filterCanRenderSonePartsForKnownSones() {
+               List<Part> parts = Arrays.<Part>asList(new SonePart(sone));
+               assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP),
+                               is((Object) "<a class=\"in-sone\" href=\"viewSone.html?sone=sone-id\" title=\"SoneName\">SoneName</a>"));
+       }
+
+       @Test
+       public void filterCanRenderPostParts() {
+               when(post.getText()).thenReturn("123456789012345678901234567890");
+               List<Part> parts = Arrays.<Part>asList(new PostPart(post));
+               assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP),
+                               is((Object) "<a class=\"in-sone\" href=\"viewPost.html?post=post-id\" title=\"SoneName\">12345678901234567890&hellip;</a>"));
+       }
+
+       @Test
+       public void filterCanRenderPostPartsWithoutBreakingWords() {
+               when(post.getText()).thenReturn("12345 12345 12345 12345 12345 12345");
+               List<Part> parts = Arrays.<Part>asList(new PostPart(post));
+               assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP),
+                               is((Object) "<a class=\"in-sone\" href=\"viewPost.html?post=post-id\" title=\"SoneName\">12345 12345 12345&hellip;</a>"));
+       }
+
+       @Test
+       public void filterCanRenderPostPartsWithShortText() {
+               when(post.getText()).thenReturn("12345 12345");
+               List<Part> parts = Arrays.<Part>asList(new PostPart(post));
+               assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP),
+                               is((Object) "<a class=\"in-sone\" href=\"viewPost.html?post=post-id\" title=\"SoneName\">12345 12345</a>"));
+       }
+
+       @Test
+       public void filterCanRenderPostPartsWithOldPostIds() {
+               when(post.getText()).thenReturn("12345 12345");
+               List<Part> parts = Arrays.<Part>asList(new PostPart(post, true));
+               assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP),
+                               is((Object) "<a class=\"internet\" href=\"viewPost.html?post=post-id\" title=\"SoneName\">12345 12345</a>"));
+       }
+
+       @Test
+       public void filterCanRenderAlbumParts() {
+               Album album = mock(Album.class);
+               when(album.getId()).thenReturn("album-id");
+               when(album.getTitle()).thenReturn("Title");
+               when(album.getDescription()).thenReturn("Description");
+               List<Part> parts = Arrays.<Part>asList(new AlbumPart(album));
+               assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP),
+                               is((Object) "<a class=\"in-sone\" href=\"imageBrowser.html?album=album-id\" title=\"Description\">Title</a>"));
+       }
+
+       @Test
+       public void filterHonorsLength() {
+               List<Part> parts = Arrays.<Part>asList(new PlainTextPart("12345678901234567890"));
+               assertThat(renderFilter.format(templateContext, parts, ImmutableMap.<String, Object>of("length", "10")),
+                               is((Object) "1234567890&hellip;"));
+       }
+
+       @Test
+       public void filterHonorsCutOffLength() {
+               List<Part> parts = Arrays.<Part>asList(new PlainTextPart("12345678901234567890"));
+               assertThat(renderFilter.format(templateContext, parts, ImmutableMap.<String, Object>of("length", "10", "cut-off-length", "5")),
+                               is((Object) "12345&hellip;"));
+       }
+
+}