package org.terst.feedreader.readability /** * Removes "recirculation" widgets -- the promo blocks a publisher's widget platform injects * into the middle of the article body to send the reader off to other stories. * * Reported live: West Hawaii Today articles "get a bunch of stray 'Trending' headers". * Confirmed by running the vendored Readability.js over the rendered page for the 12 articles * in that feed: 9 carried two surviving widgets each. The three that didn't were paywalled * stubs too short for the widgets to be reached. * * Readability keeps them because they are not in a sidebar -- they are inside the article's own * container, between real paragraphs, so every structural signal says they belong. What arrives * is a heading labelling nothing, because the list of links under it is filled in later by a * script that never runs in the reader: * * ``` *

Trending

* ``` * * The signal is the attribute, not the word "Trending": `data-mrf-recirculation` says what the * block is for, in the widget platform's own vocabulary, and no article body is ever inside one. * Matching on the heading text instead would be both narrower (the label varies by site and * section) and more dangerous (an article may legitimately discuss what is trending). * * [MAX_WIDGET_TEXT_LENGTH] is the same safety valve [ConsentNotice] uses: however the attribute * is applied, nothing with real writing in it is ever thrown away. */ object RecirculationWidgets { /** * Text length past which a block is kept whatever attribute it carries. A widget is a label * and a few headlines; anything longer is somebody's writing and is not ours to delete. */ private const val MAX_WIDGET_TEXT_LENGTH = 600 /** * Attributes marking a block as recirculation. Only ones whose own name says so -- this list * grows on evidence from a real article, never on a guess about what a vendor prefix means. */ private val WIDGET_ATTRIBUTES = listOf("data-mrf-recirculation") private val OPENING_DIV = Regex("]*>", RegexOption.IGNORE_CASE) private val TAG_REGEX = Regex("<[^>]*>") private val WHITESPACE_REGEX = Regex("\\s+") private const val OPEN_DIV = "` closing the div that opened before [from], or null when * the markup never closes it. */ private fun matchingClose(html: String, from: Int): Int? { var depth = 1 var i = from var end: Int? = null while (end == null && i < html.length) { val next = html.indexOf('<', i) when { next < 0 -> i = html.length html.startsWith(CLOSE_DIV, next, ignoreCase = true) -> { val close = html.indexOf('>', next) depth-- when { close < 0 -> i = html.length depth == 0 -> end = close + 1 else -> i = close + 1 } } html.startsWith(OPEN_DIV, next, ignoreCase = true) -> { depth++ i = next + OPEN_DIV.length } else -> i = next + 1 } } return end } private fun tooLongToBeAWidget(block: String): Boolean = block.replace(TAG_REGEX, " ").replace(WHITESPACE_REGEX, " ").trim().length > MAX_WIDGET_TEXT_LENGTH }