pmatrix/bmatrix/vmatrix LaTeX command support

This commit is contained in:
Nicolas Guillot
2025-10-01 10:22:56 +02:00
parent 80db8c66fb
commit 7a40cd704a
3 changed files with 110 additions and 16 deletions

View File

@@ -792,7 +792,14 @@ public class MTMathAtomFactory {
"Bmatrix": ["{", "}"], "Bmatrix": ["{", "}"],
"vmatrix": ["vert", "vert"], "vmatrix": ["vert", "vert"],
"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 /** 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 @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. 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) let table = MTMathTable(environment: env)
for i in 0..<rows.count { for i in 0..<rows.count {
let row = rows[i] let row = rows[i]
for j in 0..<row.count { for j in 0..<row.count {
@@ -837,6 +844,13 @@ public class MTMathAtomFactory {
} }
} }
// Apply alignment for starred matrix environments
if let align = alignment {
for col in 0..<table.numColumns {
table.set(alignment: align, forColumn: col)
}
}
if delims.count == 2 { if delims.count == 2 {
let inner = MTInner() let inner = MTInner()
inner.leftBoundary = Self.boundary(forDelimiter: delims[0]) inner.leftBoundary = Self.boundary(forDelimiter: delims[0])

View File

@@ -15,11 +15,13 @@ struct MTEnvProperties {
var envName: String? var envName: String?
var ended: Bool var ended: Bool
var numRows: Int var numRows: Int
var alignment: MTColumnAlignment? // Optional alignment for starred matrix environments
init(name: String?) {
init(name: String?, alignment: MTColumnAlignment? = nil) {
self.envName = name self.envName = name
self.numRows = 0 self.numRows = 0
self.ended = false self.ended = false
self.alignment = alignment
} }
} }
@@ -1084,7 +1086,16 @@ public struct MTMathListBuilder {
return under return under
} else if command == "begin" { } else if command == "begin" {
if let env = self.readEnvironment() { if let env = self.readEnvironment() {
let table = self.buildTable(env: env, firstList: nil, isRow: false) // Check if this is a starred matrix environment and read optional alignment
var alignment: MTColumnAlignment? = nil
if env.hasSuffix("*") {
alignment = self.readOptionalAlignment()
if self.error != nil {
return nil
}
}
let table = self.buildTable(env: env, alignment: alignment, firstList: nil, isRow: false)
return table return table
} else { } else {
return nil return nil
@@ -1113,10 +1124,10 @@ public struct MTMathListBuilder {
self.setError(.characterNotFound, message: "Missing {") self.setError(.characterNotFound, message: "Missing {")
return nil return nil
} }
self.skipSpaces() self.skipSpaces()
let env = self.readString() let env = self.readString()
if !self.expectCharacter("}") { if !self.expectCharacter("}") {
// We didn"t find an closing brace, so invalid format. // We didn"t find an closing brace, so invalid format.
self.setError(.characterNotFound, message: "Missing }") self.setError(.characterNotFound, message: "Missing }")
@@ -1124,16 +1135,58 @@ public struct MTMathListBuilder {
} }
return env return env
} }
/// Reads optional alignment parameter for starred matrix environments: [r], [l], or [c]
mutating func readOptionalAlignment() -> 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) { func MTAssertNotSpace(_ ch: Character) {
assert(ch >= "\u{21}" && ch <= "\u{7E}", "Expected non-space character \(ch)") 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. // Save the current env till an new one gets built.
let oldEnv = self.currentEnv let oldEnv = self.currentEnv
currentEnv = MTEnvProperties(name: env) currentEnv = MTEnvProperties(name: env, alignment: alignment)
var currentRow = 0 var currentRow = 0
var currentCol = 0 var currentCol = 0
@@ -1171,7 +1224,7 @@ public struct MTMathListBuilder {
} }
var error:NSError? = self.error 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 { if table == nil && self.error == nil {
self.error = error self.error = error
return nil return nil
@@ -1228,11 +1281,11 @@ public struct MTMathListBuilder {
} }
mutating func readString() -> String { 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 = "" var output = ""
while self.hasCharacters { while self.hasCharacters {
let char = self.getNextCharacter() let char = self.getNextCharacter()
if char.isLowercase || char.isUppercase { if char.isLowercase || char.isUppercase || char == "*" {
output.append(char) output.append(char)
} else { } else {
self.unlookCharacter() self.unlookCharacter()

View File

@@ -2344,19 +2344,46 @@ final class MTMathListBuilderTests: XCTestCase {
let testCases = [ let testCases = [
("\\begin{pmatrix*}[r] 1 & 2 \\\\ 3 & 4 \\end{pmatrix*}", "pmatrix* right align"), ("\\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{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 { for (latex, desc) in testCases {
print("Testing: \(desc)")
print(" LaTeX: \(latex)")
var error: NSError? = nil var error: NSError? = nil
let list = MTMathListBuilder.build(fromString: latex, error: &error) let list = MTMathListBuilder.build(fromString: latex, error: &error)
if list == nil || error != nil { if let err = error {
throw XCTSkip("Starred matrix environments (*matrix*) not implemented: \(desc). Error: \(error?.localizedDescription ?? "nil result")") 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)") 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") 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")
} }
} }