diff --git a/Sources/SwiftMath/MathRender/MTMathAtomFactory.swift b/Sources/SwiftMath/MathRender/MTMathAtomFactory.swift index 6372627..fc4d9ff 100644 --- a/Sources/SwiftMath/MathRender/MTMathAtomFactory.swift +++ b/Sources/SwiftMath/MathRender/MTMathAtomFactory.swift @@ -792,7 +792,14 @@ public class MTMathAtomFactory { "Bmatrix": ["{", "}"], "vmatrix": ["vert", "vert"], "Vmatrix": ["Vert", "Vert"], - "smallmatrix": [] + "smallmatrix": [], + // Starred versions with optional alignment + "matrix*": [], + "pmatrix*": ["(", ")"], + "bmatrix*": ["[", "]"], + "Bmatrix*": ["{", "}"], + "vmatrix*": ["vert", "vert"], + "Vmatrix*": ["Vert", "Vert"] ] /** Builds a table for a given environment with the given rows. Returns a `MTMathAtom` containing the @@ -802,9 +809,9 @@ public class MTMathAtomFactory { @note The reason this function returns a `MTMathAtom` and not a `MTMathTable` is because some matrix environments are have builtin delimiters added to the table and hence are returned as inner atoms. */ - public static func table(withEnvironment env: String?, rows: [[MTMathList]], error:inout NSError?) -> MTMathAtom? { + public static func table(withEnvironment env: String?, alignment: MTColumnAlignment? = nil, rows: [[MTMathList]], error:inout NSError?) -> MTMathAtom? { let table = MTMathTable(environment: env) - + for i in 0.. MTColumnAlignment? { + self.skipSpaces() + + // Check if there's an opening bracket + guard hasCharacters && string[currentCharIndex] == "[" else { + return nil + } + + _ = getNextCharacter() // consume '[' + self.skipSpaces() + + guard hasCharacters else { + self.setError(.characterNotFound, message: "Missing alignment specifier after [") + return nil + } + + let alignChar = getNextCharacter() + let alignment: MTColumnAlignment? + + switch alignChar { + case "l": + alignment = .left + case "c": + alignment = .center + case "r": + alignment = .right + default: + self.setError(.invalidEnv, message: "Invalid alignment specifier: \(alignChar). Must be l, c, or r") + return nil + } + + self.skipSpaces() + + if !self.expectCharacter("]") { + self.setError(.characterNotFound, message: "Missing ] after alignment specifier") + return nil + } + + return alignment + } func MTAssertNotSpace(_ ch: Character) { assert(ch >= "\u{21}" && ch <= "\u{7E}", "Expected non-space character \(ch)") } - mutating func buildTable(env: String?, firstList: MTMathList?, isRow: Bool) -> MTMathAtom? { + mutating func buildTable(env: String?, alignment: MTColumnAlignment? = nil, firstList: MTMathList?, isRow: Bool) -> MTMathAtom? { // Save the current env till an new one gets built. let oldEnv = self.currentEnv - currentEnv = MTEnvProperties(name: env) + currentEnv = MTEnvProperties(name: env, alignment: alignment) var currentRow = 0 var currentCol = 0 @@ -1171,7 +1224,7 @@ public struct MTMathListBuilder { } var error:NSError? = self.error - let table = MTMathAtomFactory.table(withEnvironment: currentEnv?.envName, rows: rows, error: &error) + let table = MTMathAtomFactory.table(withEnvironment: currentEnv?.envName, alignment: currentEnv?.alignment, rows: rows, error: &error) if table == nil && self.error == nil { self.error = error return nil @@ -1228,11 +1281,11 @@ public struct MTMathListBuilder { } mutating func readString() -> String { - // a string of all upper and lower case characters. + // a string of all upper and lower case characters (and asterisks for starred environments) var output = "" while self.hasCharacters { let char = self.getNextCharacter() - if char.isLowercase || char.isUppercase { + if char.isLowercase || char.isUppercase || char == "*" { output.append(char) } else { self.unlookCharacter() diff --git a/Tests/SwiftMathTests/MTMathListBuilderTests.swift b/Tests/SwiftMathTests/MTMathListBuilderTests.swift index 41524da..accc7c9 100644 --- a/Tests/SwiftMathTests/MTMathListBuilderTests.swift +++ b/Tests/SwiftMathTests/MTMathListBuilderTests.swift @@ -2344,19 +2344,46 @@ final class MTMathListBuilderTests: XCTestCase { let testCases = [ ("\\begin{pmatrix*}[r] 1 & 2 \\\\ 3 & 4 \\end{pmatrix*}", "pmatrix* right align"), ("\\begin{bmatrix*}[l] a & b \\\\ c & d \\end{bmatrix*}", "bmatrix* left align"), - ("\\begin{vmatrix*}[c] x & y \\\\ z & w \\end{vmatrix*}", "vmatrix* center align") + ("\\begin{vmatrix*}[c] x & y \\\\ z & w \\end{vmatrix*}", "vmatrix* center align"), + ("\\begin{matrix*}[r] 10 & 20 \\\\ 30 & 40 \\end{matrix*}", "matrix* right align (no delimiters)") ] 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("Starred matrix environments (*matrix*) 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 + var foundTable = false + for atom in unwrappedList.atoms { + if atom.type == .table { + foundTable = true + break + } + // Check inside inner atoms (for matrices with delimiters) + if atom.type == .inner, let inner = atom as? MTInner, let innerList = inner.innerList { + for innerAtom in innerList.atoms { + if innerAtom.type == .table { + foundTable = true + break + } + } + } + } + XCTAssertTrue(foundTable, "\(desc) should contain a table structure") } }