summaryrefslogtreecommitdiff
path: root/app/src/androidTest/java/org/terst/feedreader/readability/ReaderExtractorMathTest.kt
blob: 67b3bf1096332a84935c7ed447a1d424e7605b17 (plain)
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
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
 * `<div class="... latex-rendered" data-component-name="Latex">` 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 = """
        <html><head><title>Yours truly</title></head>
        <body>
        <article>
        <h1>Yours truly</h1>
        <p>Imagine the following seemingly innocent sentence:</p>
        <div class="pencraft latex-rendered" data-component-name="Latex">
          <div class="pencraft"><span style="display:block">\(p = \text{“if }p\text{ is true, then pigs can fly”}\)</span></div>
        </div>
        <p>$filler</p>
        <p>A slice of the set may look the following way:</p>
        <div class="pencraft latex-rendered" data-component-name="Latex">
          <div class="pencraft"><span style="display:block">\(\begin{align}
t_{1012} &amp;= \text{“Cindy weighs 12 pounds.”} \\
t_{1013} &amp;= \text{“Cindy doesn't weigh 13 pounds.”}
\end{align}\)</span></div>
        </div>
        <p>$filler</p>
        </article>
        </body></html>
    """.trimIndent()

    // The other two conventional delimiter styles, and the one place maths must NOT be
    // rendered: an article quoting TeX source on purpose, inside <pre><code>.
    private val delimiterFixture = """
        <html><head><title>Delimiter styles</title></head>
        <body>
        <article>
        <h1>Delimiter styles</h1>
        <p>$filler</p>
        <p>The bracket form: \[ x_{1} + y^{2} \] and the dollar form: $$ a_{n} = b^{n} $$ in one paragraph.</p>
        <p>$filler</p>
        <p>To write that yourself, type:</p>
        <pre><code>\(E = mc^{2}\)</code></pre>
        <p>$filler</p>
        </article>
        </body></html>
    """.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("<math").size - 1 == 2)
        assertFalse(
            "no raw TeX delimiters should be left in the extracted content, got:\n$content",
            content.contains("\\(") || content.contains("\\)")
        )
        assertTrue(
            "the article's own text must survive alongside the maths",
            content.contains("Imagine the following seemingly innocent sentence")
        )
    }

    @Test
    fun anAlignBlockFallsBackToDisplayMode() {
        val result = extract(substackFixture)

        val content = result?.content.orEmpty()
        assertTrue(
            "the {align} block must render as display maths (KaTeX rejects it inline), got:\n$content",
            content.contains("display=\"block\"")
        )
        // KaTeX renders every space inside \text{...} as a non-breaking space, so the
        // sentence is in there with U+00A0 between its words, not plain spaces.
        assertTrue(
            "the align block's own text should be rendered, got:\n$content",
            content.replace("&nbsp;", " ").replace('\u00a0', ' ').contains("Cindy weighs 12 pounds")
        )
    }

    @Test
    fun bracketAndDollarDelimitersAreRenderedToo() {
        val result = extract(delimiterFixture)

        val content = result?.content.orEmpty()
        assertTrue("both delimiter styles should render, got:\n$content", content.split("<math").size - 1 == 2)
        assertFalse("the bracket delimiters should be consumed", content.contains("\\["))
        assertFalse("the dollar delimiters should be consumed", content.contains("$$"))
        assertTrue("surrounding prose must be preserved", content.contains("in one paragraph"))
    }

    @Test
    fun texQuotedInACodeBlockIsLeftAsSource() {
        val result = extract(delimiterFixture)

        val content = result?.content.orEmpty()
        assertTrue(
            "TeX quoted inside <pre><code> is the point of the sentence and must stay source, got:\n$content",
            content.contains("E = mc^{2}")
        )
    }
}