Refactor model
This commit is contained in:
@@ -18,9 +18,9 @@ extension Math {
|
|||||||
|
|
||||||
private let font: Font
|
private let font: Font
|
||||||
private let unitsPerEm: UInt
|
private let unitsPerEm: UInt
|
||||||
private let table: Table
|
private let table: FontTable
|
||||||
|
|
||||||
init(font: Font, unitsPerEm: UInt, table: Table) {
|
init(font: Font, unitsPerEm: UInt, table: FontTable) {
|
||||||
self.font = font
|
self.font = font
|
||||||
self.unitsPerEm = unitsPerEm
|
self.unitsPerEm = unitsPerEm
|
||||||
self.table = table
|
self.table = table
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ extension Math {
|
|||||||
|
|
||||||
private struct Cache {
|
private struct Cache {
|
||||||
var graphicsFonts: [Font.Name: CGFont] = [:]
|
var graphicsFonts: [Font.Name: CGFont] = [:]
|
||||||
var tables: [Font.Name: Table] = [:]
|
var tables: [Font.Name: FontTable] = [:]
|
||||||
let fonts = NSCache<KeyBox<Font>, CTFont>()
|
let fonts = NSCache<KeyBox<Font>, CTFont>()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ extension Math {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func table(named name: Font.Name) -> Table? {
|
func table(named name: Font.Name) -> FontTable? {
|
||||||
cache.withValue { cache in
|
cache.withValue { cache in
|
||||||
if let table = cache.tables[name] {
|
if let table = cache.tables[name] {
|
||||||
return table
|
return table
|
||||||
@@ -67,8 +67,8 @@ extension Math {
|
|||||||
private func registerGraphicsFont(
|
private func registerGraphicsFont(
|
||||||
named name: Font.Name,
|
named name: Font.Name,
|
||||||
cache: inout Cache
|
cache: inout Cache
|
||||||
) -> (CGFont, Table)? {
|
) -> (CGFont, FontTable)? {
|
||||||
guard let graphicsFont = CGFont.named(name), let table = Table.named(name) else {
|
guard let graphicsFont = CGFont.named(name), let table = FontTable.named(name) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,8 +99,8 @@ extension CGFont {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Math.Table {
|
extension Math.FontTable {
|
||||||
fileprivate static func named(_ name: Math.Font.Name) -> Math.Table? {
|
fileprivate static func named(_ name: Math.Font.Name) -> Math.FontTable? {
|
||||||
guard
|
guard
|
||||||
let bundleURL = Bundle.module.url(forResource: "mathFonts", withExtension: "bundle"),
|
let bundleURL = Bundle.module.url(forResource: "mathFonts", withExtension: "bundle"),
|
||||||
let url = Bundle(url: bundleURL)?.url(forResource: name.rawValue, withExtension: "plist"),
|
let url = Bundle(url: bundleURL)?.url(forResource: name.rawValue, withExtension: "plist"),
|
||||||
@@ -109,6 +109,6 @@ extension Math.Table {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return try? PropertyListDecoder().decode(Math.Table.self, from: data)
|
return try? PropertyListDecoder().decode(Math.FontTable.self, from: data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension Math {
|
extension Math {
|
||||||
struct Table: Codable, Sendable {
|
struct FontTable: Codable, Sendable {
|
||||||
struct Assembly: Codable, Sendable {
|
struct Assembly: Codable, Sendable {
|
||||||
struct Part: Codable, Sendable {
|
struct Part: Codable, Sendable {
|
||||||
let advance: Int
|
let advance: Int
|
||||||
@@ -19,8 +19,8 @@ final class ReadWriteLockIsolated<Value>: @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func withValue<T: Sendable>(
|
func withValue<T>(
|
||||||
_ operation: @Sendable (inout Value) throws -> T
|
_ operation: (inout Value) throws -> T
|
||||||
) rethrows -> T {
|
) rethrows -> T {
|
||||||
try self.lock.sync {
|
try self.lock.sync {
|
||||||
var value = self._value
|
var value = self._value
|
||||||
|
|||||||
858
Sources/SwiftUIMath/Internal/Model/AtomFactory.swift
Normal file
858
Sources/SwiftUIMath/Internal/Model/AtomFactory.swift
Normal file
@@ -0,0 +1,858 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Math {
|
||||||
|
enum AtomFactory {
|
||||||
|
static let aliases: [String: String] = [
|
||||||
|
"lnot": "neg",
|
||||||
|
"land": "wedge",
|
||||||
|
"lor": "vee",
|
||||||
|
"ne": "neq",
|
||||||
|
"le": "leq",
|
||||||
|
"ge": "geq",
|
||||||
|
"lbrace": "{",
|
||||||
|
"rbrace": "}",
|
||||||
|
"Vert": "|",
|
||||||
|
"gets": "leftarrow",
|
||||||
|
"to": "rightarrow",
|
||||||
|
"iff": "Longleftrightarrow",
|
||||||
|
"AA": "angstrom",
|
||||||
|
]
|
||||||
|
|
||||||
|
static let delimiters: [String: String] = [
|
||||||
|
".": "", // . means no delimiter
|
||||||
|
"(": "(",
|
||||||
|
")": ")",
|
||||||
|
"[": "[",
|
||||||
|
"]": "]",
|
||||||
|
"<": "\u{2329}",
|
||||||
|
">": "\u{232A}",
|
||||||
|
"/": "/",
|
||||||
|
"\\": "\\",
|
||||||
|
"|": "|",
|
||||||
|
"lgroup": "\u{27EE}",
|
||||||
|
"rgroup": "\u{27EF}",
|
||||||
|
"||": "\u{2016}",
|
||||||
|
"Vert": "\u{2016}",
|
||||||
|
"vert": "|",
|
||||||
|
"uparrow": "\u{2191}",
|
||||||
|
"downarrow": "\u{2193}",
|
||||||
|
"updownarrow": "\u{2195}",
|
||||||
|
"Uparrow": "\u{21D1}",
|
||||||
|
"Downarrow": "\u{21D3}",
|
||||||
|
"Updownarrow": "\u{21D5}",
|
||||||
|
"backslash": "\\",
|
||||||
|
"rangle": "\u{232A}",
|
||||||
|
"langle": "\u{2329}",
|
||||||
|
"rbrace": "}",
|
||||||
|
"}": "}",
|
||||||
|
"{": "{",
|
||||||
|
"lbrace": "{",
|
||||||
|
"lceil": "\u{2308}",
|
||||||
|
"rceil": "\u{2309}",
|
||||||
|
"lfloor": "\u{230A}",
|
||||||
|
"rfloor": "\u{230B}",
|
||||||
|
]
|
||||||
|
|
||||||
|
static let delimValueToName: [String: String] = {
|
||||||
|
var output = [String: String]()
|
||||||
|
for (key, value) in delimiters {
|
||||||
|
if let existingValue = output[value] {
|
||||||
|
if key.count > existingValue.count {
|
||||||
|
continue
|
||||||
|
} else if key.count == existingValue.count {
|
||||||
|
if key.compare(existingValue) == .orderedDescending {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output[value] = key
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}()
|
||||||
|
|
||||||
|
static let accents: [String: String] = [
|
||||||
|
"grave": "\u{0300}",
|
||||||
|
"acute": "\u{0301}",
|
||||||
|
"hat": "\u{0302}", // In our implementation hat and widehat behave the same.
|
||||||
|
"tilde": "\u{0303}", // In our implementation tilde and widetilde behave the same.
|
||||||
|
"bar": "\u{0304}",
|
||||||
|
"breve": "\u{0306}",
|
||||||
|
"dot": "\u{0307}",
|
||||||
|
"ddot": "\u{0308}",
|
||||||
|
"check": "\u{030C}",
|
||||||
|
"vec": "\u{20D7}",
|
||||||
|
"widehat": "\u{0302}",
|
||||||
|
"widetilde": "\u{0303}",
|
||||||
|
]
|
||||||
|
|
||||||
|
static let accentValueToName: [String: String] = {
|
||||||
|
var output = [String: String]()
|
||||||
|
for (key, value) in accents {
|
||||||
|
if let existingValue = output[value] {
|
||||||
|
if key.count > existingValue.count {
|
||||||
|
continue
|
||||||
|
} else if key.count == existingValue.count {
|
||||||
|
if key.compare(existingValue) == .orderedDescending {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output[value] = key
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}()
|
||||||
|
|
||||||
|
static var supportedLatexSymbolNames: [String] {
|
||||||
|
supportedLatexSymbols.withValue { Array($0.keys) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let supportedLatexSymbols = ReadWriteLockIsolated<[String: Atom]>([
|
||||||
|
"square": placeholder(),
|
||||||
|
|
||||||
|
// Greek characters
|
||||||
|
"alpha": Atom(type: .variable, nucleus: "\u{03B1}"),
|
||||||
|
"beta": Atom(type: .variable, nucleus: "\u{03B2}"),
|
||||||
|
"gamma": Atom(type: .variable, nucleus: "\u{03B3}"),
|
||||||
|
"delta": Atom(type: .variable, nucleus: "\u{03B4}"),
|
||||||
|
"varepsilon": Atom(type: .variable, nucleus: "\u{03B5}"),
|
||||||
|
"zeta": Atom(type: .variable, nucleus: "\u{03B6}"),
|
||||||
|
"eta": Atom(type: .variable, nucleus: "\u{03B7}"),
|
||||||
|
"theta": Atom(type: .variable, nucleus: "\u{03B8}"),
|
||||||
|
"iota": Atom(type: .variable, nucleus: "\u{03B9}"),
|
||||||
|
"kappa": Atom(type: .variable, nucleus: "\u{03BA}"),
|
||||||
|
"lambda": Atom(type: .variable, nucleus: "\u{03BB}"),
|
||||||
|
"mu": Atom(type: .variable, nucleus: "\u{03BC}"),
|
||||||
|
"nu": Atom(type: .variable, nucleus: "\u{03BD}"),
|
||||||
|
"xi": Atom(type: .variable, nucleus: "\u{03BE}"),
|
||||||
|
"omicron": Atom(type: .variable, nucleus: "\u{03BF}"),
|
||||||
|
"pi": Atom(type: .variable, nucleus: "\u{03C0}"),
|
||||||
|
"rho": Atom(type: .variable, nucleus: "\u{03C1}"),
|
||||||
|
"varsigma": Atom(type: .variable, nucleus: "\u{03C1}"),
|
||||||
|
"sigma": Atom(type: .variable, nucleus: "\u{03C3}"),
|
||||||
|
"tau": Atom(type: .variable, nucleus: "\u{03C4}"),
|
||||||
|
"upsilon": Atom(type: .variable, nucleus: "\u{03C5}"),
|
||||||
|
"varphi": Atom(type: .variable, nucleus: "\u{03C6}"),
|
||||||
|
"chi": Atom(type: .variable, nucleus: "\u{03C7}"),
|
||||||
|
"psi": Atom(type: .variable, nucleus: "\u{03C8}"),
|
||||||
|
"omega": Atom(type: .variable, nucleus: "\u{03C9}"),
|
||||||
|
// We mark the following greek chars as ordinary so that we don't try
|
||||||
|
// to automatically italicize them as we do with variables.
|
||||||
|
// These characters fall outside the rules of italicization that we have defined.
|
||||||
|
"epsilon": Atom(type: .ordinary, nucleus: "\u{1D716}"),
|
||||||
|
"vartheta": Atom(type: .ordinary, nucleus: "\u{1D717}"),
|
||||||
|
"phi": Atom(type: .ordinary, nucleus: "\u{1D719}"),
|
||||||
|
"varrho": Atom(type: .ordinary, nucleus: "\u{1D71A}"),
|
||||||
|
"varpi": Atom(type: .ordinary, nucleus: "\u{1D71B}"),
|
||||||
|
|
||||||
|
// Capital greek characters
|
||||||
|
"Gamma": Atom(type: .variable, nucleus: "\u{0393}"),
|
||||||
|
"Delta": Atom(type: .variable, nucleus: "\u{0394}"),
|
||||||
|
"Theta": Atom(type: .variable, nucleus: "\u{0398}"),
|
||||||
|
"Lambda": Atom(type: .variable, nucleus: "\u{039B}"),
|
||||||
|
"Xi": Atom(type: .variable, nucleus: "\u{039E}"),
|
||||||
|
"Pi": Atom(type: .variable, nucleus: "\u{03A0}"),
|
||||||
|
"Sigma": Atom(type: .variable, nucleus: "\u{03A3}"),
|
||||||
|
"Upsilon": Atom(type: .variable, nucleus: "\u{03A5}"),
|
||||||
|
"Phi": Atom(type: .variable, nucleus: "\u{03A6}"),
|
||||||
|
"Psi": Atom(type: .variable, nucleus: "\u{03A8}"),
|
||||||
|
"Omega": Atom(type: .variable, nucleus: "\u{03A9}"),
|
||||||
|
|
||||||
|
// Open
|
||||||
|
"lceil": Atom(type: .open, nucleus: "\u{2308}"),
|
||||||
|
"lfloor": Atom(type: .open, nucleus: "\u{230A}"),
|
||||||
|
"langle": Atom(type: .open, nucleus: "\u{27E8}"),
|
||||||
|
"lgroup": Atom(type: .open, nucleus: "\u{27EE}"),
|
||||||
|
|
||||||
|
// Close
|
||||||
|
"rceil": Atom(type: .close, nucleus: "\u{2309}"),
|
||||||
|
"rfloor": Atom(type: .close, nucleus: "\u{230B}"),
|
||||||
|
"rangle": Atom(type: .close, nucleus: "\u{27E9}"),
|
||||||
|
"rgroup": Atom(type: .close, nucleus: "\u{27EF}"),
|
||||||
|
|
||||||
|
// Arrows
|
||||||
|
"leftarrow": Atom(type: .relation, nucleus: "\u{2190}"),
|
||||||
|
"uparrow": Atom(type: .relation, nucleus: "\u{2191}"),
|
||||||
|
"rightarrow": Atom(type: .relation, nucleus: "\u{2192}"),
|
||||||
|
"downarrow": Atom(type: .relation, nucleus: "\u{2193}"),
|
||||||
|
"leftrightarrow": Atom(type: .relation, nucleus: "\u{2194}"),
|
||||||
|
"updownarrow": Atom(type: .relation, nucleus: "\u{2195}"),
|
||||||
|
"nwarrow": Atom(type: .relation, nucleus: "\u{2196}"),
|
||||||
|
"nearrow": Atom(type: .relation, nucleus: "\u{2197}"),
|
||||||
|
"searrow": Atom(type: .relation, nucleus: "\u{2198}"),
|
||||||
|
"swarrow": Atom(type: .relation, nucleus: "\u{2199}"),
|
||||||
|
"mapsto": Atom(type: .relation, nucleus: "\u{21A6}"),
|
||||||
|
"Leftarrow": Atom(type: .relation, nucleus: "\u{21D0}"),
|
||||||
|
"Uparrow": Atom(type: .relation, nucleus: "\u{21D1}"),
|
||||||
|
"Rightarrow": Atom(type: .relation, nucleus: "\u{21D2}"),
|
||||||
|
"Downarrow": Atom(type: .relation, nucleus: "\u{21D3}"),
|
||||||
|
"Leftrightarrow": Atom(type: .relation, nucleus: "\u{21D4}"),
|
||||||
|
"Updownarrow": Atom(type: .relation, nucleus: "\u{21D5}"),
|
||||||
|
"longleftarrow": Atom(type: .relation, nucleus: "\u{27F5}"),
|
||||||
|
"longrightarrow": Atom(type: .relation, nucleus: "\u{27F6}"),
|
||||||
|
"longleftrightarrow": Atom(type: .relation, nucleus: "\u{27F7}"),
|
||||||
|
"Longleftarrow": Atom(type: .relation, nucleus: "\u{27F8}"),
|
||||||
|
"Longrightarrow": Atom(type: .relation, nucleus: "\u{27F9}"),
|
||||||
|
"Longleftrightarrow": Atom(type: .relation, nucleus: "\u{27FA}"),
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
"leq": Atom(type: .relation, nucleus: .lessEqual),
|
||||||
|
"geq": Atom(type: .relation, nucleus: .greaterEqual),
|
||||||
|
"neq": Atom(type: .relation, nucleus: .notEqual),
|
||||||
|
"in": Atom(type: .relation, nucleus: "\u{2208}"),
|
||||||
|
"notin": Atom(type: .relation, nucleus: "\u{2209}"),
|
||||||
|
"ni": Atom(type: .relation, nucleus: "\u{220B}"),
|
||||||
|
"propto": Atom(type: .relation, nucleus: "\u{221D}"),
|
||||||
|
"mid": Atom(type: .relation, nucleus: "\u{2223}"),
|
||||||
|
"parallel": Atom(type: .relation, nucleus: "\u{2225}"),
|
||||||
|
"sim": Atom(type: .relation, nucleus: "\u{223C}"),
|
||||||
|
"simeq": Atom(type: .relation, nucleus: "\u{2243}"),
|
||||||
|
"cong": Atom(type: .relation, nucleus: "\u{2245}"),
|
||||||
|
"approx": Atom(type: .relation, nucleus: "\u{2248}"),
|
||||||
|
"asymp": Atom(type: .relation, nucleus: "\u{224D}"),
|
||||||
|
"doteq": Atom(type: .relation, nucleus: "\u{2250}"),
|
||||||
|
"equiv": Atom(type: .relation, nucleus: "\u{2261}"),
|
||||||
|
"gg": Atom(type: .relation, nucleus: "\u{226B}"),
|
||||||
|
"ll": Atom(type: .relation, nucleus: "\u{226A}"),
|
||||||
|
"prec": Atom(type: .relation, nucleus: "\u{227A}"),
|
||||||
|
"succ": Atom(type: .relation, nucleus: "\u{227B}"),
|
||||||
|
"subset": Atom(type: .relation, nucleus: "\u{2282}"),
|
||||||
|
"supset": Atom(type: .relation, nucleus: "\u{2283}"),
|
||||||
|
"subseteq": Atom(type: .relation, nucleus: "\u{2286}"),
|
||||||
|
"supseteq": Atom(type: .relation, nucleus: "\u{2287}"),
|
||||||
|
"sqsubset": Atom(type: .relation, nucleus: "\u{228F}"),
|
||||||
|
"sqsupset": Atom(type: .relation, nucleus: "\u{2290}"),
|
||||||
|
"sqsubseteq": Atom(type: .relation, nucleus: "\u{2291}"),
|
||||||
|
"sqsupseteq": Atom(type: .relation, nucleus: "\u{2292}"),
|
||||||
|
"models": Atom(type: .relation, nucleus: "\u{22A7}"),
|
||||||
|
"perp": Atom(type: .relation, nucleus: "\u{27C2}"),
|
||||||
|
"implies": Atom(type: .relation, nucleus: "\u{27F9}"),
|
||||||
|
|
||||||
|
// operators
|
||||||
|
"times": times(),
|
||||||
|
"div": divide(),
|
||||||
|
"pm": Atom(type: .binaryOperator, nucleus: "\u{00B1}"),
|
||||||
|
"dagger": Atom(type: .binaryOperator, nucleus: "\u{2020}"),
|
||||||
|
"ddagger": Atom(type: .binaryOperator, nucleus: "\u{2021}"),
|
||||||
|
"mp": Atom(type: .binaryOperator, nucleus: "\u{2213}"),
|
||||||
|
"setminus": Atom(type: .binaryOperator, nucleus: "\u{2216}"),
|
||||||
|
"ast": Atom(type: .binaryOperator, nucleus: "\u{2217}"),
|
||||||
|
"circ": Atom(type: .binaryOperator, nucleus: "\u{2218}"),
|
||||||
|
"bullet": Atom(type: .binaryOperator, nucleus: "\u{2219}"),
|
||||||
|
"wedge": Atom(type: .binaryOperator, nucleus: "\u{2227}"),
|
||||||
|
"vee": Atom(type: .binaryOperator, nucleus: "\u{2228}"),
|
||||||
|
"cap": Atom(type: .binaryOperator, nucleus: "\u{2229}"),
|
||||||
|
"cup": Atom(type: .binaryOperator, nucleus: "\u{222A}"),
|
||||||
|
"wr": Atom(type: .binaryOperator, nucleus: "\u{2240}"),
|
||||||
|
"uplus": Atom(type: .binaryOperator, nucleus: "\u{228E}"),
|
||||||
|
"sqcap": Atom(type: .binaryOperator, nucleus: "\u{2293}"),
|
||||||
|
"sqcup": Atom(type: .binaryOperator, nucleus: "\u{2294}"),
|
||||||
|
"oplus": Atom(type: .binaryOperator, nucleus: "\u{2295}"),
|
||||||
|
"ominus": Atom(type: .binaryOperator, nucleus: "\u{2296}"),
|
||||||
|
"otimes": Atom(type: .binaryOperator, nucleus: "\u{2297}"),
|
||||||
|
"oslash": Atom(type: .binaryOperator, nucleus: "\u{2298}"),
|
||||||
|
"odot": Atom(type: .binaryOperator, nucleus: "\u{2299}"),
|
||||||
|
"star": Atom(type: .binaryOperator, nucleus: "\u{22C6}"),
|
||||||
|
"cdot": Atom(type: .binaryOperator, nucleus: "\u{22C5}"),
|
||||||
|
"amalg": Atom(type: .binaryOperator, nucleus: "\u{2A3F}"),
|
||||||
|
|
||||||
|
// No limit operators
|
||||||
|
"log": operatorWithName("log", limits: false),
|
||||||
|
"lg": operatorWithName("lg", limits: false),
|
||||||
|
"ln": operatorWithName("ln", limits: false),
|
||||||
|
"sin": operatorWithName("sin", limits: false),
|
||||||
|
"arcsin": operatorWithName("arcsin", limits: false),
|
||||||
|
"sinh": operatorWithName("sinh", limits: false),
|
||||||
|
"cos": operatorWithName("cos", limits: false),
|
||||||
|
"arccos": operatorWithName("arccos", limits: false),
|
||||||
|
"cosh": operatorWithName("cosh", limits: false),
|
||||||
|
"tan": operatorWithName("tan", limits: false),
|
||||||
|
"arctan": operatorWithName("arctan", limits: false),
|
||||||
|
"tanh": operatorWithName("tanh", limits: false),
|
||||||
|
"cot": operatorWithName("cot", limits: false),
|
||||||
|
"coth": operatorWithName("coth", limits: false),
|
||||||
|
"sec": operatorWithName("sec", limits: false),
|
||||||
|
"csc": operatorWithName("csc", limits: false),
|
||||||
|
"arg": operatorWithName("arg", limits: false),
|
||||||
|
"ker": operatorWithName("ker", limits: false),
|
||||||
|
"dim": operatorWithName("dim", limits: false),
|
||||||
|
"hom": operatorWithName("hom", limits: false),
|
||||||
|
"exp": operatorWithName("exp", limits: false),
|
||||||
|
"deg": operatorWithName("deg", limits: false),
|
||||||
|
"mod": operatorWithName("mod", limits: false),
|
||||||
|
|
||||||
|
// Limit operators
|
||||||
|
"lim": operatorWithName("lim", limits: true),
|
||||||
|
"limsup": operatorWithName("lim sup", limits: true),
|
||||||
|
"liminf": operatorWithName("lim inf", limits: true),
|
||||||
|
"max": operatorWithName("max", limits: true),
|
||||||
|
"min": operatorWithName("min", limits: true),
|
||||||
|
"sup": operatorWithName("sup", limits: true),
|
||||||
|
"inf": operatorWithName("inf", limits: true),
|
||||||
|
"det": operatorWithName("det", limits: true),
|
||||||
|
"Pr": operatorWithName("Pr", limits: true),
|
||||||
|
"gcd": operatorWithName("gcd", limits: true),
|
||||||
|
|
||||||
|
// Large operators
|
||||||
|
"prod": operatorWithName("\u{220F}", limits: true),
|
||||||
|
"coprod": operatorWithName("\u{2210}", limits: true),
|
||||||
|
"sum": operatorWithName("\u{2211}", limits: true),
|
||||||
|
"int": operatorWithName("\u{222B}", limits: false),
|
||||||
|
"iint": operatorWithName("\u{222C}", limits: false),
|
||||||
|
"iiint": operatorWithName("\u{222D}", limits: false),
|
||||||
|
"iiiint": operatorWithName("\u{2A0C}", limits: false),
|
||||||
|
"oint": operatorWithName("\u{222E}", limits: false),
|
||||||
|
"bigwedge": operatorWithName("\u{22C0}", limits: true),
|
||||||
|
"bigvee": operatorWithName("\u{22C1}", limits: true),
|
||||||
|
"bigcap": operatorWithName("\u{22C2}", limits: true),
|
||||||
|
"bigcup": operatorWithName("\u{22C3}", limits: true),
|
||||||
|
"bigodot": operatorWithName("\u{2A00}", limits: true),
|
||||||
|
"bigoplus": operatorWithName("\u{2A01}", limits: true),
|
||||||
|
"bigotimes": operatorWithName("\u{2A02}", limits: true),
|
||||||
|
"biguplus": operatorWithName("\u{2A04}", limits: true),
|
||||||
|
"bigsqcup": operatorWithName("\u{2A06}", limits: true),
|
||||||
|
|
||||||
|
// Latex command characters
|
||||||
|
"{": Atom(type: .open, nucleus: "{"),
|
||||||
|
"}": Atom(type: .close, nucleus: "}"),
|
||||||
|
"$": Atom(type: .ordinary, nucleus: "$"),
|
||||||
|
"&": Atom(type: .ordinary, nucleus: "&"),
|
||||||
|
"#": Atom(type: .ordinary, nucleus: "#"),
|
||||||
|
"%": Atom(type: .ordinary, nucleus: "%"),
|
||||||
|
"_": Atom(type: .ordinary, nucleus: "_"),
|
||||||
|
" ": Atom(type: .ordinary, nucleus: " "),
|
||||||
|
"backslash": Atom(type: .ordinary, nucleus: "\\"),
|
||||||
|
|
||||||
|
// Punctuation
|
||||||
|
// Note: \colon is different from : which is a relation
|
||||||
|
"colon": Atom(type: .punctuation, nucleus: ":"),
|
||||||
|
"cdotp": Atom(type: .punctuation, nucleus: "\u{00B7}"),
|
||||||
|
|
||||||
|
// Other symbols
|
||||||
|
"degree": Atom(type: .ordinary, nucleus: "\u{00B0}"),
|
||||||
|
"neg": Atom(type: .ordinary, nucleus: "\u{00AC}"),
|
||||||
|
"angstrom": Atom(type: .ordinary, nucleus: "\u{00C5}"),
|
||||||
|
"aa": Atom(type: .ordinary, nucleus: "\u{00E5}"),
|
||||||
|
"ae": Atom(type: .ordinary, nucleus: "\u{00E6}"),
|
||||||
|
"o": Atom(type: .ordinary, nucleus: "\u{00F8}"),
|
||||||
|
"oe": Atom(type: .ordinary, nucleus: "\u{0153}"),
|
||||||
|
"ss": Atom(type: .ordinary, nucleus: "\u{00DF}"),
|
||||||
|
"cc": Atom(type: .ordinary, nucleus: "\u{00E7}"),
|
||||||
|
"CC": Atom(type: .ordinary, nucleus: "\u{00C7}"),
|
||||||
|
"O": Atom(type: .ordinary, nucleus: "\u{00D8}"),
|
||||||
|
"AE": Atom(type: .ordinary, nucleus: "\u{00C6}"),
|
||||||
|
"OE": Atom(type: .ordinary, nucleus: "\u{0152}"),
|
||||||
|
"|": Atom(type: .ordinary, nucleus: "\u{2016}"),
|
||||||
|
"vert": Atom(type: .ordinary, nucleus: "|"),
|
||||||
|
"ldots": Atom(type: .ordinary, nucleus: "\u{2026}"),
|
||||||
|
"prime": Atom(type: .ordinary, nucleus: "\u{2032}"),
|
||||||
|
"hbar": Atom(type: .ordinary, nucleus: "\u{210F}"),
|
||||||
|
"lbar": Atom(type: .ordinary, nucleus: "\u{019B}"),
|
||||||
|
"Im": Atom(type: .ordinary, nucleus: "\u{2111}"),
|
||||||
|
"ell": Atom(type: .ordinary, nucleus: "\u{2113}"),
|
||||||
|
"wp": Atom(type: .ordinary, nucleus: "\u{2118}"),
|
||||||
|
"Re": Atom(type: .ordinary, nucleus: "\u{211C}"),
|
||||||
|
"mho": Atom(type: .ordinary, nucleus: "\u{2127}"),
|
||||||
|
"aleph": Atom(type: .ordinary, nucleus: "\u{2135}"),
|
||||||
|
"forall": Atom(type: .ordinary, nucleus: "\u{2200}"),
|
||||||
|
"exists": Atom(type: .ordinary, nucleus: "\u{2203}"),
|
||||||
|
"nexists": Atom(type: .ordinary, nucleus: "\u{2204}"),
|
||||||
|
"emptyset": Atom(type: .ordinary, nucleus: "\u{2205}"),
|
||||||
|
"nabla": Atom(type: .ordinary, nucleus: "\u{2207}"),
|
||||||
|
"infty": Atom(type: .ordinary, nucleus: "\u{221E}"),
|
||||||
|
"angle": Atom(type: .ordinary, nucleus: "\u{2220}"),
|
||||||
|
"top": Atom(type: .ordinary, nucleus: "\u{22A4}"),
|
||||||
|
"bot": Atom(type: .ordinary, nucleus: "\u{22A5}"),
|
||||||
|
"vdots": Atom(type: .ordinary, nucleus: "\u{22EE}"),
|
||||||
|
"cdots": Atom(type: .ordinary, nucleus: "\u{22EF}"),
|
||||||
|
"ddots": Atom(type: .ordinary, nucleus: "\u{22F1}"),
|
||||||
|
"triangle": Atom(type: .ordinary, nucleus: "\u{25B3}"),
|
||||||
|
"imath": Atom(type: .ordinary, nucleus: "\u{1D6A4}"),
|
||||||
|
"jmath": Atom(type: .ordinary, nucleus: "\u{1D6A5}"),
|
||||||
|
"upquote": Atom(type: .ordinary, nucleus: "\u{0027}"),
|
||||||
|
"partial": Atom(type: .ordinary, nucleus: "\u{1D715}"),
|
||||||
|
|
||||||
|
// Spacing
|
||||||
|
",": Space(amount: 3),
|
||||||
|
">": Space(amount: 4),
|
||||||
|
";": Space(amount: 5),
|
||||||
|
"!": Space(amount: -3),
|
||||||
|
"quad": Space(amount: 18),
|
||||||
|
"qquad": Space(amount: 36),
|
||||||
|
|
||||||
|
// Style
|
||||||
|
"displaystyle": Style(level: .display),
|
||||||
|
"textstyle": Style(level: .text),
|
||||||
|
"scriptstyle": Style(level: .script),
|
||||||
|
"scriptscriptstyle": Style(level: .scriptOfScript),
|
||||||
|
])
|
||||||
|
|
||||||
|
static let supportedAccentedCharacters: [Character: (String, String)] = [
|
||||||
|
"\u{00E1}": ("acute", "a"), "\u{00E9}": ("acute", "e"), "\u{00ED}": ("acute", "i"),
|
||||||
|
"\u{00F3}": ("acute", "o"), "\u{00FA}": ("acute", "u"), "\u{00FD}": ("acute", "y"),
|
||||||
|
"\u{00E0}": ("grave", "a"), "\u{00E8}": ("grave", "e"), "\u{00EC}": ("grave", "i"),
|
||||||
|
"\u{00F2}": ("grave", "o"), "\u{00F9}": ("grave", "u"),
|
||||||
|
"\u{00E2}": ("hat", "a"), "\u{00EA}": ("hat", "e"), "\u{00EE}": ("hat", "i"),
|
||||||
|
"\u{00F4}": ("hat", "o"), "\u{00FB}": ("hat", "u"),
|
||||||
|
"\u{00E4}": ("ddot", "a"), "\u{00EB}": ("ddot", "e"), "\u{00EF}": ("ddot", "i"),
|
||||||
|
"\u{00F6}": ("ddot", "o"), "\u{00FC}": ("ddot", "u"), "\u{00FF}": ("ddot", "y"),
|
||||||
|
"\u{00E3}": ("tilde", "a"), "\u{00F1}": ("tilde", "n"), "\u{00F5}": ("tilde", "o"),
|
||||||
|
"\u{00E7}": ("cc", ""), "\u{00F8}": ("o", ""), "\u{00E5}": ("aa", ""), "\u{00E6}": ("ae", ""),
|
||||||
|
"\u{0153}": ("oe", ""), "\u{00DF}": ("ss", ""),
|
||||||
|
"\u{0027}": ("upquote", ""),
|
||||||
|
"\u{00C1}": ("acute", "A"), "\u{00C9}": ("acute", "E"), "\u{00CD}": ("acute", "I"),
|
||||||
|
"\u{00D3}": ("acute", "O"), "\u{00DA}": ("acute", "U"), "\u{00DD}": ("acute", "Y"),
|
||||||
|
"\u{00C0}": ("grave", "A"), "\u{00C8}": ("grave", "E"), "\u{00CC}": ("grave", "I"),
|
||||||
|
"\u{00D2}": ("grave", "O"), "\u{00D9}": ("grave", "U"),
|
||||||
|
"\u{00C2}": ("hat", "A"), "\u{00CA}": ("hat", "E"), "\u{00CE}": ("hat", "I"),
|
||||||
|
"\u{00D4}": ("hat", "O"), "\u{00DB}": ("hat", "U"),
|
||||||
|
"\u{00C4}": ("ddot", "A"), "\u{00CB}": ("ddot", "E"), "\u{00CF}": ("ddot", "I"),
|
||||||
|
"\u{00D6}": ("ddot", "O"), "\u{00DC}": ("ddot", "U"),
|
||||||
|
"\u{00C3}": ("tilde", "A"), "\u{00D1}": ("tilde", "N"), "\u{00D5}": ("tilde", "O"),
|
||||||
|
"\u{00C7}": ("CC", ""),
|
||||||
|
"\u{00D8}": ("O", ""),
|
||||||
|
"\u{00C5}": ("AA", ""),
|
||||||
|
"\u{00C6}": ("AE", ""),
|
||||||
|
"\u{0152}": ("OE", ""),
|
||||||
|
]
|
||||||
|
|
||||||
|
private static let textToLatexSymbolName = ReadWriteLockIsolated<[String: String]?>(nil)
|
||||||
|
|
||||||
|
private static let fontStyles: [String: Atom.FontStyle] = [
|
||||||
|
"mathnormal": .default,
|
||||||
|
"mathrm": .roman,
|
||||||
|
"textrm": .roman,
|
||||||
|
"rm": .roman,
|
||||||
|
"mathbf": .bold,
|
||||||
|
"bf": .bold,
|
||||||
|
"textbf": .bold,
|
||||||
|
"mathcal": .caligraphic,
|
||||||
|
"cal": .caligraphic,
|
||||||
|
"mathtt": .typewriter,
|
||||||
|
"texttt": .typewriter,
|
||||||
|
"mathit": .italic,
|
||||||
|
"textit": .italic,
|
||||||
|
"mit": .italic,
|
||||||
|
"mathsf": .sansSerif,
|
||||||
|
"textsf": .sansSerif,
|
||||||
|
"mathfrak": .fraktur,
|
||||||
|
"frak": .fraktur,
|
||||||
|
"mathbb": .blackboard,
|
||||||
|
"mathbfit": .boldItalic,
|
||||||
|
"bm": .boldItalic,
|
||||||
|
"text": .roman,
|
||||||
|
]
|
||||||
|
|
||||||
|
private static let matrixEnvs: [String: [String]] = [
|
||||||
|
"matrix": [],
|
||||||
|
"pmatrix": ["(", ")"],
|
||||||
|
"bmatrix": ["[", "]"],
|
||||||
|
"Bmatrix": ["{", "}"],
|
||||||
|
"vmatrix": ["vert", "vert"],
|
||||||
|
"Vmatrix": ["Vert", "Vert"],
|
||||||
|
"smallmatrix": [],
|
||||||
|
"matrix*": [],
|
||||||
|
"pmatrix*": ["(", ")"],
|
||||||
|
"bmatrix*": ["[", "]"],
|
||||||
|
"Bmatrix*": ["{", "}"],
|
||||||
|
"vmatrix*": ["vert", "vert"],
|
||||||
|
"Vmatrix*": ["Vert", "Vert"],
|
||||||
|
]
|
||||||
|
|
||||||
|
private enum ParseErrorCode: Int {
|
||||||
|
case invalidEnv = 8
|
||||||
|
case invalidNumColumns = 12
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let parseErrorDomain = "ParseError"
|
||||||
|
|
||||||
|
static func fontStyle(named fontName: String) -> Atom.FontStyle? {
|
||||||
|
fontStyles[fontName]
|
||||||
|
}
|
||||||
|
|
||||||
|
static func fontName(for style: Atom.FontStyle) -> String {
|
||||||
|
switch style {
|
||||||
|
case .default: return "mathnormal"
|
||||||
|
case .roman: return "mathrm"
|
||||||
|
case .bold: return "mathbf"
|
||||||
|
case .fraktur: return "mathfrak"
|
||||||
|
case .caligraphic: return "mathcal"
|
||||||
|
case .italic: return "mathit"
|
||||||
|
case .sansSerif: return "mathsf"
|
||||||
|
case .blackboard: return "mathbb"
|
||||||
|
case .typewriter: return "mathtt"
|
||||||
|
case .boldItalic: return "bm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func times() -> Atom {
|
||||||
|
Atom(type: .binaryOperator, nucleus: .multiplication)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func divide() -> Atom {
|
||||||
|
Atom(type: .binaryOperator, nucleus: .division)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func placeholder() -> Atom {
|
||||||
|
Atom(type: .placeholder, nucleus: .whiteSquare)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func placeholderFraction() -> Fraction {
|
||||||
|
let frac = Fraction()
|
||||||
|
frac.numerator = AtomList(atom: placeholder())
|
||||||
|
frac.denominator = AtomList(atom: placeholder())
|
||||||
|
return frac
|
||||||
|
}
|
||||||
|
|
||||||
|
static func placeholderSquareRoot() -> Radical {
|
||||||
|
let rad = Radical()
|
||||||
|
rad.radicand = AtomList(atom: placeholder())
|
||||||
|
return rad
|
||||||
|
}
|
||||||
|
|
||||||
|
static func placeholderRadical() -> Radical {
|
||||||
|
let rad = Radical()
|
||||||
|
rad.radicand = AtomList(atom: placeholder())
|
||||||
|
rad.degree = AtomList(atom: placeholder())
|
||||||
|
return rad
|
||||||
|
}
|
||||||
|
|
||||||
|
static func atom(fromAccentedCharacter ch: Character) -> Atom? {
|
||||||
|
if let symbol = supportedAccentedCharacters[ch] {
|
||||||
|
if let atom = atom(forLatexSymbol: symbol.0) {
|
||||||
|
return atom
|
||||||
|
}
|
||||||
|
|
||||||
|
if let accent = accent(withName: symbol.0) {
|
||||||
|
let list = AtomList()
|
||||||
|
let character = Array(symbol.1)[0]
|
||||||
|
if let atom = atom(forCharacter: character) {
|
||||||
|
list.append(atom)
|
||||||
|
}
|
||||||
|
accent.innerList = list
|
||||||
|
return accent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
static func atom(forCharacter ch: Character) -> Atom? {
|
||||||
|
let stringValue = String(ch)
|
||||||
|
switch stringValue {
|
||||||
|
case "\u{0410}"..."\u{044F}":
|
||||||
|
return Atom(type: .ordinary, nucleus: stringValue)
|
||||||
|
case _ where supportedAccentedCharacters.keys.contains(ch):
|
||||||
|
return atom(fromAccentedCharacter: ch)
|
||||||
|
case _ where ch.utf32 < 0x0021 || ch.utf32 > 0x007E:
|
||||||
|
return nil
|
||||||
|
case "$", "%", "#", "&", "~", "\'", "^", "_", "{", "}", "\\":
|
||||||
|
return nil
|
||||||
|
case "(", "[":
|
||||||
|
return Atom(type: .open, nucleus: stringValue)
|
||||||
|
case ")", "]", "!", "?":
|
||||||
|
return Atom(type: .close, nucleus: stringValue)
|
||||||
|
case ",", ";":
|
||||||
|
return Atom(type: .punctuation, nucleus: stringValue)
|
||||||
|
case "=", ">", "<":
|
||||||
|
return Atom(type: .relation, nucleus: stringValue)
|
||||||
|
case ":":
|
||||||
|
return Atom(type: .relation, nucleus: "\u{2236}")
|
||||||
|
case "-":
|
||||||
|
return Atom(type: .binaryOperator, nucleus: "\u{2212}")
|
||||||
|
case "+", "*":
|
||||||
|
return Atom(type: .binaryOperator, nucleus: stringValue)
|
||||||
|
case ".", "0"..."9":
|
||||||
|
return Atom(type: .number, nucleus: stringValue)
|
||||||
|
case "a"..."z", "A"..."Z":
|
||||||
|
return Atom(type: .variable, nucleus: stringValue)
|
||||||
|
case "\"", "/", "@", "`", "|":
|
||||||
|
return Atom(type: .ordinary, nucleus: stringValue)
|
||||||
|
default:
|
||||||
|
assertionFailure("Unknown ASCII character '\(ch)'. Should have been handled earlier.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func atomList(for string: String) -> AtomList {
|
||||||
|
let list = AtomList()
|
||||||
|
for character in string {
|
||||||
|
if let newAtom = atom(forCharacter: character) {
|
||||||
|
list.append(newAtom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
static func atom(forLatexSymbol name: String) -> Atom? {
|
||||||
|
let resolvedName = aliases[name] ?? name
|
||||||
|
return supportedLatexSymbols.withValue { $0[resolvedName]?.copy() }
|
||||||
|
}
|
||||||
|
|
||||||
|
static func latexSymbolName(for atom: Atom) -> String? {
|
||||||
|
guard !atom.nucleus.isEmpty else { return nil }
|
||||||
|
return textToLatexSymbolNameValue()[atom.nucleus]
|
||||||
|
}
|
||||||
|
|
||||||
|
static func add(latexSymbol name: String, value: Atom) {
|
||||||
|
let _ = textToLatexSymbolNameValue()
|
||||||
|
supportedLatexSymbols.withValue { $0[name] = value }
|
||||||
|
textToLatexSymbolName.withValue { map in
|
||||||
|
guard !value.nucleus.isEmpty else { return }
|
||||||
|
map?[value.nucleus] = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func operatorWithName(_ name: String, limits: Bool) -> LargeOperator {
|
||||||
|
let op = LargeOperator(limits: limits)
|
||||||
|
op.nucleus = name
|
||||||
|
return op
|
||||||
|
}
|
||||||
|
|
||||||
|
static func accent(withName name: String) -> Accent? {
|
||||||
|
if let accentValue = accents[name] {
|
||||||
|
return Accent(value: accentValue)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
static func accentName(_ accent: Accent) -> String? {
|
||||||
|
accentValueToName[accent.nucleus]
|
||||||
|
}
|
||||||
|
|
||||||
|
static func boundary(forDelimiter name: String) -> Atom? {
|
||||||
|
if let delimValue = delimiters[name] {
|
||||||
|
return Atom(type: .boundary, nucleus: delimValue)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
static func delimiterName(of boundary: Atom) -> String? {
|
||||||
|
guard boundary.type == .boundary else { return nil }
|
||||||
|
return delimValueToName[boundary.nucleus]
|
||||||
|
}
|
||||||
|
|
||||||
|
static func fraction(withNumerator numerator: AtomList, denominator: AtomList) -> Fraction {
|
||||||
|
let fraction = Fraction()
|
||||||
|
fraction.numerator = numerator
|
||||||
|
fraction.denominator = denominator
|
||||||
|
return fraction
|
||||||
|
}
|
||||||
|
|
||||||
|
static func mathListForCharacters(_ chars: String) -> AtomList? {
|
||||||
|
let list = AtomList()
|
||||||
|
for ch in chars {
|
||||||
|
if let atom = atom(forCharacter: ch) {
|
||||||
|
list.append(atom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
static func fraction(
|
||||||
|
withNumeratorString numerator: String, denominatorString denominator: String
|
||||||
|
) -> Fraction {
|
||||||
|
let num = atomList(for: numerator)
|
||||||
|
let denom = atomList(for: denominator)
|
||||||
|
return fraction(withNumerator: num, denominator: denom)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func table(
|
||||||
|
withEnvironment env: String?,
|
||||||
|
alignment: Table.ColumnAlignment? = nil,
|
||||||
|
rows: [[AtomList]],
|
||||||
|
error: inout NSError?
|
||||||
|
) -> Atom? {
|
||||||
|
let table = Table(environment: env ?? "")
|
||||||
|
|
||||||
|
for i in 0..<rows.count {
|
||||||
|
let row = rows[i]
|
||||||
|
for j in 0..<row.count {
|
||||||
|
table.setCell(row[j], forRow: i, column: j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if env == nil {
|
||||||
|
table.interColumnSpacing = 0
|
||||||
|
table.interRowAdditionalSpacing = 1
|
||||||
|
for column in 0..<table.numberOfColumns {
|
||||||
|
table.setAlignment(.left, forColumn: column)
|
||||||
|
}
|
||||||
|
return table
|
||||||
|
} else if let env {
|
||||||
|
if let delims = matrixEnvs[env] {
|
||||||
|
table.environment = "matrix"
|
||||||
|
|
||||||
|
let isSmallMatrix = (env == "smallmatrix")
|
||||||
|
|
||||||
|
table.interRowAdditionalSpacing = 0
|
||||||
|
table.interColumnSpacing = isSmallMatrix ? 6 : 18
|
||||||
|
|
||||||
|
let style = Style(level: isSmallMatrix ? .script : .text)
|
||||||
|
|
||||||
|
for i in 0..<table.cells.count {
|
||||||
|
for j in 0..<table.cells[i].count {
|
||||||
|
table.cells[i][j].insert(style, at: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let alignment {
|
||||||
|
for column in 0..<table.numberOfColumns {
|
||||||
|
table.setAlignment(alignment, forColumn: column)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if delims.count == 2 {
|
||||||
|
let inner = Inner()
|
||||||
|
inner.leftBoundary = boundary(forDelimiter: delims[0])
|
||||||
|
inner.rightBoundary = boundary(forDelimiter: delims[1])
|
||||||
|
inner.innerList = AtomList(atoms: [table])
|
||||||
|
return inner
|
||||||
|
} else {
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
} else if env == "eqalign" || env == "split" || env == "aligned" {
|
||||||
|
if table.numberOfColumns != 2 {
|
||||||
|
let message = "\(env) environment can only have 2 columns"
|
||||||
|
if error == nil {
|
||||||
|
error = NSError(
|
||||||
|
domain: parseErrorDomain,
|
||||||
|
code: ParseErrorCode.invalidNumColumns.rawValue,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: message]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let spacer = Atom(type: .ordinary, nucleus: "")
|
||||||
|
|
||||||
|
for i in 0..<table.cells.count {
|
||||||
|
if table.cells[i].count >= 2 {
|
||||||
|
table.cells[i][1].insert(spacer, at: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table.interRowAdditionalSpacing = 1
|
||||||
|
table.interColumnSpacing = 0
|
||||||
|
|
||||||
|
table.setAlignment(.right, forColumn: 0)
|
||||||
|
table.setAlignment(.left, forColumn: 1)
|
||||||
|
|
||||||
|
return table
|
||||||
|
} else if env == "displaylines" || env == "gather" {
|
||||||
|
if table.numberOfColumns != 1 {
|
||||||
|
let message = "\(env) environment can only have 1 column"
|
||||||
|
if error == nil {
|
||||||
|
error = NSError(
|
||||||
|
domain: parseErrorDomain,
|
||||||
|
code: ParseErrorCode.invalidNumColumns.rawValue,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: message]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
table.interRowAdditionalSpacing = 1
|
||||||
|
table.interColumnSpacing = 0
|
||||||
|
|
||||||
|
table.setAlignment(.center, forColumn: 0)
|
||||||
|
|
||||||
|
return table
|
||||||
|
} else if env == "eqnarray" {
|
||||||
|
if table.numberOfColumns != 3 {
|
||||||
|
let message = "\(env) environment can only have 3 columns"
|
||||||
|
if error == nil {
|
||||||
|
error = NSError(
|
||||||
|
domain: parseErrorDomain,
|
||||||
|
code: ParseErrorCode.invalidNumColumns.rawValue,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: message]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
table.interRowAdditionalSpacing = 1
|
||||||
|
table.interColumnSpacing = 18
|
||||||
|
|
||||||
|
table.setAlignment(.right, forColumn: 0)
|
||||||
|
table.setAlignment(.center, forColumn: 1)
|
||||||
|
table.setAlignment(.left, forColumn: 2)
|
||||||
|
|
||||||
|
return table
|
||||||
|
} else if env == "cases" {
|
||||||
|
if table.numberOfColumns != 1 && table.numberOfColumns != 2 {
|
||||||
|
let message = "cases environment can have 1 or 2 columns"
|
||||||
|
if error == nil {
|
||||||
|
error = NSError(
|
||||||
|
domain: parseErrorDomain,
|
||||||
|
code: ParseErrorCode.invalidNumColumns.rawValue,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: message]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
table.interRowAdditionalSpacing = 0
|
||||||
|
table.interColumnSpacing = 18
|
||||||
|
|
||||||
|
table.setAlignment(.left, forColumn: 0)
|
||||||
|
if table.numberOfColumns == 2 {
|
||||||
|
table.setAlignment(.left, forColumn: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let inner = Inner()
|
||||||
|
inner.leftBoundary = boundary(forDelimiter: "{")
|
||||||
|
inner.rightBoundary = boundary(forDelimiter: ".")
|
||||||
|
let space = atom(forLatexSymbol: ",")!
|
||||||
|
|
||||||
|
inner.innerList = AtomList(atoms: [space, table])
|
||||||
|
|
||||||
|
return inner
|
||||||
|
} else {
|
||||||
|
let message = "Unknown environment \(env)"
|
||||||
|
error = NSError(
|
||||||
|
domain: parseErrorDomain,
|
||||||
|
code: ParseErrorCode.invalidEnv.rawValue,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: message]
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func textToLatexSymbolNameValue() -> [String: String] {
|
||||||
|
textToLatexSymbolName.withValue { map in
|
||||||
|
if let map {
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
let symbols = supportedLatexSymbols.withValue { $0 }
|
||||||
|
var output = [String: String]()
|
||||||
|
for (key, atom) in symbols {
|
||||||
|
if atom.nucleus.isEmpty {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if let existingText = output[atom.nucleus] {
|
||||||
|
if key.count > existingText.count {
|
||||||
|
continue
|
||||||
|
} else if key.count == existingText.count {
|
||||||
|
if key.compare(existingText) == .orderedDescending {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output[atom.nucleus] = key
|
||||||
|
}
|
||||||
|
|
||||||
|
map = output
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
411
Sources/SwiftUIMath/Internal/Model/AtomList.swift
Normal file
411
Sources/SwiftUIMath/Internal/Model/AtomList.swift
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Math {
|
||||||
|
enum AtomType: Int {
|
||||||
|
// A number or text in ordinary format - Ord in TeX
|
||||||
|
case ordinary = 1
|
||||||
|
// A number - Does not exist in TeX
|
||||||
|
case number
|
||||||
|
// A variable (i.e. text in italic format) - Does not exist in TeX
|
||||||
|
case variable
|
||||||
|
// A large operator such as (sin/cos, integral etc.) - Op in TeX
|
||||||
|
case largeOperator
|
||||||
|
// A binary operator - Bin in TeX
|
||||||
|
case binaryOperator
|
||||||
|
// A unary operator - Does not exist in TeX.
|
||||||
|
case unaryOperator
|
||||||
|
// A relation, e.g. = > < etc. - Rel in TeX
|
||||||
|
case relation
|
||||||
|
// Open brackets - Open in TeX
|
||||||
|
case open
|
||||||
|
// Close brackets - Close in TeX
|
||||||
|
case close
|
||||||
|
// A fraction e.g 1/2 - generalized fraction node in TeX
|
||||||
|
case fraction
|
||||||
|
// A radical operator e.g. sqrt(2)
|
||||||
|
case radical
|
||||||
|
// Punctuation such as , - Punct in TeX
|
||||||
|
case punctuation
|
||||||
|
// A placeholder square for future input. Does not exist in TeX
|
||||||
|
case placeholder
|
||||||
|
// An inner atom, i.e. an embedded math list - Inner in TeX
|
||||||
|
case inner
|
||||||
|
// An underlined atom - Under in TeX
|
||||||
|
case underline
|
||||||
|
// An overlined atom - Over in TeX
|
||||||
|
case overline
|
||||||
|
// An accented atom - Accent in TeX
|
||||||
|
case accent
|
||||||
|
|
||||||
|
// Atoms after this point do not support subscripts or superscripts
|
||||||
|
|
||||||
|
// A left atom - Left & Right in TeX. We don't need two since we track boundaries separately.
|
||||||
|
case boundary = 101
|
||||||
|
|
||||||
|
// Atoms after this are non-math TeX nodes that are still useful in math mode. They do not have
|
||||||
|
// the usual structure.
|
||||||
|
|
||||||
|
// Spacing between math atoms. This denotes both glue and kern for TeX. We do not
|
||||||
|
// distinguish between glue and kern.
|
||||||
|
case space = 201
|
||||||
|
|
||||||
|
// Denotes style changes during rendering.
|
||||||
|
case style
|
||||||
|
case color
|
||||||
|
case textColor
|
||||||
|
case colorBox
|
||||||
|
|
||||||
|
// Atoms after this point are not part of TeX and do not have the usual structure.
|
||||||
|
|
||||||
|
// An table atom. This atom does not exist in TeX. It is equivalent to the TeX command
|
||||||
|
// halign which is handled outside of the TeX math rendering engine. We bring it into our
|
||||||
|
// math typesetting to handle matrices and other tables.
|
||||||
|
case table = 1001
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Math.AtomType {
|
||||||
|
var disallowsFollowingBinaryOperator: Bool {
|
||||||
|
switch self {
|
||||||
|
case .binaryOperator, .relation, .open, .punctuation, .largeOperator:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowsScripts: Bool {
|
||||||
|
self < .boundary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Math.AtomType: Comparable {
|
||||||
|
static func < (lhs: Math.AtomType, rhs: Math.AtomType) -> Bool {
|
||||||
|
lhs.rawValue < rhs.rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Math.AtomType: CustomStringConvertible {
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .ordinary: return "Ordinary"
|
||||||
|
case .number: return "Number"
|
||||||
|
case .variable: return "Variable"
|
||||||
|
case .largeOperator: return "Large Operator"
|
||||||
|
case .binaryOperator: return "Binary Operator"
|
||||||
|
case .unaryOperator: return "Unary Operator"
|
||||||
|
case .relation: return "Relation"
|
||||||
|
case .open: return "Open"
|
||||||
|
case .close: return "Close"
|
||||||
|
case .fraction: return "Fraction"
|
||||||
|
case .radical: return "Radical"
|
||||||
|
case .punctuation: return "Punctuation"
|
||||||
|
case .placeholder: return "Placeholder"
|
||||||
|
case .inner: return "Inner"
|
||||||
|
case .underline: return "Underline"
|
||||||
|
case .overline: return "Overline"
|
||||||
|
case .accent: return "Accent"
|
||||||
|
case .boundary: return "Boundary"
|
||||||
|
case .space: return "Space"
|
||||||
|
case .style: return "Style"
|
||||||
|
case .color: return "Color"
|
||||||
|
case .textColor: return "TextColor"
|
||||||
|
case .colorBox: return "Colorbox"
|
||||||
|
case .table: return "Table"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Math {
|
||||||
|
class Atom: CustomStringConvertible {
|
||||||
|
enum FontStyle: Int {
|
||||||
|
case `default` = 0
|
||||||
|
case roman
|
||||||
|
case bold
|
||||||
|
case caligraphic
|
||||||
|
case typewriter
|
||||||
|
case italic
|
||||||
|
case sansSerif
|
||||||
|
case fraktur
|
||||||
|
case blackboard
|
||||||
|
case boldItalic
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
[
|
||||||
|
nucleus,
|
||||||
|
superscript.map { "^{\($0)}" },
|
||||||
|
`subscript`.map { "_{\($0)}" },
|
||||||
|
]
|
||||||
|
.compactMap(\.self)
|
||||||
|
.joined()
|
||||||
|
}
|
||||||
|
|
||||||
|
var type: AtomType
|
||||||
|
var nucleus: String
|
||||||
|
var indexRange: NSRange
|
||||||
|
var fontStyle: FontStyle
|
||||||
|
var fusedAtoms: [Atom]
|
||||||
|
|
||||||
|
var `subscript`: AtomList? {
|
||||||
|
didSet {
|
||||||
|
if `subscript` != nil, !allowsScripts {
|
||||||
|
assertionFailure("Subscripts are not allowed for \(type)")
|
||||||
|
`subscript` = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var superscript: AtomList? {
|
||||||
|
didSet {
|
||||||
|
if superscript != nil, !allowsScripts {
|
||||||
|
assertionFailure("Superscripts are not allowed for \(type)")
|
||||||
|
superscript = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var finalized: Atom {
|
||||||
|
let finalized = copy()
|
||||||
|
finalized.superscript = finalized.superscript?.finalized
|
||||||
|
finalized.subscript = finalized.subscript?.finalized
|
||||||
|
return finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
var string: String {
|
||||||
|
description
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowsScripts: Bool {
|
||||||
|
type.allowsScripts
|
||||||
|
}
|
||||||
|
|
||||||
|
var disallowsFollowingBinaryOperator: Bool {
|
||||||
|
type.disallowsFollowingBinaryOperator
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
type: AtomType = .ordinary,
|
||||||
|
nucleus: String = "",
|
||||||
|
indexRange: NSRange = NSRange(),
|
||||||
|
fontStyle: FontStyle = .default,
|
||||||
|
fusedAtoms: [Atom] = [],
|
||||||
|
subscript: AtomList? = nil,
|
||||||
|
superscript: AtomList? = nil
|
||||||
|
) {
|
||||||
|
self.type = type
|
||||||
|
self.nucleus = nucleus
|
||||||
|
self.indexRange = indexRange
|
||||||
|
self.fontStyle = fontStyle
|
||||||
|
self.fusedAtoms = fusedAtoms
|
||||||
|
self.subscript = `subscript`
|
||||||
|
self.superscript = superscript
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ other: Atom) {
|
||||||
|
self.type = other.type
|
||||||
|
self.nucleus = other.nucleus
|
||||||
|
self.indexRange = other.indexRange
|
||||||
|
self.fontStyle = other.fontStyle
|
||||||
|
self.fusedAtoms = other.fusedAtoms
|
||||||
|
self.subscript = other.`subscript`.map { AtomList($0) }
|
||||||
|
self.superscript = other.superscript.map { AtomList($0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
convenience init(type: AtomType, value: String) {
|
||||||
|
self.init(type: type, nucleus: type == .radical ? "" : value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copy() -> Atom {
|
||||||
|
switch type {
|
||||||
|
case .fraction:
|
||||||
|
return (self as? Fraction).map {
|
||||||
|
Fraction($0)
|
||||||
|
} ?? Fraction()
|
||||||
|
case .radical:
|
||||||
|
return (self as? Radical).map {
|
||||||
|
Radical($0)
|
||||||
|
} ?? Radical()
|
||||||
|
case .largeOperator:
|
||||||
|
return (self as? LargeOperator).map {
|
||||||
|
LargeOperator($0)
|
||||||
|
} ?? LargeOperator()
|
||||||
|
case .inner:
|
||||||
|
return (self as? Inner).map {
|
||||||
|
Inner($0)
|
||||||
|
} ?? Inner()
|
||||||
|
case .overline:
|
||||||
|
return (self as? Overline).map {
|
||||||
|
Overline($0)
|
||||||
|
} ?? Overline()
|
||||||
|
case .underline:
|
||||||
|
return (self as? Underline).map {
|
||||||
|
Underline($0)
|
||||||
|
} ?? Underline()
|
||||||
|
case .accent:
|
||||||
|
return (self as? Accent).map {
|
||||||
|
Accent($0)
|
||||||
|
} ?? Accent()
|
||||||
|
case .space:
|
||||||
|
return (self as? Space).map {
|
||||||
|
Space($0)
|
||||||
|
} ?? Space()
|
||||||
|
case .style:
|
||||||
|
return (self as? Style).map {
|
||||||
|
Style($0)
|
||||||
|
} ?? Style()
|
||||||
|
case .color:
|
||||||
|
return (self as? Color).map {
|
||||||
|
Color($0)
|
||||||
|
} ?? Color()
|
||||||
|
case .textColor:
|
||||||
|
return (self as? TextColor).map {
|
||||||
|
TextColor($0)
|
||||||
|
} ?? TextColor()
|
||||||
|
case .colorBox:
|
||||||
|
return (self as? ColorBox).map {
|
||||||
|
ColorBox($0)
|
||||||
|
} ?? ColorBox()
|
||||||
|
case .table:
|
||||||
|
return (self as? Table).map {
|
||||||
|
Table($0)
|
||||||
|
} ?? Table()
|
||||||
|
default:
|
||||||
|
return Atom(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fuse(with atom: Atom) {
|
||||||
|
guard `subscript` == nil, superscript == nil, type == atom.type else {
|
||||||
|
assertionFailure("Cannot fuse \(self) with \(atom)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if fusedAtoms.isEmpty {
|
||||||
|
fusedAtoms.append(.init(self))
|
||||||
|
}
|
||||||
|
|
||||||
|
if atom.fusedAtoms.isEmpty {
|
||||||
|
fusedAtoms.append(atom)
|
||||||
|
} else {
|
||||||
|
fusedAtoms.append(contentsOf: atom.fusedAtoms)
|
||||||
|
}
|
||||||
|
|
||||||
|
nucleus += atom.nucleus
|
||||||
|
indexRange.length += atom.indexRange.length
|
||||||
|
|
||||||
|
self.superscript = atom.superscript
|
||||||
|
self.`subscript` = atom.`subscript`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Math {
|
||||||
|
final class AtomList {
|
||||||
|
var atoms: [Atom]
|
||||||
|
|
||||||
|
var finalized: AtomList {
|
||||||
|
let finalizedList = AtomList()
|
||||||
|
|
||||||
|
var previousAtom: Atom?
|
||||||
|
|
||||||
|
for atom in atoms {
|
||||||
|
let finalizedAtom = atom.finalized
|
||||||
|
|
||||||
|
if atom.indexRange == NSRange() {
|
||||||
|
let location = (previousAtom?.indexRange).map {
|
||||||
|
$0.location + $0.length
|
||||||
|
}
|
||||||
|
finalizedAtom.indexRange = NSRange(location: location ?? 0, length: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch finalizedAtom.type {
|
||||||
|
case .binaryOperator where previousAtom.disallowsFollowingBinaryOperator:
|
||||||
|
finalizedAtom.type = .unaryOperator
|
||||||
|
case .relation, .punctuation, .close:
|
||||||
|
if case .binaryOperator = previousAtom?.type {
|
||||||
|
previousAtom?.type = .unaryOperator
|
||||||
|
}
|
||||||
|
case .number:
|
||||||
|
if let previousAtom,
|
||||||
|
case .number = previousAtom.type,
|
||||||
|
previousAtom.`subscript` == nil,
|
||||||
|
previousAtom.superscript == nil
|
||||||
|
{
|
||||||
|
previousAtom.fuse(with: finalizedAtom)
|
||||||
|
continue // skip the current node, we are done here
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
finalizedList.append(finalizedAtom)
|
||||||
|
previousAtom = finalizedAtom
|
||||||
|
}
|
||||||
|
|
||||||
|
if let previousAtom, case .binaryOperator = previousAtom.type {
|
||||||
|
previousAtom.type = .unaryOperator
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalizedList
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ list: AtomList) {
|
||||||
|
self.atoms = list.atoms.map {
|
||||||
|
$0.copy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
convenience init(atom: Atom) {
|
||||||
|
self.init(atoms: [atom])
|
||||||
|
}
|
||||||
|
|
||||||
|
init(atoms: [Atom] = []) {
|
||||||
|
self.atoms = atoms
|
||||||
|
}
|
||||||
|
|
||||||
|
func append(_ atom: Atom) {
|
||||||
|
guard canAdd(atom) else {
|
||||||
|
assertionFailure("Can't append atom of type \(atom.type)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
atoms.append(atom)
|
||||||
|
}
|
||||||
|
|
||||||
|
func insert(_ atom: Atom, at index: Int) {
|
||||||
|
guard atoms.indices.contains(index) || index == atoms.endIndex else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard canAdd(atom) else {
|
||||||
|
assertionFailure("Can't insert atom of type \(atom.type)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
atoms.insert(atom, at: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func append(contentsOf list: AtomList) {
|
||||||
|
atoms.append(contentsOf: list.atoms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Math.AtomList: CustomStringConvertible {
|
||||||
|
var description: String {
|
||||||
|
atoms.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Math.AtomList {
|
||||||
|
private func canAdd(_ atom: Math.Atom) -> Bool {
|
||||||
|
atom.type != .boundary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Optional where Wrapped: Math.Atom {
|
||||||
|
var disallowsFollowingBinaryOperator: Bool {
|
||||||
|
guard case .some(let wrapped) = self else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return wrapped.disallowsFollowingBinaryOperator
|
||||||
|
}
|
||||||
|
}
|
||||||
27
Sources/SwiftUIMath/Internal/Model/Atoms/Accent.swift
Normal file
27
Sources/SwiftUIMath/Internal/Model/Atoms/Accent.swift
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Math {
|
||||||
|
final class Accent: Atom {
|
||||||
|
var innerList: AtomList?
|
||||||
|
|
||||||
|
override var finalized: Math.Atom {
|
||||||
|
let finalized = super.finalized
|
||||||
|
|
||||||
|
if let accent = finalized as? Accent {
|
||||||
|
accent.innerList = accent.innerList?.finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ accent: Accent) {
|
||||||
|
self.innerList = accent.innerList.map { AtomList($0) }
|
||||||
|
super.init(accent)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(value: String = "", innerList: AtomList? = nil) {
|
||||||
|
self.innerList = innerList
|
||||||
|
super.init(type: .accent, nucleus: value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Sources/SwiftUIMath/Internal/Model/Atoms/Color.swift
Normal file
41
Sources/SwiftUIMath/Internal/Model/Atoms/Color.swift
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Math {
|
||||||
|
final class Color: Atom {
|
||||||
|
var colorString: String
|
||||||
|
var innerList: AtomList?
|
||||||
|
|
||||||
|
override var description: String {
|
||||||
|
[
|
||||||
|
"\\color",
|
||||||
|
"{\(colorString)}",
|
||||||
|
innerList.map { "{\($0)}" },
|
||||||
|
]
|
||||||
|
.compactMap(\.self)
|
||||||
|
.joined()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var finalized: Math.Atom {
|
||||||
|
let finalized = super.finalized
|
||||||
|
|
||||||
|
if let color = finalized as? Color {
|
||||||
|
color.innerList = color.innerList?.finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ color: Color) {
|
||||||
|
self.colorString = color.colorString
|
||||||
|
self.innerList = color.innerList.map { AtomList($0) }
|
||||||
|
|
||||||
|
super.init(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(colorString: String = "", innerList: AtomList? = nil) {
|
||||||
|
self.colorString = colorString
|
||||||
|
self.innerList = innerList
|
||||||
|
super.init(type: .color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Sources/SwiftUIMath/Internal/Model/Atoms/ColorBox.swift
Normal file
41
Sources/SwiftUIMath/Internal/Model/Atoms/ColorBox.swift
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Math {
|
||||||
|
final class ColorBox: Atom {
|
||||||
|
var colorString: String
|
||||||
|
var innerList: AtomList?
|
||||||
|
|
||||||
|
override var description: String {
|
||||||
|
[
|
||||||
|
"\\colorbox",
|
||||||
|
"{\(colorString)}",
|
||||||
|
innerList.map { "{\($0)}" },
|
||||||
|
]
|
||||||
|
.compactMap(\.self)
|
||||||
|
.joined()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var finalized: Math.Atom {
|
||||||
|
let finalized = super.finalized
|
||||||
|
|
||||||
|
if let colorBox = finalized as? ColorBox {
|
||||||
|
colorBox.innerList = colorBox.innerList?.finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ colorBox: ColorBox) {
|
||||||
|
self.colorString = colorBox.colorString
|
||||||
|
self.innerList = colorBox.innerList.map { AtomList($0) }
|
||||||
|
|
||||||
|
super.init(colorBox)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(colorString: String = "", innerList: AtomList? = nil) {
|
||||||
|
self.colorString = colorString
|
||||||
|
self.innerList = innerList
|
||||||
|
super.init(type: .colorBox)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
Sources/SwiftUIMath/Internal/Model/Atoms/Fraction.swift
Normal file
71
Sources/SwiftUIMath/Internal/Model/Atoms/Fraction.swift
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Math {
|
||||||
|
final class Fraction: Atom {
|
||||||
|
var hasRule: Bool
|
||||||
|
var leftDelimiter: String
|
||||||
|
var rightDelimiter: String
|
||||||
|
var numerator: AtomList?
|
||||||
|
var denominator: AtomList?
|
||||||
|
|
||||||
|
var isContinuedFraction: Bool = false
|
||||||
|
var alignment: String // "l", "r", "c" for left, right, center
|
||||||
|
|
||||||
|
override var description: String {
|
||||||
|
[
|
||||||
|
hasRule ? "\\frac" : "\\atop",
|
||||||
|
leftDelimiter.isEmpty ? nil : "[\(leftDelimiter)]",
|
||||||
|
rightDelimiter.isEmpty ? nil : "[\(rightDelimiter)]",
|
||||||
|
"{\(numerator?.description ?? "placeholder")}",
|
||||||
|
"{\(denominator?.description ?? "placeholder")}",
|
||||||
|
superscript.map { "^{\($0)}" },
|
||||||
|
`subscript`.map { "_{\($0)}" },
|
||||||
|
]
|
||||||
|
.compactMap(\.self)
|
||||||
|
.joined()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var finalized: Math.Atom {
|
||||||
|
let finalized = super.finalized
|
||||||
|
|
||||||
|
if let fraction = finalized as? Fraction {
|
||||||
|
fraction.numerator = fraction.numerator?.finalized
|
||||||
|
fraction.denominator = fraction.denominator?.finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ fraction: Fraction) {
|
||||||
|
self.hasRule = fraction.hasRule
|
||||||
|
self.leftDelimiter = fraction.leftDelimiter
|
||||||
|
self.rightDelimiter = fraction.rightDelimiter
|
||||||
|
self.numerator = fraction.numerator.map { AtomList($0) }
|
||||||
|
self.denominator = fraction.denominator.map { AtomList($0) }
|
||||||
|
self.isContinuedFraction = fraction.isContinuedFraction
|
||||||
|
self.alignment = fraction.alignment
|
||||||
|
|
||||||
|
super.init(fraction)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
hasRule: Bool = true,
|
||||||
|
leftDelimiter: String = "",
|
||||||
|
rightDelimiter: String = "",
|
||||||
|
numerator: AtomList? = nil,
|
||||||
|
denominator: AtomList? = nil,
|
||||||
|
isContinuedFraction: Bool = false,
|
||||||
|
alignment: String = "c"
|
||||||
|
) {
|
||||||
|
self.hasRule = hasRule
|
||||||
|
self.leftDelimiter = leftDelimiter
|
||||||
|
self.rightDelimiter = rightDelimiter
|
||||||
|
self.numerator = numerator
|
||||||
|
self.denominator = denominator
|
||||||
|
self.isContinuedFraction = isContinuedFraction
|
||||||
|
self.alignment = alignment
|
||||||
|
|
||||||
|
super.init(type: .fraction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
Sources/SwiftUIMath/Internal/Model/Atoms/Inner.swift
Normal file
68
Sources/SwiftUIMath/Internal/Model/Atoms/Inner.swift
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Math {
|
||||||
|
final class Inner: Atom {
|
||||||
|
var innerList: AtomList?
|
||||||
|
|
||||||
|
var leftBoundary: Atom? {
|
||||||
|
didSet {
|
||||||
|
if let leftBoundary, leftBoundary.type != .boundary {
|
||||||
|
assertionFailure("Left boundary must be of type 'boundary'")
|
||||||
|
self.leftBoundary = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rightBoundary: Atom? {
|
||||||
|
didSet {
|
||||||
|
if let rightBoundary, rightBoundary.type != .boundary {
|
||||||
|
assertionFailure("Right boundary must be of type 'boundary'")
|
||||||
|
self.rightBoundary = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var description: String {
|
||||||
|
[
|
||||||
|
"\\inner",
|
||||||
|
leftBoundary.map { "[\($0.nucleus)]" },
|
||||||
|
innerList.map { "{\($0)}" },
|
||||||
|
rightBoundary.map { "[\($0.nucleus)]" },
|
||||||
|
superscript.map { "^{\($0)}" },
|
||||||
|
`subscript`.map { "_{\($0)}" },
|
||||||
|
]
|
||||||
|
.compactMap(\.self)
|
||||||
|
.joined()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var finalized: Math.Atom {
|
||||||
|
let finalized = super.finalized
|
||||||
|
|
||||||
|
if let inner = finalized as? Inner {
|
||||||
|
inner.innerList = inner.innerList?.finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ inner: Inner) {
|
||||||
|
self.innerList = inner.innerList.map { AtomList($0) }
|
||||||
|
self.leftBoundary = inner.leftBoundary.map { $0.copy() }
|
||||||
|
self.rightBoundary = inner.rightBoundary.map { $0.copy() }
|
||||||
|
|
||||||
|
super.init(inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
innerList: AtomList? = nil,
|
||||||
|
leftBoundary: Atom? = nil,
|
||||||
|
rightBoundary: Atom? = nil
|
||||||
|
) {
|
||||||
|
self.innerList = innerList
|
||||||
|
self.leftBoundary = leftBoundary
|
||||||
|
self.rightBoundary = rightBoundary
|
||||||
|
|
||||||
|
super.init(type: .inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Sources/SwiftUIMath/Internal/Model/Atoms/LargeOperator.swift
Normal file
17
Sources/SwiftUIMath/Internal/Model/Atoms/LargeOperator.swift
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Math {
|
||||||
|
final class LargeOperator: Atom {
|
||||||
|
var limits: Bool
|
||||||
|
|
||||||
|
init(_ largeOperator: LargeOperator) {
|
||||||
|
self.limits = largeOperator.limits
|
||||||
|
super.init(largeOperator)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(limits: Bool = false) {
|
||||||
|
self.limits = limits
|
||||||
|
super.init(type: .largeOperator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
Sources/SwiftUIMath/Internal/Model/Atoms/Overline.swift
Normal file
27
Sources/SwiftUIMath/Internal/Model/Atoms/Overline.swift
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Math {
|
||||||
|
final class Overline: Atom {
|
||||||
|
var innerList: AtomList?
|
||||||
|
|
||||||
|
override var finalized: Math.Atom {
|
||||||
|
let finalized = super.finalized
|
||||||
|
|
||||||
|
if let overline = finalized as? Overline {
|
||||||
|
overline.innerList = overline.innerList?.finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ overline: Overline) {
|
||||||
|
self.innerList = overline.innerList.map { AtomList($0) }
|
||||||
|
super.init(overline)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(innerList: AtomList? = nil) {
|
||||||
|
self.innerList = innerList
|
||||||
|
super.init(type: .overline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
Sources/SwiftUIMath/Internal/Model/Atoms/Radical.swift
Normal file
45
Sources/SwiftUIMath/Internal/Model/Atoms/Radical.swift
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Math {
|
||||||
|
final class Radical: Atom {
|
||||||
|
var radicand: AtomList?
|
||||||
|
var degree: AtomList?
|
||||||
|
|
||||||
|
override var description: String {
|
||||||
|
[
|
||||||
|
"\\sqrt",
|
||||||
|
degree.map { "[\($0)]" },
|
||||||
|
"{\(radicand?.description ?? "placeholder")}",
|
||||||
|
superscript.map { "^{\($0)}" },
|
||||||
|
`subscript`.map { "_{\($0)}" },
|
||||||
|
]
|
||||||
|
.compactMap(\.self)
|
||||||
|
.joined()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var finalized: Math.Atom {
|
||||||
|
let finalized = super.finalized
|
||||||
|
|
||||||
|
if let radical = finalized as? Radical {
|
||||||
|
radical.radicand = radical.radicand?.finalized
|
||||||
|
radical.degree = radical.degree?.finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ radical: Radical) {
|
||||||
|
self.radicand = radical.radicand.map { AtomList($0) }
|
||||||
|
self.degree = radical.degree.map { AtomList($0) }
|
||||||
|
|
||||||
|
super.init(radical)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(radicand: AtomList? = nil, degree: AtomList? = nil) {
|
||||||
|
self.radicand = radicand
|
||||||
|
self.degree = degree
|
||||||
|
|
||||||
|
super.init(type: .radical)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Sources/SwiftUIMath/Internal/Model/Atoms/Space.swift
Normal file
17
Sources/SwiftUIMath/Internal/Model/Atoms/Space.swift
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Math {
|
||||||
|
final class Space: Atom {
|
||||||
|
var amount: CGFloat
|
||||||
|
|
||||||
|
init(_ space: Space) {
|
||||||
|
self.amount = space.amount
|
||||||
|
super.init(space)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(amount: CGFloat = 0) {
|
||||||
|
self.amount = amount
|
||||||
|
super.init(type: .space)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Sources/SwiftUIMath/Internal/Model/Atoms/Style.swift
Normal file
41
Sources/SwiftUIMath/Internal/Model/Atoms/Style.swift
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Math {
|
||||||
|
final class Style: Atom {
|
||||||
|
enum Level: Int {
|
||||||
|
case display
|
||||||
|
case text
|
||||||
|
case script
|
||||||
|
case scriptOfScript
|
||||||
|
|
||||||
|
var isScript: Bool {
|
||||||
|
switch self {
|
||||||
|
case .script, .scriptOfScript:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isNotScript: Bool {
|
||||||
|
!isScript
|
||||||
|
}
|
||||||
|
|
||||||
|
func next() -> Level {
|
||||||
|
Level(rawValue: rawValue + 1) ?? .display
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var level: Level
|
||||||
|
|
||||||
|
init(_ style: Style) {
|
||||||
|
self.level = style.level
|
||||||
|
super.init(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(level: Level = .display) {
|
||||||
|
self.level = level
|
||||||
|
super.init(type: .style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
101
Sources/SwiftUIMath/Internal/Model/Atoms/Table.swift
Normal file
101
Sources/SwiftUIMath/Internal/Model/Atoms/Table.swift
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Math {
|
||||||
|
final class Table: Atom {
|
||||||
|
enum ColumnAlignment {
|
||||||
|
case left
|
||||||
|
case center
|
||||||
|
case right
|
||||||
|
}
|
||||||
|
|
||||||
|
var alignments: [ColumnAlignment]
|
||||||
|
var cells: [[AtomList]]
|
||||||
|
var environment: String
|
||||||
|
var interColumnSpacing: CGFloat
|
||||||
|
var interRowAdditionalSpacing: CGFloat
|
||||||
|
|
||||||
|
override var finalized: Math.Atom {
|
||||||
|
let finalized = super.finalized
|
||||||
|
|
||||||
|
if let table = finalized as? Table {
|
||||||
|
table.cells = table.cells.map { row in
|
||||||
|
row.map { $0.finalized }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ table: Table) {
|
||||||
|
self.alignments = table.alignments
|
||||||
|
self.cells = table.cells.map { row in
|
||||||
|
row.map { AtomList($0) }
|
||||||
|
}
|
||||||
|
self.environment = table.environment
|
||||||
|
self.interColumnSpacing = table.interColumnSpacing
|
||||||
|
self.interRowAdditionalSpacing = table.interRowAdditionalSpacing
|
||||||
|
|
||||||
|
super.init(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
alignments: [ColumnAlignment] = [],
|
||||||
|
cells: [[AtomList]] = [],
|
||||||
|
environment: String = "",
|
||||||
|
interColumnSpacing: CGFloat = 0,
|
||||||
|
interRowAdditionalSpacing: CGFloat = 0
|
||||||
|
) {
|
||||||
|
self.alignments = alignments
|
||||||
|
self.cells = cells
|
||||||
|
self.environment = environment
|
||||||
|
self.interColumnSpacing = interColumnSpacing
|
||||||
|
self.interRowAdditionalSpacing = interRowAdditionalSpacing
|
||||||
|
super.init(type: .table)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCell(_ cell: AtomList, forRow row: Int, column: Int) {
|
||||||
|
if cells.count <= row {
|
||||||
|
for _ in cells.count...row {
|
||||||
|
cells.append([])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cells[row].count <= column {
|
||||||
|
for _ in cells[row].count...column {
|
||||||
|
cells[row].append(AtomList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cells[row][column] = cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func setAlignment(_ alignment: ColumnAlignment, forColumn column: Int) {
|
||||||
|
if alignments.count <= column {
|
||||||
|
for _ in alignments.count...column {
|
||||||
|
alignments.append(.center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alignments[column] = alignment
|
||||||
|
}
|
||||||
|
|
||||||
|
func alignment(forColumn column: Int) -> ColumnAlignment {
|
||||||
|
if alignments.count <= column {
|
||||||
|
return .center
|
||||||
|
}
|
||||||
|
return alignments[column]
|
||||||
|
}
|
||||||
|
|
||||||
|
var numberOfColumns: Int {
|
||||||
|
var count = 0
|
||||||
|
for row in cells {
|
||||||
|
count = max(count, row.count)
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
var numberOfRows: Int {
|
||||||
|
cells.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Sources/SwiftUIMath/Internal/Model/Atoms/TextColor.swift
Normal file
41
Sources/SwiftUIMath/Internal/Model/Atoms/TextColor.swift
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Math {
|
||||||
|
final class TextColor: Atom {
|
||||||
|
var colorString: String
|
||||||
|
var innerList: AtomList?
|
||||||
|
|
||||||
|
override var description: String {
|
||||||
|
[
|
||||||
|
"\\textcolor",
|
||||||
|
"{\(colorString)}",
|
||||||
|
innerList.map { "{\($0)}" },
|
||||||
|
]
|
||||||
|
.compactMap(\.self)
|
||||||
|
.joined()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var finalized: Math.Atom {
|
||||||
|
let finalized = super.finalized
|
||||||
|
|
||||||
|
if let textColor = finalized as? TextColor {
|
||||||
|
textColor.innerList = textColor.innerList?.finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ textColor: TextColor) {
|
||||||
|
self.colorString = textColor.colorString
|
||||||
|
self.innerList = textColor.innerList.map { AtomList($0) }
|
||||||
|
|
||||||
|
super.init(textColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(colorString: String = "", innerList: AtomList? = nil) {
|
||||||
|
self.colorString = colorString
|
||||||
|
self.innerList = innerList
|
||||||
|
super.init(type: .textColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
Sources/SwiftUIMath/Internal/Model/Atoms/Underline.swift
Normal file
27
Sources/SwiftUIMath/Internal/Model/Atoms/Underline.swift
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Math {
|
||||||
|
final class Underline: Atom {
|
||||||
|
var innerList: AtomList?
|
||||||
|
|
||||||
|
override var finalized: Math.Atom {
|
||||||
|
let finalized = super.finalized
|
||||||
|
|
||||||
|
if let underline = finalized as? Underline {
|
||||||
|
underline.innerList = underline.innerList?.finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalized
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ underline: Underline) {
|
||||||
|
self.innerList = underline.innerList.map { AtomList($0) }
|
||||||
|
super.init(underline)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(innerList: AtomList? = nil) {
|
||||||
|
self.innerList = innerList
|
||||||
|
super.init(type: .underline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user