diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/LatexEquation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/LatexEquation.kt index 434561608a..06e74738ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/LatexEquation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/LatexEquation.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.sp +import com.vitorpamplona.amethyst.commons.richtext.MathParser import ru.noties.jlatexmath.JLatexMathDrawable /** @@ -58,6 +59,11 @@ fun LatexEquation( val density = LocalDensity.current val fontSize = LocalTextStyle.current.fontSize + // Inline math has no legitimate `\\` line break, so a doubled backslash is an + // over-escaped command (`\\ldots` → `\ldots`). Without this JLaTeXMath would + // read `\\` as a line break and render `ldots`/`cdots` as literal letters. + val formula = if (displayMode) latex else MathParser.collapseDoubledBackslashes(latex) + // Display math renders a touch larger than the surrounding prose, matching // the visual weight KaTeX gives block equations. val textSizePx = @@ -67,10 +73,10 @@ fun LatexEquation( } val drawable = - remember(latex, color, textSizePx) { + remember(formula, color, textSizePx) { runCatching { JLatexMathDrawable - .builder(latex) + .builder(formula) .textSize(textSizePx) .color(color) .align(JLatexMathDrawable.ALIGN_LEFT) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MathParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MathParser.kt index 96a4026b67..5dbd6b8744 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MathParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MathParser.kt @@ -69,6 +69,20 @@ object MathParser { return first >= 0 && line.indexOf('$', first + 1) >= 0 } + /** + * Collapses doubled backslashes (`\\cmd` → `\cmd`). + * + * Some sources over-escape their LaTeX (every `\` written as `\\`). In inline + * math a `\\` is a TeX forced line break that is never intended, so `\\ldots` + * really means `\ldots`. Left as-is, a renderer like JLaTeXMath reads `\\` as + * a line break — splitting the equation across two lines — and the trailing + * command name (`ldots`, `cdots`) as literal letters, so the symbol is lost. + * + * No-op when the input isn't over-escaped. Only meant for inline math; display + * math (`$$…$$`) may use `\\` as a genuine line break in aligned/matrix forms. + */ + fun collapseDoubledBackslashes(latex: String): String = if (latex.contains("\\\\")) latex.replace("\\\\", "\\") else latex + /** * Splits [line] on spaces into [Token.Word]s, with any whitespace-delimited * math span surfaced as a [Token.Math]. diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/MathParserTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/MathParserTest.kt index fa21570120..67dbccf11a 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/MathParserTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/MathParserTest.kt @@ -122,6 +122,19 @@ class MathParserTest { assertTrue(math(MathParser.split("empty $d$d here")).isEmpty()) } + @Test + fun collapsesOverEscapedBackslashes() { + // `\\ldots` (over-escaped) -> `\ldots`; otherwise JLaTeXMath sees a line + // break plus the literal letters "ldots". + assertEquals("A_1, ${bs}ldots, A_n", MathParser.collapseDoubledBackslashes("A_1, $bs$bs" + "ldots, A_n")) + assertEquals("x_1 = ${bs}cdots = x_n", MathParser.collapseDoubledBackslashes("x_1 = $bs$bs" + "cdots = x_n")) + } + + @Test + fun singleBackslashLatexIsLeftAlone() { + assertEquals("${bs}frac{1}{2} + ${bs}sqrt{x}", MathParser.collapseDoubledBackslashes("${bs}frac{1}{2} + ${bs}sqrt{x}")) + } + @Test fun gluedMathStaysPlainWord() { // Math without a separating space is not whitespace-delimited, so it