Renamed the package.

This commit is contained in:
Michael Griebling
2023-01-18 11:42:38 -05:00
parent b4ea730033
commit c129258845
27 changed files with 10 additions and 11 deletions

View File

@@ -0,0 +1,33 @@
//
// MTBezierPath.swift
// MathRenderSwift
//
// Created by Mike Griebling on 2022-12-31.
//
import Foundation
#if os(macOS)
extension MTBezierPath {
func addLine(to point: CGPoint) {
self.line(to: point)
}
}
extension MTView {
var backgroundColor:MTColor? {
get {
MTColor(cgColor: self.layer?.backgroundColor ?? MTColor.clear.cgColor)
}
set {
self.layer?.backgroundColor = MTColor.clear.cgColor
self.wantsLayer = true
}
}
}
#endif

View File

@@ -0,0 +1,25 @@
//
// MTColor.swift
// MathRenderSwift
//
// Created by Mike Griebling on 2022-12-31.
//
import Foundation
extension MTColor {
static func color(fromHexString hexString:String) -> MTColor? {
if hexString.isEmpty { return nil }
if !hexString.hasPrefix("#") { return nil }
var rgbValue = UInt64(0)
let scanner = Scanner(string: hexString)
scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#")
scanner.scanHexInt64(&rgbValue)
return MTColor(red: CGFloat((rgbValue & 0xFF0000) >> 16)/255.0,
green: CGFloat((rgbValue & 0xFF00) >> 8)/255.0,
blue: CGFloat((rgbValue & 0xFF))/255.0, alpha: 1.0)
}
}

View File

@@ -0,0 +1,37 @@
//
// MTConfig.swift
// MathRenderSwift
//
// Created by Mike Griebling on 2023-01-01.
//
import Foundation
#if os(iOS)
import UIKit
public typealias MTView = UIView
public typealias MTColor = UIColor
public typealias MTBezierPath = UIBezierPath
public typealias MTLabel = UILabel
public typealias MTEdgeInsets = UIEdgeInsets
public typealias MTRect = CGRect
let MTEdgeInsetsZero = UIEdgeInsets.zero
func MTGraphicsGetCurrentContext() -> CGContext? { UIGraphicsGetCurrentContext() }
#else
import AppKit
public typealias MTView = NSView
public typealias MTColor = NSColor
public typealias MTBezierPath = NSBezierPath
public typealias MTEdgeInsets = NSEdgeInsets
public typealias MTRect = NSRect
let MTEdgeInsetsZero = NSEdgeInsets.init(top: 0, left: 0, bottom: 0, right: 0)
func MTGraphicsGetCurrentContext() -> CGContext? { NSGraphicsContext.current?.cgContext }
#endif

View File

@@ -0,0 +1,74 @@
//
// MTFont.swift
// MathRenderSwift
//
// Created by Mike Griebling on 2022-12-31.
//
import Foundation
import CoreGraphics
import CoreText
//
// Created by Kostub Deshmukh on 5/18/16.
// Modified by Michael Griebling on 17 Jan 2023.
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
public class MTFont {
var defaultCGFont: CGFont!
var ctFont: CTFont!
var mathTable: MTFontMathTable?
var rawMathTable: NSDictionary?
init() {}
convenience init(fontWithName name: String, size:CGFloat) {
// CTFontCreateWithName does not load the complete math font, it only has about half the glyphs of the full math font.
// In particular it does not have the math italic characters which breaks our variable rendering.
// So we first load a CGFont from the file and then convert it to a CTFont.
self.init()
print("Loading font \(name)")
let bundle = MTFont.fontBundle
let fontPath = bundle.path(forResource: name, ofType: "otf")
let fontDataProvider = CGDataProvider(filename: fontPath!)
self.defaultCGFont = CGFont(fontDataProvider!)!
print("Num glyphs: \(self.defaultCGFont.numberOfGlyphs)")
self.ctFont = CTFontCreateWithGraphicsFont(self.defaultCGFont, size, nil, nil);
print("Loading associated .plist")
let mathTablePlist = bundle.url(forResource:name, withExtension:"plist")
self.rawMathTable = NSDictionary(contentsOf: mathTablePlist!)
self.mathTable = MTFontMathTable(withFont:self, mathTable:rawMathTable!)
}
static var fontBundle:Bundle {
// Uses bundle for class so that this can be access by the unit tests.
Bundle(url: Bundle.module.url(forResource: "mathFonts", withExtension: "bundle")!)!
}
func copy(withSize size: CGFloat) -> MTFont {
let newFont = MTFont()
newFont.defaultCGFont = self.defaultCGFont
newFont.ctFont = CTFontCreateWithGraphicsFont(self.defaultCGFont, size, nil, nil)
newFont.rawMathTable = self.rawMathTable
newFont.mathTable = MTFontMathTable(withFont: newFont, mathTable: newFont.rawMathTable!)
return newFont
}
func get(nameForGlyph glyph:CGGlyph) -> String {
let name = defaultCGFont.name(for: glyph) as? String
return name ?? ""
}
func get(glyphWithName name:String) -> CGGlyph {
defaultCGFont.getGlyphWithGlyphName(name: name as CFString)
}
var fontSize:CGFloat { CTFontGetSize(self.ctFont) }
}

View File

@@ -0,0 +1,53 @@
//
// MTFontManager.swift
// MathRenderSwift
//
// Created by Mike Griebling on 2022-12-31.
//
import Foundation
public class MTFontManager {
static var manager:MTFontManager? = nil
let kDefaultFontSize = CGFloat(20)
static var fontManager : MTFontManager {
if manager == nil {
manager = MTFontManager()
}
return manager!
}
var nameToFontMap = [String: MTFont]()
public func font(withName name:String, size:CGFloat) -> MTFont? {
var f = self.nameToFontMap[name]
if f == nil {
f = MTFont(fontWithName: name, size: size)
self.nameToFontMap[name] = f
}
if f!.fontSize == size { return f }
else { return f!.copy(withSize: size) }
}
public func latinModernFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "latinmodern-math", size: size)
}
public func xitsFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "xits-math", size: size)
}
public func termesFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "texgyretermes-math", size: size)
}
public var defaultFont: MTFont? {
MTFontManager.fontManager.latinModernFont(withSize: kDefaultFontSize)
}
}

View File

@@ -0,0 +1,323 @@
//
// MTFontMathTable.swift
// MathRenderSwift
//
// Created by Mike Griebling on 2022-12-31.
//
import Foundation
import CoreGraphics
import CoreText
class GlyphPart {
/// The glyph that represents this part
var glyph: CGGlyph!
/// Full advance width/height for this part, in the direction of the extension in points.
var fullAdvance: CGFloat = 0
/// Advance width/ height of the straight bar connector material at the beginning of the glyph in points.
var startConnectorLength: CGFloat = 0
/// Advance width/ height of the straight bar connector material at the end of the glyph in points.
var endConnectorLength: CGFloat = 0
/// If this part is an extender. If set, the part can be skipped or repeated.
var isExtender: Bool = false
}
/** This class represents the Math table of an open type font.
The math table is documented here: https://www.microsoft.com/typography/otspec/math.htm
How the constants in this class affect the display is documented here:
http://www.tug.org/TUGboat/tb30-1/tb94vieth.pdf
@note We don't parse the math table from the open type font. Rather we parse it
in python and convert it to a .plist file which is easily consumed by this class.
This approach is preferable to spending an inordinate amount of time figuring out
how to parse the returned NSData object using the open type rules.
@remark This class is not meant to be used outside of this library.
*/
class MTFontMathTable {
// The font for this math table.
var font:MTFont? // @property (nonatomic, readonly, weak) MTFont* font;
var _unitsPerEm: UInt
var _fontSize: CGFloat
var _mathTable: NSDictionary!
let kConstants = "constants"
/** MU unit in points */
var muUnit:CGFloat { _fontSize/18 }
func fontUnitsToPt(_ fontUnits:Int) -> CGFloat {
CGFloat(fontUnits) * _fontSize / CGFloat(_unitsPerEm)
}
init(withFont font: MTFont?, mathTable:NSDictionary) {
assert(font != nil, "font has nil value")
assert(font!.ctFont != nil, "font.ctFont has nil value")
self.font = font
// do domething with font
_unitsPerEm = UInt(CTFontGetUnitsPerEm(font!.ctFont))
_fontSize = font!.fontSize;
_mathTable = mathTable
let version = _mathTable["version"] as! String
if version != "1.3" {
NSException(name: NSExceptionName.internalInconsistencyException, reason: "Invalid version of math table plist: \(version)").raise()
}
}
func constantFromTable(_ constName:String) -> CGFloat {
let consts = _mathTable[kConstants] as! NSDictionary?
let val = consts![constName] as! NSNumber?
return fontUnitsToPt(val!.intValue)
}
func percentFromTable(_ percentName:String) -> CGFloat {
let consts = _mathTable[kConstants] as! NSDictionary?
let val = consts![percentName] as! NSNumber?
return CGFloat(val!.floatValue) / 100
}
/// Math Font Metrics from the opentype specification
// MARK: - Fractions
var fractionNumeratorDisplayStyleShiftUp:CGFloat { constantFromTable("FractionNumeratorDisplayStyleShiftUp") } // \sigma_8 in TeX
var fractionNumeratorShiftUp:CGFloat { constantFromTable("FractionNumeratorShiftUp") } // \sigma_9 in TeX
var fractionDenominatorDisplayStyleShiftDown:CGFloat { constantFromTable("FractionDenominatorDisplayStyleShiftDown") } // \sigma_11 in TeX
var fractionDenominatorShiftDown:CGFloat { constantFromTable("FractionDenominatorShiftDown") } // \sigma_12 in TeX
var fractionNumeratorDisplayStyleGapMin:CGFloat { constantFromTable("FractionNumDisplayStyleGapMin") } // 3 * \xi_8 in TeX
var fractionNumeratorGapMin:CGFloat { constantFromTable("FractionNumeratorGapMin") } // \xi_8 in TeX
var fractionDenominatorDisplayStyleGapMin:CGFloat { constantFromTable("FractionDenomDisplayStyleGapMin") } // 3 * \xi_8 in TeX
var fractionDenominatorGapMin:CGFloat { constantFromTable("FractionDenominatorGapMin") } // \xi_8 in TeX
var fractionRuleThickness:CGFloat { constantFromTable("FractionRuleThickness") } // \xi_8 in TeX
var skewedFractionHorizonalGap:CGFloat { constantFromTable("SkewedFractionHorizontalGap") } // \sigma_20 in TeX
var skewedFractionVerticalGap:CGFloat { constantFromTable("SkewedFractionVerticalGap") } // \sigma_21 in TeX
// MARK: - Non-standard
// FractionDelimiterSize and FractionDelimiterDisplayStyleSize are not constants
// specified in the OpenType Math specification. Rather these are proposed LuaTeX extensions
// for the TeX parameters \sigma_20 (delim1) and \sigma_21 (delim2). Since these do not
// exist in the fonts that we have, we use the same approach as LuaTeX and use the fontSize
// to determine these values. The constants used are the same as LuaTeX and KaTeX and match the
// metrics values of the original TeX fonts.
// Note: An alternative approach is to use DelimitedSubFormulaMinHeight for \sigma21 and use a factor
// of 2 to get \sigma 20 as proposed in Vieth paper.
// The XeTeX implementation sets \sigma21 = fontSize and \sigma20 = DelimitedSubFormulaMinHeight which
// will produce smaller delimiters.
// Of all the approaches we've implemented LuaTeX's approach since it mimics LaTeX most accurately.
var fractionDelimiterSize: CGFloat { return 1.01 * _fontSize }
/// Modified constant from 2.4 to 2.39, it matches KaTeX and looks better.
var fractionDelimiterDisplayStyleSize: CGFloat { return 2.39 * _fontSize }
// MARK: - Stacks
var stackTopDisplayStyleShiftUp:CGFloat { constantFromTable("StackTopDisplayStyleShiftUp") } // \sigma_8 in TeX
var stackTopShiftUp:CGFloat { constantFromTable("StackTopShiftUp") } // \sigma_10 in TeX
var stackDisplayStyleGapMin:CGFloat { constantFromTable("StackDisplayStyleGapMin") } // 7 \xi_8 in TeX
var stackGapMin:CGFloat { constantFromTable("StackGapMin") } // 3 \xi_8 in TeX
var stackBottomDisplayStyleShiftDown:CGFloat { constantFromTable("StackBottomDisplayStyleShiftDown") } // \sigma_11 in TeX
var stackBottomShiftDown:CGFloat { constantFromTable("StackBottomShiftDown") } // \sigma_12 in TeX
var stretchStackBottomShiftDown:CGFloat { constantFromTable("StretchStackBottomShiftDown") }
var stretchStackGapAboveMin:CGFloat { constantFromTable("StretchStackGapAboveMin") }
var stretchStackGapBelowMin:CGFloat { constantFromTable("StretchStackGapBelowMin") }
var stretchStackTopShiftUp:CGFloat { constantFromTable("StretchStackTopShiftUp") }
// MARK: - super/sub scripts
var superscriptShiftUp:CGFloat { constantFromTable("SuperscriptShiftUp") } // \sigma_13, \sigma_14 in TeX
var superscriptShiftUpCramped:CGFloat { constantFromTable("SuperscriptShiftUpCramped") } // \sigma_15 in TeX
var subscriptShiftDown:CGFloat { constantFromTable("SubscriptShiftDown") } // \sigma_16, \sigma_17 in TeX
var superscriptBaselineDropMax:CGFloat { constantFromTable("SuperscriptBaselineDropMax") } // \sigma_18 in TeX
var subscriptBaselineDropMin:CGFloat { constantFromTable("SubscriptBaselineDropMin") } // \sigma_19 in TeX
var superscriptBottomMin:CGFloat { constantFromTable("SuperscriptBottomMin") } // 1/4 \sigma_5 in TeX
var subscriptTopMax:CGFloat { constantFromTable("SubscriptTopMax") } // 4/5 \sigma_5 in TeX
var subSuperscriptGapMin:CGFloat { constantFromTable("SubSuperscriptGapMin") } // 4 \xi_8 in TeX
var superscriptBottomMaxWithSubscript:CGFloat { constantFromTable("SuperscriptBottomMaxWithSubscript") } // 4/5 \sigma_5 in TeX
var spaceAfterScript:CGFloat { constantFromTable("SpaceAfterScript") }
// MARK: - radicals
var radicalExtraAscender:CGFloat { constantFromTable("RadicalExtraAscender") } // \xi_8 in Tex
var radicalRuleThickness:CGFloat { constantFromTable("RadicalRuleThickness") } // \xi_8 in Tex
var radicalDisplayStyleVerticalGap:CGFloat { constantFromTable("RadicalDisplayStyleVerticalGap") } // \xi_8 + 1/4 \sigma_5 in Tex
var radicalVerticalGap:CGFloat { constantFromTable("RadicalVerticalGap") } // 5/4 \xi_8 in Tex
var radicalKernBeforeDegree:CGFloat { constantFromTable("RadicalKernBeforeDegree") } // 5 mu in Tex
var radicalKernAfterDegree:CGFloat { constantFromTable("RadicalKernAfterDegree") } // -10 mu in Tex
var radicalDegreeBottomRaisePercent:CGFloat { percentFromTable("RadicalDegreeBottomRaisePercent") } // 60% in Tex
// MARK: - Limits
var upperLimitBaselineRiseMin:CGFloat { constantFromTable("UpperLimitBaselineRiseMin") } // \xi_11 in TeX
var upperLimitGapMin:CGFloat { constantFromTable("UpperLimitGapMin") } // \xi_9 in TeX
var lowerLimitGapMin:CGFloat { constantFromTable("LowerLimitGapMin") } // \xi_10 in TeX
var lowerLimitBaselineDropMin:CGFloat { constantFromTable("LowerLimitBaselineDropMin") } // \xi_12 in TeX
var limitExtraAscenderDescender:CGFloat { 0 } // \xi_13 in TeX, not present in OpenType so we always set it to 0.
// MARK: - Underline
var underbarVerticalGap:CGFloat { constantFromTable("UnderbarVerticalGap") } // 3 \xi_8 in TeX
var underbarRuleThickness:CGFloat { constantFromTable("UnderbarRuleThickness") } // \xi_8 in TeX
var underbarExtraDescender:CGFloat { constantFromTable("UnderbarExtraDescender") } // \xi_8 in TeX
// MARK: - Overline
var overbarVerticalGap:CGFloat { constantFromTable("OverbarVerticalGap") } // 3 \xi_8 in TeX
var overbarRuleThickness:CGFloat { constantFromTable("OverbarRuleThickness") } // \xi_8 in TeX
var overbarExtraAscender:CGFloat { constantFromTable("OverbarExtraAscender") } // \xi_8 in TeX
// MARK: - Constants
var axisHeight:CGFloat { constantFromTable("AxisHeight") } // \sigma_22 in TeX
var scriptScaleDown:CGFloat { percentFromTable("ScriptPercentScaleDown") }
var scriptScriptScaleDown:CGFloat { percentFromTable("ScriptScriptPercentScaleDown") }
var mathLeading:CGFloat { constantFromTable("MathLeading") }
var delimitedSubFormulaMinHeight:CGFloat { constantFromTable("DelimitedSubFormulaMinHeight") }
// MARK: - Accent
var accentBaseHeight:CGFloat { constantFromTable("AccentBaseHeight") } // \fontdimen5 in TeX (x-height)
var flattenedAccentBaseHeight:CGFloat { constantFromTable("FlattenedAccentBaseHeight") }
// MARK: - Variants
let kVertVariants = "v_variants"
let kHorizVariants = "h_variants"
/** Returns an Array of all the vertical variants of the glyph if any. If
there are no variants for the glyph, the array contains the given glyph. */
func getVerticalVariantsForGlyph( _ glyph:CGGlyph) -> [NSNumber?] {
let variants = _mathTable[kVertVariants] as! NSDictionary?
return self.getVariantsForGlyph(glyph, inDictionary: variants)
}
/** Returns an Array of all the horizontal variants of the glyph if any. If
there are no variants for the glyph, the array contains the given glyph. */
func getHorizontalVariantsForGlyph( _ glyph:CGGlyph) -> [NSNumber?] {
let variants = _mathTable[kHorizVariants] as! NSDictionary?
return self.getVariantsForGlyph(glyph, inDictionary:variants)
}
func getVariantsForGlyph(_ glyph: CGGlyph, inDictionary variants:NSDictionary?) -> [NSNumber] {
let glyphName = self.font!.get(nameForGlyph: glyph)
let variantGlyphs = variants![glyphName] as! NSArray?
var glyphArray = [NSNumber]()
if variantGlyphs == nil {
// There are no extra variants, so just add the current glyph to it.
let glyph = self.font!.get(glyphWithName: glyphName)
glyphArray.append(NSNumber(value:glyph))
return glyphArray
}
for gvn in variantGlyphs! {
let glyphVariantName = gvn as! String?
let variantGlyph = self.font?.get(glyphWithName: glyphVariantName!)
glyphArray.append(NSNumber(value:variantGlyph!))
}
return glyphArray
}
/** Returns a larger vertical variant of the given glyph if any.
If there is no larger version, this returns the current glyph.
*/
func getLargerGlyph(_ glyph:CGGlyph) -> CGGlyph {
let variants = _mathTable[kVertVariants] as! NSDictionary?
let glyphName = self.font?.get(nameForGlyph: glyph)
let variantGlyphs = variants![glyphName!] as! NSArray?
if variantGlyphs == nil {
// There are no extra variants, so just returnt the current glyph.
return glyph
}
// Find the first variant with a different name.
for gvn in variantGlyphs! {
let glyphVariantName = gvn as! String?
if glyphVariantName != glyphName {
let variantGlyph = self.font?.get(glyphWithName: glyphVariantName!)
return variantGlyph!
}
}
// We did not find any variants of this glyph so return it.
return glyph;
}
// MARK: - Italic Correction
let kItalic = "italic"
/** Returns the italic correction for the given glyph if any. If there
isn't any this returns 0. */
func getItalicCorrection(_ glyph: CGGlyph) -> CGFloat {
let italics = _mathTable[kItalic] as! NSDictionary?
let glyphName = self.font?.get(nameForGlyph: glyph)
let val = italics![glyphName!] as! NSNumber?
// if val is nil, this returns 0.
return self.fontUnitsToPt(val?.intValue ?? 0)
}
// MARK: - Accents
let kAccents = "accents"
/** Returns the adjustment to the top accent for the given glyph if any.
If there isn't any this returns -1. */
func getTopAccentAdjustment(_ glyph: CGGlyph) -> CGFloat {
var glyph = glyph
let accents = _mathTable[kAccents] as! NSDictionary?
let glyphName = self.font?.get(nameForGlyph: glyph)
let val = accents![glyphName!] as! NSNumber?
if let val = val {
return self.fontUnitsToPt(val.intValue)
} else {
// If no top accent is defined then it is the center of the advance width.
var advances = CGSize.zero
CTFontGetAdvancesForGlyphs(self.font!.ctFont, .horizontal, &glyph, &advances, 1)
return advances.width/2
}
}
// MARK: - Glyph Construction
/** Minimum overlap of connecting glyphs during glyph construction */
var minConnectorOverlap:CGFloat { constantFromTable("MinConnectorOverlap") }
let kVertAssembly = "v_assembly"
let kAssemblyParts = "parts"
/** Returns an array of the glyph parts to be used for constructing vertical variants
of this glyph. If there is no glyph assembly defined, returns an empty array. */
func getVerticalGlyphAssembly(forGlyph glyph:CGGlyph) -> [GlyphPart] {
let assemblyTable = _mathTable[kVertAssembly] as! NSDictionary?
let glyphName = self.font?.get(nameForGlyph: glyph) // getGlyphName:glyph];
let assemblyInfo = assemblyTable![glyphName!] as! NSDictionary?
if assemblyInfo == nil {
// No vertical assembly defined for glyph
return []
}
let parts = assemblyInfo![kAssemblyParts] as! NSArray?
if parts == nil {
// parts should always have been defined, but if it isn't return nil
return []
}
var rv = [GlyphPart]()
for part in parts! {
let partInfo = part as! NSDictionary?
let part = GlyphPart()
let adv = partInfo!["advance"] as! NSNumber?
part.fullAdvance = self.fontUnitsToPt(adv!.intValue)
let end = partInfo!["endConnector"] as! NSNumber?
part.endConnectorLength = self.fontUnitsToPt(end!.intValue)
let start = partInfo!["startConnector"] as! NSNumber?
part.startConnectorLength = self.fontUnitsToPt(start!.intValue)
let ext = partInfo!["extender"] as! NSNumber?
part.isExtender = ext!.boolValue
let glyphName = partInfo!["glyph"] as! String?
part.glyph = self.font?.get(glyphWithName: glyphName!)
rv.append(part)
}
return rv
}
}

View File

@@ -0,0 +1,35 @@
//
// MTLabel.swift
// MathRenderSwift
//
// Created by Mike Griebling on 2022-12-31.
//
import Foundation
import SwiftUI
#if os(macOS)
public class MTLabel : NSTextField {
init() {
super.init(frame: .zero)
self.stringValue = ""
self.isBezeled = false
self.drawsBackground = false
self.isEditable = false
self.isSelectable = false
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
var text:String? {
get { super.stringValue }
set { super.stringValue = newValue! }
}
}
#endif

View File

@@ -0,0 +1,833 @@
//
// MTMathAtomFactory.swift
// MathRenderSwift
//
// Created by Mike Griebling on 2022-12-31.
//
import Foundation
public class MTMathAtomFactory {
public static let aliases = [
"lnot" : "neg",
"land" : "wedge",
"lor" : "vee",
"ne" : "neq",
"le" : "leq",
"ge" : "geq",
"lbrace" : "{",
"rbrace" : "}",
"Vert" : "|",
"gets" : "leftarrow",
"to" : "rightarrow",
"iff" : "Longleftrightarrow",
"AA" : "angstrom"
]
public static let delimiters = [
"." : "", // . 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}"
]
var _delimValueToName: [String: String]? = nil
public var delimValueToName: [String: String] {
if _delimValueToName == nil {
var output = [String: String]()
for (key, value) in Self.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
}
_delimValueToName = output
}
return _delimValueToName!
}
public static let accents = [
"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 var _accentValueToName: [String: String]? = nil
public static var accentValueToName: [String: String] {
if _accentValueToName == nil {
var output = [String: String]()
for (key, value) in Self.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
}
_accentValueToName = output
}
return _accentValueToName!
}
static var supportedLatexSymbolNames:[String] {
let commands = MTMathAtomFactory.supportedLatexSymbols
return commands.keys.map { String($0) }
}
static var supportedLatexSymbols: [String: MTMathAtom] = [
"square" : MTMathAtomFactory.placeholder(),
// Greek characters
"alpha" : MTMathAtom(type: .variable, value: "\u{03B1}"),
"beta" : MTMathAtom(type: .variable, value: "\u{03B2}"),
"gamma" : MTMathAtom(type: .variable, value: "\u{03B3}"),
"delta" : MTMathAtom(type: .variable, value: "\u{03B4}"),
"varepsilon" : MTMathAtom(type: .variable, value: "\u{03B5}"),
"zeta" : MTMathAtom(type: .variable, value: "\u{03B6}"),
"eta" : MTMathAtom(type: .variable, value: "\u{03B7}"),
"theta" : MTMathAtom(type: .variable, value: "\u{03B8}"),
"iota" : MTMathAtom(type: .variable, value: "\u{03B9}"),
"kappa" : MTMathAtom(type: .variable, value: "\u{03BA}"),
"lambda" : MTMathAtom(type: .variable, value: "\u{03BB}"),
"mu" : MTMathAtom(type: .variable, value: "\u{03BC}"),
"nu" : MTMathAtom(type: .variable, value: "\u{03BD}"),
"xi" : MTMathAtom(type: .variable, value: "\u{03BE}"),
"omicron" : MTMathAtom(type: .variable, value: "\u{03BF}"),
"pi" : MTMathAtom(type: .variable, value: "\u{03C0}"),
"rho" : MTMathAtom(type: .variable, value: "\u{03C1}"),
"varsigma" : MTMathAtom(type: .variable, value: "\u{03C1}"),
"sigma" : MTMathAtom(type: .variable, value: "\u{03C3}"),
"tau" : MTMathAtom(type: .variable, value: "\u{03C4}"),
"upsilon" : MTMathAtom(type: .variable, value: "\u{03C5}"),
"varphi" : MTMathAtom(type: .variable, value: "\u{03C6}"),
"chi" : MTMathAtom(type: .variable, value: "\u{03C7}"),
"psi" : MTMathAtom(type: .variable, value: "\u{03C8}"),
"omega" : MTMathAtom(type: .variable, value: "\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" : MTMathAtom(type: .ordinary, value: "\u{0001D716}"),
"vartheta" : MTMathAtom(type: .ordinary, value: "\u{0001D717}"),
"phi" : MTMathAtom(type: .ordinary, value: "\u{0001D719}"),
"varrho" : MTMathAtom(type: .ordinary, value: "\u{0001D71A}"),
"varpi" : MTMathAtom(type: .ordinary, value: "\u{0001D71B}"),
// Capital greek characters
"Gamma" : MTMathAtom(type: .variable, value: "\u{0393}"),
"Delta" : MTMathAtom(type: .variable, value: "\u{0394}"),
"Theta" : MTMathAtom(type: .variable, value: "\u{0398}"),
"Lambda" : MTMathAtom(type: .variable, value: "\u{039B}"),
"Xi" : MTMathAtom(type: .variable, value: "\u{039E}"),
"Pi" : MTMathAtom(type: .variable, value: "\u{03A0}"),
"Sigma" : MTMathAtom(type: .variable, value: "\u{03A3}"),
"Upsilon" : MTMathAtom(type: .variable, value: "\u{03A5}"),
"Phi" : MTMathAtom(type: .variable, value: "\u{03A6}"),
"Psi" : MTMathAtom(type: .variable, value: "\u{03A8}"),
"Omega" : MTMathAtom(type: .variable, value: "\u{03A9}"),
// Open
"lceil" : MTMathAtom(type: .open, value: "\u{2308}"),
"lfloor" : MTMathAtom(type: .open, value: "\u{230A}"),
"langle" : MTMathAtom(type: .open, value: "\u{27E8}"),
"lgroup" : MTMathAtom(type: .open, value: "\u{27EE}"),
// Close
"rceil" : MTMathAtom(type: .close, value: "\u{2309}"),
"rfloor" : MTMathAtom(type: .close, value: "\u{230B}"),
"rangle" : MTMathAtom(type: .close, value: "\u{27E9}"),
"rgroup" : MTMathAtom(type: .close, value: "\u{27EF}"),
// Arrows
"leftarrow" : MTMathAtom(type: .relation, value: "\u{2190}"),
"uparrow" : MTMathAtom(type: .relation, value: "\u{2191}"),
"rightarrow" : MTMathAtom(type: .relation, value: "\u{2192}"),
"downarrow" : MTMathAtom(type: .relation, value: "\u{2193}"),
"leftrightarrow" : MTMathAtom(type: .relation, value: "\u{2194}"),
"updownarrow" : MTMathAtom(type: .relation, value: "\u{2195}"),
"nwarrow" : MTMathAtom(type: .relation, value: "\u{2196}"),
"nearrow" : MTMathAtom(type: .relation, value: "\u{2197}"),
"searrow" : MTMathAtom(type: .relation, value: "\u{2198}"),
"swarrow" : MTMathAtom(type: .relation, value: "\u{2199}"),
"mapsto" : MTMathAtom(type: .relation, value: "\u{21A6}"),
"Leftarrow" : MTMathAtom(type: .relation, value: "\u{21D0}"),
"Uparrow" : MTMathAtom(type: .relation, value: "\u{21D1}"),
"Rightarrow" : MTMathAtom(type: .relation, value: "\u{21D2}"),
"Downarrow" : MTMathAtom(type: .relation, value: "\u{21D3}"),
"Leftrightarrow" : MTMathAtom(type: .relation, value: "\u{21D4}"),
"Updownarrow" : MTMathAtom(type: .relation, value: "\u{21D5}"),
"longleftarrow" : MTMathAtom(type: .relation, value: "\u{27F5}"),
"longrightarrow" : MTMathAtom(type: .relation, value: "\u{27F6}"),
"longleftrightarrow" : MTMathAtom(type: .relation, value: "\u{27F7}"),
"Longleftarrow" : MTMathAtom(type: .relation, value: "\u{27F8}"),
"Longrightarrow" : MTMathAtom(type: .relation, value: "\u{27F9}"),
"Longleftrightarrow" : MTMathAtom(type: .relation, value: "\u{27FA}"),
// Relations
"leq" : MTMathAtom(type: .relation, value: UnicodeSymbol.lessEqual),
"geq" : MTMathAtom(type: .relation, value: UnicodeSymbol.greaterEqual),
"neq" : MTMathAtom(type: .relation, value: UnicodeSymbol.notEqual),
"in" : MTMathAtom(type: .relation, value: "\u{2208}"),
"notin" : MTMathAtom(type: .relation, value: "\u{2209}"),
"ni" : MTMathAtom(type: .relation, value: "\u{220B}"),
"propto" : MTMathAtom(type: .relation, value: "\u{221D}"),
"mid" : MTMathAtom(type: .relation, value: "\u{2223}"),
"parallel" : MTMathAtom(type: .relation, value: "\u{2225}"),
"sim" : MTMathAtom(type: .relation, value: "\u{223C}"),
"simeq" : MTMathAtom(type: .relation, value: "\u{2243}"),
"cong" : MTMathAtom(type: .relation, value: "\u{2245}"),
"approx" : MTMathAtom(type: .relation, value: "\u{2248}"),
"asymp" : MTMathAtom(type: .relation, value: "\u{224D}"),
"doteq" : MTMathAtom(type: .relation, value: "\u{2250}"),
"equiv" : MTMathAtom(type: .relation, value: "\u{2261}"),
"gg" : MTMathAtom(type: .relation, value: "\u{226A}"),
"ll" : MTMathAtom(type: .relation, value: "\u{226B}"),
"prec" : MTMathAtom(type: .relation, value: "\u{227A}"),
"succ" : MTMathAtom(type: .relation, value: "\u{227B}"),
"subset" : MTMathAtom(type: .relation, value: "\u{2282}"),
"supset" : MTMathAtom(type: .relation, value: "\u{2283}"),
"subseteq" : MTMathAtom(type: .relation, value: "\u{2286}"),
"supseteq" : MTMathAtom(type: .relation, value: "\u{2287}"),
"sqsubset" : MTMathAtom(type: .relation, value: "\u{228F}"),
"sqsupset" : MTMathAtom(type: .relation, value: "\u{2290}"),
"sqsubseteq" : MTMathAtom(type: .relation, value: "\u{2291}"),
"sqsupseteq" : MTMathAtom(type: .relation, value: "\u{2292}"),
"models" : MTMathAtom(type: .relation, value: "\u{22A7}"),
"perp" : MTMathAtom(type: .relation, value: "\u{27C2}"),
// operators
"times" : MTMathAtomFactory.times(),
"div" : MTMathAtomFactory.divide(),
"pm" : MTMathAtom(type: .binaryOperator, value: "\u{00B1}"),
"dagger" : MTMathAtom(type: .binaryOperator, value: "\u{2020}"),
"ddagger" : MTMathAtom(type: .binaryOperator, value: "\u{2021}"),
"mp" : MTMathAtom(type: .binaryOperator, value: "\u{2213}"),
"setminus" : MTMathAtom(type: .binaryOperator, value: "\u{2216}"),
"ast" : MTMathAtom(type: .binaryOperator, value: "\u{2217}"),
"circ" : MTMathAtom(type: .binaryOperator, value: "\u{2218}"),
"bullet" : MTMathAtom(type: .binaryOperator, value: "\u{2219}"),
"wedge" : MTMathAtom(type: .binaryOperator, value: "\u{2227}"),
"vee" : MTMathAtom(type: .binaryOperator, value: "\u{2228}"),
"cap" : MTMathAtom(type: .binaryOperator, value: "\u{2229}"),
"cup" : MTMathAtom(type: .binaryOperator, value: "\u{222A}"),
"wr" : MTMathAtom(type: .binaryOperator, value: "\u{2240}"),
"uplus" : MTMathAtom(type: .binaryOperator, value: "\u{228E}"),
"sqcap" : MTMathAtom(type: .binaryOperator, value: "\u{2293}"),
"sqcup" : MTMathAtom(type: .binaryOperator, value: "\u{2294}"),
"oplus" : MTMathAtom(type: .binaryOperator, value: "\u{2295}"),
"ominus" : MTMathAtom(type: .binaryOperator, value: "\u{2296}"),
"otimes" : MTMathAtom(type: .binaryOperator, value: "\u{2297}"),
"oslash" : MTMathAtom(type: .binaryOperator, value: "\u{2298}"),
"odot" : MTMathAtom(type: .binaryOperator, value: "\u{2299}"),
"star" : MTMathAtom(type: .binaryOperator, value: "\u{22C6}"),
"cdot" : MTMathAtom(type: .binaryOperator, value: "\u{22C5}"),
"amalg" : MTMathAtom(type: .binaryOperator, value: "\u{2A3F}"),
// No limit operators
"log" : MTMathAtomFactory.operatorWithName( "log", limits: false),
"lg" : MTMathAtomFactory.operatorWithName( "lg", limits: false),
"ln" : MTMathAtomFactory.operatorWithName( "ln", limits: false),
"sin" : MTMathAtomFactory.operatorWithName( "sin", limits: false),
"arcsin" : MTMathAtomFactory.operatorWithName( "arcsin", limits: false),
"sinh" : MTMathAtomFactory.operatorWithName( "sinh", limits: false),
"cos" : MTMathAtomFactory.operatorWithName( "cos", limits: false),
"arccos" : MTMathAtomFactory.operatorWithName( "arccos", limits: false),
"cosh" : MTMathAtomFactory.operatorWithName( "cosh", limits: false),
"tan" : MTMathAtomFactory.operatorWithName( "tan", limits: false),
"arctan" : MTMathAtomFactory.operatorWithName( "arctan", limits: false),
"tanh" : MTMathAtomFactory.operatorWithName( "tanh", limits: false),
"cot" : MTMathAtomFactory.operatorWithName( "cot", limits: false),
"coth" : MTMathAtomFactory.operatorWithName( "coth", limits: false),
"sec" : MTMathAtomFactory.operatorWithName( "sec", limits: false),
"csc" : MTMathAtomFactory.operatorWithName( "csc", limits: false),
"arg" : MTMathAtomFactory.operatorWithName( "arg", limits: false),
"ker" : MTMathAtomFactory.operatorWithName( "ker", limits: false),
"dim" : MTMathAtomFactory.operatorWithName( "dim", limits: false),
"hom" : MTMathAtomFactory.operatorWithName( "hom", limits: false),
"exp" : MTMathAtomFactory.operatorWithName( "exp", limits: false),
"deg" : MTMathAtomFactory.operatorWithName( "deg", limits: false),
// Limit operators
"lim" : MTMathAtomFactory.operatorWithName( "lim", limits: true),
"limsup" : MTMathAtomFactory.operatorWithName( "lim sup", limits: true),
"liminf" : MTMathAtomFactory.operatorWithName( "lim inf", limits: true),
"max" : MTMathAtomFactory.operatorWithName( "max", limits: true),
"min" : MTMathAtomFactory.operatorWithName( "min", limits: true),
"sup" : MTMathAtomFactory.operatorWithName( "sup", limits: true),
"inf" : MTMathAtomFactory.operatorWithName( "inf", limits: true),
"det" : MTMathAtomFactory.operatorWithName( "det", limits: true),
"Pr" : MTMathAtomFactory.operatorWithName( "Pr", limits: true),
"gcd" : MTMathAtomFactory.operatorWithName( "gcd", limits: true),
// Large operators
"prod" : MTMathAtomFactory.operatorWithName( "\u{220F}", limits: true),
"coprod" : MTMathAtomFactory.operatorWithName( "\u{2210}", limits: true),
"sum" : MTMathAtomFactory.operatorWithName( "\u{2211}", limits: true),
"int" : MTMathAtomFactory.operatorWithName( "\u{222B}", limits: false),
"oint" : MTMathAtomFactory.operatorWithName( "\u{222E}", limits: false),
"bigwedge" : MTMathAtomFactory.operatorWithName( "\u{22C0}", limits: true),
"bigvee" : MTMathAtomFactory.operatorWithName( "\u{22C1}", limits: true),
"bigcap" : MTMathAtomFactory.operatorWithName( "\u{22C2}", limits: true),
"bigcup" : MTMathAtomFactory.operatorWithName( "\u{22C3}", limits: true),
"bigodot" : MTMathAtomFactory.operatorWithName( "\u{2A00}", limits: true),
"bigoplus" : MTMathAtomFactory.operatorWithName( "\u{2A01}", limits: true),
"bigotimes" : MTMathAtomFactory.operatorWithName( "\u{2A02}", limits: true),
"biguplus" : MTMathAtomFactory.operatorWithName( "\u{2A04}", limits: true),
"bigsqcup" : MTMathAtomFactory.operatorWithName( "\u{2A06}", limits: true),
// Latex command characters
"{" : MTMathAtom(type: .open, value: "{"),
"}" : MTMathAtom(type: .close, value: "}"),
"$" : MTMathAtom(type: .ordinary, value: "{"),
"&" : MTMathAtom(type: .ordinary, value: "&"),
"#" : MTMathAtom(type: .ordinary, value: "#"),
"%" : MTMathAtom(type: .ordinary, value: "%"),
"_" : MTMathAtom(type: .ordinary, value: "_"),
" " : MTMathAtom(type: .ordinary, value: " "),
"backslash" : MTMathAtom(type: .ordinary, value: "\\"),
// Punctuation
// Note: \colon is different from : which is a relation
"colon" : MTMathAtom(type: .punctuation, value: ":"),
"cdotp" : MTMathAtom(type: .punctuation, value: "\u{00B7}"),
// Other symbols
"degree" : MTMathAtom(type: .ordinary, value: "\u{00B0}"),
"neg" : MTMathAtom(type: .ordinary, value: "\u{00AC}"),
"angstrom" : MTMathAtom(type: .ordinary, value: "\u{00C5}"),
"|" : MTMathAtom(type: .ordinary, value: "\u{2016}"),
"vert" : MTMathAtom(type: .ordinary, value: "|"),
"ldots" : MTMathAtom(type: .ordinary, value: "\u{2026}"),
"prime" : MTMathAtom(type: .ordinary, value: "\u{2032}"),
"hbar" : MTMathAtom(type: .ordinary, value: "\u{210F}"),
"lbar" : MTMathAtom(type: .ordinary, value: "\u{019B}"), // NEW ƛ
"Im" : MTMathAtom(type: .ordinary, value: "\u{2111}"),
"ell" : MTMathAtom(type: .ordinary, value: "\u{2113}"),
"wp" : MTMathAtom(type: .ordinary, value: "\u{2118}"),
"Re" : MTMathAtom(type: .ordinary, value: "\u{211C}"),
"mho" : MTMathAtom(type: .ordinary, value: "\u{2127}"),
"aleph" : MTMathAtom(type: .ordinary, value: "\u{2135}"),
"forall" : MTMathAtom(type: .ordinary, value: "\u{2200}"),
"exists" : MTMathAtom(type: .ordinary, value: "\u{2203}"),
"emptyset" : MTMathAtom(type: .ordinary, value: "\u{2205}"),
"nabla" : MTMathAtom(type: .ordinary, value: "\u{2207}"),
"infty" : MTMathAtom(type: .ordinary, value: "\u{221E}"),
"angle" : MTMathAtom(type: .ordinary, value: "\u{2220}"),
"top" : MTMathAtom(type: .ordinary, value: "\u{22A4}"),
"bot" : MTMathAtom(type: .ordinary, value: "\u{22A5}"),
"vdots" : MTMathAtom(type: .ordinary, value: "\u{22EE}"),
"cdots" : MTMathAtom(type: .ordinary, value: "\u{22EF}"),
"ddots" : MTMathAtom(type: .ordinary, value: "\u{22F1}"),
"triangle" : MTMathAtom(type: .ordinary, value: "\u{25B3}"),
"imath" : MTMathAtom(type: .ordinary, value: "\u{0001D6A4}"),
"jmath" : MTMathAtom(type: .ordinary, value: "\u{0001D6A5}"),
"partial" : MTMathAtom(type: .ordinary, value: "\u{0001D715}"),
// Spacing
"," : MTMathSpace(space: 3),
">" : MTMathSpace(space: 4),
";" : MTMathSpace(space: 5),
"!" : MTMathSpace(space: -3),
"quad" : MTMathSpace(space: 18), // quad = 1em = 18mu
"qquad" : MTMathSpace(space: 36), // qquad = 2em
// Style
"displaystyle" : MTMathStyle(style: .display),
"textstyle" : MTMathStyle(style: .text),
"scriptstyle" : MTMathStyle(style: .script),
"scriptscriptstyle" : MTMathStyle(style: .scriptOfScript),
]
var _textToLatexSymbolName: [String: String]? = nil
public var textToLatexSymbolName: [String: String] {
get {
if self._textToLatexSymbolName == nil {
var output = [String: String]()
for (key, atom) in Self.supportedLatexSymbols {
if atom.nucleus.count == 0 {
continue
}
if let existingText = output[atom.nucleus] {
// If there are 2 key for the same symbol, choose one deterministically.
if key.count > existingText.count {
// Keep the shorter command
continue
} else if key.count == existingText.count {
// If the length is the same, keep the alphabetically first
if key.compare(existingText) == .orderedDescending {
continue
}
}
}
output[atom.nucleus] = key
}
self._textToLatexSymbolName = output
}
return self._textToLatexSymbolName!
}
set {
self._textToLatexSymbolName = newValue
}
}
public static let sharedInstance = MTMathAtomFactory()
static let fontStyles : [String: MTFontStyle] = [
"mathnormal" : .defaultStyle,
"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,
]
public static func fontStyleWithName(_ fontName:String) -> MTFontStyle? {
return fontStyles[fontName]
}
public static func fontNameForStyle(_ fontStyle:MTFontStyle) -> String {
switch fontStyle {
case .defaultStyle: 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"
}
}
// Return an atom for times sign \times or *
public static func times() -> MTMathAtom {
MTMathAtom(type: .binaryOperator, value: UnicodeSymbol.multiplication)
}
// Return an atom for division sign \div or /
public static func divide() -> MTMathAtom {
MTMathAtom(type: .binaryOperator, value: UnicodeSymbol.division)
}
// Return an atom aka placeholder square
public static func placeholder() -> MTMathAtom {
MTMathAtom(type: .placeholder, value: UnicodeSymbol.whiteSquare)
}
public static func placeholderFraction() -> MTFraction {
let frac = MTFraction()
frac.numerator = MTMathList()
frac.numerator?.add(placeholder())
frac.denominator = MTMathList()
frac.denominator?.add(placeholder())
return frac
}
public static func placeholderSquareRoot() -> MTRadical {
let rad = MTRadical()
rad.radicand = MTMathList()
rad.radicand?.add(placeholder())
return rad
}
public static func placeholderRadical() -> MTRadical {
let rad = MTRadical()
rad.radicand = MTMathList()
rad.degree = MTMathList()
rad.radicand?.add(placeholder())
rad.degree?.add(placeholder())
return rad
}
/** Gets the atom with the right type for the given character. If an atom
cannot be determined for a given character this returns nil.
This function follows latex conventions for assigning types to the atoms.
The following characters are not supported and will return nil:
- Any non-ascii character.
- Any control character or spaces (< 0x21)
- Latex control chars: $ % # & ~ '
- Chars with special meaning in latex: ^ _ { } \
All other characters will have a non-nil atom returned.
*/
public static func atom(forCharacter ch: Character) -> MTMathAtom? {
let chStr = String(ch)
switch chStr {
case "\u{0410}"..."\u{044F}":
return MTMathAtom(type: .ordinary, value: chStr)
case _ where ch.utf32Char < 0x0021 || ch.utf32Char > 0x007E:
return nil
case "$", "%", "#", "&", "~", "\'", "^", "_", "{", "}", "\\":
return nil
case "(", "[":
return MTMathAtom(type: .open, value: chStr)
case ")", "]", "!", "?":
return MTMathAtom(type: .close, value: chStr)
case ",", ";":
return MTMathAtom(type: .punctuation, value: chStr)
case "=", ">", "<":
return MTMathAtom(type: .relation, value: chStr)
case ":":
// Math colon is ratio. Regular colon is \colon
return MTMathAtom(type: .relation, value: "\u{2236}")
case "-":
return MTMathAtom(type: .binaryOperator, value: "\u{2212}")
case "+", "*":
return MTMathAtom(type: .binaryOperator, value: chStr)
case ".", "0"..."9":
return MTMathAtom(type: .number, value: chStr)
case "a"..."z", "A"..."Z":
return MTMathAtom(type: .variable, value: chStr)
case "\"", "/", "@", "`", "|":
return MTMathAtom(type: .ordinary, value: chStr)
default:
assertionFailure("Unknown ASCII character '\(ch)'. Should have been handled earlier.")
return nil
}
}
/** Returns a `MTMathList` with one atom per character in the given string. This function
does not do any LaTeX conversion or interpretation. It simply uses `atomForCharacter` to
convert the characters to atoms. Any character that cannot be converted is ignored. */
public static func atomList(for string: String) -> MTMathList {
let list = MTMathList()
for character in string {
if let newAtom = atom(forCharacter: character) {
list.add(newAtom)
}
}
return list
}
/** Returns an atom with the right type for a given latex symbol (e.g. theta)
If the latex symbol is unknown this will return nil. This supports LaTeX aliases as well.
*/
public static func atom(forLatexSymbol name: String) -> MTMathAtom? {
var name = name
if let canonicalName = aliases[name] {
name = canonicalName
}
if let atom = supportedLatexSymbols[name] {
return atom.copy()
}
return nil
}
/** Finds the name of the LaTeX symbol name for the given atom. This function is a reverse
of the above function. If no latex symbol name corresponds to the atom, then this returns `nil`
If nucleus of the atom is empty, then this will return `nil`.
@note: This is not an exact reverse of the above in the case of aliases. If an LaTeX alias
points to a given symbol, then this function will return the original symbol name and not the
alias.
@note: This function does not convert MathSpaces to latex command names either.
*/
public static func latexSymbolName(for atom: MTMathAtom) -> String? {
if atom.nucleus.count == 0 {
return nil
}
return sharedInstance.textToLatexSymbolName[atom.nucleus]
}
/** Define a latex symbol for rendering. This function allows defining custom symbols that are
not already present in the default set, or override existing symbols with new meaning.
e.g. to define a symbol for "lcm" one can call:
`[MTMathAtomFactory addLatexSymbol:@"lcm" value:[MTMathAtomFactory operatorWithName:@"lcm" limits: false)]` */
public static func add(latexSymbol name: String, value: MTMathAtom) {
supportedLatexSymbols[name] = value
sharedInstance.textToLatexSymbolName[value.nucleus] = name
}
/** Returns a large opertor for the given name. If limits is true, limits are set up on
the operator and displayed differently. */
public static func operatorWithName(_ name: String, limits: Bool) -> MTLargeOperator {
MTLargeOperator(value: name, limits: limits)
}
/** Returns an accent with the given name. The name of the accent is the LaTeX name
such as `grave`, `hat` etc. If the name is not a recognized accent name, this
returns nil. The `innerList` of the returned `MTAccent` is nil.
*/
public static func accent(withName name: String) -> MTAccent? {
if let accentValue = accents[name] {
return MTAccent(value: accentValue)
}
return nil
}
/** Returns the accent name for the given accent. This is the reverse of the above
function. */
public static func accentName(_ accent: MTAccent) -> String? {
accentValueToName[accent.nucleus]
}
/** Creates a new boundary atom for the given delimiter name. If the delimiter name
is not recognized it returns nil. A delimiter name can be a single character such
as '(' or a latex command such as 'uparrow'.
@note In order to distinguish between the delimiter '|' and the delimiter '\|' the delimiter '\|'
the has been renamed to '||'.
*/
public static func boundary(forDelimiter name: String) -> MTMathAtom? {
if let delimValue = Self.delimiters[name] {
return MTMathAtom(type: .boundary, value: delimValue)
}
return nil
}
/** Returns the delimiter name for a boundary atom. This is a reverse of the above function.
If the atom is not a boundary atom or if the delimiter value is unknown this returns `nil`.
@note This is not an exact reverse of the above function. Some delimiters have two names (e.g.
`<` and `langle`) and this function always returns the shorter name.
*/
public static func getDelimiterName(of boundary: MTMathAtom) -> String? {
if boundary.type != .boundary {
return nil
}
return Self.sharedInstance.delimValueToName[boundary.nucleus]
}
/** Returns a fraction with the given numerator and denominator. */
public static func fraction(withNumerator num: MTMathList, denominator denom: MTMathList) -> MTFraction {
let frac = MTFraction()
frac.numerator = num
frac.denominator = denom
return frac
}
public static func mathListForCharacters(_ chars:String) -> MTMathList? {
let list = MTMathList()
for ch in chars {
if let atom = self.atom(forCharacter: ch) {
list.add(atom)
}
}
return list
}
/** Simplification of above function when numerator and denominator are simple strings.
This function uses `mathListForCharacters` to convert the strings to `MTMathList`s. */
public static func fraction(withNumeratorString numStr: String, denominatorString denomStr: String) -> MTFraction {
let num = Self.atomList(for: numStr)
let denom = Self.atomList(for: denomStr)
return Self.fraction(withNumerator: num, denominator: denom)
}
/** Builds a table for a given environment with the given rows. Returns a `MTMathAtom` containing the
table and any other atoms necessary for the given environment. Returns nil and sets error
if the table could not be built.
@param env The environment to use to build the table. If the env is nil, then the default table is built.
@note The reason this function returns a `MTMathAtom` and not a `MTMathTable` is because some
matrix environments are have builtin delimiters added to the table and hence are returned as inner atoms.
*/
static let matrixEnvs = [
"matrix": [],
"pmatrix": ["(", ")"],
"bmatrix": ["[", "]"],
"Bmatrix": ["{", "}"],
"vmatrix": ["vert", "vert"],
"Vmatrix": ["Vert", "Vert"]
]
public static func table(withEnvironment env: String?, rows: [[MTMathList]], error:inout NSError?) -> MTMathAtom? {
let table = MTMathTable(environment: env)
for i in 0..<rows.count {
let row = rows[i]
for j in 0..<row.count {
table.set(cell: row[j], forRow: i, column: j)
}
}
if env == nil {
table.interColumnSpacing = 0
table.interRowAdditionalSpacing = 1
for i in 0..<table.numColumns {
table.set(alignment: .left, forColumn: i)
}
return table
} else if let env = env {
if let delims = matrixEnvs[env] {
table.environment = "matrix"
table.interRowAdditionalSpacing = 0
table.interColumnSpacing = 18
let style = MTMathStyle(style: .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 delims.count == 2 {
let inner = MTInner()
inner.leftBoundary = Self.boundary(forDelimiter: delims[0])
inner.rightBoundary = Self.boundary(forDelimiter: delims[1])
inner.innerList = MTMathList(atoms: [table])
return inner
} else {
return table
}
} else if env == "eqalign" || env == "split" || env == "aligned" {
if table.numColumns != 2 {
let message = "\(env) environment can only have 2 columns"
if error == nil {
error = NSError(domain: MTParseError, code: MTParseErrors.invalidNumColumns.rawValue, userInfo: [NSLocalizedDescriptionKey:message])
}
return nil
}
let spacer = MTMathAtom(type: .ordinary, value: "")
for i in 0..<table.cells.count {
if table.cells[i].count >= 1 {
table.cells[i][1].insert(spacer, at: 0)
}
}
table.interRowAdditionalSpacing = 1
table.interColumnSpacing = 0
table.set(alignment: .right, forColumn: 0)
table.set(alignment: .left, forColumn: 1)
return table
} else if env == "displaylines" || env == "gather" {
if table.numColumns != 1 {
let message = "\(env) environment can only have 1 column"
if error == nil {
error = NSError(domain: MTParseError, code: MTParseErrors.invalidNumColumns.rawValue, userInfo: [NSLocalizedDescriptionKey:message])
}
return nil
}
table.interRowAdditionalSpacing = 1
table.interColumnSpacing = 0
table.set(alignment: .center, forColumn: 0)
return table
} else if env == "eqnarray" {
if table.numColumns != 3 {
let message = "\(env) environment can only have 3 columns"
if error == nil {
error = NSError(domain: MTParseError, code: MTParseErrors.invalidNumColumns.rawValue, userInfo: [NSLocalizedDescriptionKey:message])
}
return nil
}
table.interRowAdditionalSpacing = 1
table.interColumnSpacing = 18
table.set(alignment: .right, forColumn: 0)
table.set(alignment: .center, forColumn: 1)
table.set(alignment: .left, forColumn: 2)
return table
} else if env == "cases" {
if table.numColumns != 2 {
let message = "cases environment can only have 2 columns"
if error == nil {
error = NSError(domain: MTParseError, code: MTParseErrors.invalidNumColumns.rawValue, userInfo: [NSLocalizedDescriptionKey:message])
}
return nil
}
table.interRowAdditionalSpacing = 0
table.interColumnSpacing = 18
table.set(alignment: .left, forColumn: 0)
table.set(alignment: .left, forColumn: 1)
let style = MTMathStyle(style: .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 = MTInner()
inner.leftBoundary = Self.boundary(forDelimiter: "{")
inner.rightBoundary = Self.boundary(forDelimiter: ".")
let space = Self.atom(forLatexSymbol: ",")!
inner.innerList = MTMathList(atoms: [space, table])
return inner
} else {
let message = "Unknown environment \(env)"
error = NSError(domain: MTParseError, code: MTParseErrors.invalidEnv.rawValue, userInfo: [NSLocalizedDescriptionKey:message])
return nil
}
}
return nil
}
}

View File

@@ -0,0 +1,848 @@
//
// MTMathList.swift
// MathRenderSwift
//
// Created by Mike Griebling on 2022-12-31.
//
import Foundation
// type defines spacing and how it is rendered
public enum MTMathAtomType: Int, CustomStringConvertible, Comparable {
case ordinary = 1 // number or text
case number // number
case variable // text in italic
case largeOperator // sin/cos, integral
case binaryOperator // \bin
case unaryOperator //
case relation // = < >
case open // open bracket
case close // close bracket
case fraction // \frac
case radical // \sqrt
case punctuation // ,
case placeholder // inner atom
case inner // embedded list
case underline // underlined atom
case overline // overlined atom
case accent // accented atom
// these atoms do not support subscripts/superscripts:
case boundary = 101
case space = 201
// Denotes style changes during randering
case style
case color
case colorBox
case table = 1001
func isNotBinaryOperator() -> Bool {
switch self {
case .binaryOperator, .relation, .open, .punctuation, .largeOperator: return true
default: return false
}
}
func isScriptAllowed() -> Bool { self < .boundary }
// we want string representations to be capitalized
public 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 .colorBox: return "Colorbox"
case .table: return "Table"
}
}
// comparable support
public static func < (lhs: MTMathAtomType, rhs: MTMathAtomType) -> Bool { lhs.rawValue < rhs.rawValue }
}
public enum MTFontStyle:Int {
/// The default latex rendering style. i.e. variables are italic and numbers are roman.
case defaultStyle = 0,
/// Roman font style i.e. \mathrm
roman,
/// Bold font style i.e. \mathbf
bold,
/// Caligraphic font style i.e. \mathcal
caligraphic,
/// Typewriter (monospace) style i.e. \mathtt
typewriter,
/// Italic style i.e. \mathit
italic,
/// San-serif font i.e. \mathss
sansSerif,
/// Fractur font i.e \mathfrak
fraktur,
/// Blackboard font i.e. \mathbb
blackboard,
/// Bold italic
boldItalic
}
// MARK: - MTMathAtom
public class MTMathAtom: NSObject {
public var type = MTMathAtomType.ordinary
public var subScript: MTMathList? {
didSet {
if subScript != nil && !self.isScriptAllowed() {
subScript = nil
NSException(name: NSExceptionName(rawValue: "Error"), reason: "Subscripts not allowed for atom of type \(self.type)").raise()
}
}
}
public var superScript: MTMathList? {
didSet {
if superScript != nil && !self.isScriptAllowed() {
superScript = nil
NSException(name: NSExceptionName(rawValue: "Error"), reason: "Superscripts not allowed for atom of type \(self.type)").raise()
}
}
}
public var nucleus: String = ""
public var indexRange = NSRange(location: 0, length: 0) // indexRange in list that this atom tracks:
var fontStyle: MTFontStyle = .defaultStyle
var fusedAtoms = [MTMathAtom]() // atoms that fused to create this one
init(_ atom:MTMathAtom?) {
guard let atom = atom else { return }
self.type = atom.type
self.nucleus = atom.nucleus
self.subScript = MTMathList(atom.subScript)
self.superScript = MTMathList(atom.superScript)
self.indexRange = atom.indexRange
self.fontStyle = atom.fontStyle
self.fusedAtoms = atom.fusedAtoms
}
override init() { }
init(type:MTMathAtomType, value:String) {
self.type = type
self.nucleus = type == .radical ? "" : value
}
public func copy() -> MTMathAtom {
switch self.type {
case .largeOperator:
return MTLargeOperator(self as? MTLargeOperator)
case .fraction:
return MTFraction(self as? MTFraction)
case .radical:
return MTRadical(self as? MTRadical)
case .style:
return MTMathStyle(self as? MTMathStyle)
case .inner:
return MTInner(self as? MTInner)
case .underline:
return MTUnderLine(self as? MTUnderLine)
case .overline:
return MTOverLine(self as? MTOverLine)
case .accent:
return MTAccent(self as? MTAccent)
case .space:
return MTMathSpace(self as? MTMathSpace)
case .color:
return MTMathColor(self as? MTMathColor)
case .colorBox:
return MTMathColorbox(self as? MTMathColorbox)
case .table:
return MTMathTable(self as! MTMathTable)
default:
return MTMathAtom(self)
}
}
public override var description: String {
var string = ""
string += self.nucleus
if self.superScript != nil {
string += "^{\(self.superScript!.description)}"
}
if self.subScript != nil {
string += "_{\(self.subScript!.description)}"
}
return string
}
public var finalized: MTMathAtom {
let finalized : MTMathAtom = self.copy()
finalized.superScript = finalized.superScript?.finalized
finalized.subScript = finalized.subScript?.finalized
return finalized
}
public var string:String {
var str = self.nucleus
if let superScript = self.superScript {
str.append("^{\(superScript.string)}")
}
if let subScript = self.subScript {
str.append("_{\(subScript.string)}")
}
return str
}
func fuse(with atom: MTMathAtom) {
assert(self.subScript == nil, "Cannot fuse into an atom which has a subscript: \(self)");
assert(self.superScript == nil, "Cannot fuse into an atom which has a superscript: \(self)");
assert(atom.type == self.type, "Only atoms of the same type can be fused. \(self), \(atom)");
guard self.subScript == nil, self.superScript == nil, self.type == atom.type
else { print("Can't fuse these 2 atoms"); return }
// Update the fused atoms list
if self.fusedAtoms.isEmpty {
self.fusedAtoms.append(MTMathAtom(self))
}
if atom.fusedAtoms.count > 0 {
self.fusedAtoms.append(contentsOf: atom.fusedAtoms)
} else {
self.fusedAtoms.append(atom)
}
// Update nucleus:
self.nucleus += atom.nucleus
// Update range:
self.indexRange.length += atom.indexRange.length
// Update super/subscript:
self.superScript = atom.superScript
self.subScript = atom.subScript
}
func isScriptAllowed() -> Bool { self.type.isScriptAllowed() }
func isNotBinaryOperator() -> Bool { self.type.isNotBinaryOperator() }
}
func isNotBinaryOperator(_ prevNode:MTMathAtom?) -> Bool {
guard let prevNode = prevNode else { return true }
return prevNode.type.isNotBinaryOperator()
}
// MARK: - MTFraction
public class MTFraction: MTMathAtom {
public var hasRule: Bool = true
public var leftDelimiter: String?
public var rightDelimiter: String?
public var numerator: MTMathList?
public var denominator: MTMathList?
init(_ frac: MTFraction?) {
super.init(frac)
self.type = .fraction
self.numerator = MTMathList(frac!.numerator)
self.denominator = MTMathList(frac!.denominator)
self.hasRule = frac!.hasRule
self.leftDelimiter = frac!.leftDelimiter
self.rightDelimiter = frac!.rightDelimiter
}
init(hasRule rule:Bool = true) {
super.init()
self.type = .fraction
self.hasRule = rule
}
override public var description: String {
var string = ""
if self.hasRule {
string += "\\atop"
} else {
string += "\\frac"
}
if self.leftDelimiter != nil {
string += "[\(self.leftDelimiter!)]"
}
if self.rightDelimiter != nil {
string += "[\(self.rightDelimiter!)]"
}
string += "{\(self.numerator?.description ?? "placeholder")}{\(self.denominator?.description ?? "placeholder")}"
if self.superScript != nil {
string += "^{\(self.superScript!.description)}"
}
if self.subScript != nil {
string += "_{\(self.subScript!.description)}"
}
return string
}
override public var finalized: MTMathAtom {
let newFrac = super.finalized as! MTFraction
newFrac.numerator = newFrac.numerator?.finalized
newFrac.denominator = newFrac.denominator?.finalized
return newFrac
}
}
// MARK: - MTRadical
public class MTRadical: MTMathAtom {
// Under the roof
public var radicand: MTMathList?
// Value on radical sign
public var degree: MTMathList?
init(_ rad:MTRadical?) {
super.init(rad)
self.type = .radical
self.radicand = MTMathList(rad?.radicand)
self.degree = MTMathList(rad?.degree)
self.nucleus = ""
}
override init() {
super.init()
self.type = .radical
self.nucleus = ""
}
override public var description: String {
var string = "\\sqrt"
if self.degree != nil {
string += "[\(self.degree!.description)]"
}
if self.radicand != nil {
string += "{\(self.radicand?.description ?? "placeholder")}"
}
if self.superScript != nil {
string += "^{\(self.superScript!.description)}"
}
if self.subScript != nil {
string += "_{\(self.subScript!.description)}"
}
return string
}
override public var finalized: MTMathAtom {
let newRad = super.finalized as! MTRadical
newRad.radicand = newRad.radicand?.finalized
newRad.degree = newRad.degree?.finalized
return newRad
}
}
// MARK: - MTLargeOperator
public class MTLargeOperator: MTMathAtom {
public var limits: Bool = false
init(_ op:MTLargeOperator?) {
super.init(op)
self.type = .largeOperator
self.limits = op!.limits
}
init(value: String, limits: Bool) {
super.init(type: .largeOperator, value: value)
self.limits = limits
}
}
// MARK: - MTInner
public class MTInner: MTMathAtom {
public var innerList: MTMathList?
public var leftBoundary: MTMathAtom? {
didSet {
if leftBoundary != nil && leftBoundary!.type != .boundary {
leftBoundary = nil
NSException(name: NSExceptionName(rawValue: "Error"), reason: "Left boundary must be of type .boundary").raise()
}
}
}
public var rightBoundary: MTMathAtom? {
didSet {
if rightBoundary != nil && rightBoundary!.type != .boundary {
rightBoundary = nil
NSException(name: NSExceptionName(rawValue: "Error"), reason: "Right boundary must be of type .boundary").raise()
}
}
}
init(_ inner:MTInner?) {
super.init(inner)
self.type = .inner
self.innerList = MTMathList(inner?.innerList)
self.leftBoundary = MTMathAtom(inner?.leftBoundary)
self.rightBoundary = MTMathAtom(inner?.rightBoundary)
}
override init() {
super.init()
self.type = .inner
}
override public var description: String {
var string = "\\inner"
if self.leftBoundary != nil {
string += "[\(self.leftBoundary!.nucleus)]"
}
string += "{\(self.innerList!.description)}"
if self.rightBoundary != nil {
string += "[\(self.rightBoundary!.nucleus)]"
}
if self.superScript != nil {
string += "^{\(self.superScript!.description)}"
}
if self.subScript != nil {
string += "_{\(self.subScript!.description)}"
}
return string
}
override public var finalized: MTMathAtom {
let newInner = super.finalized as! MTInner
newInner.innerList = newInner.innerList?.finalized
return newInner
}
}
// MARK: - MTOverLIne
public class MTOverLine: MTMathAtom {
public var innerList: MTMathList?
override public var finalized: MTMathAtom {
let newOverline = MTOverLine(self)
newOverline.innerList = newOverline.innerList?.finalized
return newOverline
}
init(_ over: MTOverLine?) {
super.init(over)
self.type = .overline
self.innerList = MTMathList(over!.innerList)
}
override init() {
super.init()
self.type = .overline
}
}
// MARK: - MTUnderLine
public class MTUnderLine: MTMathAtom {
public var innerList: MTMathList?
override public var finalized: MTMathAtom {
let newUnderline = super.finalized as! MTUnderLine
newUnderline.innerList = newUnderline.innerList?.finalized
return newUnderline
}
init(_ under: MTUnderLine?) {
super.init(under)
self.type = .underline
self.innerList = MTMathList(under?.innerList)
}
override init() {
super.init()
self.type = .underline
}
}
// MARK: - MTAccent
public class MTAccent: MTMathAtom {
public var innerList: MTMathList?
override public var finalized: MTMathAtom {
let newAccent = super.finalized as! MTAccent
newAccent.innerList = newAccent.innerList?.finalized
return newAccent
}
init(_ accent: MTAccent?) {
super.init(accent)
self.type = .accent
self.innerList = MTMathList(accent?.innerList)
}
init(value: String) {
super.init()
self.type = .accent
self.nucleus = value
}
}
// MARK: - MTMathSpace
public class MTMathSpace: MTMathAtom {
public var space: CGFloat = 0
init(_ space: MTMathSpace?) {
super.init(space)
self.type = .space
self.space = space?.space ?? 0
}
init(space:CGFloat) {
super.init()
self.type = .space
self.space = space
}
}
public enum MTLineStyle:Int, Comparable {
case display
case text
case script
case scriptOfScript
public func inc() -> MTLineStyle {
let raw = self.rawValue + 1
if let style = MTLineStyle(rawValue: raw) { return style }
return .display
}
public var isNotScript:Bool { self < .script }
public static func < (lhs: MTLineStyle, rhs: MTLineStyle) -> Bool { lhs.rawValue < rhs.rawValue }
}
// MARK: - MTMathStyle
public class MTMathStyle: MTMathAtom {
public var style: MTLineStyle = .display
init(_ style:MTMathStyle?) {
super.init(style)
self.type = .style
self.style = style!.style
}
init(style:MTLineStyle) {
super.init()
self.type = .style
self.style = style
}
}
// MARK: - MTMathColor
public class MTMathColor: MTMathAtom {
public var colorString:String=""
public var innerList:MTMathList?
init(_ color: MTMathColor?) {
super.init(color)
self.type = .color
self.colorString = color?.colorString ?? ""
self.innerList = MTMathList(color?.innerList)
}
override init() {
super.init()
self.type = .color
}
public override var string: String {
"\\color{\(self.colorString)}{\(self.innerList!.string)}"
}
override public var finalized: MTMathAtom {
let newColor = super.finalized as! MTMathColor
newColor.innerList = newColor.innerList?.finalized
return newColor
}
}
// MARK: - MTMathColorbox
public class MTMathColorbox: MTMathAtom {
public var colorString:String=""
public var innerList:MTMathList?
init(_ cbox: MTMathColorbox?) {
super.init(cbox)
self.type = .colorBox
self.colorString = cbox?.colorString ?? ""
self.innerList = MTMathList(cbox?.innerList)
}
override init() {
super.init()
self.type = .colorBox
}
public override var string: String {
"\\colorbox{\(self.colorString)}{\(self.innerList!.string)}"
}
override public var finalized: MTMathAtom {
let newColor = super.finalized as! MTMathColorbox
newColor.innerList = newColor.innerList?.finalized
return newColor
}
}
public enum MTColumnAlignment {
case left
case center
case right
}
// MARK: - MTMathTable
public class MTMathTable: MTMathAtom {
public var alignments = [MTColumnAlignment]()
public var cells = [[MTMathList]]()
public var environment: String?
public var interColumnSpacing: CGFloat = 0
public var interRowAdditionalSpacing: CGFloat = 0
override public var finalized: MTMathAtom {
let table = super.finalized as! MTMathTable
for var row in table.cells {
for i in 0..<row.count {
row[i] = row[i].finalized
}
}
return table
}
init(environment: String?) {
super.init()
self.type = .table
self.environment = environment
}
init(_ table:MTMathTable) {
super.init(table)
self.type = .table
self.alignments = table.alignments
self.interRowAdditionalSpacing = table.interRowAdditionalSpacing
self.interColumnSpacing = table.interColumnSpacing
self.environment = table.environment
var cellCopy = [[MTMathList]]()
for row in table.cells {
var newRow = [MTMathList]()
for col in row {
newRow.append(MTMathList(col)!)
}
cellCopy.append(newRow)
}
self.cells = cellCopy
}
override init() {
super.init()
self.type = .table
}
public func set(cell list: MTMathList, forRow row:Int, column:Int) {
if self.cells.count <= row {
for _ in self.cells.count...row {
self.cells.append([])
}
}
let rows = self.cells[row].count
if rows <= column {
for _ in rows...column {
self.cells[row].append(MTMathList())
}
}
self.cells[row][column] = list
}
public func set(alignment: MTColumnAlignment, forColumn col: Int) {
if self.alignments.count <= col {
for _ in self.alignments.count...col {
self.alignments.append(MTColumnAlignment.center)
}
}
self.alignments[col] = alignment
}
public func get(alignmentForColumn col: Int) -> MTColumnAlignment {
if self.alignments.count <= col {
return MTColumnAlignment.center
} else {
return self.alignments[col]
}
}
public var numColumns: Int {
var numberOfCols = 0
for row in self.cells {
numberOfCols = max(numberOfCols, row.count)
}
return numberOfCols
}
public var numRows: Int { self.cells.count }
}
// MARK: - MTMathList
// represent list of math objects
extension MTMathList {
public override var description: String { self.atoms.description }
public var string: String { self.description }
}
public class MTMathList : NSObject {
init?(_ list:MTMathList?) {
guard let list = list else { return nil }
for atom in list.atoms {
self.atoms.append(atom.copy())
}
}
public var atoms = [MTMathAtom]()
public var finalized: MTMathList {
let finalizedList = MTMathList()
let zeroRange = NSMakeRange(0, 0)
var prevNode: MTMathAtom? = nil
for atom in self.atoms {
let newNode = atom.finalized
if NSEqualRanges(zeroRange, atom.indexRange) {
let index = prevNode == nil ? 0 : prevNode!.indexRange.location + prevNode!.indexRange.length
newNode.indexRange = NSMakeRange(index, 1)
}
switch newNode.type {
case .binaryOperator:
if isNotBinaryOperator(prevNode) {
newNode.type = .unaryOperator
}
case .relation, .punctuation, .close:
if prevNode != nil && prevNode!.type == .binaryOperator {
prevNode!.type = .unaryOperator
}
case .number:
if prevNode != nil && prevNode!.type == .number && prevNode!.subScript == nil && prevNode!.superScript == nil {
prevNode!.fuse(with: newNode)
continue // skip the current node, we are done here.
}
default: break
}
finalizedList.add(newNode)
prevNode = newNode
}
if prevNode != nil && prevNode!.type == .binaryOperator {
prevNode!.type = .unaryOperator
}
return finalizedList
}
public init(atoms: [MTMathAtom]) {
self.atoms.append(contentsOf: atoms)
}
public init(atom: MTMathAtom) {
self.atoms.append(atom)
}
public override init() { super.init() }
func NSParamException(_ param:Any?) {
if param == nil {
NSException(name: NSExceptionName(rawValue: "Error"), reason: "Parameter cannot be nil").raise()
}
}
func NSIndexException(_ array:[Any], index: Int) {
guard !array.indices.contains(index) else { return }
NSException(name: NSExceptionName(rawValue: "Error"), reason: "Index \(index) out of bounds").raise()
}
func add(_ atom: MTMathAtom?) {
guard let atom = atom else { return }
if self.isAtomAllowed(atom) {
self.atoms.append(atom)
} else {
NSException(name: NSExceptionName(rawValue: "Error"), reason: "Cannot add atom of type \(atom.type.rawValue) into mathlist").raise()
}
}
func insert(_ atom: MTMathAtom?, at index: Int) {
// NSParamException(atom)
guard let atom = atom else { return }
guard self.atoms.indices.contains(index) || index == self.atoms.endIndex else { return }
// guard self.atoms.endIndex >= index else { NSIndexException(); return }
if self.isAtomAllowed(atom) {
// NSIndexException(self.atoms, index: index)
self.atoms.insert(atom, at: index)
} else {
NSException(name: NSExceptionName(rawValue: "Error"), reason: "Cannot add atom of type \(atom.type.rawValue) into mathlist").raise()
}
}
func append(_ list: MTMathList?) {
guard let list = list else { return }
self.atoms += list.atoms
}
func removeLastAtom() {
if !self.atoms.isEmpty {
self.atoms.removeLast()
}
}
func removeAtom(at index: Int) {
NSIndexException(self.atoms, index:index)
self.atoms.remove(at: index)
}
func removeAtoms(in range: ClosedRange<Int>) {
NSIndexException(self.atoms, index: range.lowerBound)
NSIndexException(self.atoms, index: range.upperBound)
self.atoms.removeSubrange(range)
}
func isAtomAllowed(_ atom: MTMathAtom?) -> Bool { atom?.type != .boundary }
}

View File

@@ -0,0 +1,940 @@
//
// MTMathListBuilder.swift
// MathRenderSwift
//
// Created by Mike Griebling on 2022-12-31.
//
import Foundation
/** `MTMathListBuilder` is a class for parsing LaTeX into an `MTMathList` that
can be rendered and processed mathematically.
*/
struct MTEnvProperties {
var envName: String?
var ended: Bool
var numRows: Int
init(name: String?) {
self.envName = name
self.numRows = 0
self.ended = false
}
}
/**
@typedef case s
@brief The error encountered when parsing a LaTeX string.
The `code` in the `NSError` is one of the following indiciating why the LaTeX string
could not be parsed.
*/
enum MTParseErrors:Int {
/// The braces { } do not match.
case mismatchBraces = 1
/// A command in the string is not recognized.
case invalidCommand
/// An expected character such as ] was not found.
case characterNotFound
/// The \left or \right command was not followed by a delimiter.
case missingDelimiter
/// The delimiter following \left or \right was not a valid delimiter.
case invalidDelimiter
/// There is no \right corresponding to the \left command.
case missingRight
/// There is no \left corresponding to the \right command.
case missingLeft
/// The environment given to the \begin command is not recognized
case invalidEnv
/// A command is used which is only valid inside a \begin,\end environment
case missingEnv
/// There is no \begin corresponding to the \end command.
case missingBegin
/// There is no \end corresponding to the \begin command.
case missingEnd
/// The number of columns do not match the environment
case invalidNumColumns
/// Internal error, due to a programming mistake.
case internalError
/// Limit control applied incorrectly
case invalidLimits
}
let MTParseError = "ParseError"
public class MTMathListBuilder {
var string: String
var currentCharIndex: String.Index
var currentInnerAtom: MTInner?
var currentEnv: MTEnvProperties?
var currentFontStyle:MTFontStyle
var spacesAllowed:Bool
/** Contains any error that occurred during parsing. */
var error:NSError?
var hasCharacters: Bool { currentCharIndex < string.endIndex }
public static let spaceToCommands: [CGFloat: String] = [
3 : ",",
4 : ">",
5 : ";",
(-3) : "!",
18 : "quad",
36 : "qquad",
]
public static let styleToCommands: [MTLineStyle: String] = [
.display: "displaystyle",
.text: "textstyle",
.script: "scriptstyle",
.scriptOfScript: "scriptscriptstyle"
]
init(string: String) {
self.error = nil
self.string = string
self.currentCharIndex = string.startIndex
self.currentFontStyle = .defaultStyle
self.spacesAllowed = false
}
public func build() -> MTMathList? {
let list = self.buildInternal(false)
if self.hasCharacters && error == nil {
self.setError(.mismatchBraces, message: "Mismatched braces: \(self.string)")
return nil
}
if error != nil {
return nil
}
return list
}
public static func build(fromString string: String) -> MTMathList? {
let builder = MTMathListBuilder(string: string)
return builder.build()
}
public static func build(fromString string: String, error:inout NSError?) -> MTMathList? {
let builder = MTMathListBuilder(string: string)
let output = builder.build()
if builder.error != nil {
error = builder.error
return nil
}
return output
}
public static func mathListToString(_ ml: MTMathList?) -> String {
var str = ""
var currentfontStyle = MTFontStyle.defaultStyle
if let atomList = ml {
for atom in atomList.atoms {
if currentfontStyle != atom.fontStyle {
if currentfontStyle != .defaultStyle {
str += "}"
}
if atom.fontStyle != .defaultStyle {
let fontStyleName = MTMathAtomFactory.fontNameForStyle(atom.fontStyle)
str += "\\\(fontStyleName){"
}
currentfontStyle = atom.fontStyle
}
if atom.type == .fraction {
if let frac = atom as? MTFraction {
if frac.hasRule {
str += "\\frac{\(mathListToString(frac.numerator!))}{\(mathListToString(frac.denominator!))}"
} else {
var command: String? = nil
if frac.leftDelimiter == nil && frac.rightDelimiter == nil {
command = "atop"
} else if frac.leftDelimiter == "(" && frac.rightDelimiter == ")" {
command = "choose"
} else if frac.leftDelimiter == "{" && frac.rightDelimiter == "}" {
command = "brace"
} else if frac.leftDelimiter == "[" && frac.rightDelimiter == "]" {
command = "brack"
} else {
command = "atopwithdelims\(frac.leftDelimiter!)\(frac.rightDelimiter!)"
}
str += "{\(mathListToString(frac.numerator!)) \\\(command!) \(mathListToString(frac.denominator!))}"
}
}
} else if atom.type == .radical {
str += "\\sqrt"
if let rad = atom as? MTRadical {
if rad.degree != nil {
str += "[\(mathListToString(rad.degree!))]"
}
str += "{\(mathListToString(rad.radicand!))}"
}
} else if atom.type == .inner {
if let inner = atom as? MTInner {
if inner.leftBoundary != nil || inner.rightBoundary != nil {
if inner.leftBoundary != nil {
str += "\\left\(delimToString(delim: inner.leftBoundary!)) "
} else {
str += "\\left. "
}
str += mathListToString(inner.innerList!)
if inner.rightBoundary != nil {
str += "\\right\(delimToString(delim: inner.rightBoundary!)) "
} else {
str += "\\right. "
}
} else {
str += "{\(mathListToString(inner.innerList!))}"
}
}
} else if atom.type == .table {
if let table = atom as? MTMathTable {
if table.environment != nil {
str += "\\begin{\(table.environment!)}"
}
for i in 0..<table.numRows {
let row = table.cells[i]
for j in 0..<row.count {
let cell = row[j]
if table.environment == "matrix" {
if cell.atoms.count >= 1 && cell.atoms[0].type == .style {
// remove first atom
cell.atoms.removeFirst()
}
}
if table.environment == "eqalign" || table.environment == "aligned" || table.environment == "split" {
if j == 1 && cell.atoms.count >= 1 && cell.atoms[0].type == .ordinary && cell.atoms[0].nucleus.count == 0 {
// remove empty nucleus added for spacing
cell.atoms.removeFirst()
}
}
str += mathListToString(cell)
if j < row.count - 1 {
str += "&"
}
}
if i < table.numRows - 1 {
str += "\\\\ "
}
}
if table.environment != nil {
str += "\\end{\(table.environment!)}"
}
}
} else if atom.type == .overline {
if let overline = atom as? MTOverLine {
str += "\\overline"
str += "{\(mathListToString(overline.innerList!))}"
}
} else if atom.type == .underline {
if let underline = atom as? MTUnderLine {
str += "\\underline"
str += "{\(mathListToString(underline.innerList!))}"
}
} else if atom.type == .accent {
if let accent = atom as? MTAccent {
str += "\\\(MTMathAtomFactory.accentName(accent)!){\(mathListToString(accent.innerList!))}"
}
} else if atom.type == .largeOperator {
let op = atom as! MTLargeOperator
let command = MTMathAtomFactory.latexSymbolName(for: atom)
let originalOp = MTMathAtomFactory.atom(forLatexSymbol: command!) as! MTLargeOperator
str += "\\\(command!) "
if originalOp.limits != op.limits {
if op.limits {
str += "\\limits "
} else {
str += "\\nolimits "
}
}
} else if atom.type == .space {
if let space = atom as? MTMathSpace {
if let command = MTMathListBuilder.spaceToCommands[space.space] {
str += "\\\(command) "
} else {
str += String(format: "\\mkern%.1fmu", space.space)
}
}
} else if atom.type == .style {
if let style = atom as? MTMathStyle {
if let command = MTMathListBuilder.styleToCommands[style.style] {
str += "\\\(command) "
}
}
} else if atom.nucleus.isEmpty {
str += "{}"
} else if atom.nucleus == "\u{2236}" {
// math colon
str += ":"
} else if atom.nucleus == "\u{2212}" {
// math minus
str += "-"
} else {
if let command = MTMathAtomFactory.latexSymbolName(for: atom) {
str += "\\\(command) "
} else {
str += "\(atom.nucleus)"
}
}
if atom.superScript != nil {
str += "^{\(mathListToString(atom.superScript!))}"
}
if atom.subScript != nil {
str += "_{\(mathListToString(atom.subScript!))}"
}
}
}
if currentfontStyle != .defaultStyle {
str += "}"
}
return str
}
public static func delimToString(delim: MTMathAtom) -> String {
if let command = MTMathAtomFactory.getDelimiterName(of: delim) {
let singleChars = [ "(", ")", "[", "]", "<", ">", "|", ".", "/"]
if singleChars.contains(command) {
return command
} else if command == "||" {
return "\\|"
} else {
return "\\\(command)"
}
}
return ""
}
func getNextCharacter() -> Character {
assert(self.hasCharacters, "Retrieving character at index \(self.currentCharIndex) beyond length \(self.string.count)")
let ch = string[currentCharIndex]
currentCharIndex = string.index(after: currentCharIndex)
return ch
}
func unlookCharacter() {
assert(currentCharIndex > string.startIndex, "Unlooking when at the first character.")
if currentCharIndex > string.startIndex {
currentCharIndex = string.index(before: currentCharIndex)
} else {
print("unlooking at first character")
}
}
public func buildInternal(_ oneCharOnly: Bool) -> MTMathList? {
return self.buildInternal(oneCharOnly, stopChar: nil)
}
public func buildInternal(_ oneCharOnly: Bool, stopChar stop: Character?) -> MTMathList? {
let list = MTMathList()
assert(!(oneCharOnly && stop != nil), "Cannot set both oneCharOnly and stopChar.")
var prevAtom: MTMathAtom? = nil
while self.hasCharacters {
if error != nil { return nil }
var atom: MTMathAtom? = nil
let char = self.getNextCharacter()
if oneCharOnly {
if char == "^" || char == "}" || char == "_" || char == "&" {
self.unlookCharacter()
return list
}
}
if stop != nil && char == stop! {
return list
}
if char == "^" {
assert(!oneCharOnly, "This should have been handled before")
if (prevAtom == nil || prevAtom!.superScript != nil || !prevAtom!.isScriptAllowed()) {
// If there is no previous atom, or if it already has a superscript
// or if scripts are not allowed for it, then add an empty node.
prevAtom = MTMathAtom(type: .ordinary, value: "")
list.add(prevAtom!)
}
prevAtom!.superScript = self.buildInternal(true)
continue
} else if char == "_" {
assert(!oneCharOnly, "This should have been handled before")
if (prevAtom == nil || prevAtom!.subScript != nil || !prevAtom!.isScriptAllowed()) {
// If there is no previous atom, or if it already has a subcript
// or if scripts are not allowed for it, then add an empty node.
prevAtom = MTMathAtom(type: .ordinary, value: "")
list.add(prevAtom!)
}
prevAtom!.subScript = self.buildInternal(true)
continue
} else if char == "{" {
// this puts us in a recursive routine, and sets oneCharOnly to false and no stop character
let subList = self.buildInternal(false, stopChar: "}")
prevAtom = subList!.atoms.last
list.append(subList)
if oneCharOnly {
return list
}
continue
} else if char == "}" {
assert(!oneCharOnly, "This should have been handled before")
assert(stop == nil, "This should have been handled before")
// We encountered a closing brace when there is no stop set, that means there was no
// corresponding opening brace.
self.setError(.mismatchBraces, message:"Mismatched braces.")
return nil
} else if char == "\\" {
let command = readCommand()
let done = stopCommand(command, list:list, stopChar:stop)
if done != nil {
return done
} else if error != nil {
return nil
}
if self.applyModifier(command, atom:prevAtom) {
continue
}
if let fontStyle = MTMathAtomFactory.fontStyleWithName(command) {
let oldSpacesAllowed = spacesAllowed
// Text has special consideration where it allows spaces without escaping.
spacesAllowed = command == "text"
let oldFontStyle = currentFontStyle
currentFontStyle = fontStyle
let sublist = self.buildInternal(true)!
// Restore the font style.
currentFontStyle = oldFontStyle
spacesAllowed = oldSpacesAllowed
prevAtom = sublist.atoms.last
list.append(sublist)
if oneCharOnly {
return list
}
continue
}
atom = self.atomForCommand(command)
if atom == nil {
// this was an unknown command,
// we flag an error and return
// (note setError will not set the error if there is already one, so we flag internal error
// in the odd case that an _error is not set.
self.setError(.internalError, message:"Internal error")
return nil
}
} else if char == "&" {
assert(!oneCharOnly, "This should have been handled before")
if self.currentEnv != nil {
return list
} else {
let table = self.buildTable(env: nil, firstList: list, isRow: false)
return MTMathList(atom: table!)
}
} else if spacesAllowed && char == " " {
atom = MTMathAtomFactory.atom(forLatexSymbol: " ")
} else {
atom = MTMathAtomFactory.atom(forCharacter: char)
if atom == nil {
continue
}
}
assert(atom != nil, "Atom shouldn't be nil")
atom?.fontStyle = currentFontStyle
list.add(atom)
prevAtom = atom
if oneCharOnly {
return list
}
}
if stop != nil {
if stop == "}" {
// We did not find a corresponding closing brace.
self.setError(.mismatchBraces, message:"Missing closing brace")
} else {
// we never found our stop character
let errorMessage = "Expected character not found: \(stop!)"
self.setError(.characterNotFound, message:errorMessage)
}
}
return list
}
func atomForCommand(_ command:String) -> MTMathAtom? {
if let atom = MTMathAtomFactory.atom(forLatexSymbol: command) {
return atom
}
if let accent = MTMathAtomFactory.accent(withName: command) {
// The command is an accent
accent.innerList = self.buildInternal(true)
return accent;
} else if command == "frac" {
// A fraction command has 2 arguments
let frac = MTFraction()
frac.numerator = self.buildInternal(true)
frac.denominator = self.buildInternal(true)
return frac;
} else if command == "binom" {
// A binom command has 2 arguments
let frac = MTFraction(hasRule: false)
frac.numerator = self.buildInternal(true)
frac.denominator = self.buildInternal(true)
frac.leftDelimiter = "(";
frac.rightDelimiter = ")";
return frac;
} else if command == "sqrt" {
// A sqrt command with one argument
let rad = MTRadical()
let ch = self.getNextCharacter()
if (ch == "[") {
// special handling for sqrt[degree]{radicand}
rad.degree = self.buildInternal(false, stopChar:"]")
rad.radicand = self.buildInternal(true)
} else {
self.unlookCharacter()
rad.radicand = self.buildInternal(true)
}
return rad;
} else if command == "left" {
// Save the current inner while a new one gets built.
let oldInner = currentInnerAtom
currentInnerAtom = MTInner()
currentInnerAtom!.leftBoundary = self.getBoundaryAtom("left")
if currentInnerAtom!.leftBoundary == nil {
return nil;
}
currentInnerAtom!.innerList = self.buildInternal(false)
if currentInnerAtom!.rightBoundary == nil {
// A right node would have set the right boundary so we must be missing the right node.
let errorMessage = "Missing \\right"
self.setError(.missingRight, message:errorMessage)
return nil
}
// reinstate the old inner atom.
let newInner = currentInnerAtom;
currentInnerAtom = oldInner;
return newInner;
} else if command == "overline" {
// The overline command has 1 arguments
let over = MTOverLine()
over.innerList = self.buildInternal(true)
return over
} else if command == "underline" {
// The underline command has 1 arguments
let under = MTUnderLine()
under.innerList = self.buildInternal(true)
return under
} else if command == "begin" {
let env = self.readEnvironment()
if env == nil {
return nil;
}
let table = self.buildTable(env: env, firstList:nil, isRow:false)
return table
} else if command == "color" {
// A color command has 2 arguments
let mathColor = MTMathColor()
mathColor.colorString = self.readColor()!
mathColor.innerList = self.buildInternal(true)
return mathColor
} else if command == "colorbox" {
// A color command has 2 arguments
let mathColorbox = MTMathColorbox()
mathColorbox.colorString = self.readColor()!
mathColorbox.innerList = self.buildInternal(true)
return mathColorbox
} else {
let errorMessage = "Invalid command \\\(command)"
self.setError(.invalidCommand, message:errorMessage)
return nil;
}
}
func readColor() -> String? {
if !self.expectCharacter("{") {
// We didn't find an opening brace, so no env found.
self.setError(.characterNotFound, message:"Missing {")
return nil;
}
// Ignore spaces and nonascii.
self.skipSpaces()
// a string of all upper and lower case characters.
var mutable = ""
while self.hasCharacters {
let ch = self.getNextCharacter()
if ch == "#" || (ch >= "A" && ch <= "F") || (ch >= "a" && ch <= "f") || (ch >= "0" && ch <= "9") {
mutable.append(ch) // appendString:[NSString stringWithCharacters:&ch length:1]];
} else {
// we went too far
self.unlookCharacter()
break;
}
}
if !self.expectCharacter("}") {
// We didn't find an closing brace, so invalid format.
self.setError(.characterNotFound, message:"Missing }")
return nil;
}
return mutable;
}
func skipSpaces() {
while self.hasCharacters {
let ch = self.getNextCharacter().utf32Char
if ch < 0x21 || ch > 0x7E {
// skip non ascii characters and spaces
continue;
} else {
self.unlookCharacter()
return;
}
}
}
static var fractionCommands: [String:[Character]] {
[
"over": [],
"atop" : [],
"choose" : [ "(", ")"],
"brack" : [ "[", "]"],
"brace" : [ "{", "}"]
]
}
func stopCommand(_ command: String, list:MTMathList, stopChar:Character?) -> MTMathList? {
if command == "right" {
if currentInnerAtom == nil {
let errorMessage = "Missing \\left";
self.setError(.missingLeft, message:errorMessage)
return nil;
}
currentInnerAtom!.rightBoundary = self.getBoundaryAtom("right")
if currentInnerAtom!.rightBoundary == nil {
return nil;
}
// return the list read so far.
return list
} else if let delims = Self.fractionCommands[command] {
var frac:MTFraction! = nil;
if command == "over" {
frac = MTFraction()
} else {
frac = MTFraction(hasRule: false)
}
if delims.count == 2 {
frac.leftDelimiter = String(delims[0])
frac.rightDelimiter = String(delims[1])
}
frac.numerator = list;
frac.denominator = self.buildInternal(false, stopChar: stopChar)
if error != nil {
return nil;
}
let fracList = MTMathList()
fracList.add(frac)
return fracList
} else if command == "\\" || command == "cr" {
if currentEnv != nil {
// Stop the current list and increment the row count
currentEnv!.numRows+=1
return list
} else {
// Create a new table with the current list and a default env
let table = self.buildTable(env: nil, firstList:list, isRow:true)
return MTMathList(atom: table!)
}
} else if command == "end" {
if currentEnv == nil {
let errorMessage = "Missing \\begin";
self.setError(.missingBegin, message:errorMessage)
return nil
}
let env = self.readEnvironment()
if env == nil {
return nil
}
if env! != currentEnv!.envName {
let errorMessage = "Begin environment name \(currentEnv!.envName!) does not match end name: \(env!)"
self.setError(.invalidEnv, message:errorMessage)
return nil
}
// Finish the current environment.
currentEnv!.ended = true
return list
}
return nil
}
// Applies the modifier to the atom. Returns true if modifier applied.
func applyModifier(_ modifier:String, atom:MTMathAtom?) -> Bool {
if modifier == "limits" {
if atom?.type != .largeOperator {
let errorMessage = "Limits can only be applied to an operator."
self.setError(.invalidLimits, message:errorMessage)
} else {
let op = atom as! MTLargeOperator
op.limits = true
}
return true
} else if modifier == "nolimits" {
if atom?.type != .largeOperator {
let errorMessage = "No limits can only be applied to an operator."
self.setError(.invalidLimits, message:errorMessage)
} else {
let op = atom as! MTLargeOperator
op.limits = false
}
return true
}
return false
}
func setError(_ code:MTParseErrors, message:String) {
// Only record the first error.
if error == nil {
error = NSError(domain: MTParseError, code: code.rawValue, userInfo: [ NSLocalizedDescriptionKey : message ])
}
}
func atom(forCommand command: String) -> MTMathAtom? {
if let atom = MTMathAtomFactory.atom(forLatexSymbol: command) {
return atom
}
if let accent = MTMathAtomFactory.accent(withName: command) {
accent.innerList = self.buildInternal(true)
return accent
} else if command == "frac" {
let frac = MTFraction()
frac.numerator = self.buildInternal(true)
frac.denominator = self.buildInternal(true)
return frac
} else if command == "binom" {
let frac = MTFraction(hasRule: false)
frac.numerator = self.buildInternal(true)
frac.denominator = self.buildInternal(true)
frac.leftDelimiter = "("
frac.rightDelimiter = ")"
return frac
} else if command == "sqrt" {
let rad = MTRadical()
let char = self.getNextCharacter()
if char == "[" {
rad.degree = self.buildInternal(false, stopChar: "]")
rad.radicand = self.buildInternal(true)
} else {
self.unlookCharacter()
rad.radicand = self.buildInternal(true)
}
return rad
} else if command == "left" {
let oldInner = self.currentInnerAtom
self.currentInnerAtom = MTInner()
self.currentInnerAtom?.leftBoundary = self.getBoundaryAtom("left")
if self.currentInnerAtom?.leftBoundary == nil {
return nil
}
self.currentInnerAtom!.innerList = self.buildInternal(false)
if self.currentInnerAtom?.rightBoundary == nil {
self.setError(.missingRight, message: "Missing \\right")
return nil
}
let newInner = self.currentInnerAtom
currentInnerAtom = oldInner
return newInner
} else if command == "overline" {
let over = MTOverLine()
over.innerList = self.buildInternal(true)
return over
} else if command == "underline" {
let under = MTUnderLine()
under.innerList = self.buildInternal(true)
return under
} else if command == "begin" {
if let env = self.readEnvironment() {
let table = self.buildTable(env: env, firstList: nil, isRow: false)
return table
} else {
return nil
}
} else if command == "color" {
// A color command has 2 arguments
let mathColor = MTMathColor()
mathColor.colorString = self.readColor()!
mathColor.innerList = self.buildInternal(true)
return mathColor
} else if command == "colorbox" {
// A color command has 2 arguments
let mathColorbox = MTMathColorbox()
mathColorbox.colorString = self.readColor()!
mathColorbox.innerList = self.buildInternal(true)
return mathColorbox
} else {
self.setError(.invalidCommand, message: "Invalid command \\\(command)")
return nil
}
}
func readEnvironment() -> String? {
if !self.expectCharacter("{") {
// We didn't find an opening brace, so no env found.
self.setError(.characterNotFound, message: "Missing {")
return nil
}
self.skipSpaces()
let env = self.readString()
if !self.expectCharacter("}") {
// We didn"t find an closing brace, so invalid format.
self.setError(.characterNotFound, message: "Missing }")
return nil;
}
return env
}
func MTAssertNotSpace(_ ch: Character) {
assert(ch >= "\u{21}" && ch <= "\u{7E}", "Expected non-space character \(ch)")
}
func expectCharacter(_ ch: Character) -> Bool {
MTAssertNotSpace(ch)
self.skipSpaces()
if self.hasCharacters {
let nextChar = self.getNextCharacter()
MTAssertNotSpace(nextChar)
if nextChar == ch {
return true
} else {
self.unlookCharacter()
return false
}
}
return false
}
func buildTable(env: String?, firstList: MTMathList?, isRow: Bool) -> MTMathAtom? {
// Save the current env till an new one gets built.
let oldEnv = self.currentEnv
currentEnv = MTEnvProperties(name: env)
var currentRow = 0
var currentCol = 0
var rows = [[MTMathList]]()
rows.append([MTMathList]())
if firstList != nil {
rows[currentRow].append(firstList!)
if isRow {
currentEnv!.numRows+=1
currentRow+=1
rows.append([MTMathList]())
} else {
currentCol+=1
}
}
while !currentEnv!.ended && self.hasCharacters {
let list = self.buildInternal(false)
if list == nil {
// If there is an error building the list, bail out early.
return nil
}
rows[currentRow].append(list!)
currentCol+=1
if currentEnv!.numRows > currentRow {
currentRow = currentEnv!.numRows
rows.append([MTMathList]())
currentCol = 0
}
}
if !currentEnv!.ended && currentEnv!.envName != nil {
self.setError(.missingEnd, message: "Missing \\end")
return nil
}
var error:NSError? = self.error
let table = MTMathAtomFactory.table(withEnvironment: currentEnv?.envName, rows: rows, error: &error)
if table == nil && self.error == nil {
self.error = error
return nil
}
self.currentEnv = oldEnv
return table
}
func getBoundaryAtom(_ delimiterType: String) -> MTMathAtom? {
let delim = self.readDelimiter()
if delim == nil {
let errorMessage = "Missing delimiter for \\\(delimiterType)"
self.setError(.missingDelimiter, message:errorMessage)
return nil
}
let boundary = MTMathAtomFactory.boundary(forDelimiter: delim!)
if boundary == nil {
let errorMessage = "Invalid delimiter for \(delimiterType): \(delim!)"
self.setError(.invalidDelimiter, message:errorMessage)
return nil
}
return boundary
}
func readDelimiter() -> String? {
self.skipSpaces()
while self.hasCharacters {
let char = self.getNextCharacter()
MTAssertNotSpace(char)
if char == "\\" {
let command = self.readCommand()
if command == "|" {
return "||"
}
return command
} else {
return String(char)
}
}
return nil
}
func readCommand() -> String {
let singleChars = "{}$#%_| ,>;!\\"
if self.hasCharacters {
let char = self.getNextCharacter()
if let _ = singleChars.firstIndex(of: char) {
return String(char)
} else {
self.unlookCharacter()
}
}
return self.readString()
}
func readString() -> String {
// a string of all upper and lower case characters.
var output = ""
while self.hasCharacters {
let char = self.getNextCharacter()
if char.isLowercase || char.isUppercase {
output.append(char)
} else {
self.unlookCharacter()
break
}
}
return output
}
}

View File

@@ -0,0 +1,858 @@
//
// MTMathListDisplay.swift
// MathRenderSwift
//
// Created by Mike Griebling on 2022-12-31.
//
import Foundation
import QuartzCore
import CoreText
import SwiftUI
func isIos6Supported() -> Bool {
if !MTDisplay.initialized {
#if os(iOS)
let reqSysVer = "6.0"
let currSysVer = UIDevice.current.systemVersion
if currSysVer.compare(reqSysVer, options: .numeric) != .orderedAscending {
MTDisplay.supported = true
}
#else
MTDisplay.supported = true
#endif
MTDisplay.initialized = true
}
return MTDisplay.supported
}
protocol DownShift {
var shiftDown:CGFloat { set get }
}
// MARK: - MTDisplay
/// The base class for rendering a math equation.
public class MTDisplay:NSObject {
// needed for isIos6Supported() func above
static var initialized = false
static var supported = false
/// Draws itself in the given graphics context.
public func draw(_ context:CGContext) {
if self.localBackgroundColor != nil {
context.saveGState()
context.setBlendMode(.normal)
context.setFillColor(self.localBackgroundColor!.cgColor)
context.fill(self.displayBounds())
context.restoreGState()
}
}
/// Gets the bounding rectangle for the MTDisplay
func displayBounds() -> CGRect {
return CGRectMake(self.position.x, self.position.y - self.descent, self.width, self.ascent + self.descent)
}
/// For debugging. Shows the object in quick look in Xcode.
#if os(iOS)
func debugQuickLookObject() -> Any {
let size = CGSizeMake(self.width, self.ascent + self.descent);
UIGraphicsBeginImageContext(size);
// get a reference to that context we created
let context = UIGraphicsGetCurrentContext()!
// translate/flip the graphics context (for transforming from CG* coords to UI* coords
context.translateBy(x: 0, y: size.height);
context.scaleBy(x: 1.0, y: -1.0);
// move the position to (0,0)
context.translateBy(x: -self.position.x, y: -self.position.y);
// Move the line up by self.descent
context.translateBy(x: 0, y: self.descent);
// Draw self on context
self.draw(context)
// generate a new UIImage from the graphics context we drew onto
let img = UIGraphicsGetImageFromCurrentImageContext()
return img as Any
}
#endif
/// The distance from the axis to the top of the display
var ascent:CGFloat = 0
/// The distance from the axis to the bottom of the display
var descent:CGFloat = 0
/// The width of the display
var width:CGFloat = 0
/// Position of the display with respect to the parent view or display.
var position=CGPoint.zero
/// The range of characters supported by this item
var range:NSRange=NSMakeRange(0, 0)
/// Whether the display has a subscript/superscript following it.
var hasScript:Bool = false
/// The text color for this display
var textColor: MTColor?
/// The local color, if the color was mutated local with the color command
var localTextColor: MTColor?
/// The background color for this display
var localBackgroundColor: MTColor?
}
class MTDisplayDS : MTDisplay, DownShift {
var shiftDown: CGFloat = 0
}
// MARK: - MTCTLineDisplay
/// A rendering of a single CTLine as an MTDisplay
class MTCTLineDisplay : MTDisplay {
/// The CTLine being displayed
var line:CTLine!
/// The attributed string used to generate the CTLineRef. Note setting this does not reset the dimensions of
/// the display. So set only when
var attributedString:NSAttributedString? {
didSet {
line = CTLineCreateWithAttributedString(attributedString!)
}
}
/// An array of MTMathAtoms that this CTLine displays. Used for indexing back into the MTMathList
var atoms = [MTMathAtom]()
init(withString attrString:NSAttributedString?, position:CGPoint, range:NSRange, font:MTFont?, atoms:[MTMathAtom]) {
super.init()
self.position = position
self.attributedString = attrString
self.line = CTLineCreateWithAttributedString(attrString!)
self.range = range
self.atoms = atoms
// We can't use typographic bounds here as the ascent and descent returned are for the font and not for the line.
self.width = CTLineGetTypographicBounds(line, nil, nil, nil);
if isIos6Supported() {
let bounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
self.ascent = max(0, CGRectGetMaxY(bounds) - 0);
self.descent = max(0, 0 - CGRectGetMinY(bounds));
// TODO: Should we use this width vs the typographic width? They are slightly different. Don't know why.
// _width = CGRectGetMaxX(bounds);
} else {
// Our own implementation of the ios6 function to get glyph path bounds.
self.computeDimensions(font)
}
}
override var textColor: MTColor? {
set {
super.textColor = newValue
let attrStr = attributedString!.mutableCopy() as! NSMutableAttributedString
let foregroundColor = NSAttributedString.Key(kCTForegroundColorAttributeName as String)
attrStr.addAttribute(foregroundColor, value:self.textColor!.cgColor, range:NSMakeRange(0, attrStr.length))
self.attributedString = attrStr
}
get { super.textColor }
}
func computeDimensions(_ font:MTFont?) {
let runs = CTLineGetGlyphRuns(line) as NSArray
for obj in runs {
let run = obj as! CTRun?
let numGlyphs = CTRunGetGlyphCount(run!)
var glyphs = [CGGlyph]()
glyphs.reserveCapacity(numGlyphs)
CTRunGetGlyphs(run!, CFRangeMake(0, numGlyphs), &glyphs);
let bounds = CTFontGetBoundingRectsForGlyphs(font!.ctFont, .horizontal, glyphs, nil, numGlyphs);
let ascent = max(0, CGRectGetMaxY(bounds) - 0);
// Descent is how much the line goes below the origin. However if the line is all above the origin, then descent can't be negative.
let descent = max(0, 0 - CGRectGetMinY(bounds));
if (ascent > self.ascent) {
self.ascent = ascent;
}
if (descent > self.descent) {
self.descent = descent;
}
}
}
override public func draw(_ context: CGContext) {
super.draw(context)
context.saveGState()
context.textPosition = self.position
CTLineDraw(line, context)
context.restoreGState()
}
}
// MARK: - MTMathListDisplay
/// An MTLine is a rendered form of MTMathList in one line.
/// It can render itself using the draw method.
public class MTMathListDisplay : MTDisplay {
/**
@typedef MTLinePosition
@brief The type of position for a line, i.e. subscript/superscript or regular.
*/
enum LinePosition : Int {
/// Regular
case regular
/// Positioned at a subscript
case ssubscript
/// Positioned at a superscript
case superscript
}
/// Where the line is positioned
var type:LinePosition = .regular
/// An array of MTDisplays which are positioned relative to the position of the
/// the current display.
var subDisplays = [MTDisplay]()
/// If a subscript or superscript this denotes the location in the parent MTList. For a
/// regular list this is NSNotFound
var index: Int = 0
init(withDisplays displays:[MTDisplay], range:NSRange) {
super.init()
self.subDisplays = displays
self.position = CGPoint.zero
self.type = .regular
self.index = NSNotFound
self.range = range
self.recomputeDimensions()
}
override var textColor: MTColor? {
set {
super.textColor = newValue
for displayAtom in self.subDisplays {
if displayAtom.localTextColor == nil {
displayAtom.textColor = newValue
} else {
displayAtom.textColor = displayAtom.localTextColor
}
}
}
get { super.textColor }
}
override public func draw(_ context: CGContext) {
super.draw(context)
context.saveGState()
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
context.translateBy(x: self.position.x, y: self.position.y)
context.textPosition = CGPoint.zero
// draw each atom separately
for displayAtom in self.subDisplays {
displayAtom.draw(context)
}
context.restoreGState()
}
func recomputeDimensions() {
var max_ascent:CGFloat = 0
var max_descent:CGFloat = 0
var max_width:CGFloat = 0
for atom in self.subDisplays {
let ascent = max(0, atom.position.y + atom.ascent);
if (ascent > max_ascent) {
max_ascent = ascent;
}
let descent = max(0, 0 - (atom.position.y - atom.descent));
if (descent > max_descent) {
max_descent = descent;
}
let width = atom.width + atom.position.x;
if (width > max_width) {
max_width = width;
}
}
self.ascent = max_ascent;
self.descent = max_descent;
self.width = max_width;
}
}
// MARK: - MTFractionDisplay
/// Rendering of an MTFraction as an MTDisplay
class MTFractionDisplay : MTDisplay {
/** A display representing the numerator of the fraction. Its position is relative
to the parent and is not treated as a sub-display.
*/
var numerator:MTMathListDisplay?
/** A display representing the denominator of the fraction. Its position is relative
to the parent is not treated as a sub-display.
*/
var denominator:MTMathListDisplay?
var numeratorUp:CGFloat=0 {
didSet { self.updateNumeratorPosition() }
}
var denominatorDown:CGFloat=0 {
didSet { self.updateDenominatorPosition() }
}
var linePosition:CGFloat=0
var lineThickness:CGFloat=0
init(withNumerator numerator:MTMathListDisplay?, denominator:MTMathListDisplay?, position:CGPoint, range:NSRange) {
super.init()
self.numerator = numerator;
self.denominator = denominator;
self.position = position;
self.range = range;
assert(self.range.length == 1, "Fraction range length not 1 - range (\(range.location), \(range.length)")
}
override var ascent:CGFloat {
set { super.ascent = newValue }
get { numerator!.ascent + self.numeratorUp }
}
override var descent:CGFloat {
set { super.descent = newValue }
get { denominator!.descent + self.denominatorDown }
}
override var width:CGFloat {
set { super.width = newValue }
get { max(numerator!.width, denominator!.width) }
}
func updateDenominatorPosition() {
guard denominator != nil else { return }
denominator!.position = CGPointMake(self.position.x + (self.width - denominator!.width)/2, self.position.y - self.denominatorDown)
}
func updateNumeratorPosition() {
guard numerator != nil else { return }
numerator!.position = CGPointMake(self.position.x + (self.width - numerator!.width)/2, self.position.y + self.numeratorUp)
}
override var position: CGPoint {
set {
super.position = newValue
self.updateDenominatorPosition()
self.updateNumeratorPosition()
}
get { super.position }
}
override var textColor: MTColor? {
set {
super.textColor = newValue
numerator?.textColor = newValue
denominator?.textColor = newValue
}
get { super.textColor }
}
override public func draw(_ context:CGContext) {
super.draw(context)
numerator?.draw(context)
denominator?.draw(context)
context.saveGState()
self.textColor?.setStroke()
// draw the horizontal line
let path = MTBezierPath()
path.move(to: CGPointMake(self.position.x, self.position.y + self.linePosition))
path.addLine(to: CGPointMake(self.position.x + self.width, self.position.y + self.linePosition))
path.lineWidth = self.lineThickness
path.stroke()
context.restoreGState()
}
}
// MARK: - MTRadicalDisplay
/// Rendering of an MTRadical as an MTDisplay
class MTRadicalDisplay : MTDisplay {
/** A display representing the radicand of the radical. Its position is relative
to the parent is not treated as a sub-display.
*/
var radicand:MTMathListDisplay?
/** A display representing the degree of the radical. Its position is relative
to the parent is not treated as a sub-display.
*/
var degree:MTMathListDisplay?
override var position: CGPoint {
set {
super.position = newValue
self.updateRadicandPosition()
}
get { super.position }
}
override var textColor: MTColor? {
set {
super.textColor = newValue
self.radicand?.textColor = newValue
self.degree?.textColor = newValue
}
get { super.textColor }
}
private var _radicalGlyph:MTDisplay?
private var _radicalShift:CGFloat=0
var topKern:CGFloat=0
var lineThickness:CGFloat=0
init(withRadicand radicand:MTMathListDisplay?, glyph:MTDisplay, position:CGPoint, range:NSRange) {
super.init()
self.radicand = radicand
_radicalGlyph = glyph
_radicalShift = 0
self.position = position
self.range = range
}
func setDegree(_ degree:MTMathListDisplay?, fontMetrics:MTFontMathTable?) {
// sets up the degree of the radical
var kernBefore = fontMetrics!.radicalKernBeforeDegree;
let kernAfter = fontMetrics!.radicalKernAfterDegree;
let raise = fontMetrics!.radicalDegreeBottomRaisePercent * (self.ascent - self.descent);
// The layout is:
// kernBefore, raise, degree, kernAfter, radical
self.degree = degree;
// the radical is now shifted by kernBefore + degree.width + kernAfter
_radicalShift = kernBefore + degree!.width + kernAfter;
if _radicalShift < 0 {
// we can't have the radical shift backwards, so instead we increase the kernBefore such
// that _radicalShift will be 0.
kernBefore -= _radicalShift;
_radicalShift = 0;
}
// Note: position of degree is relative to parent.
self.degree!.position = CGPointMake(self.position.x + kernBefore, self.position.y + raise);
// Update the width by the _radicalShift
self.width = _radicalShift + _radicalGlyph!.width + self.radicand!.width;
// update the position of the radicand
self.updateRadicandPosition()
}
func updateRadicandPosition() {
// The position of the radicand includes the position of the MTRadicalDisplay
// This is to make the positioning of the radical consistent with fractions and
// have the cursor position finding algorithm work correctly.
// move the radicand by the width of the radical sign
self.radicand!.position = CGPointMake(self.position.x + _radicalShift + _radicalGlyph!.width, self.position.y);
}
override public func draw(_ context: CGContext) {
super.draw(context)
// draw the radicand & degree at its position
self.radicand?.draw(context)
self.degree?.draw(context)
context.saveGState();
self.textColor?.setStroke()
self.textColor?.setFill()
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
context.translateBy(x: self.position.x + _radicalShift, y: self.position.y);
context.textPosition = CGPoint.zero
// Draw the glyph.
_radicalGlyph?.draw(context)
// Draw the VBOX
// for the kern of, we don't need to draw anything.
let heightFromTop = topKern;
// draw the horizontal line with the given thickness
let path = MTBezierPath()
let lineStart = CGPointMake(_radicalGlyph!.width, self.ascent - heightFromTop - self.lineThickness / 2); // subtract half the line thickness to center the line
let lineEnd = CGPointMake(lineStart.x + self.radicand!.width, lineStart.y);
path.move(to: lineStart)
path.addLine(to: lineEnd)
path.lineWidth = lineThickness
path.lineCapStyle = .round
path.stroke()
context.restoreGState();
}
}
// MARK: - MTGlyphDisplay
/// Rendering a glyph as a display
class MTGlyphDisplay : MTDisplayDS {
var glyph:CGGlyph!
var font:MTFont?
init(withGlpyh glyph:CGGlyph, range:NSRange, font:MTFont?) {
super.init()
self.font = font
self.glyph = glyph
self.position = CGPoint.zero
self.range = range
}
override public func draw(_ context: CGContext) {
super.draw(context)
context.saveGState()
self.textColor?.setFill()
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
context.translateBy(x: self.position.x, y: self.position.y - self.shiftDown);
context.textPosition = CGPoint.zero
var pos = CGPoint.zero
CTFontDrawGlyphs(font!.ctFont, &glyph, &pos, 1, context);
context.restoreGState();
}
override var ascent:CGFloat {
set {
super.ascent = newValue
}
get {
return super.ascent - self.shiftDown;
}
}
override var descent:CGFloat {
set {
super.descent = newValue
}
get {
return super.descent + self.shiftDown;
}
}
}
// MARK: - MTGlyphConstructionDisplay
class MTGlyphConstructionDisplay:MTDisplayDS {
var glyphs = [CGGlyph]()
var positions = [CGPoint]()
var font:MTFont?
var numGlyphs:Int=0
init(withGlyphs glyphs:[NSNumber?], offsets:[NSNumber?], font:MTFont?) {
super.init()
assert(glyphs.count == offsets.count, "Glyphs and offsets need to match")
self.numGlyphs = glyphs.count;
self.glyphs = [CGGlyph](repeating: CGGlyph(), count: self.numGlyphs) //malloc(sizeof(CGGlyph) * _numGlyphs);
self.positions = [CGPoint](repeating: CGPoint.zero, count: self.numGlyphs) //malloc(sizeof(CGPoint) * _numGlyphs);
for i in 0 ..< self.numGlyphs {
self.glyphs[i] = glyphs[i]!.uint16Value
self.positions[i] = CGPointMake(0, CGFloat(offsets[i]!.floatValue))
}
self.font = font
self.position = CGPoint.zero
}
override public func draw(_ context: CGContext) {
super.draw(context)
context.saveGState()
self.textColor?.setFill()
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
context.translateBy(x: self.position.x, y: self.position.y - self.shiftDown)
context.textPosition = CGPoint.zero
// Draw the glyphs.
CTFontDrawGlyphs(font!.ctFont, glyphs, positions, numGlyphs, context)
context.restoreGState()
}
override var ascent:CGFloat {
set {
super.ascent = newValue
}
get {
return super.ascent - self.shiftDown;
}
}
override var descent:CGFloat {
set {
super.descent = newValue
}
get {
return super.descent + self.shiftDown;
}
}
}
// MARK: - MTLargeOpLimitsDisplay
/// Rendering a large operator with limits as an MTDisplay
class MTLargeOpLimitsDisplay : MTDisplay {
/** A display representing the upper limit of the large operator. Its position is relative
to the parent is not treated as a sub-display.
*/
var upperLimit:MTMathListDisplay?
/** A display representing the lower limit of the large operator. Its position is relative
to the parent is not treated as a sub-display.
*/
var lowerLimit:MTMathListDisplay?
var limitShift:CGFloat=0
var upperLimitGap:CGFloat=0 {
didSet {
self.updateUpperLimitPosition()
}
}
var lowerLimitGap:CGFloat=0 {
didSet {
self.updateUpperLimitPosition()
}
}
var extraPadding:CGFloat=0
var nucleus:MTDisplay?
init(withNucleus nucleus:MTDisplay?, upperLimit:MTMathListDisplay?, lowerLimit:MTMathListDisplay?, limitShift:CGFloat, extraPadding:CGFloat) {
super.init()
self.upperLimit = upperLimit;
self.lowerLimit = lowerLimit;
self.nucleus = nucleus;
var maxWidth = max(nucleus!.width, upperLimit?.width ?? 0)
maxWidth = max(maxWidth, lowerLimit?.width ?? 0)
self.limitShift = limitShift;
self.upperLimitGap = 0;
self.lowerLimitGap = 0;
self.extraPadding = extraPadding; // corresponds to \xi_13 in TeX
self.width = maxWidth;
}
override var ascent:CGFloat {
set {
super.ascent = newValue
}
get {
if self.upperLimit != nil {
return nucleus!.ascent + extraPadding + self.upperLimit!.ascent + upperLimitGap + self.upperLimit!.descent
} else {
return nucleus!.ascent
}
}
}
override var descent:CGFloat {
set {
super.descent = newValue
}
get {
if self.lowerLimit != nil {
return nucleus!.descent + extraPadding + lowerLimitGap + self.lowerLimit!.descent + self.lowerLimit!.ascent;
} else {
return nucleus!.descent;
}
}
}
override var position: CGPoint {
set {
super.position = newValue
self.updateLowerLimitPosition()
self.updateUpperLimitPosition()
self.updateNucleusPosition()
}
get { super.position }
}
func updateLowerLimitPosition() {
if self.lowerLimit != nil {
// The position of the lower limit includes the position of the MTLargeOpLimitsDisplay
// This is to make the positioning of the radical consistent with fractions and radicals
// Move the starting point to below the nucleus leaving a gap of _lowerLimitGap and subtract
// the ascent to to get the baseline. Also center and shift it to the left by _limitShift.
self.lowerLimit!.position = CGPointMake(self.position.x - limitShift + (self.width - lowerLimit!.width)/2,
self.position.y - nucleus!.descent - lowerLimitGap - self.lowerLimit!.ascent);
}
}
func updateUpperLimitPosition() {
if self.upperLimit != nil {
// The position of the upper limit includes the position of the MTLargeOpLimitsDisplay
// This is to make the positioning of the radical consistent with fractions and radicals
// Move the starting point to above the nucleus leaving a gap of _upperLimitGap and add
// the descent to to get the baseline. Also center and shift it to the right by _limitShift.
self.upperLimit!.position = CGPointMake(self.position.x + limitShift + (self.width - self.upperLimit!.width)/2,
self.position.y + nucleus!.ascent + upperLimitGap + self.upperLimit!.descent);
}
}
func updateNucleusPosition() {
// Center the nucleus
nucleus?.position = CGPointMake(self.position.x + (self.width - nucleus!.width)/2, self.position.y);
}
override var textColor: MTColor? {
set {
super.textColor = newValue
self.upperLimit?.textColor = newValue
self.lowerLimit?.textColor = newValue
nucleus?.textColor = newValue
}
get { super.textColor }
}
override func draw(_ context:CGContext) {
super.draw(context)
// Draw the elements.
self.upperLimit?.draw(context)
self.lowerLimit?.draw(context)
nucleus?.draw(context)
}
}
// MARK: - MTLineDisplay
/// Rendering of an list with an overline or underline
class MTLineDisplay : MTDisplay {
/** A display representing the inner list that is underlined. Its position is relative
to the parent is not treated as a sub-display.
*/
var inner:MTMathListDisplay?
var lineShiftUp:CGFloat=0
var lineThickness:CGFloat=0
init(withInner inner:MTMathListDisplay?, position:CGPoint, range:NSRange) {
super.init()
self.inner = inner;
self.position = position;
self.range = range;
}
override var textColor: MTColor? {
set {
super.textColor = newValue
inner?.textColor = newValue
}
get { super.textColor }
}
override var position: CGPoint {
set {
super.position = newValue
self.updateInnerPosition()
}
get { super.position }
}
override func draw(_ context:CGContext) {
super.draw(context)
self.inner?.draw(context)
context.saveGState();
self.textColor?.setStroke()
// draw the horizontal line
let path = MTBezierPath()
let lineStart = CGPointMake(self.position.x, self.position.y + self.lineShiftUp);
let lineEnd = CGPointMake(lineStart.x + self.inner!.width, lineStart.y);
path.move(to:lineStart)
path.addLine(to: lineEnd)
path.lineWidth = self.lineThickness;
path.stroke()
context.restoreGState();
}
func updateInnerPosition() {
self.inner?.position = CGPointMake(self.position.x, self.position.y);
}
}
// MARK: - MTAccentDisplay
/// Rendering an accent as a display
class MTAccentDisplay : MTDisplay {
/** A display representing the inner list that is accented. Its position is relative
to the parent is not treated as a sub-display.
*/
var accentee:MTMathListDisplay?
/** A display representing the accent. Its position is relative to the current display.
*/
var accent:MTGlyphDisplay?
init(withAccent glyph:MTGlyphDisplay?, accentee:MTMathListDisplay?, range:NSRange) {
super.init()
self.accent = glyph
self.accentee = accentee
self.accentee?.position = CGPoint.zero
self.range = range
}
override var textColor: MTColor? {
set {
super.textColor = newValue
accentee?.textColor = newValue
accent?.textColor = newValue
}
get { super.textColor }
}
override var position: CGPoint {
set {
super.position = newValue
self.updateAccenteePosition()
}
get { super.position }
}
func updateAccenteePosition() {
self.accentee?.position = CGPointMake(self.position.x, self.position.y);
}
override func draw(_ context:CGContext) {
super.draw(context)
self.accentee?.draw(context)
context.saveGState();
context.translateBy(x: self.position.x, y: self.position.y);
context.textPosition = CGPoint.zero
self.accent?.draw(context)
context.restoreGState();
}
}

View File

@@ -0,0 +1,168 @@
//
// MTMathListIndex.swift
// MathRenderSwift
//
// Created by Mike Griebling on 2022-12-31.
//
import Foundation
public class MTMathListIndex {
public enum MTMathListSubIndexType: Int {
case none = 0
case nucleus
case superScript
case subScript
case numerator
case denominator
case radicand
case degree
}
/// The index of the associated atom.
var atomIndex: Int
/// The type of subindex, e.g. superscript, numerator etc.
var subIndexType: MTMathListSubIndexType = .none
/// The index into the sublist.
var subIndex: MTMathListIndex?
var finalIndex: Int {
if self.subIndexType == .none {
return self.atomIndex
} else {
return self.subIndex?.finalIndex ?? 0
}
}
func prevIndex() -> MTMathListIndex? {
if self.subIndexType == .none {
if self.atomIndex > 0 {
return MTMathListIndex(level0Index: self.atomIndex - 1)
}
} else {
if let prevSubIndex = self.subIndex?.prevIndex() {
return MTMathListIndex(at: self.atomIndex, with: prevSubIndex, type: self.subIndexType)
}
}
return nil
}
func nextIndex() -> MTMathListIndex {
if self.subIndexType == .none {
return MTMathListIndex(level0Index: self.atomIndex + 1)
} else if self.subIndexType == .nucleus {
return MTMathListIndex(at: self.atomIndex + 1, with: self.subIndex, type: self.subIndexType)
} else {
return MTMathListIndex(at: self.atomIndex, with: self.subIndex?.nextIndex(), type: self.subIndexType)
}
}
/**
* Returns true if this index represents the beginning of a line. Note there may be multiple lines in a MTMathList,
* e.g. a superscript or a fraction numerator. This returns true if the innermost subindex points to the beginning of a
* line.
*/
func isBeginningOfLine() -> Bool {
return self.finalIndex == 0
}
func isAtSameLevel(with index: MTMathListIndex?) -> Bool {
if self.subIndexType != index?.subIndexType {
return false
} else if self.subIndexType == .none {
// No subindexes, they are at the same level.
return true
} else if (self.atomIndex != index?.atomIndex) {
return false
} else {
return self.subIndex?.isAtSameLevel(with: index?.subIndex) ?? false
}
}
/** Returns the type of the innermost sub index. */
func finalSubIndexType() -> MTMathListSubIndexType {
if self.subIndex?.subIndex != nil {
return self.subIndex!.finalSubIndexType()
} else {
return self.subIndexType
}
}
/** Returns true if any of the subIndexes of this index have the given type. */
func hasSubIndex(ofType type: MTMathListSubIndexType) -> Bool {
if self.subIndexType == type {
return true
} else {
return self.subIndex?.hasSubIndex(ofType: type) ?? false
}
}
func levelUp(with subIndex: MTMathListIndex?, type: MTMathListSubIndexType) -> MTMathListIndex {
if self.subIndexType == .none {
return MTMathListIndex(at: self.atomIndex, with: subIndex, type: type)
}
return MTMathListIndex(at: self.atomIndex, with: self.subIndex?.levelUp(with: subIndex, type: type), type: self.subIndexType)
}
func levelDown() -> MTMathListIndex? {
if self.subIndexType == .none {
return nil
}
if let subIndexDown = self.subIndex?.levelDown() {
return MTMathListIndex(at: self.atomIndex, with: subIndexDown, type: self.subIndexType)
} else {
return MTMathListIndex(level0Index: self.atomIndex)
}
}
/** Factory function to create a `MTMathListIndex` with no subindexes.
@param index The index of the atom that the `MTMathListIndex` points at.
*/
public init(level0Index: Int) {
self.atomIndex = level0Index
}
public convenience init(at location: Int, with subIndex: MTMathListIndex?, type: MTMathListSubIndexType) {
self.init(level0Index: location)
self.subIndexType = type
self.subIndex = subIndex
}
}
extension MTMathListIndex: CustomStringConvertible {
public var description: String {
if self.subIndex != nil {
return "[\(self.atomIndex), \(self.subIndexType.rawValue):\(self.subIndex!)]"
}
return "[\(self.atomIndex)]"
}
}
extension MTMathListIndex: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(self.atomIndex)
hasher.combine(self.subIndexType)
hasher.combine(self.subIndex)
}
}
extension MTMathListIndex: Equatable {
public static func ==(lhs: MTMathListIndex, rhs: MTMathListIndex) -> Bool {
if lhs.atomIndex != rhs.atomIndex || lhs.subIndexType != rhs.subIndexType {
return false
}
if rhs.subIndex != nil {
return rhs.subIndex == lhs.subIndex
} else {
return lhs.subIndex == nil
}
}
}

View File

@@ -0,0 +1,296 @@
//
// MTMathUILabel.swift
// MathRenderSwift
//
// Created by Mike Griebling on 2023-01-01.
//
import Foundation
import CoreText
/**
Different display styles supported by the `MTMathUILabel`.
The only significant difference between the two modes is how fractions
and limits on large operators are displayed.
*/
public enum MTMathUILabelMode {
/// Display mode. Equivalent to $$ in TeX
case display
/// Text mode. Equivalent to $ in TeX.
case text
}
/**
Horizontal text alignment for `MTMathUILabel`.
*/
public enum MTTextAlignment : UInt {
/// Align left.
case left
/// Align center.
case center
/// Align right.
case right
}
/** The main view for rendering math.
`MTMathLabel` accepts either a string in LaTeX or an `MTMathList` to display. Use
`MTMathList` directly only if you are building it programmatically (e.g. using an
editor), otherwise using LaTeX is the preferable method.
The math display is centered vertically in the label. The default horizontal alignment is
is left. This can be changed by setting `textAlignment`. The math is default displayed in
*Display* mode. This can be changed using `labelMode`.
When created it uses `[MTFontManager defaultFont]` as its font. This can be changed using
the `font` parameter.
*/
@IBDesignable
public class MTMathUILabel : MTView {
/** The `MTMathList` to render. Setting this will remove any
`latex` that has already been set. If `latex` has been set, this will
return the parsed `MTMathList` if the `latex` parses successfully. Use this
setting if the `MTMathList` has been programmatically constructed, otherwise it
is preferred to use `latex`.
*/
public var mathList:MTMathList? {
set {
_mathList = newValue
_error = nil
_latex = MTMathListBuilder.mathListToString(newValue)
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _mathList }
}
private var _mathList:MTMathList?
/** The latex string to be displayed. Setting this will remove any `mathList` that
has been set. If latex has not been set, this will return the latex output for the
`mathList` that is set.
@see error */
@IBInspectable
public var latex:String {
set {
_latex = newValue
_error = nil
var error : NSError? = nil
_mathList = MTMathListBuilder.build(fromString: newValue, error: &error)
if error != nil {
_mathList = nil
_error = error
self.errorLabel?.text = error!.localizedDescription
self.errorLabel?.frame = self.bounds
self.errorLabel?.isHidden = !self.displayErrorInline
} else {
self.errorLabel?.isHidden = true
}
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _latex }
}
private var _latex = ""
/** This contains any error that occurred when parsing the latex. */
public var error:NSError? { _error }
private var _error:NSError?
/** If true, if there is an error it displays the error message inline. Default true. */
public var displayErrorInline = true
/** The MTFont to use for rendering. */
public var font:MTFont? {
set {
guard newValue != nil else { return }
_font = newValue
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _font }
}
private var _font:MTFont?
/** Convenience method to just set the size of the font without changing the fontface. */
@IBInspectable
public var fontSize:CGFloat {
set {
_fontSize = newValue
let font = font?.copy(withSize: newValue)
self.font = font // also forces an update
}
get { _fontSize }
}
private var _fontSize:CGFloat=0
/** This sets the text color of the rendered math formula. The default color is black. */
@IBInspectable
public var textColor:MTColor? {
set {
guard newValue != nil else { return }
_textColor = newValue
self.displayList?.textColor = newValue
self.setNeedsDisplay()
}
get { _textColor }
}
private var _textColor:MTColor?
/** The minimum distance from the margin of the view to the rendered math. This value is
`UIEdgeInsetsZero` by default. This is useful if you need some padding between the math and
the border/background color. sizeThatFits: will have its returned size increased by these insets.
*/
@IBInspectable
public var contentInsets:MTEdgeInsets {
set {
_contentInsets = newValue
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _contentInsets }
}
private var _contentInsets = MTEdgeInsetsZero
/** The Label mode for the label. The default mode is Display */
public var labelMode:MTMathUILabelMode {
set {
_labelMode = newValue
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _labelMode }
}
private var _labelMode = MTMathUILabelMode.display
/** Horizontal alignment for the text. The default is align left. */
public var textAlignment:MTTextAlignment {
set {
_textAlignment = newValue
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _textAlignment }
}
private var _textAlignment = MTTextAlignment.left
/** The internal display of the MTMathUILabel. This is for advanced use only. */
public var displayList: MTMathListDisplay? { _displayList }
private var _displayList:MTMathListDisplay?
public var currentStyle:MTLineStyle {
switch _labelMode {
case .display: return .display
case .text: return .text
}
}
public var errorLabel: MTLabel?
public override init(frame: CGRect) {
super.init(frame: frame)
self.initCommon()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
self.initCommon()
}
func initCommon() {
#if os(macOS)
self.layer?.isGeometryFlipped = true
#else
self.layer.isGeometryFlipped = true
#endif
_fontSize = 20
_contentInsets = MTEdgeInsetsZero
_labelMode = .display
let font = MTFontManager.fontManager.defaultFont
self.font = font
_textAlignment = .left
_displayList = nil
displayErrorInline = true
self.backgroundColor = MTColor.clear
_textColor = MTColor.black
let label = MTLabel()
self.errorLabel = label
#if os(macOS)
label.layer?.isGeometryFlipped = true
#else
label.layer.isGeometryFlipped = true
#endif
label.isHidden = true
label.textColor = MTColor.red
self.addSubview(label)
}
override public func draw(_ dirtyRect: MTRect) {
super.draw(dirtyRect)
if self.mathList == nil { return }
// drawing code
let context = MTGraphicsGetCurrentContext()!
context.saveGState()
displayList!.draw(context)
context.restoreGState()
}
func _layoutSubviews() {
if _mathList != nil {
// print("Pre list = \(_mathList!)")
_displayList = MTTypesetter.createLineForMathList(_mathList, font: font, style: currentStyle)
_displayList!.textColor = textColor
// print("Post list = \(_mathList!)")
var textX = CGFloat(0)
switch self.textAlignment {
case .left: textX = contentInsets.left
case .center: textX = (bounds.size.width - contentInsets.left - contentInsets.right - _displayList!.width) / 2 + contentInsets.left
case .right: textX = bounds.size.width - _displayList!.width - contentInsets.right
}
let availableHeight = bounds.size.height - contentInsets.bottom - contentInsets.top
// center things vertically
var height = _displayList!.ascent + _displayList!.descent
if height < fontSize/2 {
height = fontSize/2 // set height to half the font size
}
let textY = (availableHeight - height) / 2 + _displayList!.descent + contentInsets.bottom
_displayList!.position = CGPointMake(textX, textY)
} else {
_displayList = nil
}
errorLabel?.frame = self.bounds
self.setNeedsDisplay()
}
func _sizeThatFits(_ size:CGSize) -> CGSize {
var size = size
var displayList:MTMathListDisplay? = nil
if _mathList != nil {
displayList = MTTypesetter.createLineForMathList(_mathList, font: font, style: currentStyle)
}
size.width = displayList!.width + contentInsets.left + contentInsets.right
size.height = displayList!.ascent + displayList!.descent + contentInsets.top + contentInsets.bottom
return size
}
//override public var intrinsicContentSize: CGSize { _sizeThatFits(CGSizeZero) }
#if os(macOS)
func setNeedsDisplay() { self.needsDisplay = true }
func setNeedsLayout() { self.needsLayout = true }
public override var fittingSize: CGSize { _sizeThatFits(CGSizeZero) }
override public var isFlipped: Bool { false }
override public func layout() {
self._layoutSubviews()
super.layout()
}
#else
public override var intrinsicContentSize: CGSize { _sizeThatFits(CGSizeZero) }
override public func layoutSubviews() { _layoutSubviews() }
#endif
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
//
// MTUnicode.swift
// MathRenderSwift
//
// Created by Mike Griebling on 2022-12-31.
//
import Foundation
public struct UnicodeSymbol {
static let multiplication = "\u{00D7}"
static let division = "\u{00F7}"
static let fractionSlash = "\u{2044}"
static let whiteSquare = "\u{25A1}"
static let blackSquare = "\u{25A0}"
static let lessEqual = "\u{2264}"
static let greaterEqual = "\u{2265}"
static let notEqual = "\u{2260}"
static let squareRoot = "\u{221A}" // \sqrt
static let cubeRoot = "\u{221B}"
static let infinity = "\u{221E}" // \infty
static let angle = "\u{2220}" // \angle
static let degree = "\u{00B0}" // \circ
static let capitalGreekStart = UInt32(0x0391)
static let capitalGreekEnd = UInt32(0x03A9)
static let lowerGreekStart = UInt32(0x03B1)
static let lowerGreekEnd = UInt32(0x03C9)
static let planksConstant = UInt32(0x210e)
static let lowerItalicStart = UInt32(0x1D44E)
static let capitalItalicStart = UInt32(0x1D434)
static let greekLowerItalicStart = UInt32(0x1D6FC)
static let greekCapitalItalicStart = UInt32(0x1D6E2)
static let greekSymbolItalicStart = UInt32(0x1D716)
static let mathCapitalBoldStart = UInt32(0x1D400)
static let mathLowerBoldStart = UInt32(0x1D41A)
static let greekCapitalBoldStart = UInt32(0x1D6A8)
static let greekLowerBoldStart = UInt32(0x1D6C2)
static let greekSymbolBoldStart = UInt32(0x1D6DC)
static let numberBoldStart = UInt32(0x1D7CE)
static let mathCapitalBoldItalicStart = UInt32(0x1D468)
static let mathLowerBoldItalicStart = UInt32(0x1D482)
static let greekCapitalBoldItalicStart = UInt32(0x1D71C)
static let greekLowerBoldItalicStart = UInt32(0x1D736)
static let greekSymbolBoldItalicStart = UInt32(0x1D750)
static let mathCapitalScriptStart = UInt32(0x1D49C)
static let mathCapitalTTStart = UInt32(0x1D670)
static let mathLowerTTStart = UInt32(0x1D68A)
static let numberTTStart = UInt32(0x1D7F6)
static let mathCapitalSansSerifStart = UInt32(0x1D5A0)
static let mathLowerSansSerifStart = UInt32(0x1D5BA)
static let numberSansSerifStart = UInt32(0x1D7E2)
static let mathCapitalFrakturStart = UInt32(0x1D504)
static let mathLowerFrakturStart = UInt32(0x1D51E)
static let mathCapitalBlackboardStart = UInt32(0x1D538)
static let mathLowerBlackboardStart = UInt32(0x1D552)
static let numberBlackboardStart = UInt32(0x1D7D8)
}
extension Character {
var utf32Char: UTF32Char { self.unicodeScalars.map { $0.value }.reduce(0, +) }
var isLowerEnglish : Bool { self >= "a" && self <= "z" }
var isUpperEnglish : Bool { self >= "A" && self <= "Z" }
var isNumber : Bool { self >= "0" && self <= "9" }
var isLowerGreek : Bool {
let uch = self.utf32Char
return uch >= UnicodeSymbol.lowerGreekStart && uch <= UnicodeSymbol.lowerGreekEnd
}
var isCapitalGreek : Bool {
let uch = self.utf32Char
return uch >= UnicodeSymbol.capitalGreekStart && uch <= UnicodeSymbol.capitalGreekEnd
}
var greekSymbolOrder : UInt32? {
let greekSymbols : [UTF32Char] = [0x03F5, 0x03D1, 0x03F0, 0x03D5, 0x03F1, 0x03D6]
let index = greekSymbols.firstIndex(of: self.utf32Char)
if let pos = index { return UInt32(pos) }
return nil
}
var isGreekSymbol : Bool { self.greekSymbolOrder != nil }
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.