Show loading animation while loading elements
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 13 Nov 2016 07:09:47 +0000 (08:09 +0100)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 13 Nov 2016 07:09:47 +0000 (08:09 +0100)
19 files changed:
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/DefaultImageLoader.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/core/ElementLoader.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/ImageLoader.kt [deleted file]
src/main/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilter.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/template/LinkedElementsFilter.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/template/LinkedImagesFilter.kt [deleted file]
src/main/resources/static/css/sone.css
src/main/resources/static/images/loading-animation.gif [new file with mode: 0644]
src/main/resources/templates/include/viewPost.html
src/main/resources/templates/include/viewReply.html
src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/core/DefaultImageLoaderTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/core/ElementLoaderTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/core/ImageLoaderTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilterTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/LinkedElementsFilterTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/template/LinkedImagesFilterTest.kt [deleted file]

index 046a377..50d3323 100644 (file)
@@ -40,7 +40,7 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.core.ImageLoader;
+import net.pterodactylus.sone.core.ElementLoader;
 import net.pterodactylus.sone.core.event.ImageInsertAbortedEvent;
 import net.pterodactylus.sone.core.event.ImageInsertFailedEvent;
 import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent;
@@ -85,7 +85,8 @@ import net.pterodactylus.sone.template.IdentityAccessor;
 import net.pterodactylus.sone.template.ImageAccessor;
 import net.pterodactylus.sone.template.ImageLinkFilter;
 import net.pterodactylus.sone.template.JavascriptFilter;
-import net.pterodactylus.sone.template.LinkedImagesFilter;
+import net.pterodactylus.sone.template.LinkedElementRenderFilter;
+import net.pterodactylus.sone.template.LinkedElementsFilter;
 import net.pterodactylus.sone.template.ParserFilter;
 import net.pterodactylus.sone.template.PostAccessor;
 import net.pterodactylus.sone.template.ProfileAccessor;
@@ -258,7 +259,7 @@ public class WebInterface {
         *            The Sone plugin
         */
        @Inject
-       public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilter listNotificationFilter, PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter, ImageLoader imageLoader) {
+       public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilter listNotificationFilter, PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter, ElementLoader elementLoader) {
                this.sonePlugin = sonePlugin;
                this.loaders = loaders;
                this.listNotificationFilter = listNotificationFilter;
@@ -293,7 +294,8 @@ public class WebInterface {
                templateContextFactory.addFilter("parse", parserFilter = new ParserFilter(getCore(), soneTextParser));
                templateContextFactory.addFilter("shorten", shortenFilter = new ShortenFilter());
                templateContextFactory.addFilter("render", renderFilter = new RenderFilter(getCore(), templateContextFactory));
-               templateContextFactory.addFilter("linked-images", new LinkedImagesFilter(imageLoader));
+               templateContextFactory.addFilter("linked-elements", new LinkedElementsFilter(elementLoader));
+               templateContextFactory.addFilter("render-linked-element", new LinkedElementRenderFilter(templateContextFactory));
                templateContextFactory.addFilter("reparse", new ReparseFilter());
                templateContextFactory.addFilter("unknown", new UnknownDateFilter(getL10n(), "View.Sone.Text.UnknownDate"));
                templateContextFactory.addFilter("format", new FormatFilter());
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt b/src/main/kotlin/net/pterodactylus/sone/core/DefaultElementLoader.kt
new file mode 100644 (file)
index 0000000..eabfed8
--- /dev/null
@@ -0,0 +1,56 @@
+package net.pterodactylus.sone.core
+
+import com.google.common.cache.CacheBuilder
+import freenet.keys.FreenetURI
+import java.io.ByteArrayInputStream
+import javax.imageio.ImageIO
+import javax.inject.Inject
+
+/**
+ * [ElementLoader] implementation that uses a simple Guava [com.google.common.cache.Cache].
+ */
+class DefaultElementLoader @Inject constructor(private val freenetInterface: FreenetInterface) : ElementLoader {
+
+       private val loadingLinks = CacheBuilder.newBuilder().build<String, Boolean>()
+       private val imageCache = CacheBuilder.newBuilder().build<String, LinkedImage>()
+       private val callback = object : FreenetInterface.BackgroundFetchCallback {
+               override fun loaded(uri: FreenetURI, mimeType: String, data: ByteArray) {
+                       if (!mimeType.startsWith("image/")) {
+                               return
+                       }
+                       ByteArrayInputStream(data).use {
+                               ImageIO.read(it)
+                       }?.let {
+                               imageCache.get(uri.toString()) { LinkedImage(uri.toString()) }
+                       }
+                       removeLoadingLink(uri)
+               }
+
+               override fun failed(uri: FreenetURI) {
+                       removeLoadingLink(uri)
+               }
+
+               private fun removeLoadingLink(uri: FreenetURI) {
+                       synchronized(loadingLinks) {
+                               loadingLinks.invalidate(uri.toString())
+                       }
+               }
+       }
+
+       override fun loadElement(link: String): LinkedElement {
+               synchronized(loadingLinks) {
+                       imageCache.getIfPresent(link)?.run {
+                               return this
+                       }
+                       if (loadingLinks.getIfPresent(link) == null) {
+                               loadingLinks.put(link, true)
+                               freenetInterface.startFetch(FreenetURI(link), callback)
+                       }
+               }
+               return object : LinkedElement {
+                       override val link = link
+                       override val loading = true
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/DefaultImageLoader.kt b/src/main/kotlin/net/pterodactylus/sone/core/DefaultImageLoader.kt
deleted file mode 100644 (file)
index 01e177a..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-package net.pterodactylus.sone.core
-
-import com.google.common.cache.CacheBuilder
-import freenet.keys.FreenetURI
-import java.io.ByteArrayInputStream
-import javax.imageio.ImageIO
-import javax.inject.Inject
-
-/**
- * [ImageLoader] implementation that uses a simple Guava [com.google.common.cache.Cache].
- */
-class DefaultImageLoader @Inject constructor(private val freenetInterface: FreenetInterface) : ImageLoader {
-
-       private val imageCache = CacheBuilder.newBuilder().build<String, LoadedImage>()
-       private val callback = object : FreenetInterface.BackgroundFetchCallback {
-               override fun loaded(uri: FreenetURI, mimeType: String, data: ByteArray) {
-                       if (!mimeType.startsWith("image/")) {
-                               return
-                       }
-                       val image = ByteArrayInputStream(data).use {
-                               ImageIO.read(it)
-                       }
-                       val loadedImage = LoadedImage(uri.toString(), mimeType, image.width, image.height)
-                       imageCache.get(uri.toString()) { loadedImage }
-               }
-
-               override fun failed(uri: FreenetURI) {
-               }
-       }
-
-       override fun toLoadedImage(link: String): LoadedImage? {
-               imageCache.getIfPresent(link)?.run {
-                       return this
-               }
-               freenetInterface.startFetch(FreenetURI(link), callback)
-               return null
-       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/ElementLoader.kt b/src/main/kotlin/net/pterodactylus/sone/core/ElementLoader.kt
new file mode 100644 (file)
index 0000000..2424cf1
--- /dev/null
@@ -0,0 +1,22 @@
+package net.pterodactylus.sone.core
+
+import com.google.inject.ImplementedBy
+
+/**
+ * Component that loads images and supplies information about them.
+ */
+@ImplementedBy(DefaultElementLoader::class)
+interface ElementLoader {
+
+       fun loadElement(link: String): LinkedElement
+
+}
+
+interface LinkedElement {
+
+       val link: String
+       val loading: Boolean
+
+}
+
+data class LinkedImage(override val link: String, override val loading: Boolean = false) : LinkedElement
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/ImageLoader.kt b/src/main/kotlin/net/pterodactylus/sone/core/ImageLoader.kt
deleted file mode 100644 (file)
index 5211015..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-package net.pterodactylus.sone.core
-
-import com.google.inject.ImplementedBy
-
-/**
- * Component that loads images and supplies information about them.
- */
-@ImplementedBy(DefaultImageLoader::class)
-interface ImageLoader {
-
-       fun toLoadedImage(link: String): LoadedImage?
-
-}
-
-data class LoadedImage(val link: String, val mimeType: String, val width: Int, val height: Int)
diff --git a/src/main/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilter.kt b/src/main/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilter.kt
new file mode 100644 (file)
index 0000000..6255f9d
--- /dev/null
@@ -0,0 +1,47 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.LinkedElement
+import net.pterodactylus.sone.core.LinkedImage
+import net.pterodactylus.util.template.Filter
+import net.pterodactylus.util.template.TemplateContext
+import net.pterodactylus.util.template.TemplateContextFactory
+import net.pterodactylus.util.template.TemplateParser
+import java.io.StringReader
+import java.io.StringWriter
+
+/**
+ * Renders all kinds of [LinkedElement]s.
+ */
+class LinkedElementRenderFilter(private val templateContextFactory: TemplateContextFactory) : Filter {
+
+       companion object {
+               private val loadedImageTemplate = """<a href="/<% link|html>"><span class="linked-element" title="<% link|html>" style="background-image: url('/<% link|html>')"></span></a>""".parse()
+               private val notLoadedImageTemplate = """<span class="linked-element" title="<% link|html>" style="background-image: url('images/loading-animation.gif')"></span>""".parse()
+
+               private fun String.parse() = StringReader(this).use { TemplateParser.parse(it) }
+       }
+
+       override fun format(templateContext: TemplateContext?, data: Any?, parameters: Map<String, Any?>?) =
+                       when {
+                               data is LinkedElement && data.loading -> renderNotLoadedLinkedElement(data)
+                               data is LinkedImage -> renderLinkedImage(data)
+                               else -> null
+                       }
+
+       private fun renderLinkedImage(linkedImage: LinkedImage) =
+                       StringWriter().use {
+                               val templateContext = templateContextFactory.createTemplateContext()
+                               templateContext["link"] = linkedImage.link
+                               loadedImageTemplate.render(templateContext, it)
+                               it
+                       }.toString()
+
+       private fun renderNotLoadedLinkedElement(linkedElement: LinkedElement) =
+                       StringWriter().use {
+                               val templateContext = templateContextFactory.createTemplateContext()
+                               templateContext["link"] = linkedElement.link
+                               notLoadedImageTemplate.render(templateContext, it)
+                               it
+                       }.toString()
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/template/LinkedElementsFilter.kt b/src/main/kotlin/net/pterodactylus/sone/template/LinkedElementsFilter.kt
new file mode 100644 (file)
index 0000000..f95d6c0
--- /dev/null
@@ -0,0 +1,23 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.ElementLoader
+import net.pterodactylus.sone.core.LinkedElement
+import net.pterodactylus.sone.text.FreenetLinkPart
+import net.pterodactylus.sone.text.Part
+import net.pterodactylus.util.template.Filter
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * Filter that takes a number of pre-rendered [Part]s and replaces all identified links to freenet elements
+ * with [LinkedElement]s.
+ */
+class LinkedElementsFilter(private val elementLoader: ElementLoader) : Filter {
+
+       @Suppress("UNCHECKED_CAST")
+       override fun format(templateContext: TemplateContext?, data: Any?, parameters: MutableMap<String, Any?>?) =
+                       (data as? Iterable<Part>)
+                                       ?.filterIsInstance<FreenetLinkPart>()
+                                       ?.map { elementLoader.loadElement(it.link) }
+                                       ?: listOf<LinkedElement>()
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/template/LinkedImagesFilter.kt b/src/main/kotlin/net/pterodactylus/sone/template/LinkedImagesFilter.kt
deleted file mode 100644 (file)
index 584bec3..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-package net.pterodactylus.sone.template
-
-import net.pterodactylus.sone.core.ImageLoader
-import net.pterodactylus.sone.core.LoadedImage
-import net.pterodactylus.sone.text.FreenetLinkPart
-import net.pterodactylus.sone.text.Part
-import net.pterodactylus.util.template.Filter
-import net.pterodactylus.util.template.TemplateContext
-
-/**
- * Filter that takes a number of pre-rendered [Part]s and replaces all identified links to freenet images
- * with [LoadedImage]s.
- */
-class LinkedImagesFilter(private val imageLoader: ImageLoader) : Filter {
-
-       @Suppress("UNCHECKED_CAST")
-       override fun format(templateContext: TemplateContext?, data: Any?, parameters: MutableMap<String, Any?>?) =
-                       (data as? Iterable<Part>)
-                                       ?.filterIsInstance<FreenetLinkPart>()
-                                       ?.mapNotNull { imageLoader.toLoadedImage(it.link) }
-                                       ?: listOf<LoadedImage>()
-
-}
index 8e008c7..34dad50 100644 (file)
@@ -440,11 +440,11 @@ textarea {
        color: green;
 }
 
-#sone .post .linked-images {
+#sone .post .linked-elements {
        margin-top: 1ex;
 }
 
-#sone .post .linked-image {
+#sone .post .linked-element {
        display: inline-block;
        border: solid 1px black;
        width: 160px;
@@ -511,11 +511,11 @@ textarea {
        font-size: inherit;
 }
 
-#sone .post .reply .linked-images {
+#sone .post .reply .linked-elements {
        margin-top: 1ex;
 }
 
-#sone .post .reply .linked-image {
+#sone .post .reply .linked-element {
        display: inline-block;
        border: solid 1px black;
        width: 120px;
diff --git a/src/main/resources/static/images/loading-animation.gif b/src/main/resources/static/images/loading-animation.gif
new file mode 100644 (file)
index 0000000..8d4f3f4
Binary files /dev/null and b/src/main/resources/static/images/loading-animation.gif differ
index ac149f8..734a8b4 100644 (file)
                        <div class="post-text short-text<%if raw> hidden<%/if><%if shortText|match key=renderedText> hidden<%/if>"><% shortText></div>
                        <%if !shortText|match value=renderedText><%if !raw><a class="expand-post-text" href="viewPost.html?post=<% post.id|html>&amp;raw=true"><%= View.Post.ShowMore|l10n|html></a><%/if><%/if>
                        <%if !shortText|match value=renderedText><%if !raw><a class="shrink-post-text hidden"><%= View.Post.ShowLess|l10n|html></a><%/if><%/if>
-                       <% parsedText|linked-images|store key==linkedImages>
-                       <% foreach linkedImages linkedImage>
+                       <% parsedText|linked-elements|store key==linkedElements>
+                       <% foreach linkedElements linkedElement>
                                <% first>
-                                       <div class="linked-images">
+                                       <div class="linked-elements">
                                <%/first>
-                               <a href="/<% linkedImage.link|html>">
-                                       <span class="linked-image" title="<% linkedImage.link|html>" style="background-image: url('/<% linkedImage.link|html>')"/>
-                               </a>
+                               <% linkedElement|render-linked-element>
                                <% last>
                                        </div>
                                <%/last>
index cd56e32..6091d40 100644 (file)
                        <div class="reply-text short-text<%if raw> hidden<%/if><%if shortText|match key=renderedText> hidden<%/if>"><% shortText></div>
                        <%if !shortText|match value=renderedText><%if !raw><a class="expand-reply-text" href="viewPost.html?post=<% reply.postId|html>&amp;raw=true"><%= View.Post.ShowMore|l10n|html></a><%/if><%/if>
                        <%if !shortText|match value=renderedText><%if !raw><a class="shrink-reply-text hidden"><%= View.Post.ShowLess|l10n|html></a><%/if><%/if>
-                       <% parsedText|linked-images|store key==linkedImages>
-                       <% foreach linkedImages linkedImage>
+                       <% parsedText|linked-elements|store key==linkedElements>
+                       <% foreach linkedElements linkedElement>
                                <% first>
-                                       <div class="linked-images">
+                                       <div class="linked-elements">
                                <%/first>
-                               <a href="/<% linkedImage.link|html>">
-                                       <span class="linked-image" title="<% linkedImage.link|html>" style="background-image: url('/<% linkedImage.link|html>')"/>
-                               </a>
+                               <% linkedElement|render-linked-element>
                                <% last>
                                        </div>
                                <%/last>
diff --git a/src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/DefaultElementLoaderTest.kt
new file mode 100644 (file)
index 0000000..adc5bc9
--- /dev/null
@@ -0,0 +1,81 @@
+package net.pterodactylus.sone.core
+
+import com.google.common.io.ByteStreams
+import com.google.common.io.Files
+import freenet.keys.FreenetURI
+import net.pterodactylus.sone.core.FreenetInterface.BackgroundFetchCallback
+import net.pterodactylus.sone.test.capture
+import net.pterodactylus.sone.test.mock
+import org.hamcrest.MatcherAssert
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers
+import org.hamcrest.Matchers.`is`
+import org.hamcrest.Matchers.instanceOf
+import org.hamcrest.Matchers.nullValue
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.io.ByteArrayOutputStream
+
+/**
+ * Unit test for [DefaultElementLoaderTest].
+ */
+class DefaultElementLoaderTest {
+
+       companion object {
+               private const val IMAGE_ID = "KSK@gpl.png"
+       }
+
+       private val freenetInterface = mock<FreenetInterface>()
+       private val elementLoader = DefaultElementLoader(freenetInterface)
+       private val callback = capture<BackgroundFetchCallback>()
+
+       @Test
+       fun `image loader starts request for link that is not known`() {
+               elementLoader.loadElement(IMAGE_ID)
+               verify(freenetInterface).startFetch(eq(FreenetURI(IMAGE_ID)), any<BackgroundFetchCallback>())
+       }
+
+       @Test
+       fun `element loader only starts request once`() {
+               elementLoader.loadElement(IMAGE_ID)
+               elementLoader.loadElement(IMAGE_ID)
+               verify(freenetInterface).startFetch(eq(FreenetURI(IMAGE_ID)), any<BackgroundFetchCallback>())
+       }
+
+       @Test
+       fun `element loader returns loading element on first call`() {
+               assertThat(elementLoader.loadElement(IMAGE_ID).loading, `is`(true))
+       }
+
+       @Test
+       fun `image loader can load image`() {
+               elementLoader.loadElement(IMAGE_ID)
+               verify(freenetInterface).startFetch(eq(FreenetURI(IMAGE_ID)), callback.capture())
+           callback.value.loaded(FreenetURI(IMAGE_ID), "image/png", read("/static/images/unknown-image-0.png"))
+               val linkedElement = elementLoader.loadElement(IMAGE_ID)
+               assertThat(linkedElement.link, `is`(IMAGE_ID))
+               assertThat(linkedElement.loading, `is`(false))
+               assertThat(linkedElement, instanceOf(LinkedImage::class.java))
+       }
+
+       @Test
+       fun `image can be loaded again after it failed`() {
+               elementLoader.loadElement(IMAGE_ID)
+               verify(freenetInterface).startFetch(eq(FreenetURI(IMAGE_ID)), callback.capture())
+               callback.value.failed(FreenetURI(IMAGE_ID))
+               elementLoader.loadElement(IMAGE_ID)
+               verify(freenetInterface, times(2)).startFetch(eq(FreenetURI(IMAGE_ID)), callback.capture())
+       }
+
+       private fun read(resource: String): ByteArray =
+                       javaClass.getResourceAsStream(resource)?.use { input ->
+                               ByteArrayOutputStream().use {
+                                       ByteStreams.copy(input, it)
+                                       it
+                               }.toByteArray()
+                       } ?: ByteArray(0)
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/core/DefaultImageLoaderTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/DefaultImageLoaderTest.kt
deleted file mode 100644 (file)
index cc31665..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-package net.pterodactylus.sone.core
-
-import com.google.common.io.ByteStreams
-import com.google.common.io.Files
-import freenet.keys.FreenetURI
-import net.pterodactylus.sone.core.FreenetInterface.BackgroundFetchCallback
-import net.pterodactylus.sone.test.capture
-import net.pterodactylus.sone.test.mock
-import org.hamcrest.MatcherAssert
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers
-import org.hamcrest.Matchers.`is`
-import org.hamcrest.Matchers.nullValue
-import org.junit.Test
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mockito.verify
-import java.io.ByteArrayOutputStream
-
-/**
- * Unit test for [DefaultImageLoaderTest].
- */
-class DefaultImageLoaderTest {
-
-       companion object {
-               private const val IMAGE_ID = "KSK@gpl.png"
-       }
-
-       private val freenetInterface = mock<FreenetInterface>()
-       private val imageLoader = DefaultImageLoader(freenetInterface)
-       private val callback = capture<BackgroundFetchCallback>()
-
-       @Test
-       fun `image loader starts request for link that is not known`() {
-               assertThat(imageLoader.toLoadedImage(IMAGE_ID), nullValue())
-               verify(freenetInterface).startFetch(eq(FreenetURI(IMAGE_ID)), any<BackgroundFetchCallback>())
-       }
-
-       @Test
-       fun `image loader can load image`() {
-               assertThat(imageLoader.toLoadedImage(IMAGE_ID), nullValue())
-               verify(freenetInterface).startFetch(eq(FreenetURI(IMAGE_ID)), callback.capture())
-           callback.value.loaded(FreenetURI(IMAGE_ID), "image/png", read("/static/images/unknown-image-0.png"))
-               val loadedImage = imageLoader.toLoadedImage(IMAGE_ID)!!
-               assertThat(loadedImage.link, `is`(IMAGE_ID))
-               assertThat(loadedImage.mimeType, `is`("image/png"))
-               assertThat(loadedImage.width, `is`(200))
-               assertThat(loadedImage.height, `is`(150))
-       }
-
-       private fun read(resource: String): ByteArray =
-                       javaClass.getResourceAsStream(resource)?.use { input ->
-                               ByteArrayOutputStream().use {
-                                       ByteStreams.copy(input, it)
-                                       it
-                               }.toByteArray()
-                       } ?: ByteArray(0)
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/core/ElementLoaderTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/ElementLoaderTest.kt
new file mode 100644 (file)
index 0000000..dbb5bcc
--- /dev/null
@@ -0,0 +1,20 @@
+package net.pterodactylus.sone.core
+
+import com.google.inject.Guice.createInjector
+import net.pterodactylus.sone.test.bindMock
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.notNullValue
+import org.junit.Test
+
+/**
+ * Unit test for [ElementLoader].
+ */
+class ElementLoaderTest {
+
+       @Test
+       fun `default image loader can be loaded by guice`() {
+               val injector = createInjector(bindMock<FreenetInterface>())
+               assertThat(injector.getInstance(ElementLoader::class.java), notNullValue());
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/core/ImageLoaderTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/ImageLoaderTest.kt
deleted file mode 100644 (file)
index d3a021c..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-package net.pterodactylus.sone.core
-
-import com.google.inject.Guice.createInjector
-import net.pterodactylus.sone.test.bindMock
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.notNullValue
-import org.junit.Test
-
-/**
- * Unit test for [ImageLoader].
- */
-class ImageLoaderTest {
-
-       @Test
-       fun `default image loader can be loaded by guice`() {
-               val injector = createInjector(bindMock<FreenetInterface>())
-               assertThat(injector.getInstance(ImageLoader::class.java), notNullValue());
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/LinkedElementRenderFilterTest.kt
new file mode 100644 (file)
index 0000000..20ee99a
--- /dev/null
@@ -0,0 +1,37 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.LinkedImage
+import net.pterodactylus.util.template.HtmlFilter
+import net.pterodactylus.util.template.TemplateContextFactory
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.`is`
+import org.jsoup.Jsoup
+import org.junit.Test
+
+/**
+ * Unit test for [LinkedElementRenderFilter].
+ */
+class LinkedElementRenderFilterTest {
+
+       private val templateContextFactory = TemplateContextFactory()
+
+       init {
+               templateContextFactory.addFilter("html", HtmlFilter())
+       }
+
+       private val filter = LinkedElementRenderFilter(templateContextFactory)
+
+       @Test
+       fun `filter can render linked images`() {
+               val html = filter.format(null, LinkedImage("KSK@gpl.png"), emptyMap<String, Any?>()) as String
+               val linkNode = Jsoup.parseBodyFragment(html).body().child(0)
+               assertThat(linkNode.nodeName(), `is`("a"))
+               assertThat(linkNode.attr("href"), `is`("/KSK@gpl.png"))
+               val spanNode = linkNode.child(0)
+               assertThat(spanNode.nodeName(), `is`("span"))
+               assertThat(spanNode.attr("class"), `is`("linked-element"))
+               assertThat(spanNode.attr("title"), `is`("KSK@gpl.png"))
+               assertThat(spanNode.attr("style"), `is`("background-image: url('/KSK@gpl.png')"))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/LinkedElementsFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/LinkedElementsFilterTest.kt
new file mode 100644 (file)
index 0000000..d519e41
--- /dev/null
@@ -0,0 +1,40 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.ElementLoader
+import net.pterodactylus.sone.core.LinkedElement
+import net.pterodactylus.sone.core.LinkedImage
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.text.FreenetLinkPart
+import net.pterodactylus.sone.text.LinkPart
+import net.pterodactylus.sone.text.PlainTextPart
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.junit.Test
+import org.mockito.Mockito.`when`
+
+/**
+ * Unit test for [LinkedElementsFilter].
+ */
+class LinkedElementsFilterTest {
+
+       private val imageLoader = mock<ElementLoader>()
+       private val filter = LinkedElementsFilter(imageLoader)
+
+       @Test
+       fun `filter finds all loaded freenet images`() {
+               val parts = listOf(
+                               PlainTextPart("text"),
+                               LinkPart("http://link", "link"),
+                               FreenetLinkPart("KSK@link", "link", false),
+                               FreenetLinkPart("KSK@link.png", "link", false)
+               )
+               `when`(imageLoader.loadElement("KSK@link")).thenReturn(LinkedImage("KSK@link", true))
+               `when`(imageLoader.loadElement("KSK@link.png")).thenReturn(LinkedImage("KSK@link.png"))
+               val loadedImages = filter.format(null, parts, null)
+               assertThat(loadedImages, contains<LinkedElement>(
+                               LinkedImage("KSK@link", true),
+                               LinkedImage("KSK@link.png")
+               ))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/LinkedImagesFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/LinkedImagesFilterTest.kt
deleted file mode 100644 (file)
index 8e86bd4..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-package net.pterodactylus.sone.template
-
-import net.pterodactylus.sone.core.ImageLoader
-import net.pterodactylus.sone.core.LoadedImage
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.text.FreenetLinkPart
-import net.pterodactylus.sone.text.LinkPart
-import net.pterodactylus.sone.text.PlainTextPart
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers
-import org.junit.Test
-import org.mockito.Mockito.`when`
-
-/**
- * Unit test for [LinkedImagesFilter].
- */
-class LinkedImagesFilterTest {
-
-       private val imageLoader = mock<ImageLoader>()
-       private val filter = LinkedImagesFilter(imageLoader)
-
-       @Test
-       fun `filter finds all loaded freenet images`() {
-               val parts = listOf(
-                               PlainTextPart("text"),
-                               LinkPart("http://link", "link"),
-                               FreenetLinkPart("KSK@link", "link", false),
-                               FreenetLinkPart("KSK@link.png", "link", false)
-               )
-               `when`(imageLoader.toLoadedImage("KSK@link.png")).thenReturn(LoadedImage("KSK@link.png", "image/png", 1440, 900))
-               val loadedImages = filter.format(null, parts, null)
-               assertThat(loadedImages, Matchers.contains(
-                               LoadedImage("KSK@link.png", "image/png", 1440, 900)
-               ))
-       }
-
-}