Re-arrange source code and tests
This commit is contained in:
411
Sources/SwiftUIMath/Internal/Syntax/AtomList.swift
Normal file
411
Sources/SwiftUIMath/Internal/Syntax/AtomList.swift
Normal file
@@ -0,0 +1,411 @@
|
||||
import Foundation
|
||||
|
||||
extension Math {
|
||||
enum AtomType: Int {
|
||||
// A number or text in ordinary format - Ord in TeX
|
||||
case ordinary = 1
|
||||
// A number - Does not exist in TeX
|
||||
case number
|
||||
// A variable (i.e. text in italic format) - Does not exist in TeX
|
||||
case variable
|
||||
// A large operator such as (sin/cos, integral etc.) - Op in TeX
|
||||
case largeOperator
|
||||
// A binary operator - Bin in TeX
|
||||
case binaryOperator
|
||||
// A unary operator - Does not exist in TeX.
|
||||
case unaryOperator
|
||||
// A relation, e.g. = > < etc. - Rel in TeX
|
||||
case relation
|
||||
// Open brackets - Open in TeX
|
||||
case open
|
||||
// Close brackets - Close in TeX
|
||||
case close
|
||||
// A fraction e.g 1/2 - generalized fraction node in TeX
|
||||
case fraction
|
||||
// A radical operator e.g. sqrt(2)
|
||||
case radical
|
||||
// Punctuation such as , - Punct in TeX
|
||||
case punctuation
|
||||
// A placeholder square for future input. Does not exist in TeX
|
||||
case placeholder
|
||||
// An inner atom, i.e. an embedded math list - Inner in TeX
|
||||
case inner
|
||||
// An underlined atom - Under in TeX
|
||||
case underline
|
||||
// An overlined atom - Over in TeX
|
||||
case overline
|
||||
// An accented atom - Accent in TeX
|
||||
case accent
|
||||
|
||||
// Atoms after this point do not support subscripts or superscripts
|
||||
|
||||
// A left atom - Left & Right in TeX. We don't need two since we track boundaries separately.
|
||||
case boundary = 101
|
||||
|
||||
// Atoms after this are non-math TeX nodes that are still useful in math mode. They do not have
|
||||
// the usual structure.
|
||||
|
||||
// Spacing between math atoms. This denotes both glue and kern for TeX. We do not
|
||||
// distinguish between glue and kern.
|
||||
case space = 201
|
||||
|
||||
// Denotes style changes during rendering.
|
||||
case style
|
||||
case color
|
||||
case textColor
|
||||
case colorBox
|
||||
|
||||
// Atoms after this point are not part of TeX and do not have the usual structure.
|
||||
|
||||
// An table atom. This atom does not exist in TeX. It is equivalent to the TeX command
|
||||
// halign which is handled outside of the TeX math rendering engine. We bring it into our
|
||||
// math typesetting to handle matrices and other tables.
|
||||
case table = 1001
|
||||
}
|
||||
}
|
||||
|
||||
extension Math.AtomType {
|
||||
var disallowsFollowingBinaryOperator: Bool {
|
||||
switch self {
|
||||
case .binaryOperator, .relation, .open, .punctuation, .largeOperator:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var allowsScripts: Bool {
|
||||
self < .boundary
|
||||
}
|
||||
}
|
||||
|
||||
extension Math.AtomType: Comparable {
|
||||
static func < (lhs: Math.AtomType, rhs: Math.AtomType) -> Bool {
|
||||
lhs.rawValue < rhs.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
extension Math.AtomType: CustomStringConvertible {
|
||||
var description: String {
|
||||
switch self {
|
||||
case .ordinary: return "Ordinary"
|
||||
case .number: return "Number"
|
||||
case .variable: return "Variable"
|
||||
case .largeOperator: return "Large Operator"
|
||||
case .binaryOperator: return "Binary Operator"
|
||||
case .unaryOperator: return "Unary Operator"
|
||||
case .relation: return "Relation"
|
||||
case .open: return "Open"
|
||||
case .close: return "Close"
|
||||
case .fraction: return "Fraction"
|
||||
case .radical: return "Radical"
|
||||
case .punctuation: return "Punctuation"
|
||||
case .placeholder: return "Placeholder"
|
||||
case .inner: return "Inner"
|
||||
case .underline: return "Underline"
|
||||
case .overline: return "Overline"
|
||||
case .accent: return "Accent"
|
||||
case .boundary: return "Boundary"
|
||||
case .space: return "Space"
|
||||
case .style: return "Style"
|
||||
case .color: return "Color"
|
||||
case .textColor: return "TextColor"
|
||||
case .colorBox: return "Colorbox"
|
||||
case .table: return "Table"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Math {
|
||||
class Atom: CustomStringConvertible {
|
||||
enum FontStyle: Int {
|
||||
case `default` = 0
|
||||
case roman
|
||||
case bold
|
||||
case caligraphic
|
||||
case typewriter
|
||||
case italic
|
||||
case sansSerif
|
||||
case fraktur
|
||||
case blackboard
|
||||
case boldItalic
|
||||
}
|
||||
|
||||
var description: String {
|
||||
[
|
||||
nucleus,
|
||||
superscript.map { "^{\($0)}" },
|
||||
`subscript`.map { "_{\($0)}" },
|
||||
]
|
||||
.compactMap(\.self)
|
||||
.joined()
|
||||
}
|
||||
|
||||
var type: AtomType
|
||||
var nucleus: String
|
||||
var indexRange: NSRange
|
||||
var fontStyle: FontStyle
|
||||
var fusedAtoms: [Atom]
|
||||
|
||||
var `subscript`: AtomList? {
|
||||
didSet {
|
||||
if `subscript` != nil, !allowsScripts {
|
||||
assertionFailure("Subscripts are not allowed for \(type)")
|
||||
`subscript` = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var superscript: AtomList? {
|
||||
didSet {
|
||||
if superscript != nil, !allowsScripts {
|
||||
assertionFailure("Superscripts are not allowed for \(type)")
|
||||
superscript = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var finalized: Atom {
|
||||
let finalized = copy()
|
||||
finalized.superscript = finalized.superscript?.finalized
|
||||
finalized.subscript = finalized.subscript?.finalized
|
||||
return finalized
|
||||
}
|
||||
|
||||
var string: String {
|
||||
description
|
||||
}
|
||||
|
||||
var allowsScripts: Bool {
|
||||
type.allowsScripts
|
||||
}
|
||||
|
||||
var disallowsFollowingBinaryOperator: Bool {
|
||||
type.disallowsFollowingBinaryOperator
|
||||
}
|
||||
|
||||
init(
|
||||
type: AtomType = .ordinary,
|
||||
nucleus: String = "",
|
||||
indexRange: NSRange = NSRange(),
|
||||
fontStyle: FontStyle = .default,
|
||||
fusedAtoms: [Atom] = [],
|
||||
subscript: AtomList? = nil,
|
||||
superscript: AtomList? = nil
|
||||
) {
|
||||
self.type = type
|
||||
self.nucleus = nucleus
|
||||
self.indexRange = indexRange
|
||||
self.fontStyle = fontStyle
|
||||
self.fusedAtoms = fusedAtoms
|
||||
self.subscript = `subscript`
|
||||
self.superscript = superscript
|
||||
}
|
||||
|
||||
init(_ other: Atom) {
|
||||
self.type = other.type
|
||||
self.nucleus = other.nucleus
|
||||
self.indexRange = other.indexRange
|
||||
self.fontStyle = other.fontStyle
|
||||
self.fusedAtoms = other.fusedAtoms
|
||||
self.subscript = other.`subscript`.map { AtomList($0) }
|
||||
self.superscript = other.superscript.map { AtomList($0) }
|
||||
}
|
||||
|
||||
convenience init(type: AtomType, value: String) {
|
||||
self.init(type: type, nucleus: type == .radical ? "" : value)
|
||||
}
|
||||
|
||||
func copy() -> Atom {
|
||||
switch type {
|
||||
case .fraction:
|
||||
return (self as? Fraction).map {
|
||||
Fraction($0)
|
||||
} ?? Fraction()
|
||||
case .radical:
|
||||
return (self as? Radical).map {
|
||||
Radical($0)
|
||||
} ?? Radical()
|
||||
case .largeOperator:
|
||||
return (self as? LargeOperator).map {
|
||||
LargeOperator($0)
|
||||
} ?? LargeOperator()
|
||||
case .inner:
|
||||
return (self as? Inner).map {
|
||||
Inner($0)
|
||||
} ?? Inner()
|
||||
case .overline:
|
||||
return (self as? Overline).map {
|
||||
Overline($0)
|
||||
} ?? Overline()
|
||||
case .underline:
|
||||
return (self as? Underline).map {
|
||||
Underline($0)
|
||||
} ?? Underline()
|
||||
case .accent:
|
||||
return (self as? Accent).map {
|
||||
Accent($0)
|
||||
} ?? Accent()
|
||||
case .space:
|
||||
return (self as? Space).map {
|
||||
Space($0)
|
||||
} ?? Space()
|
||||
case .style:
|
||||
return (self as? Style).map {
|
||||
Style($0)
|
||||
} ?? Style()
|
||||
case .color:
|
||||
return (self as? Color).map {
|
||||
Color($0)
|
||||
} ?? Color()
|
||||
case .textColor:
|
||||
return (self as? TextColor).map {
|
||||
TextColor($0)
|
||||
} ?? TextColor()
|
||||
case .colorBox:
|
||||
return (self as? ColorBox).map {
|
||||
ColorBox($0)
|
||||
} ?? ColorBox()
|
||||
case .table:
|
||||
return (self as? Table).map {
|
||||
Table($0)
|
||||
} ?? Table()
|
||||
default:
|
||||
return Atom(self)
|
||||
}
|
||||
}
|
||||
|
||||
func fuse(with atom: Atom) {
|
||||
guard `subscript` == nil, superscript == nil, type == atom.type else {
|
||||
assertionFailure("Cannot fuse \(self) with \(atom)")
|
||||
return
|
||||
}
|
||||
|
||||
if fusedAtoms.isEmpty {
|
||||
fusedAtoms.append(.init(self))
|
||||
}
|
||||
|
||||
if atom.fusedAtoms.isEmpty {
|
||||
fusedAtoms.append(atom)
|
||||
} else {
|
||||
fusedAtoms.append(contentsOf: atom.fusedAtoms)
|
||||
}
|
||||
|
||||
nucleus += atom.nucleus
|
||||
indexRange.length += atom.indexRange.length
|
||||
|
||||
self.superscript = atom.superscript
|
||||
self.`subscript` = atom.`subscript`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Math {
|
||||
final class AtomList {
|
||||
var atoms: [Atom]
|
||||
|
||||
var finalized: AtomList {
|
||||
let finalizedList = AtomList()
|
||||
|
||||
var previousAtom: Atom?
|
||||
|
||||
for atom in atoms {
|
||||
let finalizedAtom = atom.finalized
|
||||
|
||||
if atom.indexRange == NSRange() {
|
||||
let location = (previousAtom?.indexRange).map {
|
||||
$0.location + $0.length
|
||||
}
|
||||
finalizedAtom.indexRange = NSRange(location: location ?? 0, length: 1)
|
||||
}
|
||||
|
||||
switch finalizedAtom.type {
|
||||
case .binaryOperator where previousAtom.disallowsFollowingBinaryOperator:
|
||||
finalizedAtom.type = .unaryOperator
|
||||
case .relation, .punctuation, .close:
|
||||
if case .binaryOperator = previousAtom?.type {
|
||||
previousAtom?.type = .unaryOperator
|
||||
}
|
||||
case .number:
|
||||
if let previousAtom,
|
||||
case .number = previousAtom.type,
|
||||
previousAtom.`subscript` == nil,
|
||||
previousAtom.superscript == nil
|
||||
{
|
||||
previousAtom.fuse(with: finalizedAtom)
|
||||
continue // skip the current node, we are done here
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
finalizedList.append(finalizedAtom)
|
||||
previousAtom = finalizedAtom
|
||||
}
|
||||
|
||||
if let previousAtom, case .binaryOperator = previousAtom.type {
|
||||
previousAtom.type = .unaryOperator
|
||||
}
|
||||
|
||||
return finalizedList
|
||||
}
|
||||
|
||||
init(_ list: AtomList) {
|
||||
self.atoms = list.atoms.map {
|
||||
$0.copy()
|
||||
}
|
||||
}
|
||||
|
||||
convenience init(atom: Atom) {
|
||||
self.init(atoms: [atom])
|
||||
}
|
||||
|
||||
init(atoms: [Atom] = []) {
|
||||
self.atoms = atoms
|
||||
}
|
||||
|
||||
func append(_ atom: Atom) {
|
||||
guard canAdd(atom) else {
|
||||
assertionFailure("Can't append atom of type \(atom.type)")
|
||||
return
|
||||
}
|
||||
atoms.append(atom)
|
||||
}
|
||||
|
||||
func insert(_ atom: Atom, at index: Int) {
|
||||
guard atoms.indices.contains(index) || index == atoms.endIndex else {
|
||||
return
|
||||
}
|
||||
guard canAdd(atom) else {
|
||||
assertionFailure("Can't insert atom of type \(atom.type)")
|
||||
return
|
||||
}
|
||||
atoms.insert(atom, at: index)
|
||||
}
|
||||
|
||||
func append(contentsOf list: AtomList) {
|
||||
atoms.append(contentsOf: list.atoms)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Math.AtomList: CustomStringConvertible {
|
||||
var description: String {
|
||||
atoms.description
|
||||
}
|
||||
}
|
||||
|
||||
extension Math.AtomList {
|
||||
private func canAdd(_ atom: Math.Atom) -> Bool {
|
||||
atom.type != .boundary
|
||||
}
|
||||
}
|
||||
|
||||
extension Optional where Wrapped: Math.Atom {
|
||||
var disallowsFollowingBinaryOperator: Bool {
|
||||
guard case .some(let wrapped) = self else {
|
||||
return true
|
||||
}
|
||||
return wrapped.disallowsFollowingBinaryOperator
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user