package org.terst.feedreader.readability import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import kotlinx.coroutines.runBlocking import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.junit.After import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith /** * Maths rendering during extraction ("we should render tex", reported against lcamtuf's * Substack). * * What the live page actually ships -- confirmed by fetching * https://blog.coredump.cx/p/yours-truly -- is TeX SOURCE, in a text node, inside a * `
` wrapper: Substack renders * it client-side with its own script, which never runs in this app. Readability keeps that * text verbatim (it strips the class but not the text), so the reader showed the source. * * So extraction renders it: KaTeX (vendored in assets, never fetched) converts each * expression to MathML before Readability runs, and the reader WebView -- still with * JavaScript off -- renders the MathML natively. * * Needs a real WebView, so this is instrumented. Fixtures reproduce the exact markup shape * from the live page rather than being a scraped copy of it (see ReaderExtractorFixtureTest * for why). */ @RunWith(AndroidJUnit4::class) class ReaderExtractorMathTest { private lateinit var server: MockWebServer @Before fun startServer() { server = MockWebServer() server.start() } @After fun stopServer() { server.shutdown() } private fun extract(html: String): ExtractedArticle? { server.enqueue(MockResponse().setBody(html).setResponseCode(200)) val url = server.url("/article").toString() val context = InstrumentationRegistry.getInstrumentation().targetContext return runBlocking { ReaderExtractor.extract(context, url) } } private val filler = "Considered as a whole, an if-then statement cannot be true when its antecedent holds " + "and its consequent does not, which is the whole of the trick being played here. " // Substack's shape, verbatim from the live page: inline \( \) delimiters in a text node, // inside a block-styled span inside the latex-rendered wrapper. Note the align block is // ALSO wrapped in inline delimiters -- KaTeX refuses {align} outside display mode, so // rendering has to fall back to display mode rather than give up. private val substackFixture = """ Yours truly

Yours truly

Imagine the following seemingly innocent sentence:

\(p = \text{“if }p\text{ is true, then pigs can fly”}\)

$filler

A slice of the set may look the following way:

\(\begin{align} t_{1012} &= \text{“Cindy weighs 12 pounds.”} \\ t_{1013} &= \text{“Cindy doesn't weigh 13 pounds.”} \end{align}\)

$filler

""".trimIndent() // The other two conventional delimiter styles, and the one place maths must NOT be // rendered: an article quoting TeX source on purpose, inside
.
    private val delimiterFixture = """
        Delimiter styles
        
        

Delimiter styles

$filler

The bracket form: \[ x_{1} + y^{2} \] and the dollar form: $$ a_{n} = b^{n} $$ in one paragraph.

$filler

To write that yourself, type:

\(E = mc^{2}\)

$filler

""".trimIndent() @Test fun substackTexSourceIsRenderedToMathMl() { val result = extract(substackFixture) assertNotNull("extraction should succeed", result) val content = result?.content.orEmpty() assertTrue("both expressions should be rendered as MathML, got:\n$content", content.split(" is the point of the sentence and must stay source, got:\n$content", content.contains("E = mc^{2}") ) } }