Refactor model

This commit is contained in:
Guille Gonzalez
2026-01-01 12:41:56 +01:00
parent fbfc1d0ecf
commit e66eeb4564
19 changed files with 1845 additions and 12 deletions

View File

@@ -18,9 +18,9 @@ extension Math {
private let font: Font
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.unitsPerEm = unitsPerEm
self.table = table

View File

@@ -8,7 +8,7 @@ extension Math {
private struct Cache {
var graphicsFonts: [Font.Name: CGFont] = [:]
var tables: [Font.Name: Table] = [:]
var tables: [Font.Name: FontTable] = [:]
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
if let table = cache.tables[name] {
return table
@@ -67,8 +67,8 @@ extension Math {
private func registerGraphicsFont(
named name: Font.Name,
cache: inout Cache
) -> (CGFont, Table)? {
guard let graphicsFont = CGFont.named(name), let table = Table.named(name) else {
) -> (CGFont, FontTable)? {
guard let graphicsFont = CGFont.named(name), let table = FontTable.named(name) else {
return nil
}
@@ -99,8 +99,8 @@ extension CGFont {
}
}
extension Math.Table {
fileprivate static func named(_ name: Math.Font.Name) -> Math.Table? {
extension Math.FontTable {
fileprivate static func named(_ name: Math.Font.Name) -> Math.FontTable? {
guard
let bundleURL = Bundle.module.url(forResource: "mathFonts", withExtension: "bundle"),
let url = Bundle(url: bundleURL)?.url(forResource: name.rawValue, withExtension: "plist"),
@@ -109,6 +109,6 @@ extension Math.Table {
return nil
}
return try? PropertyListDecoder().decode(Math.Table.self, from: data)
return try? PropertyListDecoder().decode(Math.FontTable.self, from: data)
}
}

View File

@@ -1,7 +1,7 @@
import Foundation
extension Math {
struct Table: Codable, Sendable {
struct FontTable: Codable, Sendable {
struct Assembly: Codable, Sendable {
struct Part: Codable, Sendable {
let advance: Int

View File

@@ -19,8 +19,8 @@ final class ReadWriteLockIsolated<Value>: @unchecked Sendable {
}
}
func withValue<T: Sendable>(
_ operation: @Sendable (inout Value) throws -> T
func withValue<T>(
_ operation: (inout Value) throws -> T
) rethrows -> T {
try self.lock.sync {
var value = self._value

View 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
}
}
}
}

View 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
}
}

View 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)
}
}
}

View 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)
}
}
}

View 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)
}
}
}

View 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)
}
}
}

View 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)
}
}
}

View 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)
}
}
}

View 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)
}
}
}

View 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)
}
}
}

View 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)
}
}
}

View 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)
}
}
}

View 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
}
}
}

View 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)
}
}
}

View 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)
}
}
}