diff options
Diffstat (limited to 'app/src/main/java/org/terst/feedreader/readability/UnrenderedTex.kt')
| -rw-r--r-- | app/src/main/java/org/terst/feedreader/readability/UnrenderedTex.kt | 84 |
1 files changed, 84 insertions, 0 deletions
diff --git a/app/src/main/java/org/terst/feedreader/readability/UnrenderedTex.kt b/app/src/main/java/org/terst/feedreader/readability/UnrenderedTex.kt new file mode 100644 index 0000000..aa46f7e --- /dev/null +++ b/app/src/main/java/org/terst/feedreader/readability/UnrenderedTex.kt @@ -0,0 +1,84 @@ +package org.terst.feedreader.readability + +/** + * Recognises a cached extraction that still holds raw TeX source instead of rendered maths. + * + * Needed for the same reason [ConsentNotice] is: fixing the extractor only changes what + * FUTURE extractions produce. The reported article ("we should render tex", lcamtuf's + * Substack) came with `extractedContent cached: true` -- its raw `\(p = \text{...}\)` + * source was already on disk, and every open read it straight back out, so the reader + * would have gone on showing TeX source no matter what the extractor learned to do. + * Recognising it on read re-extracts the article the next time it is opened, including + * everything extracted before maths rendering existed. + * + * Over-detecting is the real risk here, and it is a specific one: a false positive makes + * an article re-extract on EVERY open, for ever, because re-extracting cannot change a + * verdict the content itself causes. So the bar is a complete delimiter pair around + * something that actually looks like an expression, and never anything that already + * carries rendered MathML. + */ +object UnrenderedTex { + + /** + * Length past which a delimiter pair is read as two unrelated delimiters rather than + * one expression. Generous: the reported article's longest block (a ten-line `align` + * environment) is well under half this. + */ + private const val MAX_EXPRESSION_LENGTH = 2_000 + + /** Delimiter pairs, matching the ones extraction renders (see ReaderExtractor). */ + private val DELIMITERS = listOf("\\(" to "\\)", "\\[" to "\\]", "$$" to "$$") + + /** + * Characters no plain sentence between two stray delimiters is likely to contain but + * essentially every real expression does -- a command, a group, a sub- or superscript. + */ + private const val TEX_MARKERS = "\\{_^" + + /** + * Quoted source, not maths: extraction deliberately leaves `<pre>`/`<code>` alone, so + * an article demonstrating TeX would otherwise be convicted on every open and still, + * correctly, come back unchanged. + */ + private val CODE_BLOCK_REGEX = Regex( + "<(pre|code)\\b[^>]*>.*?</\\1\\s*>", + setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL) + ) + + /** + * True when [html] is an extraction whose maths never got rendered. + * + * Rendered maths anywhere in it means extraction already had its go at this page, and + * nothing is to be gained by sending it round again: an expression KaTeX could not + * render is still in there as source and always will be. + */ + fun looksLikeUnrenderedTex(html: String?): Boolean { + if (html.isNullOrBlank() || html.contains("<math", ignoreCase = true)) return false + + val prose = html.replace(CODE_BLOCK_REGEX, " ") + return DELIMITERS.any { (open, close) -> containsExpression(prose, open, close) } + } + + private fun containsExpression(text: String, open: String, close: String): Boolean { + var opensAt = text.indexOf(open) + while (opensAt >= 0) { + val innerStart = opensAt + open.length + val closesAt = text.indexOf(close, innerStart) + // No closing delimiter left in the content: whatever follows is prose that + // happens to contain an opening one. + if (closesAt < 0) break + if (isExpression(text.substring(innerStart, closesAt))) return true + opensAt = text.indexOf(open, innerStart) + } + return false + } + + private fun isExpression(inner: String): Boolean = + inner.isNotBlank() && + inner.length <= MAX_EXPRESSION_LENGTH && + // The span between two stray delimiters is markup, and markup brings its own + // braces and underscores along (ids, filenames, inline styles). A tag anywhere + // inside is on its own enough to say the two delimiters are unrelated. + !inner.contains('<') && + inner.any { it in TEX_MARKERS } +} |
