diff --git a/Sources/SwiftMath/MathRender/MTMathListBuilder.swift b/Sources/SwiftMath/MathRender/MTMathListBuilder.swift index f662d4d..1af8dd9 100644 --- a/Sources/SwiftMath/MathRender/MTMathListBuilder.swift +++ b/Sources/SwiftMath/MathRender/MTMathListBuilder.swift @@ -366,6 +366,13 @@ public struct MTMathListBuilder { // \ means a command assert(!oneCharOnly, "This should have been handled before") assert(stop == nil, "This should have been handled before") + // Special case: } terminates implicit table (envName == nil) created by \\ + // This happens when \\ is used inside braces: \substack{a \\ b} + if self.currentEnv != nil && self.currentEnv!.envName == nil { + // Mark environment as ended, don't consume the } + self.currentEnv!.ended = true + return list + } // We encountered a closing brace when there is no stop set, that means there was no // corresponding opening brace. self.setError(.mismatchBraces, message:"Mismatched braces.") @@ -740,6 +747,35 @@ public struct MTMathListBuilder { let under = MTUnderLine() under.innerList = self.buildInternal(true) return under + } else if command == "substack" { + // \substack reads ONE braced argument containing rows separated by \\ + // Similar to how \frac reads {numerator}{denominator} + + // Read the braced content using standard pattern + let content = self.buildInternal(true) + + if content == nil { + return nil + } + + // The content may already be a table if \\ was encountered + // Check if we got a table from the \\ parsing + if content!.atoms.count == 1, let tableAtom = content!.atoms.first as? MTMathTable { + return tableAtom + } + + // Otherwise, single row - wrap in table + var rows = [[MTMathList]]() + rows.append([content!]) + + var error: NSError? = self.error + let table = MTMathAtomFactory.table(withEnvironment: nil, rows: rows, error: &error) + if table == nil && self.error == nil { + self.error = error + return nil + } + + return table } else if command == "begin" { let env = self.readEnvironment() if env == nil { diff --git a/Tests/SwiftMathTests/MTMathListBuilderTests.swift b/Tests/SwiftMathTests/MTMathListBuilderTests.swift index 76672e2..4009105 100644 --- a/Tests/SwiftMathTests/MTMathListBuilderTests.swift +++ b/Tests/SwiftMathTests/MTMathListBuilderTests.swift @@ -2174,22 +2174,53 @@ final class MTMathListBuilderTests: XCTestCase { func testSubstack() throws { // Test \substack for multi-line subscripts and limits + let testCases = [ + ("\\substack{a \\\\ b}", "simple substack"), + ("x_{\\substack{a \\\\ b}}", "substack in subscript"), ("\\sum_{\\substack{0 \\le i \\le m \\\\ 0 < j < n}} P(i,j)", "substack in sum limits"), ("\\prod_{\\substack{p \\text{ prime} \\\\ p < 100}} p", "substack with text"), - ("A_{\\substack{n \\\\ k}}", "substack in subscript") + ("A_{\\substack{n \\\\ k}}", "subscript with substack"), + ("\\substack{\\frac{a}{b} \\\\ c}", "substack with frac"), + ("\\substack{a}", "single row substack"), + ("\\substack{a \\\\ b \\\\ c \\\\ d}", "multi-row substack") ] for (latex, desc) in testCases { + print("Testing: \(desc)") + print(" LaTeX: \(latex)") var error: NSError? = nil let list = MTMathListBuilder.build(fromString: latex, error: &error) - if list == nil || error != nil { - throw XCTSkip("\\substack not implemented: \(desc). Error: \(error?.localizedDescription ?? "nil result")") + if let err = error { + print(" ERROR: \(err.localizedDescription)") + } else if list == nil { + print(" List is nil but no error") + } else { + print(" SUCCESS: Got \(list!.atoms.count) atoms") } let unwrappedList = try XCTUnwrap(list, "Should parse: \(desc)") + XCTAssertNil(error, "Should not error on \(desc): \(error?.localizedDescription ?? "")") XCTAssertTrue(unwrappedList.atoms.count >= 1, "\(desc) should have atoms") + + // Verify we have a table structure (either directly or in subscript) + var foundTable = false + for atom in unwrappedList.atoms { + if atom.type == .table { + foundTable = true + break + } + if let subScript = atom.subScript { + for subAtom in subScript.atoms { + if subAtom.type == .table { + foundTable = true + break + } + } + } + } + XCTAssertTrue(foundTable, "\(desc) should contain a table structure") } }