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 spaceArray = getInterElementSpaces()[Int(leftIndex)]
|
||||||
let spaceTypeObj = spaceArray[Int(rightIndex)]
|
let spaceTypeObj = spaceArray[Int(rightIndex)]
|
||||||
let spaceType = spaceTypeObj
|
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)
|
let spaceMultipler = self.getSpacingInMu(spaceType)
|
||||||
if spaceMultipler > 0 {
|
if spaceMultipler > 0 {
|
||||||
|
|||||||
@@ -658,10 +658,12 @@ extension Math {
|
|||||||
static func table(
|
static func table(
|
||||||
withEnvironment env: String?,
|
withEnvironment env: String?,
|
||||||
alignment: Table.ColumnAlignment? = nil,
|
alignment: Table.ColumnAlignment? = nil,
|
||||||
|
columnAlignments: [Table.ColumnAlignment]? = nil,
|
||||||
|
columnFormat: String? = nil,
|
||||||
rows: [[AtomList]],
|
rows: [[AtomList]],
|
||||||
error: inout ParserError?
|
error: inout ParserError?
|
||||||
) -> Atom? {
|
) -> Atom? {
|
||||||
let table = Table(environment: env ?? "")
|
let table = Table(environment: env ?? "", columnFormat: columnFormat)
|
||||||
|
|
||||||
for i in 0..<rows.count {
|
for i in 0..<rows.count {
|
||||||
let row = rows[i]
|
let row = rows[i]
|
||||||
@@ -709,6 +711,32 @@ extension Math {
|
|||||||
} else {
|
} else {
|
||||||
return table
|
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" {
|
} else if env == "eqalign" || env == "split" || env == "aligned" {
|
||||||
if table.numberOfColumns != 2 {
|
if table.numberOfColumns != 2 {
|
||||||
let message = "\(env) environment can only have 2 columns"
|
let message = "\(env) environment can only have 2 columns"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ extension Math {
|
|||||||
var alignments: [ColumnAlignment]
|
var alignments: [ColumnAlignment]
|
||||||
var cells: [[AtomList]]
|
var cells: [[AtomList]]
|
||||||
var environment: String
|
var environment: String
|
||||||
|
var columnFormat: String?
|
||||||
var interColumnSpacing: CGFloat
|
var interColumnSpacing: CGFloat
|
||||||
var interRowAdditionalSpacing: CGFloat
|
var interRowAdditionalSpacing: CGFloat
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ extension Math {
|
|||||||
row.map { AtomList($0) }
|
row.map { AtomList($0) }
|
||||||
}
|
}
|
||||||
self.environment = table.environment
|
self.environment = table.environment
|
||||||
|
self.columnFormat = table.columnFormat
|
||||||
self.interColumnSpacing = table.interColumnSpacing
|
self.interColumnSpacing = table.interColumnSpacing
|
||||||
self.interRowAdditionalSpacing = table.interRowAdditionalSpacing
|
self.interRowAdditionalSpacing = table.interRowAdditionalSpacing
|
||||||
|
|
||||||
@@ -42,12 +44,14 @@ extension Math {
|
|||||||
alignments: [ColumnAlignment] = [],
|
alignments: [ColumnAlignment] = [],
|
||||||
cells: [[AtomList]] = [],
|
cells: [[AtomList]] = [],
|
||||||
environment: String = "",
|
environment: String = "",
|
||||||
|
columnFormat: String? = nil,
|
||||||
interColumnSpacing: CGFloat = 0,
|
interColumnSpacing: CGFloat = 0,
|
||||||
interRowAdditionalSpacing: CGFloat = 0
|
interRowAdditionalSpacing: CGFloat = 0
|
||||||
) {
|
) {
|
||||||
self.alignments = alignments
|
self.alignments = alignments
|
||||||
self.cells = cells
|
self.cells = cells
|
||||||
self.environment = environment
|
self.environment = environment
|
||||||
|
self.columnFormat = columnFormat
|
||||||
self.interColumnSpacing = interColumnSpacing
|
self.interColumnSpacing = interColumnSpacing
|
||||||
self.interRowAdditionalSpacing = interRowAdditionalSpacing
|
self.interRowAdditionalSpacing = interRowAdditionalSpacing
|
||||||
super.init(type: .table)
|
super.init(type: .table)
|
||||||
|
|||||||
@@ -31,12 +31,21 @@ extension Math {
|
|||||||
var ended: Bool
|
var ended: Bool
|
||||||
var numberOfRows: Int
|
var numberOfRows: Int
|
||||||
var alignment: Table.ColumnAlignment? // Optional alignment for starred matrix environments
|
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.name = name
|
||||||
self.numberOfRows = 0
|
self.numberOfRows = 0
|
||||||
self.ended = false
|
self.ended = false
|
||||||
self.alignment = alignment
|
self.alignment = alignment
|
||||||
|
self.columnAlignments = columnAlignments
|
||||||
|
self.columnFormat = columnFormat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,13 +528,16 @@ extension Math {
|
|||||||
if let table = atom as? Table {
|
if let table = atom as? Table {
|
||||||
if !table.environment.isEmpty {
|
if !table.environment.isEmpty {
|
||||||
str += "\\begin{\(table.environment)}"
|
str += "\\begin{\(table.environment)}"
|
||||||
|
if table.environment == "array" {
|
||||||
|
str += "{\(table.columnFormat ?? "")}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for i in 0..<table.numberOfRows {
|
for i in 0..<table.numberOfRows {
|
||||||
let row = table.cells[i]
|
let row = table.cells[i]
|
||||||
for j in 0..<row.count {
|
for j in 0..<row.count {
|
||||||
let cell = row[j]
|
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 {
|
if cell.atoms.count >= 1 && cell.atoms[0].type == Math.AtomType.style {
|
||||||
// remove first atom
|
// remove first atom
|
||||||
cell.atoms.removeFirst()
|
cell.atoms.removeFirst()
|
||||||
@@ -752,6 +764,12 @@ extension Math {
|
|||||||
// reinstate the old inner atom.
|
// reinstate the old inner atom.
|
||||||
let newInner = currentInnerAtom
|
let newInner = currentInnerAtom
|
||||||
currentInnerAtom = oldInner
|
currentInnerAtom = oldInner
|
||||||
|
if newInner?.leftBoundary?.nucleus.isEmpty == true {
|
||||||
|
newInner?.leftBoundary = nil
|
||||||
|
}
|
||||||
|
if newInner?.rightBoundary?.nucleus.isEmpty == true {
|
||||||
|
newInner?.rightBoundary = nil
|
||||||
|
}
|
||||||
return newInner
|
return newInner
|
||||||
} else if command == "overline" {
|
} else if command == "overline" {
|
||||||
// The overline command has 1 arguments
|
// The overline command has 1 arguments
|
||||||
@@ -793,11 +811,20 @@ extension Math {
|
|||||||
|
|
||||||
return table
|
return table
|
||||||
} else if command == "begin" {
|
} else if command == "begin" {
|
||||||
let env = self.readEnvironment()
|
guard let env = self.readEnvironment() else {
|
||||||
if env == nil {
|
|
||||||
return nil
|
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
|
return table
|
||||||
} else if command == "color" {
|
} else if command == "color" {
|
||||||
// A color command has 2 arguments
|
// A color command has 2 arguments
|
||||||
@@ -1141,6 +1168,12 @@ extension Math {
|
|||||||
}
|
}
|
||||||
let newInner = self.currentInnerAtom
|
let newInner = self.currentInnerAtom
|
||||||
currentInnerAtom = oldInner
|
currentInnerAtom = oldInner
|
||||||
|
if newInner?.leftBoundary?.nucleus.isEmpty == true {
|
||||||
|
newInner?.leftBoundary = nil
|
||||||
|
}
|
||||||
|
if newInner?.rightBoundary?.nucleus.isEmpty == true {
|
||||||
|
newInner?.rightBoundary = nil
|
||||||
|
}
|
||||||
return newInner
|
return newInner
|
||||||
} else if command == "overline" {
|
} else if command == "overline" {
|
||||||
let over = Overline()
|
let over = Overline()
|
||||||
@@ -1154,18 +1187,15 @@ extension Math {
|
|||||||
return under
|
return under
|
||||||
} else if command == "begin" {
|
} else if command == "begin" {
|
||||||
if let env = self.readEnvironment() {
|
if let env = self.readEnvironment() {
|
||||||
// Check if this is a starred matrix environment and read optional alignment
|
guard let environmentOptions = self.readEnvironmentOptions(for: env) else {
|
||||||
var alignment: Table.ColumnAlignment? = nil
|
return nil
|
||||||
if env.hasSuffix("*") {
|
|
||||||
alignment = self.readOptionalAlignment()
|
|
||||||
if self.error != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let table = self.buildTable(
|
let table = self.buildTable(
|
||||||
environment: env,
|
environment: env,
|
||||||
alignment: alignment,
|
alignment: environmentOptions.alignment,
|
||||||
|
columnAlignments: environmentOptions.columnAlignments,
|
||||||
|
columnFormat: environmentOptions.columnFormat,
|
||||||
firstList: nil,
|
firstList: nil,
|
||||||
isRow: false
|
isRow: false
|
||||||
)
|
)
|
||||||
@@ -1254,6 +1284,84 @@ extension Math {
|
|||||||
return alignment
|
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) {
|
func assertNotSpace(_ 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)")
|
||||||
}
|
}
|
||||||
@@ -1261,13 +1369,20 @@ extension Math {
|
|||||||
mutating func buildTable(
|
mutating func buildTable(
|
||||||
environment: String?,
|
environment: String?,
|
||||||
alignment: Table.ColumnAlignment? = nil,
|
alignment: Table.ColumnAlignment? = nil,
|
||||||
|
columnAlignments: [Table.ColumnAlignment]? = nil,
|
||||||
|
columnFormat: String? = nil,
|
||||||
firstList: AtomList?,
|
firstList: AtomList?,
|
||||||
isRow: Bool
|
isRow: Bool
|
||||||
) -> Atom? {
|
) -> Atom? {
|
||||||
// Save the current env till an new one gets built.
|
// Save the current env till an new one gets built.
|
||||||
let oldEnv = self.currentEnvironment
|
let oldEnv = self.currentEnvironment
|
||||||
|
|
||||||
currentEnvironment = Environment(name: environment, alignment: alignment)
|
currentEnvironment = Environment(
|
||||||
|
name: environment,
|
||||||
|
alignment: alignment,
|
||||||
|
columnAlignments: columnAlignments,
|
||||||
|
columnFormat: columnFormat
|
||||||
|
)
|
||||||
|
|
||||||
var currentRow = 0
|
var currentRow = 0
|
||||||
var currentCol = 0
|
var currentCol = 0
|
||||||
@@ -1306,7 +1421,10 @@ extension Math {
|
|||||||
|
|
||||||
var error: ParserError? = self.error
|
var error: ParserError? = self.error
|
||||||
let table = AtomFactory.table(
|
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)
|
rows: rows, error: &error)
|
||||||
if table == nil && self.error == nil {
|
if table == nil && self.error == nil {
|
||||||
self.error = error
|
self.error = error
|
||||||
|
|||||||
@@ -594,13 +594,21 @@ struct ParserTests {
|
|||||||
let innerList = try #require(inner.innerList)
|
let innerList = try #require(inner.innerList)
|
||||||
checkAtomTypes(innerList, types: testCase.type2)
|
checkAtomTypes(innerList, types: testCase.type2)
|
||||||
|
|
||||||
let leftBoundary = try #require(inner.leftBoundary)
|
if testCase.left.isEmpty {
|
||||||
#expect(leftBoundary.type == .boundary)
|
#expect(inner.leftBoundary == nil)
|
||||||
#expect(leftBoundary.nucleus == testCase.left)
|
} else {
|
||||||
|
let leftBoundary = try #require(inner.leftBoundary)
|
||||||
|
#expect(leftBoundary.type == .boundary)
|
||||||
|
#expect(leftBoundary.nucleus == testCase.left)
|
||||||
|
}
|
||||||
|
|
||||||
let rightBoundary = try #require(inner.rightBoundary)
|
if testCase.right.isEmpty {
|
||||||
#expect(rightBoundary.type == .boundary)
|
#expect(inner.rightBoundary == nil)
|
||||||
#expect(rightBoundary.nucleus == testCase.right)
|
} else {
|
||||||
|
let rightBoundary = try #require(inner.rightBoundary)
|
||||||
|
#expect(rightBoundary.type == .boundary)
|
||||||
|
#expect(rightBoundary.nucleus == testCase.right)
|
||||||
|
}
|
||||||
|
|
||||||
// convert it back to latex
|
// convert it back to latex
|
||||||
let latex = Math.Parser.atomListToString(list)
|
let latex = Math.Parser.atomListToString(list)
|
||||||
@@ -1052,6 +1060,65 @@ struct ParserTests {
|
|||||||
#expect(latex == "\\left( \\begin{matrix}x&y\\\\ z&w\\end{matrix}\\right) ")
|
#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
|
@Test
|
||||||
func defaultTable() throws {
|
func defaultTable() throws {
|
||||||
let str = "x \\\\ y"
|
let str = "x \\\\ y"
|
||||||
|
|||||||
Reference in New Issue
Block a user