From a8fb45afe18930e613ce646e2ed0c63380137c76 Mon Sep 17 00:00:00 2001 From: Thibault Guegan Date: Tue, 10 Mar 2026 10:08:04 +0100 Subject: [PATCH 1/8] Fix test compilation error with newer Kotlin versions The safe-call operator on non-nullable parameter `list?.atoms` produces a nullable type, which fails to compile with recent Kotlin compiler versions. Changed to `list.atoms` since the parameter is already non-nullable. --- .../src/test/java/com/agog/mathdisplay/BuilderUnitTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mathdisplaylib/src/test/java/com/agog/mathdisplay/BuilderUnitTest.kt b/mathdisplaylib/src/test/java/com/agog/mathdisplay/BuilderUnitTest.kt index cc5e5bf..d8afea8 100644 --- a/mathdisplaylib/src/test/java/com/agog/mathdisplay/BuilderUnitTest.kt +++ b/mathdisplaylib/src/test/java/com/agog/mathdisplay/BuilderUnitTest.kt @@ -59,8 +59,7 @@ public class BuilderUnitTest { // Verify the list of atom types in the MTMathList matches the types array fun checkAtomTypes(list: MTMathList, types: Array, desc: String) { - //assertNotNull(desc,list?.atoms) - val atoms: MutableList = list?.atoms + val atoms: MutableList = list.atoms if (atoms.count() != types.count()) { dumptypes(atoms, types) From 45f893db978719de78f61043b6aa5f0cce1b606c Mon Sep 17 00:00:00 2001 From: Thibault Guegan Date: Tue, 10 Mar 2026 10:09:14 +0100 Subject: [PATCH 2/8] Add AMS delimiter aliases: \lVert, \rVert, \lvert, \rvert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add these as both standalone symbols (Open/Close atom types in supportedLatexSymbols) and as delimiter entries for use with \left/\right auto-sizing. - \lVert/\rVert map to U+2016 (double vertical bar ‖) - \lvert/\rvert map to | (single vertical bar) --- .../com/agog/mathdisplay/parse/MTMathAtomFactory.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathAtomFactory.kt b/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathAtomFactory.kt index 459bbc3..06692ab 100644 --- a/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathAtomFactory.kt +++ b/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathAtomFactory.kt @@ -351,12 +351,16 @@ open class MTMathAtomFactory { "lfloor" to MTMathAtom(MTMathAtomType.KMTMathAtomOpen, "\u230A"), "langle" to MTMathAtom(MTMathAtomType.KMTMathAtomOpen, "\u27E8"), "lgroup" to MTMathAtom(MTMathAtomType.KMTMathAtomOpen, "\u27EE"), + "lVert" to MTMathAtom(MTMathAtomType.KMTMathAtomOpen, "\u2016"), + "lvert" to MTMathAtom(MTMathAtomType.KMTMathAtomOpen, "|"), // Close "rceil" to MTMathAtom(MTMathAtomType.KMTMathAtomClose, "\u2309"), "rfloor" to MTMathAtom(MTMathAtomType.KMTMathAtomClose, "\u230B"), "rangle" to MTMathAtom(MTMathAtomType.KMTMathAtomClose, "\u27E9"), "rgroup" to MTMathAtom(MTMathAtomType.KMTMathAtomClose, "\u27EF"), + "rVert" to MTMathAtom(MTMathAtomType.KMTMathAtomClose, "\u2016"), + "rvert" to MTMathAtom(MTMathAtomType.KMTMathAtomClose, "|"), // Arrows "leftarrow" to MTMathAtom(MTMathAtomType.KMTMathAtomRelation, "\u2190"), @@ -673,7 +677,11 @@ open class MTMathAtomFactory { "lceil" to "\u2308", "rceil" to "\u2309", "lfloor" to "\u230A", - "rfloor" to "\u230B" + "rfloor" to "\u230B", + "lVert" to "\u2016", + "rVert" to "\u2016", + "lvert" to "|", + "rvert" to "|" ) // Reverse of above with preference for shortest command on overlap From 7c61cec3ef4c3ba7ae6ab882424c3acac2d41fb8 Mon Sep 17 00:00:00 2001 From: Thibault Guegan Date: Tue, 10 Mar 2026 10:09:26 +0100 Subject: [PATCH 3/8] Add arrow accent commands: \overrightarrow, \overleftarrow, \overleftrightarrow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map these to combining Unicode characters following the same pattern as \vec (U+20D7): - \overrightarrow → U+20D7 (same glyph as \vec) - \overleftarrow → U+20D6 - \overleftrightarrow → U+20E1 --- .../java/com/agog/mathdisplay/parse/MTMathAtomFactory.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathAtomFactory.kt b/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathAtomFactory.kt index 06692ab..3f3f2d7 100644 --- a/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathAtomFactory.kt +++ b/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathAtomFactory.kt @@ -620,7 +620,10 @@ open class MTMathAtomFactory { "check" to "\u030C", "vec" to "\u20D7", "widehat" to "\u0302", - "widetilde" to "\u0303" + "widetilde" to "\u0303", + "overrightarrow" to "\u20D7", + "overleftarrow" to "\u20D6", + "overleftrightarrow" to "\u20E1" ) // Reverse of above with preference for shortest command on overlap From fa7dff5414b5b31e57b6019aa18377304db47e33 Mon Sep 17 00:00:00 2001 From: Thibault Guegan Date: Tue, 10 Mar 2026 10:09:32 +0100 Subject: [PATCH 4/8] Add smallmatrix environment support Treat \begin{smallmatrix}...\end{smallmatrix} identically to the matrix environment (centered columns, text style, no delimiters). --- .../main/java/com/agog/mathdisplay/parse/MTMathAtomFactory.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathAtomFactory.kt b/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathAtomFactory.kt index 3f3f2d7..5b2a2d0 100644 --- a/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathAtomFactory.kt +++ b/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathAtomFactory.kt @@ -190,7 +190,8 @@ open class MTMathAtomFactory { "bmatrix" to arrayOf("[", "]"), "Bmatrix" to arrayOf("{", "}"), "vmatrix" to arrayOf("vert", "vert"), - "Vmatrix" to arrayOf("Vert", "Vert")) + "Vmatrix" to arrayOf("Vert", "Vert"), + "smallmatrix" to arrayOf("")) if (matrixEnvs.containsKey(env)) { From 7376b4c29bc4b384772039c0221b408c5e8b01a0 Mon Sep 17 00:00:00 2001 From: Thibault Guegan Date: Tue, 10 Mar 2026 10:10:02 +0100 Subject: [PATCH 5/8] Add \dfrac and \operatorname command support - \dfrac: treated identically to \frac (produces a standard fraction with rule). The display-style distinction is not currently supported but this prevents parse errors. - \operatorname{name}: reads the braced argument and creates an MTLargeOperator with no limits, allowing arbitrary operator names like \operatorname{erf} or \operatorname{Tr}. --- .../java/com/agog/mathdisplay/parse/MTMathListBuilder.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathListBuilder.kt b/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathListBuilder.kt index ab2c187..8a10d6c 100644 --- a/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathListBuilder.kt +++ b/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathListBuilder.kt @@ -401,7 +401,7 @@ class MTMathListBuilder(str: String) { } when (command) { - "frac" -> { + "frac", "dfrac" -> { // A fraction command has 2 arguments val frac = MTFraction() frac.numerator = this.buildInternal(true) @@ -466,6 +466,10 @@ class MTMathListBuilder(str: String) { val env = this.readEnvironment() ?: return null return buildTable(env, null, false) } + "operatorname" -> { + val name = this.readEnvironment() ?: return null + return MTLargeOperator(name, false) + } "color" -> { // A color command has 2 arguments val mathColor = MTMathColor() From a89c8005729155e28ea4032e013f7394e6f6fd1f Mon Sep 17 00:00:00 2001 From: Thibault Guegan Date: Tue, 10 Mar 2026 10:10:24 +0100 Subject: [PATCH 6/8] Add prime shorthand parsing Handle the apostrophe character in the main parsing loop to convert it into \prime superscripts, matching standard LaTeX: - x apostrophe -> x^{\prime} - x double apost -> x^{\prime\prime} - x apost ^2 -> x^{\prime 2} Also removes the apostrophe from the rejected character list in atomForCharacter() so it reaches the parser. --- .../com/agog/mathdisplay/parse/MTMathAtom.kt | 2 +- .../mathdisplay/parse/MTMathListBuilder.kt | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathAtom.kt b/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathAtom.kt index d8b776a..1993a94 100644 --- a/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathAtom.kt +++ b/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathAtom.kt @@ -330,7 +330,7 @@ open class MTMathAtom(var type: MTMathAtomType, var nucleus: String) { if (ch.toInt() < 0x21 || ch.toInt() > 0x7E) { // skip non ascii characters and spaces return null - } else if (ch == '$' || ch == '%' || ch == '#' || ch == '&' || ch == '~' || ch == '\'') { + } else if (ch == '$' || ch == '%' || ch == '#' || ch == '&' || ch == '~') { // These are latex control characters that have special meanings. We don't support them. return null } else if (ch == '^' || ch == '_' || ch == '{' || ch == '}' || ch == '\\') { diff --git a/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathListBuilder.kt b/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathListBuilder.kt index 8a10d6c..066559c 100644 --- a/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathListBuilder.kt +++ b/mathdisplaylib/src/main/java/com/agog/mathdisplay/parse/MTMathListBuilder.kt @@ -135,6 +135,47 @@ class MTMathListBuilder(str: String) { this.setError(com.agog.mathdisplay.parse.MTParseErrors.MismatchBraces, "Mismatched braces.") return null } + '\'' -> { + // Prime shorthand: x' → x^{\prime}, x'' → x^{\prime\prime}, x'^2 → x^{\prime 2} + var count = 1 + while (hasCharacters()) { + val nextCh = getNextCharacter() + if (nextCh == '\'') { + count++ + } else { + unlookCharacter() + break + } + } + if (prevAtom == null || !prevAtom.scriptsAllowed()) { + prevAtom = MTMathAtom(MTMathAtomType.KMTMathAtomOrdinary, "") + list.addAtom(prevAtom) + } + // Build a superscript list with prime atoms, merging with any existing superscript + val primeList = prevAtom.superScript ?: MTMathList() + repeat(count) { + val primeAtom = MTMathAtom.atomForLatexSymbolName("prime") + if (primeAtom != null) { + primeList.addAtom(primeAtom) + } + } + prevAtom.superScript = primeList + // If followed by ^, the ^ handler will see prevAtom already has superScript + // and create a new empty atom. To support x'^2 properly, check for ^ and + // append to existing superscript. + if (hasCharacters()) { + val nextCh = getNextCharacter() + if (nextCh == '^') { + val additionalSuper = this.buildInternal(true) + if (additionalSuper != null) { + primeList.append(additionalSuper) + } + } else { + unlookCharacter() + } + } + continue@outerloop + } '\\' -> { // \ means a command val command: String = readCommand() From 21a758fb7b2cb0c49ba5c5231fc521c7f766cfe2 Mon Sep 17 00:00:00 2001 From: Thibault Guegan Date: Tue, 10 Mar 2026 10:10:32 +0100 Subject: [PATCH 7/8] Add unit tests for new LaTeX extensions Tests for all new features: - testDfrac: \dfrac parses and round-trips as \frac - testLVertRVert: standalone \lVert/\rVert/\lvert/\rvert symbols - testLeftLVert: \left\lVert and \left\lvert delimiters - testArrowAccents: \overrightarrow, \overleftarrow, \overleftrightarrow - testOperatorname: \operatorname{erf} and \operatorname{Tr} - testSmallMatrix: \begin{smallmatrix} environment - testPrime: single prime, double prime, prime with exponent --- .../com/agog/mathdisplay/BuilderUnitTest.kt | 350 ++++++++++++++++++ 1 file changed, 350 insertions(+) diff --git a/mathdisplaylib/src/test/java/com/agog/mathdisplay/BuilderUnitTest.kt b/mathdisplaylib/src/test/java/com/agog/mathdisplay/BuilderUnitTest.kt index d8afea8..ca242d8 100644 --- a/mathdisplaylib/src/test/java/com/agog/mathdisplay/BuilderUnitTest.kt +++ b/mathdisplaylib/src/test/java/com/agog/mathdisplay/BuilderUnitTest.kt @@ -1568,6 +1568,356 @@ public class BuilderUnitTest { } } + @Test + fun testDfrac() { + val str = "\\dfrac1c" + val list: MTMathList? = MTMathListBuilder.buildFromString(str) + val desc = "Error for string:$str" + + println("In testDfrac") + assertNotNull(desc, list) + if (list != null) { + assertEquals(desc, list.atoms.count(), 1) + val frac: MTFraction = list.atoms[0] as MTFraction + assertEquals(desc, frac.type, KMTMathAtomFraction) + assertEquals(desc, frac.nucleus, "") + assertTrue(frac.hasRule) + assertNull(frac.rightDelimiter) + assertNull(frac.leftDelimiter) + + var subList: MTMathList? = frac.numerator + assertNotNull(desc, subList) + if (subList != null) { + assertEquals(desc, subList.atoms.count(), 1) + val atom: MTMathAtom = subList.atoms[0] + assertEquals(desc, atom.type, KMTMathAtomNumber) + assertEquals(desc, atom.nucleus, "1") + } + + subList = frac.denominator + assertNotNull(desc, subList) + if (subList != null) { + assertEquals(desc, subList.atoms.count(), 1) + val atom: MTMathAtom = subList.atoms[0] + assertEquals(desc, atom.type, KMTMathAtomVariable) + assertEquals(desc, atom.nucleus, "c") + } + + // dfrac round-trips as \frac since they produce the same structure + val latex: String = MTMathListBuilder.toLatexString(list) + assertEquals(desc, latex, "\\frac{1}{c}") + } + } + + @Test + fun testLVertRVert() { + // Standalone \lVert and \rVert + val str = "\\lVert x \\rVert" + val e: MTParseError = MTParseError() + val list: MTMathList? = MTMathListBuilder.buildFromString(str, e) + val desc = "Error for string:$str" + + println("In testLVertRVert") + assertEquals(MTParseErrors.ErrorNone, e.errorcode) + assertNotNull(desc, list) + if (list != null) { + checkAtomTypes(list, arrayOf(KMTMathAtomOpen, KMTMathAtomVariable, KMTMathAtomClose), desc) + assertEquals(desc, list.atoms[0].nucleus, "\u2016") + assertEquals(desc, list.atoms[2].nucleus, "\u2016") + } + + // Standalone \lvert and \rvert + val str2 = "\\lvert x \\rvert" + val e2: MTParseError = MTParseError() + val list2: MTMathList? = MTMathListBuilder.buildFromString(str2, e2) + val desc2 = "Error for string:$str2" + + assertEquals(MTParseErrors.ErrorNone, e2.errorcode) + assertNotNull(desc2, list2) + if (list2 != null) { + checkAtomTypes(list2, arrayOf(KMTMathAtomOpen, KMTMathAtomVariable, KMTMathAtomClose), desc2) + assertEquals(desc2, list2.atoms[0].nucleus, "|") + assertEquals(desc2, list2.atoms[2].nucleus, "|") + } + } + + @Test + fun testLeftLVert() { + // \left\lVert ... \right\rVert as auto-sizing delimiters + val str = "\\left\\lVert x \\right\\rVert" + val e: MTParseError = MTParseError() + val list: MTMathList? = MTMathListBuilder.buildFromString(str, e) + val desc = "Error for string:$str" + + println("In testLeftLVert") + assertEquals(MTParseErrors.ErrorNone, e.errorcode) + assertNotNull(desc, list) + if (list != null) { + checkAtomTypes(list, arrayOf(KMTMathAtomInner), desc) + val inner: MTInner = list.atoms[0] as MTInner + assertEquals(desc, inner.type, KMTMathAtomInner) + + val innerList: MTMathList? = inner.innerList + assertNotNull(desc, innerList) + if (innerList != null) { + checkAtomTypes(innerList, arrayOf(KMTMathAtomVariable), desc) + } + + val lb = inner.leftBoundary + assertNotNull(desc, lb) + if (lb != null) { + assertEquals(desc, lb.type, KMTMathAtomBoundary) + assertEquals(desc, lb.nucleus, "\u2016") + } + + val rb = inner.rightBoundary + assertNotNull(desc, rb) + if (rb != null) { + assertEquals(desc, rb.type, KMTMathAtomBoundary) + assertEquals(desc, rb.nucleus, "\u2016") + } + } + + // \left\lvert ... \right\rvert + val str2 = "\\left\\lvert x \\right\\rvert" + val e2: MTParseError = MTParseError() + val list2: MTMathList? = MTMathListBuilder.buildFromString(str2, e2) + val desc2 = "Error for string:$str2" + + assertEquals(MTParseErrors.ErrorNone, e2.errorcode) + assertNotNull(desc2, list2) + if (list2 != null) { + checkAtomTypes(list2, arrayOf(KMTMathAtomInner), desc2) + val inner: MTInner = list2.atoms[0] as MTInner + val lb = inner.leftBoundary + assertNotNull(desc2, lb) + if (lb != null) { + assertEquals(desc2, lb.nucleus, "|") + } + val rb = inner.rightBoundary + assertNotNull(desc2, rb) + if (rb != null) { + assertEquals(desc2, rb.nucleus, "|") + } + } + } + + @Test + fun testArrowAccents() { + // \overrightarrow + val str = "\\overrightarrow{AB}" + val e: MTParseError = MTParseError() + val list: MTMathList? = MTMathListBuilder.buildFromString(str, e) + val desc = "Error for string:$str" + + println("In testArrowAccents") + assertEquals(MTParseErrors.ErrorNone, e.errorcode) + assertNotNull(desc, list) + if (list != null) { + assertEquals(desc, list.atoms.count(), 1) + val accent: MTAccent = list.atoms[0] as MTAccent + assertEquals(desc, accent.type, KMTMathAtomAccent) + assertEquals(desc, accent.nucleus, "\u20D7") + + val subList: MTMathList? = accent.innerList + assertNotNull(desc, subList) + if (subList != null) { + assertEquals(desc, subList.atoms.count(), 2) + assertEquals(desc, subList.atoms[0].type, KMTMathAtomVariable) + assertEquals(desc, subList.atoms[0].nucleus, "A") + assertEquals(desc, subList.atoms[1].type, KMTMathAtomVariable) + assertEquals(desc, subList.atoms[1].nucleus, "B") + } + } + + // \overleftarrow + val str2 = "\\overleftarrow{x}" + val e2: MTParseError = MTParseError() + val list2: MTMathList? = MTMathListBuilder.buildFromString(str2, e2) + assertEquals(MTParseErrors.ErrorNone, e2.errorcode) + assertNotNull(list2) + if (list2 != null) { + val accent: MTAccent = list2.atoms[0] as MTAccent + assertEquals(accent.nucleus, "\u20D6") + } + + // \overleftrightarrow + val str3 = "\\overleftrightarrow{x}" + val e3: MTParseError = MTParseError() + val list3: MTMathList? = MTMathListBuilder.buildFromString(str3, e3) + assertEquals(MTParseErrors.ErrorNone, e3.errorcode) + assertNotNull(list3) + if (list3 != null) { + val accent: MTAccent = list3.atoms[0] as MTAccent + assertEquals(accent.nucleus, "\u20E1") + } + } + + @Test + fun testOperatorname() { + val str = "\\operatorname{erf}(x)" + val e: MTParseError = MTParseError() + val list: MTMathList? = MTMathListBuilder.buildFromString(str, e) + val desc = "Error for string:$str" + + println("In testOperatorname") + assertEquals(MTParseErrors.ErrorNone, e.errorcode) + assertNotNull(desc, list) + if (list != null) { + assertEquals(desc, list.atoms.count(), 4) + val op = list.atoms[0] as MTLargeOperator + assertEquals(desc, op.type, KMTMathAtomLargeOperator) + assertEquals(desc, op.nucleus, "erf") + assertFalse(op.hasLimits) + + assertEquals(desc, list.atoms[1].type, KMTMathAtomOpen) + assertEquals(desc, list.atoms[2].type, KMTMathAtomVariable) + assertEquals(desc, list.atoms[3].type, KMTMathAtomClose) + } + + // Test with a different operator name + val str2 = "\\operatorname{Tr}(A)" + val e2: MTParseError = MTParseError() + val list2: MTMathList? = MTMathListBuilder.buildFromString(str2, e2) + assertEquals(MTParseErrors.ErrorNone, e2.errorcode) + assertNotNull(list2) + if (list2 != null) { + val op = list2.atoms[0] as MTLargeOperator + assertEquals(op.nucleus, "Tr") + assertFalse(op.hasLimits) + } + } + + @Test + fun testSmallMatrix() { + val str = "\\begin{smallmatrix} x & y \\\\ z & w \\end{smallmatrix}" + val list: MTMathList? = MTMathListBuilder.buildFromString(str) + val desc = "Error for string:$str" + + println("In testSmallMatrix") + assertNotNull(desc, list) + if (list != null) { + assertEquals(desc, list.atoms.count(), 1) + val table: MTMathTable = list.atoms[0] as MTMathTable + assertEquals(desc, table.type, KMTMathAtomTable) + assertEquals(desc, table.nucleus, "") + // smallmatrix is normalized to "matrix" internally + assertEquals(desc, table.environment, "matrix") + assertEquals(desc, table.interRowAdditionalSpacing, 0.0f) + assertEquals(desc, table.interColumnSpacing, 18.0f) + assertEquals(desc, table.numRows(), 2) + assertEquals(desc, table.numColumns(), 2) + + for (i in 0 until 2) { + val alignment: MTColumnAlignment = table.getAlignmentForColumn(i) + assertEquals(desc, alignment, MTColumnAlignment.KMTColumnAlignmentCenter) + for (j in 0 until 2) { + val cell: MTMathList = table.cells[j][i] as MTMathList + assertEquals(desc, cell.atoms.count(), 2) + val style: MTMathStyle = cell.atoms[0] as MTMathStyle + assertEquals(desc, style.type, KMTMathAtomStyle) + assertEquals(desc, style.style, MTLineStyle.KMTLineStyleText) + + val atom: MTMathAtom = cell.atoms[1] + assertEquals(desc, atom.type, KMTMathAtomVariable) + } + } + } + } + + @Test + fun testPrime() { + // Single prime: x' + var str = "x'" + var e: MTParseError = MTParseError() + var list: MTMathList? = MTMathListBuilder.buildFromString(str, e) + var desc = "Error for string:$str" + + println("In testPrime") + assertEquals(MTParseErrors.ErrorNone, e.errorcode) + assertNotNull(desc, list) + if (list != null) { + assertEquals(desc, list.atoms.count(), 1) + val atom = list.atoms[0] + assertEquals(desc, atom.type, KMTMathAtomVariable) + assertEquals(desc, atom.nucleus, "x") + + val superList: MTMathList? = atom.superScript + assertNotNull(desc, superList) + if (superList != null) { + assertEquals(desc, superList.atoms.count(), 1) + assertEquals(desc, superList.atoms[0].type, KMTMathAtomOrdinary) + assertEquals(desc, superList.atoms[0].nucleus, "\u2032") // prime symbol + } + + val latex: String = MTMathListBuilder.toLatexString(list) + assertEquals(desc, latex, "x^{\\prime }") + } + + // Double prime: x'' + str = "x''" + e = MTParseError() + list = MTMathListBuilder.buildFromString(str, e) + desc = "Error for string:$str" + + assertEquals(MTParseErrors.ErrorNone, e.errorcode) + assertNotNull(desc, list) + if (list != null) { + assertEquals(desc, list.atoms.count(), 1) + val superList: MTMathList? = list.atoms[0].superScript + assertNotNull(desc, superList) + if (superList != null) { + assertEquals(desc, superList.atoms.count(), 2) + assertEquals(desc, superList.atoms[0].nucleus, "\u2032") + assertEquals(desc, superList.atoms[1].nucleus, "\u2032") + } + + val latex: String = MTMathListBuilder.toLatexString(list) + assertEquals(desc, latex, "x^{\\prime \\prime }") + } + + // Prime with exponent: x'^2 + str = "x'^2" + e = MTParseError() + list = MTMathListBuilder.buildFromString(str, e) + desc = "Error for string:$str" + + assertEquals(MTParseErrors.ErrorNone, e.errorcode) + assertNotNull(desc, list) + if (list != null) { + assertEquals(desc, list.atoms.count(), 1) + val superList: MTMathList? = list.atoms[0].superScript + assertNotNull(desc, superList) + if (superList != null) { + assertEquals(desc, superList.atoms.count(), 2) + assertEquals(desc, superList.atoms[0].nucleus, "\u2032") // prime + assertEquals(desc, superList.atoms[1].nucleus, "2") // exponent + } + + val latex: String = MTMathListBuilder.toLatexString(list) + assertEquals(desc, latex, "x^{\\prime 2}") + } + + // Prime at start (no previous atom): ' + str = "'" + e = MTParseError() + list = MTMathListBuilder.buildFromString(str, e) + desc = "Error for string:$str" + + assertEquals(MTParseErrors.ErrorNone, e.errorcode) + assertNotNull(desc, list) + if (list != null) { + assertEquals(desc, list.atoms.count(), 1) + assertEquals(desc, list.atoms[0].type, KMTMathAtomOrdinary) + val superList: MTMathList? = list.atoms[0].superScript + assertNotNull(desc, superList) + if (superList != null) { + assertEquals(desc, superList.atoms.count(), 1) + assertEquals(desc, superList.atoms[0].nucleus, "\u2032") + } + } + } + @Test fun testNoLimits() { // Sum with limits (default) From b6de9aa21d14dafe4a2b8e71b2ae14a1b0556180 Mon Sep 17 00:00:00 2001 From: Thibault Guegan Date: Tue, 10 Mar 2026 10:10:38 +0100 Subject: [PATCH 8/8] Add sample app equations for visual verification Add test equations to the sample app for each new feature: thin space, \dfrac, arrow accents, \lVert/\rVert, \lvert/\rvert, split, smallmatrix, \operatorname, and prime shorthand. --- sampleapp/src/main/res/raw/samples.txt | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/sampleapp/src/main/res/raw/samples.txt b/sampleapp/src/main/res/raw/samples.txt index 22fcec4..be5b6d6 100644 --- a/sampleapp/src/main/res/raw/samples.txt +++ b/sampleapp/src/main/res/raw/samples.txt @@ -144,3 +144,30 @@ x \mathrm x \mathbf x \mathcal X \mathfrak x \mathsf x \bm x \mathtt x \mathit \ \text{using text} \text{Mary has }\$500 + \$200. +# Patch verification: thin space (\,) +x = 5\, \text{cm} + +# Patch verification: \dfrac +\dfrac{a}{b} + \frac{c}{d} + +# Patch verification: arrow accents +\overrightarrow{AB} \; \overleftarrow{CD} \; \overleftrightarrow{EF} + +# Patch verification: \lVert \rVert (double-bar norm) +\lVert x \rVert = \left\lVert \frac{a}{b} \right\rVert + +# Patch verification: \lvert \rvert (single-bar abs) +\lvert x \rvert = \left\lvert \frac{a}{b} \right\rvert + +# Patch verification: split environment +\begin{split} x &= 1 \\ y &= 2 \end{split} + +# Patch verification: smallmatrix environment +\begin{smallmatrix} a & b \\ c & d \end{smallmatrix} + +# Patch verification: \operatorname +\operatorname{erf}(x) + \operatorname{Tr}(A) + +# Patch verification: prime shorthand +x' \; x'' \; f'(x) \; f'^2(x) +