summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/readability
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/org/terst/feedreader/readability')
-rw-r--r--app/src/main/java/org/terst/feedreader/readability/ArticleLitter.kt24
-rw-r--r--app/src/main/java/org/terst/feedreader/readability/EvalDecision.kt2
-rw-r--r--app/src/main/java/org/terst/feedreader/readability/PreferredSourceBadge.kt69
3 files changed, 94 insertions, 1 deletions
diff --git a/app/src/main/java/org/terst/feedreader/readability/ArticleLitter.kt b/app/src/main/java/org/terst/feedreader/readability/ArticleLitter.kt
new file mode 100644
index 0000000..decddc6
--- /dev/null
+++ b/app/src/main/java/org/terst/feedreader/readability/ArticleLitter.kt
@@ -0,0 +1,24 @@
+package org.terst.feedreader.readability
+
+/**
+ * Everything that gets cleaned out of an extracted article body, in one call.
+ *
+ * There are two places a body is cleaned -- a fresh extraction on its way out of [EvalDecision],
+ * and a cached one on its way back in through `loadCachedExtraction` -- and they must stay in
+ * step. A cached article is cleaned in place rather than discarded, so a cleaner wired into only
+ * the fresh path would leave every already-read article carrying litter that nothing would ever
+ * remove: the body is good, so it is never re-fetched.
+ *
+ * Hence one entry point rather than a list of calls repeated at both sites. Adding a cleaner here
+ * reaches both; adding one to a call site reaches one, and which one it misses would depend on
+ * which file was open at the time.
+ *
+ * None of these discard an article. They remove things from inside it, which is why they sit
+ * apart from [ConsentNotice] and [UnrenderedTex] -- those two decide a body is not worth keeping.
+ */
+object ArticleLitter {
+
+ /** [html] with the publisher's promo furniture removed. */
+ fun strip(html: String?): String? =
+ PreferredSourceBadge.strip(RecirculationWidgets.strip(html))
+}
diff --git a/app/src/main/java/org/terst/feedreader/readability/EvalDecision.kt b/app/src/main/java/org/terst/feedreader/readability/EvalDecision.kt
index dd77014..d0bd1cd 100644
--- a/app/src/main/java/org/terst/feedreader/readability/EvalDecision.kt
+++ b/app/src/main/java/org/terst/feedreader/readability/EvalDecision.kt
@@ -44,7 +44,7 @@ internal fun decideEvalOutcome(
// never reach `articles.extractedContent` either.
usable -> EvalDecision.Done(
ExtractionResult.Success(
- article!!.copy(content = RecirculationWidgets.strip(article.content))
+ article!!.copy(content = ArticleLitter.strip(article.content))
)
)
diff --git a/app/src/main/java/org/terst/feedreader/readability/PreferredSourceBadge.kt b/app/src/main/java/org/terst/feedreader/readability/PreferredSourceBadge.kt
new file mode 100644
index 0000000..4ea1370
--- /dev/null
+++ b/app/src/main/java/org/terst/feedreader/readability/PreferredSourceBadge.kt
@@ -0,0 +1,69 @@
+package org.terst.feedreader.readability
+
+/**
+ * Removes the "Add as a preferred source on Google" badge publishers put beside the byline.
+ *
+ * Reported live on a West Hawaii Today article: "remove giant Google icon". Giant because the
+ * page caps it with its own stylesheet -- `.preferred_source_badge { max-width: 120px }` -- and
+ * the reader drops the page's CSS, so what is a small badge on the site becomes a full-width
+ * image in the reader, above the first paragraph.
+ *
+ * The signal is the link's address, not the class or the image filename, and that is forced
+ * rather than chosen. Confirmed by running the vendored Readability.js over the real page: the
+ * wrapper's `preferred_source_badge` class does not survive extraction at all -- the only class
+ * left in the output is Readability's own -- so a class rule would match nothing. What does
+ * survive is the anchor, intact:
+ *
+ * ```
+ * <p><a href="https://www.google.com/preferences/source?q=westhawaiitoday.com" target="_blank"
+ * rel="noopener"> <img src=".../google_preferred_source_badge_light_en.png"
+ * alt="Add as a preferred source on Google"> </a> </p>
+ * ```
+ *
+ * The address is Google's own and identical for every publisher in the programme; the image
+ * filename sits under the site's theme directory and is not. So matching the address is both the
+ * only thing available and the one that generalises past this one newspaper.
+ *
+ * A link with real words in it is never touched, which is the same safety valve [ConsentNotice]
+ * and [RecirculationWidgets] use: an article may legitimately link to that page and say so in
+ * writing, and that sentence is the author's, not litter. Only a link whose entire content is an
+ * image goes.
+ */
+object PreferredSourceBadge {
+
+ /** Google's address for the programme. Not the badge image, which is per-publisher. */
+ private const val BADGE_LINK = "google.com/preferences/source"
+
+ private val ANCHOR = Regex("""<a\b[^>]*>.*?</a\s*>""", setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL))
+ private val TAG = Regex("<[^>]*>")
+ private val EMPTY_PARAGRAPH = Regex("""<p>\s*</p>""", RegexOption.IGNORE_CASE)
+
+ /** [html] with any preferred-source badge removed. */
+ fun strip(html: String?): String? =
+ if (html.isNullOrBlank() || !html.contains(BADGE_LINK, ignoreCase = true)) {
+ html
+ } else {
+ removeBadges(html)
+ }
+
+ private fun removeBadges(html: String): String {
+ val withoutBadges = ANCHOR.replace(html) { match ->
+ if (isBadge(match.value)) "" else match.value
+ }
+ // The badge is normally the only thing in its paragraph, and a <p> holding nothing but
+ // the whitespace around the image it used to wrap still takes up a line on the screen.
+ return EMPTY_PARAGRAPH.replace(withoutBadges, "")
+ }
+
+ /**
+ * A badge is a link to the programme whose whole content is an image.
+ *
+ * Both halves are required. The address alone would take a sentence linking to the page with
+ * it; "contains an image" alone would take every photo credit on the site.
+ */
+ private fun isBadge(anchor: String): Boolean {
+ val opening = anchor.substringBefore('>')
+ if (!opening.contains(BADGE_LINK, ignoreCase = true)) return false
+ return TAG.replace(anchor, "").isBlank()
+ }
+}