From e50f4fbbca21b85648f6c4045cbd50f672d315cd Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Fri, 16 Oct 2026 21:04:38 +0000 Subject: Render an article's maths instead of showing its TeX source Reported against lcamtuf's Substack ("we should render tex", blog.coredump.cx/p/yours-truly): reader mode showed raw "\(p = \text{...}\)" where equations should be. What the page actually ships is the TeX SOURCE, as text, inside a "latex-rendered" div -- Substack renders it in the browser with its own script, which never runs here. Readability keeps that text verbatim (it strips the class, not the text), so nothing was being discarded during extraction and no amount of extraction-side repair would have helped: there was simply never any rendered markup to keep. So render it during extraction, where JavaScript is already running for Readability's sake. KaTeX (vendored in assets, never fetched -- this runs against untrusted third-party pages) converts each expression to MathML before Readability sees the document, and the reader WebView renders that natively. The reader keeps JavaScript OFF, as it must, and needs no stylesheet, webfont or script of KaTeX's; the only reader-side change is containing a wide equation so it scrolls within its own box instead of dragging the whole article sideways. Two details worth knowing: - KaTeX refuses {align} outside display mode, and Substack wraps display maths in INLINE delimiters (a block-styled span does the centring), so a mode that renders nothing is retried in the other one. An expression neither mode can render is left as source -- no worse than before. - The reported article came with "extractedContent cached: true", so the raw source was already on disk and every open read it back. Recognised and dropped on read (same mechanism as the cached consent notice), which re-extracts the article the next time it is opened. APK grows by ~90KB: the vendored katex.min.js (267KB) compresses to 91KB in the APK. No new permissions, no schema change, no new JS-enabled surface. Verified on the emulator against the live article: stale raw-TeX cache discarded, article re-extracted, all four equations rendered, and a wide align block scrolls inside itself. --- app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt | 5 +++-- app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) (limited to 'app/src/main/java/org/terst/feedreader/ui') diff --git a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt index f08088b..9952dc5 100644 --- a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt +++ b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt @@ -221,7 +221,8 @@ private fun ReaderContent( feedContent = loaded.feedContentHtml // A previously-cached extraction can be the site's cookie-consent notice rather - // than the article (reported live on a New Republic feature). That is dropped + // than the article (reported live on a New Republic feature), or an older + // extraction holding raw TeX source where its maths should be. Either is dropped // from the cache on read, which leaves nothing to show -- so re-extract, or the // repair would just turn a wrong article into a blank one. val cachedExtraction = loadCachedExtraction(loaded.extractedContent) { @@ -239,7 +240,7 @@ private fun ReaderContent( if (loadedFeed?.readerModeEnabled == true) { isReaderMode = true } - if (loadedFeed?.readerModeEnabled == true || cachedExtraction.discardedConsentNotice) { + if (loadedFeed?.readerModeEnabled == true || cachedExtraction.discarded) { if (readerContent == null) { isExtracting = true val result = ReaderExtractor.extractResult(context, loaded.link) diff --git a/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt b/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt index 6d96e79..f1400d3 100644 --- a/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt +++ b/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt @@ -514,6 +514,13 @@ fun wrapReaderHtml( explicit width/height was deliberately sized by the page itself, so leave those alone -- just cap the ones that would otherwise size themselves. */ svg:not([width]):not([height]) { max-width: 32px; max-height: 32px; } + /* Maths arrives as MathML, which the WebView renders itself -- no script, no webfont, + no stylesheet of KaTeX's (see ReaderExtractor). It only needs containing: an aligned + multi-line derivation is routinely wider than a phone screen, and obeys + neither the img nor the svg clamp above, so without this one wide equation drags the + whole article into horizontal scrolling. */ + math { max-width: 100%; } + math[display="block"] { display: block; overflow-x: auto; overflow-y: hidden; margin: 1em 0; } ${if (!title.isNullOrBlank()) "

${escapeHtml(title)}

" else ""}${ if (publishedAt != null) "

${escapeHtml(absoluteDateText(publishedAt))}

" else "" -- cgit v1.2.3