From e66eeb45648e13791473594f448f149104136f4c Mon Sep 17 00:00:00 2001 From: Guille Gonzalez Date: Thu, 1 Jan 2026 12:41:56 +0100 Subject: [PATCH] Refactor model --- .../Internal/Font/FontMetrics.swift | 4 +- .../Internal/Font/FontRegistry.swift | 14 +- .../Font/{Table.swift => FontTable.swift} | 2 +- .../Helpers/ReadWriteLockIsolated.swift | 4 +- .../Internal/Model/AtomFactory.swift | 858 ++++++++++++++++++ .../SwiftUIMath/Internal/Model/AtomList.swift | 411 +++++++++ .../Internal/Model/Atoms/Accent.swift | 27 + .../Internal/Model/Atoms/Color.swift | 41 + .../Internal/Model/Atoms/ColorBox.swift | 41 + .../Internal/Model/Atoms/Fraction.swift | 71 ++ .../Internal/Model/Atoms/Inner.swift | 68 ++ .../Internal/Model/Atoms/LargeOperator.swift | 17 + .../Internal/Model/Atoms/Overline.swift | 27 + .../Internal/Model/Atoms/Radical.swift | 45 + .../Internal/Model/Atoms/Space.swift | 17 + .../Internal/Model/Atoms/Style.swift | 41 + .../Internal/Model/Atoms/Table.swift | 101 +++ .../Internal/Model/Atoms/TextColor.swift | 41 + .../Internal/Model/Atoms/Underline.swift | 27 + 19 files changed, 1845 insertions(+), 12 deletions(-) rename Sources/SwiftUIMath/Internal/Font/{Table.swift => FontTable.swift} (95%) create mode 100644 Sources/SwiftUIMath/Internal/Model/AtomFactory.swift create mode 100644 Sources/SwiftUIMath/Internal/Model/AtomList.swift create mode 100644 Sources/SwiftUIMath/Internal/Model/Atoms/Accent.swift create mode 100644 Sources/SwiftUIMath/Internal/Model/Atoms/Color.swift create mode 100644 Sources/SwiftUIMath/Internal/Model/Atoms/ColorBox.swift create mode 100644 Sources/SwiftUIMath/Internal/Model/Atoms/Fraction.swift create mode 100644 Sources/SwiftUIMath/Internal/Model/Atoms/Inner.swift create mode 100644 Sources/SwiftUIMath/Internal/Model/Atoms/LargeOperator.swift create mode 100644 Sources/SwiftUIMath/Internal/Model/Atoms/Overline.swift create mode 100644 Sources/SwiftUIMath/Internal/Model/Atoms/Radical.swift create mode 100644 Sources/SwiftUIMath/Internal/Model/Atoms/Space.swift create mode 100644 Sources/SwiftUIMath/Internal/Model/Atoms/Style.swift create mode 100644 Sources/SwiftUIMath/Internal/Model/Atoms/Table.swift create mode 100644 Sources/SwiftUIMath/Internal/Model/Atoms/TextColor.swift create mode 100644 Sources/SwiftUIMath/Internal/Model/Atoms/Underline.swift diff --git a/Sources/SwiftUIMath/Internal/Font/FontMetrics.swift b/Sources/SwiftUIMath/Internal/Font/FontMetrics.swift index 149bf6c..cfd6d78 100644 --- a/Sources/SwiftUIMath/Internal/Font/FontMetrics.swift +++ b/Sources/SwiftUIMath/Internal/Font/FontMetrics.swift @@ -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 diff --git a/Sources/SwiftUIMath/Internal/Font/FontRegistry.swift b/Sources/SwiftUIMath/Internal/Font/FontRegistry.swift index 758da31..c038b79 100644 --- a/Sources/SwiftUIMath/Internal/Font/FontRegistry.swift +++ b/Sources/SwiftUIMath/Internal/Font/FontRegistry.swift @@ -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, 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) } } diff --git a/Sources/SwiftUIMath/Internal/Font/Table.swift b/Sources/SwiftUIMath/Internal/Font/FontTable.swift similarity index 95% rename from Sources/SwiftUIMath/Internal/Font/Table.swift rename to Sources/SwiftUIMath/Internal/Font/FontTable.swift index f0c70f2..2a74bed 100644 --- a/Sources/SwiftUIMath/Internal/Font/Table.swift +++ b/Sources/SwiftUIMath/Internal/Font/FontTable.swift @@ -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 diff --git a/Sources/SwiftUIMath/Internal/Helpers/ReadWriteLockIsolated.swift b/Sources/SwiftUIMath/Internal/Helpers/ReadWriteLockIsolated.swift index 9a9df95..83ab215 100644 --- a/Sources/SwiftUIMath/Internal/Helpers/ReadWriteLockIsolated.swift +++ b/Sources/SwiftUIMath/Internal/Helpers/ReadWriteLockIsolated.swift @@ -19,8 +19,8 @@ final class ReadWriteLockIsolated: @unchecked Sendable { } } - func withValue( - _ operation: @Sendable (inout Value) throws -> T + func withValue( + _ operation: (inout Value) throws -> T ) rethrows -> T { try self.lock.sync { var value = self._value diff --git a/Sources/SwiftUIMath/Internal/Model/AtomFactory.swift b/Sources/SwiftUIMath/Internal/Model/AtomFactory.swift new file mode 100644 index 0000000..10b5d0a --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Model/AtomFactory.swift @@ -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..= 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.. [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 + } + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Model/AtomList.swift b/Sources/SwiftUIMath/Internal/Model/AtomList.swift new file mode 100644 index 0000000..2b8983e --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Model/AtomList.swift @@ -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 + } +} diff --git a/Sources/SwiftUIMath/Internal/Model/Atoms/Accent.swift b/Sources/SwiftUIMath/Internal/Model/Atoms/Accent.swift new file mode 100644 index 0000000..1f07af9 --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Model/Atoms/Accent.swift @@ -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) + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Model/Atoms/Color.swift b/Sources/SwiftUIMath/Internal/Model/Atoms/Color.swift new file mode 100644 index 0000000..642ba72 --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Model/Atoms/Color.swift @@ -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) + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Model/Atoms/ColorBox.swift b/Sources/SwiftUIMath/Internal/Model/Atoms/ColorBox.swift new file mode 100644 index 0000000..cbfcffd --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Model/Atoms/ColorBox.swift @@ -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) + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Model/Atoms/Fraction.swift b/Sources/SwiftUIMath/Internal/Model/Atoms/Fraction.swift new file mode 100644 index 0000000..adaf12f --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Model/Atoms/Fraction.swift @@ -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) + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Model/Atoms/Inner.swift b/Sources/SwiftUIMath/Internal/Model/Atoms/Inner.swift new file mode 100644 index 0000000..af52e07 --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Model/Atoms/Inner.swift @@ -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) + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Model/Atoms/LargeOperator.swift b/Sources/SwiftUIMath/Internal/Model/Atoms/LargeOperator.swift new file mode 100644 index 0000000..bbba3ad --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Model/Atoms/LargeOperator.swift @@ -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) + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Model/Atoms/Overline.swift b/Sources/SwiftUIMath/Internal/Model/Atoms/Overline.swift new file mode 100644 index 0000000..76d21fe --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Model/Atoms/Overline.swift @@ -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) + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Model/Atoms/Radical.swift b/Sources/SwiftUIMath/Internal/Model/Atoms/Radical.swift new file mode 100644 index 0000000..185ff7d --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Model/Atoms/Radical.swift @@ -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) + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Model/Atoms/Space.swift b/Sources/SwiftUIMath/Internal/Model/Atoms/Space.swift new file mode 100644 index 0000000..63128c0 --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Model/Atoms/Space.swift @@ -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) + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Model/Atoms/Style.swift b/Sources/SwiftUIMath/Internal/Model/Atoms/Style.swift new file mode 100644 index 0000000..516a659 --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Model/Atoms/Style.swift @@ -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) + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Model/Atoms/Table.swift b/Sources/SwiftUIMath/Internal/Model/Atoms/Table.swift new file mode 100644 index 0000000..5740087 --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Model/Atoms/Table.swift @@ -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 + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Model/Atoms/TextColor.swift b/Sources/SwiftUIMath/Internal/Model/Atoms/TextColor.swift new file mode 100644 index 0000000..0b107f5 --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Model/Atoms/TextColor.swift @@ -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) + } + } +} diff --git a/Sources/SwiftUIMath/Internal/Model/Atoms/Underline.swift b/Sources/SwiftUIMath/Internal/Model/Atoms/Underline.swift new file mode 100644 index 0000000..c30d42f --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Model/Atoms/Underline.swift @@ -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) + } + } +}