From f8f668f6491c74d4a6c5ea3831723c7e7db3d511 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:31:52 +0000 Subject: [PATCH] Add array environment parsing Agent-Logs-Url: https://github.com/wesleyel/swiftui-math/sessions/56436444-e15b-4dd0-8a70-c87df1e3dc4e Co-authored-by: wesleyel <48174882+wesleyel@users.noreply.github.com> --- .../Internal/Syntax/AtomFactory.swift | 36 +++++- .../Internal/Syntax/Atoms/Table.swift | 4 + .../SwiftUIMath/Internal/Syntax/Parser.swift | 117 +++++++++++++++++- .../Internal/Syntax/ParserTests.swift | 37 ++++++ 4 files changed, 188 insertions(+), 6 deletions(-) diff --git a/Sources/SwiftUIMath/Internal/Syntax/AtomFactory.swift b/Sources/SwiftUIMath/Internal/Syntax/AtomFactory.swift index 4561ffc..4c71b22 100644 --- a/Sources/SwiftUIMath/Internal/Syntax/AtomFactory.swift +++ b/Sources/SwiftUIMath/Internal/Syntax/AtomFactory.swift @@ -658,10 +658,12 @@ extension Math { static func table( withEnvironment env: String?, alignment: Table.ColumnAlignment? = nil, + columnAlignments: [Table.ColumnAlignment]? = nil, + columnFormat: String? = nil, rows: [[AtomList]], error: inout ParserError? ) -> Atom? { - let table = Table(environment: env ?? "") + let table = Table(environment: env ?? "", columnFormat: columnFormat) for i in 0.. columnAlignments.count { + let message = "array environment has more columns than alignment specifiers" + if error == nil { + error = ParserError(code: .invalidNumberOfColumns, message: message) + } + return nil + } + + table.interRowAdditionalSpacing = 0 + table.interColumnSpacing = 18 + + let style = Style(level: .text) + for i in 0..= 1 && cell.atoms[0].type == Math.AtomType.style { // remove first atom cell.atoms.removeFirst() @@ -797,7 +809,30 @@ extension Math { if env == nil { return nil } - let table = self.buildTable(environment: env, firstList: nil, isRow: false) + var alignment: Table.ColumnAlignment? = nil + var columnAlignments: [Table.ColumnAlignment]? = nil + var columnFormat: String? = nil + if let env { + if env.hasSuffix("*") { + alignment = self.readOptionalAlignment() + if self.error != nil { + return nil + } + } else if env == "array" { + (columnAlignments, columnFormat) = self.readArrayColumnFormat() + if self.error != nil { + return nil + } + } + } + let table = self.buildTable( + environment: env, + alignment: alignment, + columnAlignments: columnAlignments, + columnFormat: columnFormat, + firstList: nil, + isRow: false + ) return table } else if command == "color" { // A color command has 2 arguments @@ -1156,16 +1191,25 @@ extension Math { if let env = self.readEnvironment() { // Check if this is a starred matrix environment and read optional alignment var alignment: Table.ColumnAlignment? = nil + var columnAlignments: [Table.ColumnAlignment]? = nil + var columnFormat: String? = nil if env.hasSuffix("*") { alignment = self.readOptionalAlignment() if self.error != nil { return nil } + } else if env == "array" { + (columnAlignments, columnFormat) = self.readArrayColumnFormat() + if self.error != nil { + return nil + } } let table = self.buildTable( environment: env, alignment: alignment, + columnAlignments: columnAlignments, + columnFormat: columnFormat, firstList: nil, isRow: false ) @@ -1254,6 +1298,59 @@ extension Math { return alignment } + mutating func readArrayColumnFormat() -> ([Table.ColumnAlignment]?, String?) { + guard self.expectCharacter("{") else { + self.setError(.characterNotFound, message: "Missing { after \\begin{array}") + return (nil, nil) + } + + self.skipSpaces() + + var columnAlignments = [Table.ColumnAlignment]() + var format = "" + var foundClosingBrace = false + + while self.hasCharacters { + let char = self.nextCharacter() + if char == "}" { + foundClosingBrace = true + break + } + + switch char { + case "l": + columnAlignments.append(.left) + format.append(char) + case "c": + columnAlignments.append(.center) + format.append(char) + case "r": + columnAlignments.append(.right) + format.append(char) + case "|": + format.append(char) + case " ": + continue + default: + self.setError( + .invalidEnvironment, + message: "Unsupported array column format specifier: \(char)" + ) + return (nil, nil) + } + } + + if self.error == nil && !foundClosingBrace { + self.setError(.characterNotFound, message: "Missing } after array column format") + } + + if self.error == nil && columnAlignments.isEmpty { + self.setError(.invalidEnvironment, message: "array environment requires column alignment specifiers") + } + + return self.error == nil ? (columnAlignments, format) : (nil, nil) + } + func assertNotSpace(_ ch: Character) { assert(ch >= "\u{21}" && ch <= "\u{7E}", "Expected non-space character \(ch)") } @@ -1261,13 +1358,20 @@ extension Math { mutating func buildTable( environment: String?, alignment: Table.ColumnAlignment? = nil, + columnAlignments: [Table.ColumnAlignment]? = nil, + columnFormat: String? = nil, firstList: AtomList?, isRow: Bool ) -> Atom? { // Save the current env till an new one gets built. let oldEnv = self.currentEnvironment - currentEnvironment = Environment(name: environment, alignment: alignment) + currentEnvironment = Environment( + name: environment, + alignment: alignment, + columnAlignments: columnAlignments, + columnFormat: columnFormat + ) var currentRow = 0 var currentCol = 0 @@ -1306,7 +1410,10 @@ extension Math { var error: ParserError? = self.error let table = AtomFactory.table( - withEnvironment: currentEnvironment?.name, alignment: currentEnvironment?.alignment, + withEnvironment: currentEnvironment?.name, + alignment: currentEnvironment?.alignment, + columnAlignments: currentEnvironment?.columnAlignments, + columnFormat: currentEnvironment?.columnFormat, rows: rows, error: &error) if table == nil && self.error == nil { self.error = error diff --git a/Tests/SwiftUIMathTests/Internal/Syntax/ParserTests.swift b/Tests/SwiftUIMathTests/Internal/Syntax/ParserTests.swift index c9dfb58..7f96821 100644 --- a/Tests/SwiftUIMathTests/Internal/Syntax/ParserTests.swift +++ b/Tests/SwiftUIMathTests/Internal/Syntax/ParserTests.swift @@ -1052,6 +1052,43 @@ struct ParserTests { #expect(latex == "\\left( \\begin{matrix}x&y\\\\ z&w\\end{matrix}\\right) ") } + @Test + func array() throws { + let str = "\\left\\{\\begin{array}{ll}1,&|x|\\leq1,\\\\0,&|x|>1,\\end{array}\\right." + let list = try #require(Math.Parser.build(fromString: str)) + #expect(list.atoms.count == 1) + + let inner = try #require(list.atoms[0] as? Math.Inner) + let leftBoundary = try #require(inner.leftBoundary) + #expect(leftBoundary.type == .boundary) + #expect(leftBoundary.nucleus == "{") + #expect(inner.rightBoundary == nil) + + let innerList = try #require(inner.innerList) + #expect(innerList.atoms.count == 1) + + let table = try #require(innerList.atoms[0] as? Math.Table) + #expect(table.environment == "array") + #expect(table.columnFormat == "ll") + #expect(table.interRowAdditionalSpacing == 0) + #expect(table.interColumnSpacing == 18) + #expect(table.numberOfRows == 2) + #expect(table.numberOfColumns == 2) + #expect(table.alignment(forColumn: 0) == .left) + #expect(table.alignment(forColumn: 1) == .left) + + for row in 0..