summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/ui/ImageZoomLinks.kt
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/org/terst/feedreader/ui/ImageZoomLinks.kt')
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/ImageZoomLinks.kt39
1 files changed, 32 insertions, 7 deletions
diff --git a/app/src/main/java/org/terst/feedreader/ui/ImageZoomLinks.kt b/app/src/main/java/org/terst/feedreader/ui/ImageZoomLinks.kt
index b83b600..d924c02 100644
--- a/app/src/main/java/org/terst/feedreader/ui/ImageZoomLinks.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/ImageZoomLinks.kt
@@ -1,5 +1,7 @@
package org.terst.feedreader.ui
+import java.net.URI
+
// How a tap on an article image is noticed at all, and turned into a URL the viewer
// (ImageZoom.kt) can open. Split from the viewer itself because this half runs at
// HTML-assembly time, inside wrapReaderHtml -- it's reader-markup plumbing, not gestures.
@@ -35,7 +37,7 @@ private val imageFileExtensions = listOf(".jpg", ".jpeg", ".png", ".gif", ".webp
* and wrapping it would swallow the tap that plays the video). Those are still zoomable when
* the link points at an image file, which [zoomableImageTarget] handles on the other side.
*/
-fun linkifyImagesForZoom(html: String): String = buildString {
+fun linkifyImagesForZoom(html: String, baseUrl: String? = null): String = buildString {
var copiedUpTo = 0
var anchorDepth = 0
for (tag in anchorOrImageTagRegex.findAll(html)) {
@@ -45,7 +47,7 @@ fun linkifyImagesForZoom(html: String): String = buildString {
text.startsWith("<a", ignoreCase = true) -> anchorDepth++
anchorDepth == 0 -> {
val src = imgSrcRegex.find(text)?.let { it.groupValues[1].ifEmpty { it.groupValues[2] } }
- val href = src?.let { zoomHrefForSrc(it) }
+ val href = src?.let { zoomHrefForSrc(it, baseUrl) }
if (href != null) {
append(html, copiedUpTo, tag.range.first)
append("<a href=\"").append(href).append("\">").append(text).append("</a>")
@@ -57,17 +59,14 @@ fun linkifyImagesForZoom(html: String): String = buildString {
append(html, copiedUpTo, html.length)
}
-private fun zoomHrefForSrc(src: String): String? {
+private fun zoomHrefForSrc(src: String, baseUrl: String?): String? {
val trimmed = src.trim()
val absolute = when {
// Protocol-relative (//host/path) is common in feed markup and resolves fine in the
// page itself, but our scheme-prefixed href has to carry a complete URL on its own.
trimmed.startsWith("//") -> "https:$trimmed"
trimmed.startsWith("http://", ignoreCase = true) || trimmed.startsWith("https://", ignoreCase = true) -> trimmed
- // Anything else (site-relative, data:, javascript:) is skipped rather than guessed at:
- // a relative src resolves against the WebView's base URL, which a custom-scheme href
- // can't reproduce, so wrapping it would only build a dead link.
- else -> null
+ else -> resolvedAgainstBase(trimmed, baseUrl)
} ?: return null
// The src came out of a quoted attribute and goes straight back into one, already escaped
// as the source wrote it. A raw quote or angle bracket could only arrive via a
@@ -76,6 +75,32 @@ private fun zoomHrefForSrc(src: String): String? {
}
/**
+ * What a relative src actually points at, or null if there's no way to tell or it isn't
+ * something the viewer could open.
+ *
+ * A feed's own HTML routinely keeps its images site-relative (Ftrain's <content:encoded> is
+ * entirely `<img src="/art/uploads/...">`), and that path is what the non-reader rendering
+ * shows verbatim -- reader mode never hits this because Readability rewrites every src to an
+ * absolute URL before we see it. The page resolves those against the base URL that
+ * loadReaderHtml hands loadDataWithBaseURL, which is the article's own link, so resolving them
+ * the same way here yields exactly the URL the WebView itself fetched. With no base URL there
+ * is still nothing to resolve against, and the image stays un-zoomable rather than getting a
+ * dead link.
+ */
+private fun resolvedAgainstBase(src: String, baseUrl: String?): String? {
+ if (src.isEmpty() || baseUrl.isNullOrBlank()) return null
+ // URI() rejects a src with a space or other illegal character in it rather than guessing,
+ // which is the right answer here too -- an un-zoomable image beats a dead link.
+ val resolved = runCatching { URI(baseUrl).resolve(src) }.getOrNull()
+ // data: and javascript: srcs are already absolute, so resolve() hands them straight back;
+ // only a real network URL is something Coil could ever open.
+ return when (resolved?.scheme?.lowercase()) {
+ "http", "https" -> resolved.toString()
+ else -> null
+ }
+}
+
+/**
* The image a tapped URL should open full-screen, or null if it isn't an image at all.
*
* Covers both halves of the problem: links [linkifyImagesForZoom] built, and links the article