1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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()
}
}
|