412 lines
11 KiB
Swift
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
|
|
}
|
|
}
|