Files
swiftui-math/Sources/SwiftUIMath/Internal/Model/AtomList.swift
Guille Gonzalez e66eeb4564 Refactor model
2026-01-02 15:24:11 +01:00

412 lines
11 KiB
Swift

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