From 5777a17f983636923bc2231daec8d0383955d1f5 Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Sat, 25 Jul 2015 10:51:31 +0200 Subject: [PATCH] Split text parsing and rendering into two filters --- .../pterodactylus/sone/template/ParserFilter.java | 251 +-------------------- .../pterodactylus/sone/template/RenderFilter.java | 190 ++++++++++++++++ .../net/pterodactylus/sone/web/WebInterface.java | 7 +- .../sone/web/ajax/EditImageAjaxPage.java | 20 +- src/main/resources/templates/imageBrowser.html | 8 +- .../resources/templates/include/browseAlbums.html | 2 +- src/main/resources/templates/include/viewPost.html | 5 +- .../resources/templates/include/viewReply.html | 5 +- src/main/resources/templates/invalid.html | 4 +- .../templates/notify/newVersionNotification.html | 2 +- .../templates/notify/soneInsertNotification.html | 6 +- src/main/resources/templates/viewSone.html | 2 +- .../sone/template/ParserFilterTest.java | 83 +++++++ .../sone/template/RenderFilterTest.java | 172 ++++++++++++++ 14 files changed, 491 insertions(+), 266 deletions(-) create mode 100644 src/main/java/net/pterodactylus/sone/template/RenderFilter.java create mode 100644 src/test/java/net/pterodactylus/sone/template/ParserFilterTest.java create mode 100644 src/test/java/net/pterodactylus/sone/template/RenderFilterTest.java diff --git a/src/main/java/net/pterodactylus/sone/template/ParserFilter.java b/src/main/java/net/pterodactylus/sone/template/ParserFilter.java index 2aafd1f..2625441 100644 --- a/src/main/java/net/pterodactylus/sone/template/ParserFilter.java +++ b/src/main/java/net/pterodactylus/sone/template/ParserFilter.java @@ -18,73 +18,33 @@ 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 David ‘Bombe’ Roden */ 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("\" href=\"<%link|html>\" title=\"<%title|html>\"><%text|html>")); - - /** - * 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 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 parts = soneTextParser.parse(context, new StringReader(text)); - if (length > -1) { - int allPartsLength = 0; - List shortenedParts = new ArrayList(); - 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.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 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); - } - } - - /** - * 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 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 index 0000000..1ab0d9e --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/template/RenderFilter.java @@ -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 David ‘Bombe’ Roden + */ +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("\" href=\"<%link|html>\" title=\"<%title|html>\"><%text|html>")); + 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 parameters) { + if (!(data instanceof Iterable)) { + return null; + } + Iterable parts = (Iterable) 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 shortenedParts = new ArrayList(); + 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 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); + } + } + + 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 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); + } + +} diff --git a/src/main/java/net/pterodactylus/sone/web/WebInterface.java b/src/main/java/net/pterodactylus/sone/web/WebInterface.java index 0b7ec52..c1fae80 100644 --- a/src/main/java/net/pterodactylus/sone/web/WebInterface.java +++ b/src/main/java/net/pterodactylus/sone/web/WebInterface.java @@ -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 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))); diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java index 79de200..a40a955 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java @@ -17,8 +17,12 @@ 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.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 parts = (Iterable) parserFilter.format(new TemplateContext(), image.getDescription(), + ImmutableMap.builder().put("sone", image.getSone()).build()); + return (String) renderFilter.format(new TemplateContext(), parts, Collections.emptyMap()); } } diff --git a/src/main/resources/templates/imageBrowser.html b/src/main/resources/templates/imageBrowser.html index 7dd4c29..d399862 100644 --- a/src/main/resources/templates/imageBrowser.html +++ b/src/main/resources/templates/imageBrowser.html @@ -341,7 +341,7 @@ <%/foreach> -

<% album.description|parse sone=album.sone>

+

<% album.description|parse sone=album.sone|render>

<%if album.sone.local> @@ -400,7 +400,7 @@
<% image.title|html>
-
<% image.description|parse sone=image.sone>
+
<% image.description|parse sone=image.sone|render>
<%if album.sone.local>
@@ -512,7 +512,7 @@ <%/if> -

<%image.description|parse sone=image.sone>

+

<%image.description|parse sone=image.sone|render>

<%if image.sone.local> @@ -630,7 +630,7 @@
<% album.title|html> (<%= View.Sone.Stats.Images|l10n 0=album.images.size>)
-
<% album.description|parse sone=album.sone>
+
<% album.description|parse sone=album.sone|render>
<%= false|store key==endRow> diff --git a/src/main/resources/templates/include/browseAlbums.html b/src/main/resources/templates/include/browseAlbums.html index 9aacad3..7c8c490 100644 --- a/src/main/resources/templates/include/browseAlbums.html +++ b/src/main/resources/templates/include/browseAlbums.html @@ -14,7 +14,7 @@
<% album.title|html> (<%= View.Sone.Stats.Images|l10n 0=album.images.size>)
-
<% album.description|parse sone=album.sone>
+
<% album.description|parse sone=album.sone|render>
<%if album.sone.local> diff --git a/src/main/resources/templates/include/viewPost.html b/src/main/resources/templates/include/viewPost.html index 9a24671..f899e36 100644 --- a/src/main/resources/templates/include/viewPost.html +++ b/src/main/resources/templates/include/viewPost.html @@ -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>
<% originalText>
<% parsedText>
<% shortText>
diff --git a/src/main/resources/templates/include/viewReply.html b/src/main/resources/templates/include/viewReply.html index 2e56275..ec8b447 100644 --- a/src/main/resources/templates/include/viewReply.html +++ b/src/main/resources/templates/include/viewReply.html @@ -15,8 +15,9 @@
<% 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>
<% originalText>
<% parsedText>
<% shortText>
diff --git a/src/main/resources/templates/invalid.html b/src/main/resources/templates/invalid.html index 3ac37cf..922829c 100644 --- a/src/main/resources/templates/invalid.html +++ b/src/main/resources/templates/invalid.html @@ -4,9 +4,9 @@ <%foreach messages message> <%if message|substring start==0 length==1|match value=='!'> -

<% message|substring start==1|parse>

+

<% message|substring start==1|parse|render>

<%else> -

<% message|parse>

+

<% message|parse|render>

<%/if> <%foreachelse>

<%= Page.Invalid.Text|l10n|html|replace needle=="{link}" replacement==''|replace needle=="{/link}" replacement==''>

diff --git a/src/main/resources/templates/notify/newVersionNotification.html b/src/main/resources/templates/notify/newVersionNotification.html index 599a7e6..4cee9ce 100644 --- a/src/main/resources/templates/notify/newVersionNotification.html +++ b/src/main/resources/templates/notify/newVersionNotification.html @@ -1 +1 @@ -
<%= Notification.NewVersion.Text|l10n|replace needle=="{version}" replacement=latestVersion|replace needle=="{edition}" replacement=latestEdition|parse sone=="nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI">
+
<%= Notification.NewVersion.Text|l10n|replace needle=="{version}" replacement=latestVersion|replace needle=="{edition}" replacement=latestEdition|parse sone=="nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI"|render>
diff --git a/src/main/resources/templates/notify/soneInsertNotification.html b/src/main/resources/templates/notify/soneInsertNotification.html index c15864f..8965e36 100644 --- a/src/main/resources/templates/notify/soneInsertNotification.html +++ b/src/main/resources/templates/notify/soneInsertNotification.html @@ -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> diff --git a/src/main/resources/templates/viewSone.html b/src/main/resources/templates/viewSone.html index 4b88474..dd994ea 100644 --- a/src/main/resources/templates/viewSone.html +++ b/src/main/resources/templates/viewSone.html @@ -52,7 +52,7 @@ <%foreach sone.profile.fields field>
<% field.name|html>
-
<% field.value|parse sone=sone>
+
<% field.value|parse sone=sone|render>
<%/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 index 0000000..268a3fa --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/template/ParserFilterTest.java @@ -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 David ‘Bombe’ Roden + */ +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 parts = setupSoneTextParser(); + assertThat(parserFilter.format(templateContext, "Text", Collections.emptyMap()), is((Object) parts)); + } + + private List setupSoneTextParser() throws IOException { + List parts = Arrays.asList(new PlainTextPart("Text")); + when(soneTextParser.parse(any(SoneTextParserContext.class), any(StringReader.class))).thenReturn(parts); + return parts; + } + + @Test + public void filterUsesGivenSone() throws IOException { + List parts = setupSoneTextParser(); + assertThat(parserFilter.format(templateContext, "Text", ImmutableMap.of("sone", sone)), is((Object) parts)); + verifyThatContextContainsCorrectSone(); + } + + @Test + public void filterGetsCorrectSoneFromCore() throws IOException { + when(core.getSone("sone-id")).thenReturn(Optional.of(sone)); + List parts = setupSoneTextParser(); + assertThat(parserFilter.format(templateContext, "Text", ImmutableMap.of("sone", "sone-id")), is((Object) parts)); + verifyThatContextContainsCorrectSone(); + } + + private void verifyThatContextContainsCorrectSone() throws IOException { + ArgumentCaptor 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) parserFilter.format(templateContext, "Text", Collections.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 index 0000000..a600cdd --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/template/RenderFilterTest.java @@ -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 David ‘Bombe’ Roden + */ +public class RenderFilterTest { + + private static final Map 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 parts = Arrays.asList(new PlainTextPart("")); + assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP), is((Object) "<Text>")); + } + + @Test + public void filterCanRenderMultiplePlainTextParts() { + List parts = Arrays.asList(new PlainTextPart(""), new PlainTextPart("")); + assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP), is((Object) "<Text><Foo>")); + } + + @Test + public void filterCanRenderUntrustedFreenetLinks() { + List parts = Arrays.asList(new FreenetLinkPart("SSK@foo,bar/baz", "foo/baz", false)); + assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP), + is((Object) "foo/baz")); + } + + @Test + public void filterCanRenderTrustedFreenetLinks() { + List parts = Arrays.asList(new FreenetLinkPart("SSK@foo,bar/baz", "foo/baz", true)); + assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP), + is((Object) "foo/baz")); + } + + @Test + public void filterCanRenderInternetLinks() { + List parts = Arrays.asList(new LinkPart("http://link.sone", "link.sone")); + assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP), + is((Object) "link.sone")); + } + + @Test + public void filterCanRenderSonePartsForUnknownSones() { + Sone unknownSone = mock(Sone.class); + when(unknownSone.getId()).thenReturn("sone-id"); + List parts = Arrays.asList(new SonePart(unknownSone)); + assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP), + is((Object) "sone-id")); + } + + @Test + public void filterCanRenderSonePartsForKnownSones() { + List parts = Arrays.asList(new SonePart(sone)); + assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP), + is((Object) "SoneName")); + } + + @Test + public void filterCanRenderPostParts() { + when(post.getText()).thenReturn("123456789012345678901234567890"); + List parts = Arrays.asList(new PostPart(post)); + assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP), + is((Object) "12345678901234567890…")); + } + + @Test + public void filterCanRenderPostPartsWithoutBreakingWords() { + when(post.getText()).thenReturn("12345 12345 12345 12345 12345 12345"); + List parts = Arrays.asList(new PostPart(post)); + assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP), + is((Object) "12345 12345 12345…")); + } + + @Test + public void filterCanRenderPostPartsWithShortText() { + when(post.getText()).thenReturn("12345 12345"); + List parts = Arrays.asList(new PostPart(post)); + assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP), + is((Object) "12345 12345")); + } + + @Test + public void filterCanRenderPostPartsWithOldPostIds() { + when(post.getText()).thenReturn("12345 12345"); + List parts = Arrays.asList(new PostPart(post, true)); + assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP), + is((Object) "12345 12345")); + } + + @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 parts = Arrays.asList(new AlbumPart(album)); + assertThat(renderFilter.format(templateContext, parts, EMPTY_MAP), + is((Object) "Title")); + } + + @Test + public void filterHonorsLength() { + List parts = Arrays.asList(new PlainTextPart("12345678901234567890")); + assertThat(renderFilter.format(templateContext, parts, ImmutableMap.of("length", "10")), + is((Object) "1234567890…")); + } + + @Test + public void filterHonorsCutOffLength() { + List parts = Arrays.asList(new PlainTextPart("12345678901234567890")); + assertThat(renderFilter.format(templateContext, parts, ImmutableMap.of("length", "10", "cut-off-length", "5")), + is((Object) "12345…")); + } + +} -- 2.7.4