Merge pull request #3 from wesleyel/copilot/fix-rendering-issue-formula
Add `array` environment support for piecewise formulas
This commit is contained in:
@@ -1846,7 +1846,11 @@ extension Math {
|
||||
let spaceArray = getInterElementSpaces()[Int(leftIndex)]
|
||||
let spaceTypeObj = spaceArray[Int(rightIndex)]
|
||||
let spaceType = spaceTypeObj
|
||||
assert(spaceType != .invalid, "Invalid space between \(left) and \(right)")
|
||||
// Keep malformed LaTeX from crashing debug builds. Propagating NaN lets callers
|
||||
// detect the failed layout and surface it as a validation issue instead.
|
||||
if spaceType == .invalid {
|
||||
return .nan
|
||||
}
|
||||
|
||||
let spaceMultipler = self.getSpacingInMu(spaceType)
|
||||
if spaceMultipler > 0 {
|
||||
|
||||
@@ -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..<rows.count {
|
||||
let row = rows[i]
|
||||
@@ -709,6 +711,32 @@ extension Math {
|
||||
} else {
|
||||
return table
|
||||
}
|
||||
} else if env == "array" {
|
||||
let columnAlignments = columnAlignments ?? []
|
||||
|
||||
if table.numberOfColumns > 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..<table.cells.count {
|
||||
for j in 0..<table.cells[i].count {
|
||||
table.cells[i][j].insert(style, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
for (column, alignment) in columnAlignments.enumerated() {
|
||||
table.setAlignment(alignment, forColumn: column)
|
||||
}
|
||||
|
||||
return table
|
||||
} else if env == "eqalign" || env == "split" || env == "aligned" {
|
||||
if table.numberOfColumns != 2 {
|
||||
let message = "\(env) environment can only have 2 columns"
|
||||
|
||||
@@ -11,6 +11,7 @@ extension Math {
|
||||
var alignments: [ColumnAlignment]
|
||||
var cells: [[AtomList]]
|
||||
var environment: String
|
||||
var columnFormat: String?
|
||||
var interColumnSpacing: CGFloat
|
||||
var interRowAdditionalSpacing: CGFloat
|
||||
|
||||
@@ -32,6 +33,7 @@ extension Math {
|
||||
row.map { AtomList($0) }
|
||||
}
|
||||
self.environment = table.environment
|
||||
self.columnFormat = table.columnFormat
|
||||
self.interColumnSpacing = table.interColumnSpacing
|
||||
self.interRowAdditionalSpacing = table.interRowAdditionalSpacing
|
||||
|
||||
@@ -42,12 +44,14 @@ extension Math {
|
||||
alignments: [ColumnAlignment] = [],
|
||||
cells: [[AtomList]] = [],
|
||||
environment: String = "",
|
||||
columnFormat: String? = nil,
|
||||
interColumnSpacing: CGFloat = 0,
|
||||
interRowAdditionalSpacing: CGFloat = 0
|
||||
) {
|
||||
self.alignments = alignments
|
||||
self.cells = cells
|
||||
self.environment = environment
|
||||
self.columnFormat = columnFormat
|
||||
self.interColumnSpacing = interColumnSpacing
|
||||
self.interRowAdditionalSpacing = interRowAdditionalSpacing
|
||||
super.init(type: .table)
|
||||
|
||||
@@ -31,12 +31,21 @@ extension Math {
|
||||
var ended: Bool
|
||||
var numberOfRows: Int
|
||||
var alignment: Table.ColumnAlignment? // Optional alignment for starred matrix environments
|
||||
var columnAlignments: [Table.ColumnAlignment]?
|
||||
var columnFormat: String?
|
||||
|
||||
init(name: String?, alignment: Table.ColumnAlignment? = nil) {
|
||||
init(
|
||||
name: String?,
|
||||
alignment: Table.ColumnAlignment? = nil,
|
||||
columnAlignments: [Table.ColumnAlignment]? = nil,
|
||||
columnFormat: String? = nil
|
||||
) {
|
||||
self.name = name
|
||||
self.numberOfRows = 0
|
||||
self.ended = false
|
||||
self.alignment = alignment
|
||||
self.columnAlignments = columnAlignments
|
||||
self.columnFormat = columnFormat
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,13 +528,16 @@ extension Math {
|
||||
if let table = atom as? Table {
|
||||
if !table.environment.isEmpty {
|
||||
str += "\\begin{\(table.environment)}"
|
||||
if table.environment == "array" {
|
||||
str += "{\(table.columnFormat ?? "")}"
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..<table.numberOfRows {
|
||||
let row = table.cells[i]
|
||||
for j in 0..<row.count {
|
||||
let cell = row[j]
|
||||
if table.environment == "matrix" {
|
||||
if table.environment == "matrix" || table.environment == "array" {
|
||||
if cell.atoms.count >= 1 && cell.atoms[0].type == Math.AtomType.style {
|
||||
// remove first atom
|
||||
cell.atoms.removeFirst()
|
||||
@@ -752,6 +764,12 @@ extension Math {
|
||||
// reinstate the old inner atom.
|
||||
let newInner = currentInnerAtom
|
||||
currentInnerAtom = oldInner
|
||||
if newInner?.leftBoundary?.nucleus.isEmpty == true {
|
||||
newInner?.leftBoundary = nil
|
||||
}
|
||||
if newInner?.rightBoundary?.nucleus.isEmpty == true {
|
||||
newInner?.rightBoundary = nil
|
||||
}
|
||||
return newInner
|
||||
} else if command == "overline" {
|
||||
// The overline command has 1 arguments
|
||||
@@ -793,11 +811,20 @@ extension Math {
|
||||
|
||||
return table
|
||||
} else if command == "begin" {
|
||||
let env = self.readEnvironment()
|
||||
if env == nil {
|
||||
guard let env = self.readEnvironment() else {
|
||||
return nil
|
||||
}
|
||||
let table = self.buildTable(environment: env, firstList: nil, isRow: false)
|
||||
guard let environmentOptions = self.readEnvironmentOptions(for: env) else {
|
||||
return nil
|
||||
}
|
||||
let table = self.buildTable(
|
||||
environment: env,
|
||||
alignment: environmentOptions.alignment,
|
||||
columnAlignments: environmentOptions.columnAlignments,
|
||||
columnFormat: environmentOptions.columnFormat,
|
||||
firstList: nil,
|
||||
isRow: false
|
||||
)
|
||||
return table
|
||||
} else if command == "color" {
|
||||
// A color command has 2 arguments
|
||||
@@ -1141,6 +1168,12 @@ extension Math {
|
||||
}
|
||||
let newInner = self.currentInnerAtom
|
||||
currentInnerAtom = oldInner
|
||||
if newInner?.leftBoundary?.nucleus.isEmpty == true {
|
||||
newInner?.leftBoundary = nil
|
||||
}
|
||||
if newInner?.rightBoundary?.nucleus.isEmpty == true {
|
||||
newInner?.rightBoundary = nil
|
||||
}
|
||||
return newInner
|
||||
} else if command == "overline" {
|
||||
let over = Overline()
|
||||
@@ -1154,18 +1187,15 @@ extension Math {
|
||||
return under
|
||||
} else if command == "begin" {
|
||||
if let env = self.readEnvironment() {
|
||||
// Check if this is a starred matrix environment and read optional alignment
|
||||
var alignment: Table.ColumnAlignment? = nil
|
||||
if env.hasSuffix("*") {
|
||||
alignment = self.readOptionalAlignment()
|
||||
if self.error != nil {
|
||||
return nil
|
||||
}
|
||||
guard let environmentOptions = self.readEnvironmentOptions(for: env) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let table = self.buildTable(
|
||||
environment: env,
|
||||
alignment: alignment,
|
||||
alignment: environmentOptions.alignment,
|
||||
columnAlignments: environmentOptions.columnAlignments,
|
||||
columnFormat: environmentOptions.columnFormat,
|
||||
firstList: nil,
|
||||
isRow: false
|
||||
)
|
||||
@@ -1254,6 +1284,84 @@ extension Math {
|
||||
return alignment
|
||||
}
|
||||
|
||||
mutating func readEnvironmentOptions(for env: String) -> (
|
||||
alignment: Table.ColumnAlignment?,
|
||||
columnAlignments: [Table.ColumnAlignment]?,
|
||||
columnFormat: String?
|
||||
)? {
|
||||
if env.hasSuffix("*") {
|
||||
let alignment = self.readOptionalAlignment()
|
||||
return self.error == nil ? (alignment, nil, nil) : nil
|
||||
}
|
||||
|
||||
if env == "array" {
|
||||
let (columnAlignments, columnFormat) = self.readArrayColumnFormat()
|
||||
return self.error == nil ? (nil, columnAlignments, columnFormat) : nil
|
||||
}
|
||||
|
||||
return (nil, nil, nil)
|
||||
}
|
||||
|
||||
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 _ where char.isWhitespace:
|
||||
continue
|
||||
default:
|
||||
self.setError(
|
||||
.invalidEnvironment,
|
||||
message: "Unsupported array column format specifier: \(char)"
|
||||
)
|
||||
return (nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
if !foundClosingBrace {
|
||||
self.setError(.characterNotFound, message: "Missing } after array column format")
|
||||
}
|
||||
|
||||
if columnAlignments.isEmpty {
|
||||
self.setError(
|
||||
.invalidEnvironment,
|
||||
message: "array environment requires at least one column alignment specifier (l, c, or r)"
|
||||
)
|
||||
}
|
||||
|
||||
if self.error != nil {
|
||||
return (nil, nil)
|
||||
}
|
||||
|
||||
return (columnAlignments, format)
|
||||
}
|
||||
|
||||
func assertNotSpace(_ ch: Character) {
|
||||
assert(ch >= "\u{21}" && ch <= "\u{7E}", "Expected non-space character \(ch)")
|
||||
}
|
||||
@@ -1261,13 +1369,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 +1421,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
|
||||
|
||||
@@ -594,13 +594,21 @@ struct ParserTests {
|
||||
let innerList = try #require(inner.innerList)
|
||||
checkAtomTypes(innerList, types: testCase.type2)
|
||||
|
||||
let leftBoundary = try #require(inner.leftBoundary)
|
||||
#expect(leftBoundary.type == .boundary)
|
||||
#expect(leftBoundary.nucleus == testCase.left)
|
||||
if testCase.left.isEmpty {
|
||||
#expect(inner.leftBoundary == nil)
|
||||
} else {
|
||||
let leftBoundary = try #require(inner.leftBoundary)
|
||||
#expect(leftBoundary.type == .boundary)
|
||||
#expect(leftBoundary.nucleus == testCase.left)
|
||||
}
|
||||
|
||||
let rightBoundary = try #require(inner.rightBoundary)
|
||||
#expect(rightBoundary.type == .boundary)
|
||||
#expect(rightBoundary.nucleus == testCase.right)
|
||||
if testCase.right.isEmpty {
|
||||
#expect(inner.rightBoundary == nil)
|
||||
} else {
|
||||
let rightBoundary = try #require(inner.rightBoundary)
|
||||
#expect(rightBoundary.type == .boundary)
|
||||
#expect(rightBoundary.nucleus == testCase.right)
|
||||
}
|
||||
|
||||
// convert it back to latex
|
||||
let latex = Math.Parser.atomListToString(list)
|
||||
@@ -1052,6 +1060,65 @@ 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..<table.numberOfRows {
|
||||
for column in 0..<table.numberOfColumns {
|
||||
let style = try #require(table.cells[row][column].atoms.first as? Math.Style)
|
||||
#expect(style.level == .text)
|
||||
}
|
||||
}
|
||||
|
||||
let latex = Math.Parser.atomListToString(list)
|
||||
#expect(latex.contains("\\begin{array}{ll}"))
|
||||
#expect(latex.contains("\\right."))
|
||||
}
|
||||
|
||||
@Test
|
||||
func arrayRejectsInvalidColumnFormat() throws {
|
||||
let str = "\\begin{array}{xyz}a\\end{array}"
|
||||
var error: Math.ParserError? = nil
|
||||
|
||||
let list = Math.Parser.build(fromString: str, error: &error)
|
||||
|
||||
#expect(list == nil)
|
||||
#expect(error?.code == .invalidEnvironment)
|
||||
}
|
||||
|
||||
@Test
|
||||
func arrayRejectsTooFewColumnSpecifiers() throws {
|
||||
let str = "\\begin{array}{l}a&b\\end{array}"
|
||||
var error: Math.ParserError? = nil
|
||||
|
||||
let list = Math.Parser.build(fromString: str, error: &error)
|
||||
|
||||
#expect(list == nil)
|
||||
#expect(error?.code == .invalidNumberOfColumns)
|
||||
}
|
||||
|
||||
@Test
|
||||
func defaultTable() throws {
|
||||
let str = "x \\\\ y"
|
||||
|
||||
Reference in New Issue
Block a user