substack LaTeX command support
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user