Added color and font styles.

This commit is contained in:
Michael Griebling
2023-01-03 13:26:38 -05:00
parent c364e56d31
commit a5e6d37d0c
16 changed files with 6611 additions and 5 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))/255.0,
green: CGFloat((rgbValue & 0xFF00))/255.0,
blue: CGFloat((rgbValue & 0xFF))/255.0, alpha: 1.0)
}
}

View File

@@ -0,0 +1,36 @@
//
// MTConfig.swift
// MathRenderSwift
//
// Created by Mike Griebling on 2023-01-01.
//
import Foundation
#if os(iOS)
import UIKit
typealias MTView = UIView
typealias MTColor = UIColor
typealias MTBezierPath = UIBezierPath
typealias MTLabel = UILabel
typealias MTRect = CGRect
let MTEdgeInsetsZero = UIEdgeInsets.zero
func MTGraphicsGetCurrentContext() -> CGContext? { UIGraphicsGetCurrentContext() }
#else
import AppKit
typealias MTView = NSView
typealias MTColor = NSColor
typealias MTBezierPath = NSBezierPath
typealias MTEdgeInsets = NSEdgeInsets
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,78 @@
//
// MTFont.swift
// MathRenderSwift
//
// Created by Mike Griebling on 2022-12-31.
//
import Foundation
import CoreGraphics
import CoreText
//
// Font.swift
// iosMath
//
// Created by Kostub Deshmukh on 5/18/16.
//
// 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: %zd", self.defaultCGFont.numberOfGlyphs)
self.ctFont = CTFontCreateWithGraphicsFont(self.defaultCGFont, size, nil, nil);
let mathTablePlist = bundle.url(forResource:name, withExtension:"plist")
let dict = NSDictionary(contentsOf: mathTablePlist!) // dictionaryWithContentsOfFile:mathTablePlist];
self.rawMathTable = dict
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.
return Bundle(url: Bundle(for: self).url(forResource: "iosMathFonts", 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 = self.defaultCGFont.name(for: glyph)
return name! as String
}
func get(glyphWithName name:String) -> CGGlyph {
return self.defaultCGFont.getGlyphWithGlyphName(name: name as CFString)
}
var fontSize:CGFloat {
return CTFontGetSize(self.ctFont)
}
}

View File

@@ -0,0 +1,52 @@
//
// 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.
weak 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 {
return 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?
let glyphArray = NSMutableArray(capacity: variantGlyphs!.count)
if variantGlyphs == nil {
// There are no extra variants, so just add the current glyph to it.
let glyph = self.font?.get(glyphWithName: glyphName!)
glyphArray.add(glyph as Any)
return glyphArray as! [NSNumber?]
}
for gvn in variantGlyphs! {
let glyphVariantName = gvn as! String?
let variantGlyph = self.font?.get(glyphWithName: glyphVariantName!)
glyphArray.add(variantGlyph as Any)
}
return glyphArray as! [NSNumber?]
}
/** 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 nil. */
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)
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,815 @@
//
// 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}"
]
var _accentValueToName: [String: String]? = nil
public 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!
}
public 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.getOperator(withName: "log", limits: false),
"lg" : MTMathAtomFactory.getOperator(withName: "lg", limits: false),
"ln" : MTMathAtomFactory.getOperator(withName: "ln", limits: false),
"sin" : MTMathAtomFactory.getOperator(withName: "sin", limits: false),
"arcsin" : MTMathAtomFactory.getOperator(withName: "arcsin", limits: false),
"sinh" : MTMathAtomFactory.getOperator(withName: "sinh", limits: false),
"cos" : MTMathAtomFactory.getOperator(withName: "cos", limits: false),
"arccos" : MTMathAtomFactory.getOperator(withName: "arccos", limits: false),
"cosh" : MTMathAtomFactory.getOperator(withName: "cosh", limits: false),
"tan" : MTMathAtomFactory.getOperator(withName: "tan", limits: false),
"arctan" : MTMathAtomFactory.getOperator(withName: "arctan", limits: false),
"tanh" : MTMathAtomFactory.getOperator(withName: "tanh", limits: false),
"cot" : MTMathAtomFactory.getOperator(withName: "cot", limits: false),
"coth" : MTMathAtomFactory.getOperator(withName: "coth", limits: false),
"sec" : MTMathAtomFactory.getOperator(withName: "sec", limits: false),
"csc" : MTMathAtomFactory.getOperator(withName: "csc", limits: false),
"arg" : MTMathAtomFactory.getOperator(withName: "arg", limits: false),
"ker" : MTMathAtomFactory.getOperator(withName: "ker", limits: false),
"dim" : MTMathAtomFactory.getOperator(withName: "dim", limits: false),
"hom" : MTMathAtomFactory.getOperator(withName: "hom", limits: false),
"exp" : MTMathAtomFactory.getOperator(withName: "exp", limits: false),
"deg" : MTMathAtomFactory.getOperator(withName: "deg", limits: false),
// Limit operators
"lim" : MTMathAtomFactory.getOperator(withName: "lim", limits: true),
"limsup" : MTMathAtomFactory.getOperator(withName: "lim sup", limits: true),
"liminf" : MTMathAtomFactory.getOperator(withName: "lim inf", limits: true),
"max" : MTMathAtomFactory.getOperator(withName: "max", limits: true),
"min" : MTMathAtomFactory.getOperator(withName: "min", limits: true),
"sup" : MTMathAtomFactory.getOperator(withName: "sup", limits: true),
"inf" : MTMathAtomFactory.getOperator(withName: "inf", limits: true),
"det" : MTMathAtomFactory.getOperator(withName: "det", limits: true),
"Pr" : MTMathAtomFactory.getOperator(withName: "Pr", limits: true),
"gcd" : MTMathAtomFactory.getOperator(withName: "gcd", limits: true),
// Large operators
"prod" : MTMathAtomFactory.getOperator(withName: "\u{220F}", limits: true),
"coprod" : MTMathAtomFactory.getOperator(withName: "\u{2210}", limits: true),
"sum" : MTMathAtomFactory.getOperator(withName: "\u{2211}", limits: true),
"int" : MTMathAtomFactory.getOperator(withName: "\u{222B}", limits: false),
"oint" : MTMathAtomFactory.getOperator(withName: "\u{222E}", limits: false),
"bigwedge" : MTMathAtomFactory.getOperator(withName: "\u{22C0}", limits: true),
"bigvee" : MTMathAtomFactory.getOperator(withName: "\u{22C1}", limits: true),
"bigcap" : MTMathAtomFactory.getOperator(withName: "\u{22C2}", limits: true),
"bigcup" : MTMathAtomFactory.getOperator(withName: "\u{22C3}", limits: true),
"bigodot" : MTMathAtomFactory.getOperator(withName: "\u{2A00}", limits: true),
"bigoplus" : MTMathAtomFactory.getOperator(withName: "\u{2A01}", limits: true),
"bigotimes" : MTMathAtomFactory.getOperator(withName: "\u{2A02}", limits: true),
"biguplus" : MTMathAtomFactory.getOperator(withName: "\u{2A04}", limits: true),
"bigsqcup" : MTMathAtomFactory.getOperator(withName: "\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}"),
"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 latexSymbolNames = [String]()
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? {
if let style = fontStyles[fontName] {
return style
}
return nil
}
// Return an atom for times sign \times or *
public static func times() -> MTMathAtom {
return MTMathAtom(type: .binaryOperator, value: UnicodeSymbol.multiplication)
}
// Return an atom for division sign \div or /
public static func divide() -> MTMathAtom {
return MTMathAtom(type: .binaryOperator, value: UnicodeSymbol.division)
}
// Return an atom aka placeholder square
public static func placeholder() -> MTMathAtom {
return 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(for char: String) -> MTMathAtom? {
let atomCharacterSet = CharacterSet(charactersIn: UnicodeScalar(0x21)!...UnicodeScalar(0x7E)!)
if char.rangeOfCharacter(from: atomCharacterSet) != nil {
switch char {
case "$", "%", "#", "&", "~", "\'", "^", "_", "{", "}", "\\":
return nil
case "(", "[":
return MTMathAtom(type: .open, value: char)
case ")", "]", "!", "?":
return MTMathAtom(type: .close, value: char)
case ",", ";":
return MTMathAtom(type: .punctuation, value: char)
case "=", ">", "<":
return MTMathAtom(type: .relation, value: char)
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: char)
case ".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
return MTMathAtom(type: .number, value: char)
case _ where
(char.rangeOfCharacter(from: CharacterSet.lowercaseLetters) != nil) ||
(char.rangeOfCharacter(from: CharacterSet.lowercaseLetters) != nil) :
return MTMathAtom(type: .variable, value: char)
case "\"", "/", "@", "`", "|":
return MTMathAtom(type: .ordinary, value: char)
default:
assert(false, "Unknown Character: \(char)")
print("Unknown characters: \(char)")
return nil
}
}
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(for: "\(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 = sharedInstance.supportedLatexSymbols[_name] {
return atom
}
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 Self.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 define(latexSymbol name: String, value: MTMathAtom) {
Self.sharedInstance.supportedLatexSymbols[name] = value
Self.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 displyed differently. */
public static func getOperator(withName name: String, limits: Bool) -> MTLargeOperator {
return 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 getAccent(withName name: String) -> MTAccent? {
if let accentValue = Self.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 getName(of accent: MTAccent) -> String? {
return Self.sharedInstance.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
}
/** 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.
*/
public static func table(withEnvironment env: String?, rows: [[MTMathList]]) -> 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)
}
}
let matrixEnvs = [
"matrix": [],
"pmatrix": ["(", ")"],
"bmatrix": ["[", "]"],
"Bmatrix": ["{", "}"],
"vmatrix": ["vert", "vert"],
"Vmatrix": ["Vert", "Vert"]
]
if env == nil {
table.interColumnSpacing = 0
table.interRowAdditionalSpacing = 1
for i in 0..<table.numberOfCols() {
table.set(alignment: .left, forCol: i)
}
return table
} else {
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.numberOfCols() != 2 {
print("\(env!) environment can only have 2 columns")
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, forCol: 0)
table.set(alignment: .left, forCol: 1)
return table
} else if env == "displaylines" || env == "gather" {
if table.numberOfCols() != 1 {
print("\(env!) environment can only have 1 columns")
return nil
}
table.interRowAdditionalSpacing = 1
table.interColumnSpacing = 0
table.set(alignment: .center, forCol: 0)
return table
} else if env == "eqnarray" {
if table.numberOfCols() != 3 {
print("\(env!) environment can only have 3 columns")
return nil
}
table.interRowAdditionalSpacing = 1
table.interColumnSpacing = 18
table.set(alignment: .right, forCol: 0)
table.set(alignment: .center, forCol: 1)
table.set(alignment: .left, forCol: 2)
return table
} else if env == "cases" {
if table.numberOfCols() != 2 {
print("\(env!) environment can only have 2 columns")
return nil
}
table.interRowAdditionalSpacing = 0
table.interColumnSpacing = 18
table.set(alignment: .left, forCol: 0)
table.set(alignment: .left, forCol: 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 {
print("Unknown table environment")
return nil
}
}
}
}

View File

@@ -0,0 +1,702 @@
//
// MTMathList.swift
// MathRenderSwift
//
// Created by Mike Griebling on 2022-12-31.
//
import Foundation
// type defines spacing and how it is rendered
public enum MTMathAtomType: String, CustomStringConvertible {
case ordinary // 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
case space
// Denotes style changes during randering
case style
case color
case colorBox
case table
func isNotBinaryOperator() -> Bool {
switch self {
case .binaryOperator, .relation, .open, .punctuation, .largeOperator: return true
default: return false
}
}
func isScriptAllowed() -> Bool {
return self != .boundary && self != .space && self != .style && self != .table
}
// we want string representations to be capitalized
public var description: String {
self.rawValue.capitalized
}
}
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
}
public class MTMathAtom: Any, CustomStringConvertible {
public var type: MTMathAtomType
public var subScript: MTMathList?
public var superScript: MTMathList?
var fontStyle: MTFontStyle = .defaultStyle
var fusedAtoms: MTMathList?
public static func atom(withType type:MTMathAtomType, value:String) -> MTMathAtom {
switch type {
case .largeOperator:
return MTLargeOperator(value: value, limits: true)
case .fraction:
return MTFraction()
case .radical:
return MTRadical()
case .placeholder:
return MTMathAtom(type: type, value: UnicodeSymbol.whiteSquare)
case .inner:
return MTInner()
case .underline:
return MTUnderLine()
case .overline:
return MTOverLine()
case .accent:
return MTAccent(value: value)
case .space:
return MTMathSpace(space: 0)
case .color:
return MTMathColor()
case .colorBox:
return MTMathColorbox()
default:
return MTMathAtom(type: type, value: value)
}
}
public func setSuperScript(_ list: MTMathList?) {
if self.isScriptAllowed() {
self.superScript = list
} else {
print("superscripts not allowed for atom \(self.type.rawValue)")
self.superScript = nil
}
}
public func setSubScript(_ list: MTMathList?) {
if self.isScriptAllowed() {
self.subScript = list
} else {
print("subscripts not allowed for atom \(self.type.rawValue)")
self.subScript = nil
}
}
public 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 nucleus: String = ""
public var finalized: MTMathAtom {
let finalized = self
if finalized.superScript != nil {
finalized.superScript = finalized.superScript!.finalized
}
if finalized.subScript != nil {
finalized.subScript = finalized.subScript!.finalized
}
return finalized
}
// atoms that fused to create this one
public var childAtoms = [MTMathAtom]()
// indexRange in list that this atom tracks:
public var indexRange = NSRange(location: 0, length: 0)
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 atom")
return
}
self.childAtoms.append(self)
if atom.childAtoms.count > 0 {
self.childAtoms += atom.childAtoms
} else {
self.childAtoms.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() }
public init(type: MTMathAtomType, value: String) {
self.type = type
self.nucleus = value
}
func isNotBinaryOperator() -> Bool { self.type.isNotBinaryOperator() }
}
func isNotBinaryOperator(_ prevNode:MTMathAtom?) -> Bool {
if prevNode == nil { return true }
return prevNode!.type.isNotBinaryOperator()
}
public class MTFraction: MTMathAtom {
public var hasRule: Bool = true
public var leftDelimiter: String?
public var rightDelimiter: String?
public var numerator: MTMathList? = MTMathList()
public var denominator: MTMathList? = MTMathList()
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 finalized: MTFraction = super.finalized as! MTFraction
finalized.numerator = finalized.numerator?.finalized
finalized.denominator = finalized.denominator?.finalized
return finalized
}
convenience init(hasRule: Bool = true) {
self.init(type: .fraction, value: "")
self.hasRule = hasRule
}
}
public class MTRadical: MTMathAtom {
// Under the roof
var radicand: MTMathList? = MTMathList()
// Value on radical sign
var degree: MTMathList?
convenience init() {
self.init(type: .radical, value: "")
}
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 finalized: MTRadical = super.finalized as! MTRadical
finalized.radicand = finalized.radicand?.finalized
finalized.degree = finalized.degree?.finalized
return finalized
}
}
public class MTLargeOperator: MTMathAtom {
var limits: Bool = false
convenience init(value: String, limits: Bool = false) {
self.init(type: .largeOperator, value: value)
self.limits = limits
}
}
// MARK: - MTInner
public class MTInner: MTMathAtom {
var innerList: MTMathList?
var leftBoundary: MTMathAtom? {
didSet {
if leftBoundary != nil && leftBoundary!.type != .boundary {
assertionFailure("Left boundary must be of type .boundary")
}
}
}
var rightBoundary: MTMathAtom? {
didSet {
if rightBoundary != nil && rightBoundary!.type != .boundary {
assertionFailure("Right boundary must be of type .boundary")
}
}
}
init() {
super.init(type: .inner, value: "")
}
public override convenience init(type: MTMathAtomType, value: String) {
if type == .inner {
self.init(); return
}
assertionFailure("MTInner(type:value:) cannot be called. Use MTInner() instead.")
self.init()
}
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 finalized: MTInner = super.finalized as! MTInner
finalized.innerList = finalized.innerList?.finalized
return finalized
}
}
public class MTOverLine: MTMathAtom {
var innerList: MTMathList?
override public var finalized: MTMathAtom {
let finalized: MTOverLine = super.finalized as! MTOverLine
finalized.innerList = finalized.innerList?.finalized
return finalized
}
convenience init() {
self.init(type: .overline, value: "")
}
}
public class MTUnderLine: MTMathAtom {
var innerList: MTMathList?
override public var finalized: MTMathAtom {
let finalized: MTUnderLine = super.finalized as! MTUnderLine
finalized.innerList = finalized.innerList?.finalized
return finalized
}
convenience init() {
self.init(type: .underline, value: "")
}
}
public class MTAccent: MTMathAtom {
var innerList: MTMathList?
override public var finalized: MTMathAtom {
let finalized: MTAccent = super.finalized as! MTAccent
finalized.innerList = finalized.innerList?.finalized
return finalized
}
convenience init(value: String) {
self.init(type: .accent, value: value)
}
}
public class MTMathSpace: MTMathAtom {
var space: CGFloat = 0
convenience init(space: CGFloat) {
self.init(type: .space, value: "")
self.space = space
}
}
public enum MTLineStyle {
case display
case text
case script
case scriptOfScript
public func inc() -> MTLineStyle {
switch self {
case .display: return .text
case .text: return .script
case .script: return .scriptOfScript
case .scriptOfScript: return .display
}
}
public var isNotScript:Bool {
self == .display || self == .text
}
}
public class MTMathStyle: MTMathAtom {
var style: MTLineStyle = .display
convenience init(style: MTLineStyle = .display) {
self.init(type: .space, value: "")
self.style = style
}
}
public class MTMathColor: MTMathAtom {
public var colorString:String=""
public var innerList:MTMathList?
init() {
super.init(type: .color, value: "")
}
public override convenience init(type: MTMathAtomType, value: String) {
if type == .color {
self.init(); return
}
NSException(name: NSExceptionName("InvalidMethod"), reason: "MTMathColor(type:value) cannot be called. Use MTMathColor() instead.").raise()
self.init()
}
public override var string: String {
"\\color{\(self.colorString)}{\(self.innerList!.string)}"
}
}
public class MTMathColorbox: MTMathAtom {
public var colorString:String=""
public var innerList:MTMathList?
init() {
super.init(type: .color, value: "")
}
public override convenience init(type: MTMathAtomType, value: String) {
if type == .color {
self.init(); return
}
NSException(name: NSExceptionName("InvalidMethod"), reason: "MTMathColorbox(type:value) cannot be called. Use MTMathColorbox() instead.").raise()
self.init()
}
public override var string: String {
"\\colorbox{\(self.colorString)}{\(self.innerList!.string)}"
}
}
public enum MTColumnAlignment {
case left
case center
case right
}
public class MTMathTable: MTMathAtom {
var alignments = [MTColumnAlignment]()
var cells = [[MTMathList]]()
var environment: String?
var interColumnSpacing: CGFloat = 0
var interRowAdditionalSpacing: CGFloat = 0
var numColumns = 0
var numRows = 0
override public var finalized: MTMathAtom {
let finalized: MTMathTable = super.finalized as! MTMathTable
for var row in finalized.cells {
for i in 0..<row.count {
row[i] = row[i].finalized
}
}
return finalized
}
convenience init(environment: String? = nil) {
self.init(type: .table, value: "")
self.environment = environment
}
func set(cell list: MTMathList, forRow row:Int, column:Int) {
if self.cells.count <= row {
for _ in self.cells.count...row {
self.cells.append([])
}
}
var rowArray = self.cells[row]
if rowArray.count <= column {
for _ in rowArray.count...column {
rowArray.append(MTMathList())
}
}
rowArray[column] = list
}
func set(alignment: MTColumnAlignment, forCol col: Int) {
if self.alignments.count <= col {
for _ in self.alignments.count...col {
self.alignments.append(MTColumnAlignment.center)
}
}
self.alignments[col] = alignment
}
func getAlignmentOf(col: Int) -> MTColumnAlignment {
if self.alignments.count <= col {
return MTColumnAlignment.center
} else {
return self.alignments[col]
}
}
func numberOfCols() -> Int {
var numberOfCols = 0
for row in self.cells {
numberOfCols = max(numberOfCols, row.count)
}
return numberOfCols
}
func numberOfRows() -> Int {
return self.cells.count
}
}
// represent list of math objects
extension MTMathList: CustomStringConvertible {
public var description: String { self.atoms.description }
public var string: String { self.description }
}
public class MTMathList {
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 prevNode == nil || prevNode!.isNotBinaryOperator() {
newNode.type = .unaryOperator
}
break
case .relation, .punctuation, .close:
if prevNode != nil &&
prevNode!.type == .binaryOperator {
prevNode!.type = .unaryOperator
}
break
case .number:
if prevNode != nil &&
prevNode!.type == .number &&
prevNode!.subScript == nil &&
prevNode!.superScript == nil {
prevNode!.fuse(with: newNode)
continue
}
break
default: break
}
finalizedList.add(newNode)
prevNode = newNode
}
if prevNode != nil && prevNode!.type == .binaryOperator {
prevNode!.type = .unaryOperator
finalizedList.removeLastAtom()
finalizedList.add(prevNode!)
}
return finalizedList
}
public init(atoms: [MTMathAtom]) {
self.atoms.append(contentsOf: atoms)
}
public init(atom: MTMathAtom) {
self.atoms.append(atom)
}
public init() {
self.atoms = []
}
func add(_ atom: MTMathAtom) {
if self.isAtomAllowed(atom) {
self.atoms.append(atom)
} else {
print("error, cannot add atom of type \(atom.type.rawValue) into atomlist")
}
}
func insert(_ atom: MTMathAtom, at index: Int) {
if self.isAtomAllowed(atom) {
self.atoms.insert(atom, at: index)
} else {
print("error, cannot add atom of type \(atom.type.rawValue) into atomlist")
}
}
func append(_ list: MTMathList) {
self.atoms += list.atoms
}
func removeLastAtom() {
if self.atoms.count > 0 {
self.atoms.removeLast()
}
}
func removeAtom(at index: Int) {
self.atoms.remove(at: index)
}
func removeAtoms(in range: ClosedRange<Int>) {
self.atoms.removeSubrange(range)
}
func isAtomAllowed(_ atom: MTMathAtom) -> Bool {
return atom.type != .boundary
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,816 @@
//
// 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.
class MTDisplay {
// needed for isIos6Supported() func above
static var initialized = false
static var supported = false
/// Draws itself in the given graphics context.
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?
/// 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.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)
}
}
func set(attrString: NSAttributedString?) {
attributedString = attrString
line = CTLineCreateWithAttributedString(attributedString!)
}
func set(textColor:MTColor) {
self.textColor = textColor
let attrStr = NSMutableAttributedString(attributedString: self.attributedString!)
let foregroundColor = NSAttributedString.Key(kCTForegroundColorAttributeName as String)
attrStr.addAttribute(foregroundColor, value:self.textColor!.cgColor, range:NSMakeRange(0, attrStr.length))
self.attributedString = attrStr
}
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 func draw(_ context: CGContext) {
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.
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 //kMTLinePositionRegular;
self.index = NSNotFound
self.range = range
self.recomputeDimensions()
}
func setType(_ type:LinePosition) {
self.type = type
}
func setIndex(_ index:Int) {
self.index = index
}
func setTextColor(_ textColor:MTColor) {
// Set the color on all subdisplays
self.textColor = textColor
for displayAtom in self.subDisplays {
displayAtom.textColor = textColor
}
}
func draw(context: CGContext) {
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() {
denominator?.position = CGPointMake(self.position.x + (self.width - denominator!.width)/2, self.position.y - self.denominatorDown)
}
func updateNumeratorPosition() {
numerator?.position = CGPointMake(self.position.x + (self.width - numerator!.width)/2, self.position.y + self.numeratorUp)
}
func setPosition(_ position: CGPoint) {
super.position = position
self.updateDenominatorPosition()
self.updateNumeratorPosition()
}
func setTextColor(_ textColor:MTColor) {
super.textColor = textColor
numerator?.textColor = textColor
denominator?.textColor = textColor
}
override func draw(_ context:CGContext) {
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?
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 setPosition(_ position:CGPoint) {
super.position = position
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);
}
func setTextColor(textColor:MTColor) {
super.textColor = textColor
self.radicand!.textColor = textColor
self.degree!.textColor = textColor
}
func draw(context: CGContext) {
// 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
}
func draw(context: CGContext) {
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 func draw(_ context: CGContext) {
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);
maxWidth = max(maxWidth, lowerLimit!.width);
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;
}
}
}
func setPosition(_ position:CGPoint) {
super.position = position;
self.updateLowerLimitPosition()
self.updateUpperLimitPosition()
self.updateNucleusPosition()
}
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);
}
func setTextColor(_ textColor:MTColor) {
super.textColor = textColor
self.upperLimit?.textColor = textColor
self.lowerLimit?.textColor = textColor
nucleus?.textColor = textColor
}
override func draw(_ context:CGContext) {
// 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;
}
func setTextColor(_ textColor:MTColor) {
super.textColor = textColor
inner?.textColor = textColor
}
override func draw(_ context:CGContext) {
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 setPosition(_ position:CGPoint) {
super.position = position;
self.updateInnerPosition()
}
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
}
func setTextColor(_ textColor:MTColor) {
super.textColor = textColor
accentee?.textColor = textColor
accent?.textColor = textColor
}
func setPosition(_ position:CGPoint) {
super.position = position
self.updateAccenteePosition()
}
func updateAccenteePosition() {
self.accentee?.position = CGPointMake(self.position.x, self.position.y);
}
override func draw(_ context:CGContext) {
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)
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,250 @@
//
// 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
}
/**
@typedef MTTextAlignment
@brief 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.
*/
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`.
*/
var mathList:MTMathList? {
didSet {
self.error = nil
self.latex = MTMathListBuilder.mathListToString(mathList)
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
}
/** 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 */
var latex = "" {
didSet {
self.error = nil
var error: NSError? = nil
self.mathList = MTMathListBuilder.build(fromString: latex, error: &error)
if error != nil {
self.mathList = nil
self.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()
}
}
/** This contains any error that occurred when parsing the latex. */
var error:NSError?
/** If true, if there is an error it displays the error message inline. Default true. */
var displayErrorInline = true
/** The MTFont to use for rendering. */
var font = MTFontManager.fontManager.defaultFont {
didSet {
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
}
/** Convenience method to just set the size of the font without changing the fontface. */
var fontSize = MTFontManager.fontManager.kDefaultFontSize {
didSet {
self.font = font?.copy(withSize: fontSize)
}
}
/** This sets the text color of the rendered math formula. The default color is black. */
var textColor:MTColor? = MTColor.black {
didSet {
self.displayList?.textColor = textColor
self.setNeedsDisplay()
}
}
/** 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.
*/
var contentInsets = MTEdgeInsetsZero {
didSet {
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
}
/** The Label mode for the label. The default mode is Display */
var labelMode = MTMathUILabelMode.display {
didSet {
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
}
/** Horizontal alignment for the text. The default is align left. */
var textAlignment = MTTextAlignment.left {
didSet {
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
}
/** The internal display of the MTMathUILabel. This is for advanced use only. */
var displayList: MTMathListDisplay? = nil
var currentStyle:MTLineStyle {
switch labelMode {
case .display: return .display
case .text: return .text
}
}
var errorLabel: MTLabel?
override init(frame: CGRect) {
super.init(frame: frame)
self.initCommon()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.initCommon()
}
func initCommon() {
#if os(macOS)
self.layer?.isGeometryFlipped = true
errorLabel?.layer?.isGeometryFlipped = true
#else
self.layer.isGeometryFlipped = true
errorLabel?.layer.isGeometryFlipped = true
#endif
self.backgroundColor = MTColor.clear
errorLabel = MTLabel()
errorLabel?.isHidden = true
errorLabel?.textColor = MTColor.red
self.addSubview(errorLabel!)
}
override 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 {
displayList = MTTypesetter.createLineForMathList(mathList, font: font, style: currentStyle)
displayList?.textColor = textColor
var textX = CGFloat(0)
switch self.textAlignment {
case .left:
textX = self.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 var intrinsicContentSize: CGSize { _sizeThatFits(CGSizeZero) }
#if os(macOS)
override var isFlipped: Bool { false }
func setNeedsDisplay() { self.needsDisplay = true }
func setNeedsLayout() { self.needsLayout = true }
override func layout() {
self._layoutSubviews()
super.layout()
}
#else
override func layoutSubviews() { self._layoutSubviews() }
override func sizeThatFits(_ size: CGSize) -> CGSize { self._sizeThatFits(size) }
#endif
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,96 @@
//
// 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 }
}
extension String {
var unicodeLength:Int {
self.lengthOfBytes(using: .utf32) / 4
}
}

View File

@@ -1,11 +1,468 @@
import XCTest import XCTest
@testable import SwiftMathRender @testable import SwiftMathRender
//
// MathRenderSwiftTests.swift
// MathRenderSwiftTests
//
// Created by Mike Griebling on 2023-01-02.
//
final class SwiftMathRenderTests: XCTestCase { final class SwiftMathRenderTests: XCTestCase {
func testExample() throws {
// This is an example of a functional test case. func checkAtomTypes(_ list:MTMathList?, types:[MTMathAtomType], desc:String) {
// Use XCTAssert and related functions to verify your tests produce the correct if let list = list {
// results. XCTAssertEqual(list.atoms.count, types.count, desc)
XCTAssertEqual(SwiftMathRender().text, "Hello, World!") for i in 0..<list.atoms.count {
let atom = list.atoms[i]
XCTAssertNotNil(atom, desc)
XCTAssertEqual(atom.type, types[i], desc)
}
} else {
XCTAssert(types.count == 0, "MathList should have no atoms!")
}
} }
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
struct TestRecord {
let build : String
let atomType : [MTMathAtomType]
let types : [MTMathAtomType]
let extra : [MTMathAtomType]
let result : String
init(build: String, atomType: [MTMathAtomType], types: [MTMathAtomType], extra: [MTMathAtomType] = [MTMathAtomType](), result: String) {
self.build = build
self.atomType = atomType
self.types = types
self.extra = extra
self.result = result
}
}
func getTestDataSuperScript() -> [TestRecord] {
[
TestRecord(build: "x^2", atomType: [.variable], types: [.number], result: "x^{2}"),
TestRecord(build: "x^23", atomType: [ .variable, .number ], types: [ .number ], result: "x^{2}3"),
TestRecord(build: "x^{23}", atomType: [ .variable ], types: [ .number, .number ], result: "x^{23}"),
TestRecord(build: "x^2^3", atomType: [ .variable, .ordinary ], types: [ .number ], result: "x^{2}{}^{3}" ),
TestRecord(build: "x^{2^3}", atomType: [ .variable ], types: [ .number], extra: [ .number ], result: "x^{2^{3}}"),
TestRecord(build: "x^{^2*}", atomType: [ .variable ], types: [ .ordinary, .binaryOperator], extra:[ .number ], result:"x^{{}^{2}*}"),
TestRecord(build: "^2", atomType: [ .ordinary], types: [ .number ], result: "{}^{2}"),
TestRecord(build: "{}^2", atomType: [ .ordinary], types: [ .number ], result: "{}^{2}"),
TestRecord(build: "x^^2", atomType: [ .variable, .ordinary ], types: [ ], result: "x^{}{}^{2}"),
TestRecord(build: "5{x}^2", atomType: [ .number, .variable], types: [ ], result: "5x^{2}"),
]
}
func getTestDataSubScript() -> [TestRecord] {
[
TestRecord(build: "x_2", atomType: [.variable], types: [.number], result: "x_{2}"),
TestRecord(build: "x_23", atomType: [ .variable, .number ], types: [ .number ], result: "x_{2}3"),
TestRecord(build: "x_{23}", atomType: [ .variable ], types: [ .number, .number ], result: "x_{23}"),
TestRecord(build: "x_2_3", atomType: [ .variable, .ordinary ], types: [ .number ], result: "x_{2}{}_{3}" ),
TestRecord(build: "x_{2_3}", atomType: [ .variable ], types: [ .number], extra: [ .number ], result: "x_{2_{3}}"),
TestRecord(build: "x_{_2*}", atomType: [ .variable ], types: [ .ordinary, .binaryOperator], extra:[ .number ], result:"x_{{}_{2}*}"),
TestRecord(build: "_2", atomType: [ .ordinary], types: [ .number ], result: "{}_{2}"),
TestRecord(build: "{}_2", atomType: [ .ordinary], types: [ .number ], result: "{}_{2}"),
TestRecord(build: "x__2", atomType: [ .variable, .ordinary ], types: [ ], result: "x_{}{}_{2}"),
TestRecord(build: "5{x}_2", atomType: [ .number, .variable], types: [ ], result: "5x_{2}"),
]
}
func getTestDataSuperSubScript() -> [TestRecord] {
[
TestRecord(build: "x_2^*", atomType: [.variable], types: [.number], extra: [.binaryOperator], result: "x^{*}_{2}"),
TestRecord(build: "x^*_2", atomType: [.variable], types: [.number], extra: [.binaryOperator], result: "x^{*}_{2}"),
TestRecord(build: "x_^*", atomType: [.variable], types: [ ], extra: [.binaryOperator], result: "x^{*}_{}"),
TestRecord(build: "x^_2", atomType: [.variable], types: [.number], result: "x^{}_{2}"),
TestRecord(build: "x_{2^*}", atomType: [.variable], types: [.number], result: "x_{2^{*}}"),
TestRecord(build: "x^{*_2}", atomType: [.variable], types: [ ], extra: [.binaryOperator], result: "x^{*_{2}}"),
TestRecord(build: "_2^*", atomType: [.ordinary], types: [.number], extra: [.binaryOperator], result: "{}^{*}_{2}")
]
}
struct TestRecord2 {
let build : String
let type1 : [MTMathAtomType]
let number : Int
let type2 : [MTMathAtomType]
let left : String
let right : String
let result : String
}
func getTestDataLeftRight() -> [TestRecord2] {
[
TestRecord2(build: "\\left( 2 \\right)", type1: [ .inner ], number: 0, type2: [ .number], left: "(", right: ")", result: "\\left( 2\\right) "),
// spacing
TestRecord2(build: "\\left ( 2 \\right )", type1: [ .inner ], number: 0, type2: [ .number], left: "(", right: ")", result: "\\left( 2\\right) "),
// commands
TestRecord2(build: "\\left\\{ 2 \\right\\}", type1: [ .inner ], number: 0, type2: [ .number], left: "{", right: "}", result: "\\left\\{ 2\\right\\} "),
// complex commands
TestRecord2(build: "\\left\\langle x \\right\\rangle", type1: [ .inner ], number: 0, type2: [ .variable], left: "\u{2329}", right: "\u{232A}", result: "\\left< x\\right> "),
// bars
TestRecord2(build: "\\left| x \\right\\|", type1: [ .inner ], number: 0, type2: [ .variable], left: "|", right: "\u{2016}", result: "\\left| x\\right\\| "),
// inner in between
TestRecord2(build: "5 + \\left( 2 \\right) - 2", type1: [ .number, .binaryOperator, .inner, .binaryOperator, .number ], number: 2, type2: [ .number], left: "(", right: ")", result: "5+\\left( 2\\right) -2"),
// long inner
TestRecord2(build: "\\left( 2 + \\frac12\\right)", type1: [ .inner ], number: 0, type2: [ .number, .binaryOperator, .fraction], left: "(", right: ")", result: "\\left( 2+\\frac{1}{2}\\right) "),
// nested
TestRecord2(build: "\\left[ 2 + \\left|\\frac{-x}{2}\\right| \\right]", type1: [ .inner ], number: 0, type2: [ .number, .binaryOperator, .inner], left: "[", right: "]", result: "\\left[ 2+\\left| \\frac{-x}{2}\\right| \\right] "),
// With scripts
TestRecord2(build: "\\left( 2 \\right)^2", type1: [ .inner ], number: 0, type2: [ .number], left: "(", right: ")", result: "\\left( 2\\right) ^{2}"),
// Scripts on left
TestRecord2(build: "\\left(^2 \\right )", type1: [ .inner], number: 0, type2: [ .ordinary], left: "(", right: ")", result: "\\left( {}^{2}\\right) "),
// Dot
TestRecord2(build: "\\left( 2 \\right.", type1: [ .inner], number: 0, type2: [ .number], left: "(", right: "", result: "\\left( 2\\right. ")
]
}
func testSuperScript() throws {
let data = getTestDataSuperScript()
for testCase in data {
let str = testCase.build
var error:NSError?
let list = MTMathListBuilder.build(fromString: str, error:&error)
XCTAssertNil(error)
let desc = "Error for string:\(str)"
let atomTypes = testCase.atomType
checkAtomTypes(list, types:atomTypes, desc:desc)
// get the first atom
let first = list!.atoms[0]
// check it's superscript
let types = testCase.types
if types.count > 0 {
XCTAssertNotNil(first.superScript, desc)
}
let superlist = first.superScript
checkAtomTypes(superlist, types:types, desc:desc)
if !testCase.extra.isEmpty {
// one more level
let superFirst = superlist!.atoms[0]
let supersuperList = superFirst.superScript
checkAtomTypes(supersuperList, types:testCase.extra, desc:desc)
}
// convert it back to latex
let latex = MTMathListBuilder.mathListToString(list)
XCTAssertEqual(latex, testCase.result, desc)
}
}
func testSubScript() throws {
let data = getTestDataSubScript()
for testCase in data {
let str = testCase.build
var error:NSError?
let list = MTMathListBuilder.build(fromString: str, error:&error)
XCTAssertNil(error)
let desc = "Error for string:\(str)"
let atomTypes = testCase.atomType
checkAtomTypes(list, types:atomTypes, desc:desc)
// get the first atom
let first = list!.atoms[0]
// check it's superscript
let types = testCase.types
if (types.count > 0) {
XCTAssertNotNil(first.subScript, desc);
}
let sublist = first.subScript
checkAtomTypes(sublist, types:types, desc:desc)
if !testCase.extra.isEmpty {
// one more level
let subFirst = sublist!.atoms[0]
let subsubList = subFirst.subScript
checkAtomTypes(subsubList, types:testCase.extra, desc:desc)
}
// convert it back to latex
let latex = MTMathListBuilder.mathListToString(list)
XCTAssertEqual(latex, testCase.result, desc)
}
}
func testSuperSubScript() throws {
let data = getTestDataSuperSubScript()
for testCase in data {
let str = testCase.build
var error:NSError?
let list = MTMathListBuilder.build(fromString: str, error:&error)
XCTAssertNil(error)
let desc = "Error for string:\(str)"
let atomTypes = testCase.atomType
checkAtomTypes(list, types:atomTypes, desc:desc)
// get the first atom
let first = list!.atoms[0]
// check its subscript
let sub = testCase.types
if sub.count > 0 {
XCTAssertNotNil(first.subScript, desc)
let sublist = first.subScript
checkAtomTypes(sublist, types: sub, desc: desc)
}
let sup = testCase.extra
if sup.count > 0 {
XCTAssertNotNil(first.superScript, desc)
let sublist = first.superScript
checkAtomTypes(sublist, types: sup, desc: desc)
}
// convert it back to latex
let latex = MTMathListBuilder.mathListToString(list)
XCTAssertEqual(latex, testCase.result, desc)
}
}
func testSymbols() throws {
let str = "5\\times3^{2\\div2}";
let list = MTMathListBuilder.build(fromString: str)!
let desc = "Error for string:\(str)"
XCTAssertNotNil(list, desc)
XCTAssertEqual((list.atoms.count), 3, desc)
var atom = list.atoms[0];
XCTAssertEqual(atom.type, .number, desc)
XCTAssertEqual(atom.nucleus, "5", desc)
atom = list.atoms[1];
XCTAssertEqual(atom.type, .binaryOperator, desc)
XCTAssertEqual(atom.nucleus, "\u{00D7}", desc)
atom = list.atoms[2];
XCTAssertEqual(atom.type, .number, desc)
XCTAssertEqual(atom.nucleus, "3", desc)
// super script
let superList = atom.superScript!
XCTAssertNotNil(superList, desc)
XCTAssertEqual((superList.atoms.count), 3, desc)
atom = superList.atoms[0];
XCTAssertEqual(atom.type, .number, desc)
XCTAssertEqual(atom.nucleus, "2", desc)
atom = superList.atoms[1];
XCTAssertEqual(atom.type, .binaryOperator, desc)
XCTAssertEqual(atom.nucleus, "\u{00F7}", desc)
atom = superList.atoms[2];
XCTAssertEqual(atom.type, .number, desc)
XCTAssertEqual(atom.nucleus, "2", desc)
}
func testFrac() throws {
let str = "\\frac1c";
let list = MTMathListBuilder.build(fromString: str)!
let desc = "Error for string:\(str)"
XCTAssertNotNil(list, desc)
XCTAssertEqual((list.atoms.count), 1, desc)
let frac = list.atoms[0] as! MTFraction
XCTAssertEqual(frac.type, .fraction, desc)
XCTAssertEqual(frac.nucleus, "", desc)
XCTAssertTrue(frac.hasRule);
XCTAssertNil(frac.rightDelimiter);
XCTAssertNil(frac.leftDelimiter);
var subList = frac.numerator!
XCTAssertNotNil(subList, desc)
XCTAssertEqual((subList.atoms.count), 1, desc)
var atom = subList.atoms[0];
XCTAssertEqual(atom.type, .number, desc)
XCTAssertEqual(atom.nucleus, "1", desc)
atom = list.atoms[0];
subList = frac.denominator!
XCTAssertNotNil(subList, desc)
XCTAssertEqual((subList.atoms.count), 1, desc)
atom = subList.atoms[0];
XCTAssertEqual(atom.type, .variable, desc)
XCTAssertEqual(atom.nucleus, "c", desc)
// convert it back to latex
let latex = MTMathListBuilder.mathListToString(list)
XCTAssertEqual(latex, "\\frac{1}{c}", desc)
}
func testFracInFrac() throws {
let str = "\\frac1\\frac23";
let list = MTMathListBuilder.build(fromString: str)!
let desc = "Error for string:\(str)"
XCTAssertNotNil(list, desc)
XCTAssertEqual((list.atoms.count), 1, desc)
var frac = list.atoms[0] as! MTFraction
XCTAssertEqual(frac.type, .fraction, desc)
XCTAssertEqual(frac.nucleus, "", desc)
XCTAssertTrue(frac.hasRule);
var subList = frac.numerator!
XCTAssertNotNil(subList, desc)
XCTAssertEqual((subList.atoms.count), 1, desc)
var atom = subList.atoms[0];
XCTAssertEqual(atom.type, .number, desc)
XCTAssertEqual(atom.nucleus, "1", desc)
subList = frac.denominator!
XCTAssertNotNil(subList, desc)
XCTAssertEqual((subList.atoms.count), 1, desc)
frac = subList.atoms[0] as! MTFraction
XCTAssertEqual(frac.type, .fraction, desc)
XCTAssertEqual(frac.nucleus, "", desc)
subList = frac.numerator!
XCTAssertNotNil(subList, desc)
XCTAssertEqual((subList.atoms.count), 1, desc)
atom = subList.atoms[0];
XCTAssertEqual(atom.type, .number, desc)
XCTAssertEqual(atom.nucleus, "2", desc)
subList = frac.denominator!
XCTAssertNotNil(subList, desc)
XCTAssertEqual((subList.atoms.count), 1, desc)
atom = subList.atoms[0];
XCTAssertEqual(atom.type, .number, desc)
XCTAssertEqual(atom.nucleus, "3", desc)
// convert it back to latex
let latex = MTMathListBuilder.mathListToString(list)
XCTAssertEqual(latex, "\\frac{1}{\\frac{2}{3}}", desc)
}
func testSqrt() throws {
let str = "\\sqrt2";
let list = MTMathListBuilder.build(fromString: str)!
let desc = "Error for string:\(str)"
XCTAssertNotNil(list, desc)
XCTAssertEqual((list.atoms.count), 1, desc)
let rad = list.atoms[0] as! MTRadical
XCTAssertEqual(rad.type, .radical, desc)
XCTAssertEqual(rad.nucleus, "", desc)
let subList = rad.radicand!
XCTAssertNotNil(subList, desc)
XCTAssertEqual((subList.atoms.count), 1, desc)
let atom = subList.atoms[0]
XCTAssertEqual(atom.type, .number, desc)
XCTAssertEqual(atom.nucleus, "2", desc)
// convert it back to latex
let latex = MTMathListBuilder.mathListToString(list)
XCTAssertEqual(latex, "\\sqrt{2}", desc)
}
func testSqrtInSqrt() throws {
let str = "\\sqrt\\sqrt2";
let list = MTMathListBuilder.build(fromString: str)!
let desc = "Error for string:\(str)"
XCTAssertNotNil(list, desc)
XCTAssertEqual((list.atoms.count), 1, desc)
var rad = list.atoms[0] as! MTRadical
XCTAssertEqual(rad.type, .radical, desc)
XCTAssertEqual(rad.nucleus, "", desc)
var subList = rad.radicand!
XCTAssertNotNil(subList, desc)
XCTAssertEqual((subList.atoms.count), 1, desc)
rad = subList.atoms[0] as! MTRadical
XCTAssertEqual(rad.type, .radical, desc)
XCTAssertEqual(rad.nucleus, "", desc)
subList = rad.radicand!
XCTAssertNotNil(subList, desc)
XCTAssertEqual((subList.atoms.count), 1, desc)
let atom = subList.atoms[0];
XCTAssertEqual(atom.type, .number, desc)
XCTAssertEqual(atom.nucleus, "2", desc)
// convert it back to latex
let latex = MTMathListBuilder.mathListToString(list)
XCTAssertEqual(latex, "\\sqrt{\\sqrt{2}}", desc)
}
func testRad() throws {
let str = "\\sqrt[3]2";
let list = MTMathListBuilder.build(fromString: str)!
XCTAssertNotNil(list);
XCTAssertEqual((list.atoms.count), 1);
let rad = list.atoms[0] as! MTRadical
XCTAssertEqual(rad.type, .radical);
XCTAssertEqual(rad.nucleus, "");
var subList = rad.radicand!
XCTAssertNotNil(subList);
XCTAssertEqual((subList.atoms.count), 1);
var atom = subList.atoms[0];
XCTAssertEqual(atom.type, .number);
XCTAssertEqual(atom.nucleus, "2");
subList = rad.degree!
XCTAssertNotNil(subList);
XCTAssertEqual((subList.atoms.count), 1);
atom = subList.atoms[0];
XCTAssertEqual(atom.type, .number);
XCTAssertEqual(atom.nucleus, "3");
// convert it back to latex
let latex = MTMathListBuilder.mathListToString(list)
XCTAssertEqual(latex, "\\sqrt[3]{2}");
}
func testLeftRight() throws {
let data = getTestDataLeftRight()
for testCase in data {
let str = testCase.build
var error:NSError?
let list = MTMathListBuilder.build(fromString: str, error: &error)!
XCTAssertNotNil(list, str);
XCTAssertNil(error, str);
checkAtomTypes(list, types:testCase.type1, desc:"\(str) outer")
let innerLoc = testCase.number
let inner = list.atoms[innerLoc] as! MTInner
XCTAssertEqual(inner.type, .inner, str);
XCTAssertEqual(inner.nucleus, "", str);
let innerList = inner.innerList!
XCTAssertNotNil(innerList, str);
checkAtomTypes(innerList, types:testCase.type2, desc:"\(str) inner")
XCTAssertNotNil(inner.leftBoundary, str);
XCTAssertEqual(inner.leftBoundary!.type, .boundary, str);
XCTAssertEqual(inner.leftBoundary!.nucleus, testCase.left, str);
XCTAssertNotNil(inner.rightBoundary, str);
XCTAssertEqual(inner.rightBoundary!.type, .boundary, str);
XCTAssertEqual(inner.rightBoundary!.nucleus, testCase.right, str);
// convert it back to latex
let latex = MTMathListBuilder.mathListToString(list)
XCTAssertEqual(latex, testCase.result, str);
}
}
// func testPerformanceExample() throws {
// // This is an example of a performance test case.
// measure {
// // Put the code you want to measure the time of here.
// }
// }
} }