diff options
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.kt | 39 |
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 |
