pmatrix/bmatrix/vmatrix LaTeX command support
This commit is contained in:
@@ -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])
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user