diff --git a/Sources/SwiftMathRender/MathRender/MTBezierPath.swift b/Sources/SwiftMathRender/MathRender/MTBezierPath.swift new file mode 100644 index 0000000..754a785 --- /dev/null +++ b/Sources/SwiftMathRender/MathRender/MTBezierPath.swift @@ -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 + diff --git a/Sources/SwiftMathRender/MathRender/MTColor.swift b/Sources/SwiftMathRender/MathRender/MTColor.swift new file mode 100644 index 0000000..2af29bb --- /dev/null +++ b/Sources/SwiftMathRender/MathRender/MTColor.swift @@ -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) + } + +} diff --git a/Sources/SwiftMathRender/MathRender/MTConfig.swift b/Sources/SwiftMathRender/MathRender/MTConfig.swift new file mode 100644 index 0000000..d3a1df8 --- /dev/null +++ b/Sources/SwiftMathRender/MathRender/MTConfig.swift @@ -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 diff --git a/Sources/SwiftMathRender/MathRender/MTFont.swift b/Sources/SwiftMathRender/MathRender/MTFont.swift new file mode 100644 index 0000000..5689753 --- /dev/null +++ b/Sources/SwiftMathRender/MathRender/MTFont.swift @@ -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) + } + +} diff --git a/Sources/SwiftMathRender/MathRender/MTFontManager.swift b/Sources/SwiftMathRender/MathRender/MTFontManager.swift new file mode 100644 index 0000000..0206e08 --- /dev/null +++ b/Sources/SwiftMathRender/MathRender/MTFontManager.swift @@ -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) + } + + +} diff --git a/Sources/SwiftMathRender/MathRender/MTFontMathTable.swift b/Sources/SwiftMathRender/MathRender/MTFontMathTable.swift new file mode 100644 index 0000000..0ffedf9 --- /dev/null +++ b/Sources/SwiftMathRender/MathRender/MTFontMathTable.swift @@ -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 + } + + +} diff --git a/Sources/SwiftMathRender/MathRender/MTLabel.swift b/Sources/SwiftMathRender/MathRender/MTLabel.swift new file mode 100644 index 0000000..62945ba --- /dev/null +++ b/Sources/SwiftMathRender/MathRender/MTLabel.swift @@ -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 diff --git a/Sources/SwiftMathRender/MathRender/MTMathAtomFactory.swift b/Sources/SwiftMathRender/MathRender/MTMathAtomFactory.swift new file mode 100644 index 0000000..c617d6c --- /dev/null +++ b/Sources/SwiftMathRender/MathRender/MTMathAtomFactory.swift @@ -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..= 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.. + 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.. 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) { + self.atoms.removeSubrange(range) + } + + func isAtomAllowed(_ atom: MTMathAtom) -> Bool { + return atom.type != .boundary + } +} diff --git a/Sources/SwiftMathRender/MathRender/MTMathListBuilder.swift b/Sources/SwiftMathRender/MathRender/MTMathListBuilder.swift new file mode 100644 index 0000000..14145f7 --- /dev/null +++ b/Sources/SwiftMathRender/MathRender/MTMathListBuilder.swift @@ -0,0 +1,1016 @@ +// +// MTMathListBuilder.swift +// MathRenderSwift +// +// Created by Mike Griebling on 2022-12-31. +// + +import Foundation + +/** `MTMathListBuilder` is a class for parsing LaTeX into an `MTMathList` that + can be rendered and processed mathematically. + */ +class MTEnvProperties { + var envName: String? + var ended: Bool + var numRows: Int + + init(name: String?) { + self.envName = name + self.numRows = 0 + self.ended = false + } +} + +/** + @typedef case s + @brief The error encountered when parsing a LaTeX string. + + The `code` in the `NSError` is one of the following indiciating why the LaTeX string + could not be parsed. + */ +enum MTParseErrors:Int { + /// The braces { } do not match. + case mismatchBraces = 1 + /// A command in the string is not recognized. + case invalidCommand + /// An expected character such as ] was not found. + case characterNotFound + /// The \left or \right command was not followed by a delimiter. + case missingDelimiter + /// The delimiter following \left or \right was not a valid delimiter. + case invalidDelimiter + /// There is no \right corresponding to the \left command. + case missingRight + /// There is no \left corresponding to the \right command. + case missingLeft + /// The environment given to the \begin command is not recognized + case invalidEnv + /// A command is used which is only valid inside a \begin,\end environment + case missingEnv + /// There is no \begin corresponding to the \end command. + case missingBegin + /// There is no \end corresponding to the \begin command. + case missingEnd + /// The number of columns do not match the environment + case invalidNumColumns + /// Internal error, due to a programming mistake. + case internalError + /// Limit control applied incorrectly + case invalidLimits +} + +let MTParseError = "ParseError" + +public class MTMathListBuilder { + var string: String + var currentCharIndex: String.Index + var currentInnerAtom: MTInner? + var currentEnv: MTEnvProperties? + var currentFontStyle:MTFontStyle + var spacesAllowed:Bool + + /** Contains any error that occurred during parsing. */ + var error:NSError? + + var hasCharacters: Bool { currentCharIndex < string.endIndex } + + public static let spaceToCommands: [CGFloat: String] = [ + 3 : ",", + 4 : ">", + 5 : ";", + (-3) : "!", + 18 : "quad", + 36 : "qquad", + ] + + public static let styleToCommands: [MTLineStyle: String] = [ + .display: "displaystyle", + .text: "textstyle", + .script: "scriptstyle", + .scriptOfScript: "scriptscriptstyle" + ] + + init(string: String) { + self.string = string + self.currentCharIndex = string.startIndex + self.currentFontStyle = .defaultStyle + self.spacesAllowed = false + } + + func build() -> MTMathList? { + if let list = self.buildInternal(false) { + if self.hasCharacters { + print("Mismatched braces: \(self.string)") + return nil + } + return list + } else { + return nil + } + } + + public static func build(fromString string: String) -> MTMathList? { + let builder = MTMathListBuilder(string: string) + return builder.build() + } + + public static func build(fromString string: String, error:inout NSError?) -> MTMathList? { + let builder = MTMathListBuilder(string: string) + let output = builder.build() + if builder.error != nil { + if error != nil { + error = builder.error + } + return nil + } + return output + } + + public static func mathListToString(_ ml: MTMathList?) -> String { + var output = "" + + if let atomList = ml { + for atom in atomList.atoms { + switch atom.type { + case .fraction: + if let frac = atom as? MTFraction { + if frac.hasRule { + output += "\\frac{\(mathListToString(frac.numerator!))}{\(mathListToString(frac.denominator!))}" + } else { + var command: String? = nil + + if frac.leftDelimiter == nil && frac.rightDelimiter == nil { + command = "atop" + } else if frac.leftDelimiter == "(" && frac.rightDelimiter == ")" { + command = "choose" + } else if frac.leftDelimiter == "{" && frac.rightDelimiter == "}" { + command = "brace" + } else if frac.leftDelimiter == "[" && frac.rightDelimiter == "]" { + command = "brack" + } else { + command = "atopwithdelims\(frac.leftDelimiter!)\(frac.rightDelimiter!)" + } + + output += "{\(mathListToString(frac.numerator!)) \\\(command!) \(mathListToString(frac.denominator!))}" + } + } + break + case .radical: + output += "\\sqrt" + if let rad = atom as? MTRadical { + if rad.degree != nil { + output += "[\(mathListToString(rad.degree!))]" + } + output += "{\(mathListToString(rad.radicand!))}" + } + break + case .inner: + if let inner = atom as? MTInner { + if inner.leftBoundary != nil || inner.rightBoundary != nil { + if inner.leftBoundary != nil { + output += "\\left\(delimToString(delim: inner.leftBoundary!))" + } else { + output += "\\left. " + } + + output += mathListToString(inner.innerList!) + + if inner.rightBoundary != nil { + output += "\\right\(delimToString(delim: inner.rightBoundary!))" + } else { + output += "\\right. " + } + } else { + output += "{\(mathListToString(inner.innerList!))}" + } + } + break + case .table: + if let table = atom as? MTMathTable { + if table.environment != nil { + output += "\\begin{\(table.environment!)}" + } + + for i in 0..= 1 && cell.atoms[0].type == .style { + // remove first atom + cell.atoms.removeFirst() + } + } + if table.environment == "eqalign" || table.environment == "aligned" || table.environment == "split" { + if j == 1 && cell.atoms.count >= 1 && cell.atoms[0].type == .ordinary && cell.atoms[0].nucleus.count == 0 { + // remove empty nucleus added for spacing + cell.atoms.removeFirst() + } + } + + output += mathListToString(cell) + + if j < table.cells[i].count { + output += "&" + } + } + if i < table.numberOfRows() - 1 { + output += "\\\\ " + } + } + + if table.environment != nil { + output += "\\end{\(table.environment!)}" + } + } + break + case .overline: + if let overline = atom as? MTOverLine { + output += "\\overline" + output += "{\(mathListToString(overline.innerList!))}" + } + break + case .underline: + if let underline = atom as? MTUnderLine { + output += "\\underline" + output += "{\(mathListToString(underline.innerList!))}" + } + break + case .accent: + if let accent = atom as? MTAccent { + output += "\\\(MTMathAtomFactory.getName(of: accent)!){\(mathListToString(accent.innerList!))}" + } + break + case .space: + if let space = atom as? MTMathSpace { + if let command = MTMathListBuilder.spaceToCommands[space.space] { + output += "\\\(command)" + } else { + output += String.init(format: "\\mkern%.1fmu", space.space) + } + } + break + case .style: + if let style = atom as? MTMathStyle { + if let command = MTMathListBuilder.styleToCommands[style.style] { + output += "\\\(command)" + } + } + break + default: + if atom.nucleus.count == 0 { + output += "{}" + } else if atom.nucleus == "\u{2236}" { + output += ":" + } else if atom.nucleus == "\u{2212}" { + output += "-" + } else { + if let command = MTMathAtomFactory.latexSymbolName(for: atom) { + output += "\\\(command)" + } else { + output += "\(atom.nucleus)" + } + } + break + } + + if atom.superScript != nil { + output += "^{\(mathListToString(atom.superScript!))}" + } + + if atom.subScript != nil { + output += "_{\(mathListToString(atom.subScript!))}" + } + } + } + return output + } + + public static func delimToString(delim: MTMathAtom) -> String { + if let command = MTMathAtomFactory.getDelimiterName(of: delim) { + let singleChars = [ "(", ")", "[", "]", "<", ">", "|", ".", "/"] + if singleChars.contains(command) { + return command + } else if command == "||" { + return "\\|" + } else { + return "\\\(command)" + } + } + + return "" + } + + func getNextCharacter() -> Character? { + assert(self.hasCharacters, "Retrieving character at index \(self.currentCharIndex) beyond length \(self.string.count)") + if self.hasCharacters { + let ch = string[currentCharIndex] + currentCharIndex = string.index(after: currentCharIndex) + return ch + } + return nil + } + + func unlookCharacter() { + assert(currentCharIndex > string.startIndex, "Unlooking when at the first character.") + if currentCharIndex > string.startIndex { + currentCharIndex = string.index(before: currentCharIndex) + } else { + print("unlooking at first character") + } + } + + func buildInternal(_ oneCharOnly: Bool) -> MTMathList? { + return self.buildInternal(oneCharOnly, stop: nil) + } + + func buildInternal(_ oneCharOnly: Bool, stop: Character?) -> MTMathList? { + let list = MTMathList() + assert(!(oneCharOnly && stop != nil), "Cannot set both oneCharOnly and stopChar.") + var prevAtom: MTMathAtom? = nil + while self.hasCharacters { + if error != nil { return nil } + + var atom: MTMathAtom? = nil + let char = self.getNextCharacter()! + + if oneCharOnly { + if char == "^" || char == "}" || char == "_" || char == "&" { + self.unlookCharacter() + return list + } + } + + if stop != nil && char == stop { + return list + } + + if char == "^" { + assert(!oneCharOnly, "This should have been handled before") + if (prevAtom == nil || prevAtom!.superScript != nil || !prevAtom!.isScriptAllowed()) { + // If there is no previous atom, or if it already has a superscript + // or if scripts are not allowed for it, then add an empty node. + prevAtom = MTMathAtom(type: .ordinary, value: "") + list.add(prevAtom!) + } + + prevAtom!.setSuperScript(self.buildInternal(true)) + continue + } else if char == "_" { + assert(!oneCharOnly, "This should have been handled before") + if (prevAtom == nil || prevAtom!.subScript != nil || !prevAtom!.isScriptAllowed()) { + // If there is no previous atom, or if it already has a subcript + // or if scripts are not allowed for it, then add an empty node. + prevAtom = MTMathAtom(type: .ordinary, value: "") + list.add(prevAtom!) + } + prevAtom!.setSubScript(self.buildInternal(true)) + continue + } else if char == "{" { + // this puts us in a recursive routine, and sets oneCharOnly to false and no stop character + if let subList = self.buildInternal(false, stop: "}") { + prevAtom = subList.atoms.last + list.append(subList) + if oneCharOnly { + return list + } + continue + } else { + print("open brackets but inner...") + return nil + } + } else if char == "}" { + assert(!oneCharOnly, "This should have been handled before") + assert(stop == nil, "This should have been handled before") + // We encountered a closing brace when there is no stop set, that means there was no + // corresponding opening brace. + print("Mismatched braces") + return nil + } else if char == "\\" { + let command = readCommand() + let done = stopCommand(command, list:list, stop:stop) + if done != nil { + return done + } else if error != nil { + return nil + } + if self.applyModifier(command, atom:prevAtom) { + continue; + } + let fontStyle = MTMathAtomFactory.fontStyleWithName(command) + if fontStyle != nil { + let oldSpacesAllowed = spacesAllowed + // Text has special consideration where it allows spaces without escaping. + spacesAllowed = command == "text" + let oldFontStyle = currentFontStyle + currentFontStyle = fontStyle! + let sublist = self.buildInternal(true)! + // Restore the font style. + currentFontStyle = oldFontStyle + spacesAllowed = oldSpacesAllowed + + prevAtom = sublist.atoms.last + list.append(sublist) + if oneCharOnly { + return list + } + continue + } + atom = self.atomForCommand(command) + if atom == nil { + // this was an unknown command, + // we flag an error and return + // (note setError will not set the error if there is already one, so we flag internal error + // in the odd case that an _error is not set. + self.setError(.internalError, message:"Internal error") + return nil; + } + } else if char == "&" { + assert(!oneCharOnly, "This should have been handled before") + if self.currentEnv != nil { + return list + } else { + if let table = self.buildTable(env: nil, firstList: list, isRow: false) { + return MTMathList(atom: table) + } + } + } else if spacesAllowed && char == " " { + atom = MTMathAtomFactory.atom(forLatexSymbol: " ") + } else { + atom = MTMathAtomFactory.atom(for: String(char)) + + if atom == nil { + continue + } + } + + assert(atom != nil, "Atom shouldn't be nil") + + if atom == nil { + print("wtf atom why nil?") + return nil + } + + list.add(atom!) + prevAtom = atom + + if oneCharOnly { + return list + } + } + + if stop != nil { + if stop == "}" { + print("Missing Closing Brace") + } else { + print("Expected Character not found: \(stop!)") + } + } + + return list + } + + func atomForCommand(_ command:String) -> MTMathAtom? { + if let atom = MTMathAtomFactory.atom(forLatexSymbol: command) { + return atom + } + if let accent = MTMathAtomFactory.getAccent(withName: command) { + // The command is an accent + accent.innerList = self.buildInternal(true) + return accent; + } else if command == "frac" { + // A fraction command has 2 arguments + let frac = MTFraction() + frac.numerator = self.buildInternal(true) + frac.denominator = self.buildInternal(true) + return frac; + } else if command == "binom" { + // A binom command has 2 arguments + let frac = MTFraction(hasRule: false) + frac.numerator = self.buildInternal(true) + frac.denominator = self.buildInternal(true) + frac.leftDelimiter = "("; + frac.rightDelimiter = ")"; + return frac; + } else if command == "sqrt" { + // A sqrt command with one argument + let rad = MTRadical() + let ch = self.getNextCharacter() + if (ch == "[") { + // special handling for sqrt[degree]{radicand} + rad.degree = self.buildInternal(false, stop:"]") + rad.radicand = self.buildInternal(true) + } else { + self.unlookCharacter() + rad.radicand = self.buildInternal(true) + } + return rad; + } else if command == "left" { + // Save the current inner while a new one gets built. + let oldInner = currentInnerAtom + currentInnerAtom = MTInner() + currentInnerAtom!.leftBoundary = self.getBoundaryAtom("left") + if currentInnerAtom!.leftBoundary == nil { + return nil; + } + currentInnerAtom!.innerList = self.buildInternal(false) + if currentInnerAtom!.rightBoundary == nil { + // A right node would have set the right boundary so we must be missing the right node. + let errorMessage = "Missing \\right" + self.setError(.missingRight, message:errorMessage) + return nil + } + // reinstate the old inner atom. + let newInner = currentInnerAtom; + currentInnerAtom = oldInner; + return newInner; + } else if command == "overline" { + // The overline command has 1 arguments + let over = MTOverLine() + over.innerList = self.buildInternal(true) + return over + } else if command == "underline" { + // The underline command has 1 arguments + let under = MTUnderLine() + under.innerList = self.buildInternal(true) + return under + } else if command == "begin" { + let env = self.readEnvironment() + if env == nil { + return nil; + } + let table = self.buildTable(env: env, firstList:nil, isRow:false) + return table + } else if command == "color" { + // A color command has 2 arguments + let mathColor = MTMathColor() + mathColor.colorString = self.readColor()! + mathColor.innerList = self.buildInternal(true) + return mathColor + } else if command == "colorbox" { + // A color command has 2 arguments + let mathColorbox = MTMathColorbox(); + mathColorbox.colorString = self.readColor()! + mathColorbox.innerList = self.buildInternal(true) + return mathColorbox + } else { + let errorMessage = "Invalid command \\\(command)" + self.setError(.invalidCommand, message:errorMessage) + return nil; + } + } + + func readColor() -> String? { + if !self.expectCharacter("{") { + // We didn't find an opening brace, so no env found. + self.setError(.characterNotFound, message:"Missing {") + return nil; + } + + // Ignore spaces and nonascii. + self.skipSpaces() + + // a string of all upper and lower case characters. + var mutable = "" + while self.hasCharacters { + let ch = self.getNextCharacter()! + if (ch == "#" || (ch >= "A" && ch <= "F") || (ch >= "a" && ch <= "f") || (ch >= "0" && ch <= "9")) { + mutable.append(ch) // appendString:[NSString stringWithCharacters:&ch length:1]]; + } else { + // we went too far + self.unlookCharacter() + break; + } + } + + if !self.expectCharacter("}") { + // We didn't find an closing brace, so invalid format. + self.setError(.characterNotFound, message:"Missing }") + return nil; + } + return mutable; + } + + func skipSpaces() { + while self.hasCharacters { + let ch = self.getNextCharacter()?.asciiValue ?? 0 + if ch < 0x21 || ch > 0x7E { + // skip non ascii characters and spaces + continue; + } else { + self.unlookCharacter() + return; + } + } + } + + func stopCommand(_ command: String, list:MTMathList, stop:Character?) -> MTMathList? { + var fractionCommands: [String:[Character]] { + [ + "over": [], + "atop" : [], + "choose" : [ "(", ")"], + "brack" : [ "[", "]"], + "brace" : [ "{", "}"] + ] + } + if command == "right" { + if currentInnerAtom == nil { + let errorMessage = "Missing \\left"; + self.setError(.missingLeft, message:errorMessage) + return nil; + } + currentInnerAtom!.rightBoundary = self.getBoundaryAtom("right") + if currentInnerAtom!.rightBoundary == nil { + return nil; + } + // return the list read so far. + return list + } else if let _ = fractionCommands[command] { + var frac:MTFraction! = nil; + if command == "over" { + frac = MTFraction() + } else { + frac = MTFraction(hasRule: false) + } + let delims = fractionCommands[command]! + if delims.count == 2 { + frac.leftDelimiter = String(delims[0]) + frac.rightDelimiter = String(delims[1]) + } + frac.numerator = list; + frac.denominator = self.buildInternal(false, stop: stop) + if error != nil { + return nil; + } + let fracList = MTMathList() + fracList.add(frac) + return fracList + } else if command == "\\" || command == "cr" { + if currentEnv != nil { + // Stop the current list and increment the row count + currentEnv!.numRows+=1 + return list + } else { + // Create a new table with the current list and a default env + let table = self.buildTable(env: nil, firstList:list, isRow:true) + return MTMathList(atom: table!) + } + } else if command == "end" { + if currentEnv != nil { + let errorMessage = "Missing \\begin"; + self.setError(.missingBegin, message:errorMessage) + return nil; + } + let env = self.readEnvironment() + if env == nil { + return nil; + } + if env! != currentEnv!.envName { + let errorMessage = "Begin environment name \(currentEnv!.envName!) does not match end name: \(env!)" + self.setError(.invalidEnv, message:errorMessage) + return nil + } + // Finish the current environment. + currentEnv!.ended = true + return list + } + return nil + } + + // Applies the modifier to the atom. Returns true if modifier applied. + func applyModifier(_ modifier:String, atom:MTMathAtom?) -> Bool { + if modifier == "limits" { + if atom!.type != .largeOperator { + let errorMessage = "Limits can only be applied to an operator." + self.setError(.invalidLimits, message:errorMessage) + } else { + let op = atom as! MTLargeOperator + op.limits = true + } + return true + } else if modifier == "nolimits" { + if atom!.type != .largeOperator { + let errorMessage = "No limits can only be applied to an operator." + self.setError(.invalidLimits, message:errorMessage) + return true + } else { + let op = atom as! MTLargeOperator + op.limits = false + } + return true; + } + return false; + } + + func setError(_ code:MTParseErrors, message:String) { + // Only record the first error. + if error == nil { + error = NSError(domain: MTParseError, code: code.rawValue, userInfo: [ NSLocalizedDescriptionKey : message ]) + } + } + + func atom(forCommand command: String) -> MTMathAtom? { + if let atom = MTMathAtomFactory.atom(forLatexSymbol: command) { + return atom + } + + if let accent = MTMathAtomFactory.getAccent(withName: command) { + accent.innerList = self.buildInternal(true) + return accent + } else if command == "frac" { + let frac = MTFraction() + frac.numerator = self.buildInternal(true) + frac.denominator = self.buildInternal(true) + return frac + } else if command == "binom" { + let frac = MTFraction(hasRule: false) + frac.numerator = self.buildInternal(true) + frac.denominator = self.buildInternal(true) + frac.leftDelimiter = "(" + frac.rightDelimiter = ")" + return frac + } else if command == "sqrt" { + let rad = MTRadical() + let char = self.getNextCharacter() + + if char == "[" { + rad.degree = self.buildInternal(false, stop: "]") + rad.radicand = self.buildInternal(true) + } else { + self.unlookCharacter() + rad.radicand = self.buildInternal(true) + } + return rad + } else if command == "left" { + let oldInner = self.currentInnerAtom + self.currentInnerAtom = MTInner() + self.currentInnerAtom?.leftBoundary = self.getBoundaryAtom("left") + if self.currentInnerAtom?.leftBoundary == nil { + return nil + } + self.currentInnerAtom!.innerList = self.buildInternal(false) + if self.currentInnerAtom?.rightBoundary == nil { + print("Missing \\right") + return nil + } + let newInner = self.currentInnerAtom + currentInnerAtom = oldInner + return newInner + } else if command == "overline" { + let over = MTOverLine() + over.innerList = self.buildInternal(true) + + return over + } else if command == "underline" { + let under = MTUnderLine() + under.innerList = self.buildInternal(true) + + return under + } else if command == "begin" { + if let env = self.readEnvironment() { + let table = self.buildTable(env: env, firstList: nil, isRow: false) + return table + } else { + return nil + } + } else { + print("Invalid Command") + return nil + } + } + +// func stop(command: String, list: MTMathList, stopChar: Character) -> MTMathList? { +// let fractionCommands = [ +// "over": [], +// "atop": [], +// "choose": ["(", ")"], +// "brack": ["[", "]"], +// "brace": ["{", "}"] +// ] +// +// if command == "right" { +// if self.currentInnerAtom == nil { +// print("missing left") +// return nil +// } +// +// if let rightBoundary = self.getBoundaryAtom("right") { +// self.currentInnerAtom!.rightBoundary = rightBoundary +// +// return list +// } else { +// return nil +// } +// } else if let delims = fractionCommands[command] { +// let frac: MTFraction +// if command == "over" { +// frac = MTFraction() +// } else { +// frac = MTFraction(hasRule: false) +// } +// +// if delims.count == 2 { +// frac.leftDelimiter = delims[0] +// frac.rightDelimiter = delims[1] +// } +// +// frac.numerator = list +// frac.denominator = self.buildInternal(false, stopChar: stopChar) +// +// let fracList = MTMathList() +// fracList.add(frac) +// return fracList +// } else if command == "\\" || command == "cr" { +// if currentEnv != nil { +// currentEnv!.numRows += 1 +// return list +// } else { +// if let table = self.buildTable(env: nil, firstList: list, isRow: true) { +// return MTMathList(atoms: [table]) +// } else { +// return nil +// } +// } +// } else if command == "end" { +// if self.currentEnv == nil { +// print("Missing \\begin") +// return nil +// } +// +// if let env = self.readEnvironment() { +// if env != self.currentEnv?.envName { +// print("Begin environment name \(currentEnv!.envName!) does not match end name: \(env)") +// return nil +// } +// +// currentEnv?.ended = true +// +// return list +// } else { +// return nil +// } +// } +// return nil +// } + + func readEnvironment() -> String? { + if !self.expectCharacter("{") { + // We didn't find an opening brace, so no env found. + print("Missing {") + return nil + } + + self.skipSpace() + let env = self.readString() + + if !self.expectCharacter("}") { + // We didn"t find an closing brace, so invalid format. + print("Missing {") + return nil; + } + return env + } + + func expectCharacter(_ char: Character) -> Bool { + self.skipSpace() + + if self.hasCharacters { + let nextChar = self.getNextCharacter()! + + if nextChar == char { + return true + } else { + self.unlookCharacter() + return false + } + } + return false + } + + func buildTable(env: String?, firstList: MTMathList?, isRow: Bool) -> MTMathAtom? { + // Save the current env till an new one gets built. + let oldEnv = self.currentEnv + + currentEnv = MTEnvProperties(name: env) + + var currentRow = 0 + var currentCol = 0 + + var rows = [[MTMathList]]() + + if firstList != nil { + rows[currentRow][currentCol] = firstList! + + if isRow { + currentEnv?.numRows += 1 + currentRow += 1 + } else { + currentCol += 1 + } + } + + while !currentEnv!.ended && self.hasCharacters { + if let list = self.buildInternal(false) { + rows[currentRow][currentCol] = list + currentCol += 1 + if self.currentEnv!.numRows > currentRow { + currentRow = self.currentEnv!.numRows + currentCol = 0 + } + } else { + return nil + } + } + + if !currentEnv!.ended && currentEnv?.envName == nil { + print("Missing \\end") + return nil + } + + if let table = MTMathAtomFactory.table(withEnvironment: currentEnv?.envName, rows: rows) { + self.currentEnv = oldEnv + return table + } else { + return nil + } + } + + func getBoundaryAtom(_ delimiterType: String) -> MTMathAtom? { + let delim = self.readDelimiter() + if delim == nil { + assertionFailure("Missing delimiter for \(delimiterType)") + return nil + } + let boundary = MTMathAtomFactory.boundary(forDelimiter: delim!) + if boundary == nil { + assertionFailure("Invalid delimiter for \(delimiterType): \(delim!)") + return nil + } + return boundary + } + + func readDelimiter() -> String? { + self.skipSpace() + + while self.hasCharacters { + let char = self.getNextCharacter()! + + if char == "\\" { + let command = self.readCommand() + if command == "|" { + return "||" + } + return command + } else { + return String(char) + } + } + return nil + } + + func skipSpace() { + while self.hasCharacters { + let char = self.getNextCharacter() + let asciiCharacterSet = CharacterSet(charactersIn: UnicodeScalar(0x21)...UnicodeScalar(0x7e)) + if String(char!).rangeOfCharacter(from: asciiCharacterSet) != nil { + self.unlookCharacter() + return + } else { + continue + } + } + } + + func readCommand() -> String { + let singleChars = Array("{}$#%_| ,>;!\\") + if self.hasCharacters { + if let char = self.getNextCharacter() { + if let _ = singleChars.firstIndex(of: char) { + return String(char) + } else { + self.unlookCharacter() + } + } + } + + return self.readString() + } + + func readString() -> String { + // a string of all upper and lower case characters. + var output = "" + while self.hasCharacters { + if let char = self.getNextCharacter() { + if char.isLowercase || char.isUppercase { + output.append(char) + } else { + self.unlookCharacter() + break + } + } + } + return output + } +} diff --git a/Sources/SwiftMathRender/MathRender/MTMathListDisplay.swift b/Sources/SwiftMathRender/MathRender/MTMathListDisplay.swift new file mode 100644 index 0000000..375c35b --- /dev/null +++ b/Sources/SwiftMathRender/MathRender/MTMathListDisplay.swift @@ -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(); + } + +} diff --git a/Sources/SwiftMathRender/MathRender/MTMathListIndex.swift b/Sources/SwiftMathRender/MathRender/MTMathListIndex.swift new file mode 100644 index 0000000..1a49be6 --- /dev/null +++ b/Sources/SwiftMathRender/MathRender/MTMathListIndex.swift @@ -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 + } + } +} diff --git a/Sources/SwiftMathRender/MathRender/MTMathUILabel.swift b/Sources/SwiftMathRender/MathRender/MTMathUILabel.swift new file mode 100644 index 0000000..3e5258e --- /dev/null +++ b/Sources/SwiftMathRender/MathRender/MTMathUILabel.swift @@ -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 + +} diff --git a/Sources/SwiftMathRender/MathRender/MTTypesetter.swift b/Sources/SwiftMathRender/MathRender/MTTypesetter.swift new file mode 100644 index 0000000..bff1d3b --- /dev/null +++ b/Sources/SwiftMathRender/MathRender/MTTypesetter.swift @@ -0,0 +1,1704 @@ +// +// MTTypesetter.swift +// MathRenderSwift +// +// Created by Mike Griebling on 2023-01-01. +// + +import Foundation +import CoreGraphics +import CoreText + +// Mark: - Inter Element Spacing + +enum InterElementSpaceType : Int { + case invalid = -1 + case none = 0 + case thin + case nsThin // Thin but not in script mode + case nsMedium + case nsThick +} + +var interElementSpaceArray:[[InterElementSpaceType]]? = nil + +func getInterElementSpaces() -> [[InterElementSpaceType]] { + if interElementSpaceArray == nil { + interElementSpaceArray = + // ordinary operator binary relation open close punct fraction + [ [.none, .thin, .nsMedium, .nsThick, .none, .none, .none, .nsThin], // ordinary + [.thin, .thin, .invalid, .nsThick, .none, .none, .none, .nsThin], // operator + [.nsMedium, .nsMedium, .invalid, .invalid, .nsMedium, .invalid, .invalid, .nsMedium], // binary + [.nsThick, .nsThick, .invalid, .none, .nsThick, .none, .none, .nsThick], // relation + [.none, .none, .invalid, .none, .none, .none, .none, .none], // open + [.none, .thin, .nsMedium, .nsThick, .none, .none, .none, .nsThin], // close + [.nsThin, .nsThin, .invalid, .nsThin, .nsThin, .nsThin, .nsThin, .nsThin], // punct + [.nsThin, .thin, .nsMedium, .nsThick, .nsThin, .none, .nsThin, .nsThin], // fraction + [.nsMedium, .nsThin, .nsMedium, .nsThick, .none, .none, .none, .nsThin]] // radical + } + return interElementSpaceArray! +} + + +// Get's the index for the given type. If row is true, the index is for the row (i.e. left element) otherwise it is for the column (right element) +func getInterElementSpaceArrayIndexForType(_ type:MTMathAtomType, row:Bool) -> UInt { + switch type { + case .ordinary, .placeholder: // A placeholder is treated as ordinary + return 0 + case .largeOperator: + return 1 + case .binaryOperator: + return 2; + case .relation: + return 3; + case .open: + return 4; + case .close: + return 5; + case .punctuation: + return 6; + case .fraction, // Fraction and inner are treated the same. + .inner: + return 7; + case .radical: + if (row) { + // Radicals have inter element spaces only when on the left side. + // Note: This is a departure from latex but we don't want \sqrt{4}4 to look weird so we put a space in between. + // They have the same spacing as ordinary except with ordinary. + return 8; + } else { + assert(false, "Interelement space undefined for radical on the right. Treat radical as ordinary.") + return UInt.max + } + default: + assert(false, "Interelement space undefined for type \(type)") + return UInt.max + } +} + +// Mark: - Italics +// mathit +func getItalicized(_ ch:Character) -> UTF32Char { + var unicode = ch.utf32Char + + // Special cases for italics + if ch == "h" { return UnicodeSymbol.planksConstant } + + if ch.isUpperEnglish { + unicode = UnicodeSymbol.capitalItalicStart + (ch.utf32Char - Character("A").utf32Char) + } else if ch.isLowerEnglish { + unicode = UnicodeSymbol.lowerItalicStart + (ch.utf32Char - Character("a").utf32Char) + } else if ch.isCapitalGreek { + // Capital Greek characters + unicode = UnicodeSymbol.greekCapitalItalicStart + (ch.utf32Char - UnicodeSymbol.capitalGreekStart) + } else if ch.isLowerGreek { + // Greek characters + unicode = UnicodeSymbol.greekLowerItalicStart + (ch.utf32Char - UnicodeSymbol.lowerGreekStart) + } else if ch.isGreekSymbol { + return UnicodeSymbol.greekSymbolItalicStart + ch.greekSymbolOrder! + } + // Note there are no italicized numbers in unicode so we don't support italicizing numbers. + return unicode +} + +// mathbf +func getBold(_ ch:Character) -> UTF32Char { + var unicode = ch.utf32Char + if ch.isUpperEnglish { + unicode = UnicodeSymbol.mathCapitalBoldStart + (ch.utf32Char - Character("A").utf32Char) + } else if ch.isLowerEnglish { + unicode = UnicodeSymbol.mathLowerBoldStart + (ch.utf32Char - Character("a").utf32Char) + } else if ch.isCapitalGreek { + // Capital Greek characters + unicode = UnicodeSymbol.greekCapitalBoldStart + (ch.utf32Char - UnicodeSymbol.capitalGreekStart); + } else if ch.isLowerGreek { + // Greek characters + unicode = UnicodeSymbol.greekLowerBoldStart + (ch.utf32Char - UnicodeSymbol.lowerGreekStart); + } else if ch.isGreekSymbol { + return UnicodeSymbol.greekSymbolBoldStart + ch.greekSymbolOrder! + } else if ch.isNumber { + unicode = UnicodeSymbol.numberBoldStart + (ch.utf32Char - Character("0").utf32Char) + } + return unicode +} + +// mathbfit +func getBoldItalic(_ ch:Character) -> UTF32Char { + var unicode = ch.utf32Char + if ch.isUpperEnglish { + unicode = UnicodeSymbol.mathCapitalBoldItalicStart + (ch.utf32Char - Character("A").utf32Char) + } else if ch.isLowerEnglish { + unicode = UnicodeSymbol.mathLowerBoldItalicStart + (ch.utf32Char - Character("a").utf32Char) + } else if ch.isCapitalGreek { + // Capital Greek characters + unicode = UnicodeSymbol.greekCapitalBoldItalicStart + (ch.utf32Char - UnicodeSymbol.capitalGreekStart); + } else if ch.isLowerGreek { + // Greek characters + unicode = UnicodeSymbol.greekLowerBoldItalicStart + (ch.utf32Char - UnicodeSymbol.lowerGreekStart); + } else if ch.isGreekSymbol { + return UnicodeSymbol.greekSymbolBoldItalicStart + ch.greekSymbolOrder! + } else if ch.isNumber { + // No bold italic for numbers so we just bold them. + unicode = getBold(ch); + } + return unicode; +} + +// LaTeX default +func getDefaultStyle(_ ch:Character) -> UTF32Char { + if ch.isLowerEnglish || ch.isUpperEnglish || ch.isLowerGreek || ch.isGreekSymbol { + return getItalicized(ch); + } else if ch.isNumber || ch.isCapitalGreek { + // In the default style numbers and capital greek is roman + return ch.utf32Char + } else if ch == "." { + // . is treated as a number in our code, but it doesn't change fonts. + return ch.utf32Char + } else { + NSException(name: NSExceptionName("IllegalCharacter"), reason: "Unknown character \(ch) for default style.").raise() + } + return ch.utf32Char +} + +// mathcal/mathscr (caligraphic or script) +func getCaligraphic(_ ch:Character) -> UTF32Char { + // Caligraphic has lots of exceptions: + switch ch { + case "B": + return 0x212C; // Script B (bernoulli) + case "E": + return 0x2130; // Script E (emf) + case "F": + return 0x2131; // Script F (fourier) + case "H": + return 0x210B; // Script H (hamiltonian) + case "I": + return 0x2110; // Script I + case "L": + return 0x2112; // Script L (laplace) + case "M": + return 0x2133; // Script M (M-matrix) + case "R": + return 0x211B; // Script R (Riemann integral) + case "e": + return 0x212F; // Script e (Natural exponent) + case "g": + return 0x210A; // Script g (real number) + case "o": + return 0x2134; // Script o (order) + default: + break; + } + var unicode:UTF32Char + if ch.isUpperEnglish { + unicode = UnicodeSymbol.mathCapitalScriptStart + (ch.utf32Char - Character("A").utf32Char) + } else if ch.isLowerEnglish { + // Latin Modern Math does not have lower case caligraphic characters, so we use + // the default style instead of showing a ? + unicode = getDefaultStyle(ch) + } else { + // Caligraphic characters don't exist for greek or numbers, we give them the + // default treatment. + unicode = getDefaultStyle(ch) + } + return unicode; +} + +// mathtt (monospace) +func getTypewriter(_ ch:Character) -> UTF32Char { + if ch.isUpperEnglish { + return UnicodeSymbol.mathCapitalTTStart + (ch.utf32Char - Character("A").utf32Char) + } else if ch.isLowerEnglish { + return UnicodeSymbol.mathLowerTTStart + (ch.utf32Char - Character("a").utf32Char) + } else if ch.isNumber { + return UnicodeSymbol.numberTTStart + (ch.utf32Char - Character("0").utf32Char) + } + // Monospace characters don't exist for greek, we give them the + // default treatment. + return getDefaultStyle(ch); +} + +// mathsf +func getSansSerif(_ ch:Character) -> UTF32Char { + if ch.isUpperEnglish { + return UnicodeSymbol.mathCapitalSansSerifStart + (ch.utf32Char - Character("A").utf32Char) + } else if ch.isLowerEnglish { + return UnicodeSymbol.mathLowerSansSerifStart + (ch.utf32Char - Character("a").utf32Char) + } else if ch.isNumber { + return UnicodeSymbol.numberSansSerifStart + (ch.utf32Char - Character("0").utf32Char) + } + // Sans-serif characters don't exist for greek, we give them the + // default treatment. + return getDefaultStyle(ch); +} + +// mathfrak +func getFraktur(_ ch:Character) -> UTF32Char { + // Fraktur has exceptions: + switch(ch) { + case "C": + return 0x212D; // C Fraktur + case "H": + return 0x210C; // Hilbert space + case "I": + return 0x2111; // Imaginary + case "R": + return 0x211C; // Real + case "Z": + return 0x2128; // Z Fraktur + default: + break; + } + if ch.isUpperEnglish { + return UnicodeSymbol.mathCapitalFrakturStart + (ch.utf32Char - Character("A").utf32Char) + } else if ch.isLowerEnglish { + return UnicodeSymbol.mathLowerFrakturStart + (ch.utf32Char - Character("a").utf32Char) + } + // Fraktur characters don't exist for greek & numbers, we give them the + // default treatment. + return getDefaultStyle(ch); +} + +// mathbb (double struck) +func getBlackboard(_ ch:Character) -> UTF32Char { + // Blackboard has lots of exceptions: + switch(ch) { + case "C": + return 0x2102; // Complex numbers + case "H": + return 0x210D; // Quarternions + case "N": + return 0x2115; // Natural numbers + case "P": + return 0x2119; // Primes + case "Q": + return 0x211A; // Rationals + case "R": + return 0x211D; // Reals + case "Z": + return 0x2124; // Integers + default: + break; + } + if ch.isUpperEnglish { + return UnicodeSymbol.mathCapitalBlackboardStart + (ch.utf32Char - Character("A").utf32Char) + } else if ch.isLowerEnglish { + return UnicodeSymbol.mathLowerBlackboardStart + (ch.utf32Char - Character("a").utf32Char) + } else if ch.isNumber { + return UnicodeSymbol.numberBlackboardStart + (ch.utf32Char - Character("0").utf32Char) + } + // Blackboard characters don't exist for greek, we give them the + // default treatment. + return getDefaultStyle(ch); +} + +func styleCharacter(_ ch:Character, fontStyle:MTFontStyle) -> UTF32Char { + switch fontStyle { + case .defaultStyle: + return getDefaultStyle(ch); + + case .roman: + return ch.utf32Char + + case .bold: + return getBold(ch); + + case .italic: + return getItalicized(ch); + + case .boldItalic: + return getBoldItalic(ch); + + case .caligraphic: + return getCaligraphic(ch); + + case .typewriter: + return getTypewriter(ch); + + case .sansSerif: + return getSansSerif(ch); + + case .fraktur: + return getFraktur(ch); + + case .blackboard: + return getBlackboard(ch); + +// default: +// NSException(name: NSExceptionName("Invalid style"), reason: "Unknown style \(fontStyle) for font.").raise() + } +// return ch.utf32Char +} + +func changeFont(_ str:String, fontStyle:MTFontStyle) -> String { + var retval = "" + let codes = Array(str) + for i in 0.. String { +// let retval = NSMutableString(capacity: str.count) +// var charBuffer = [unichar]() +// charBuffer.reserveCapacity(str.count) +// (str as NSString).getCharacters(&charBuffer, range: NSMakeRange(0, str.count)) +// for i in 0 ..< str.count { +// let ch = charBuffer[i] +// var unicode = getItalicized(ch) +// unicode = NSSwapHostIntToLittle(unicode) +// let charStr = NSString(bytes: &unicode, length: MemoryLayout.size(ofValue: unicode), encoding: NSUTF32LittleEndianStringEncoding) +// retval.append(charStr! as String) +// } +// return retval as String +//} + +func getBboxDetails(_ bbox:CGRect, ascent:inout CGFloat, descent:inout CGFloat) { + if ascent != 0 { + ascent = max(0, CGRectGetMaxY(bbox) - 0) + } + + if descent != 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. + descent = max(0, 0 - CGRectGetMinY(bbox)) + } +} + +// Mark: - MTTypesetter + +class MTTypesetter { + var font:MTFont! + var displayAtoms = [MTDisplay]() + var currentPosition = CGPoint.zero + var currentLine:NSMutableAttributedString! + var currentAtoms = [MTMathAtom]() // List of atoms that make the line + var currentLineIndexRange = NSMakeRange(0, 0) + var style:MTLineStyle = .display + var styleFont:MTFont! + var cramped = false + var spaced = false + + static func createLineForMathList(_ mathList:MTMathList?, font:MTFont?, style:MTLineStyle) -> MTMathListDisplay? { + let finalizedList = mathList?.finalized + // default is not cramped + return self.createLineForMathList(finalizedList, font:font, style:style, cramped:false) + } + + // Internal + static func createLineForMathList(_ mathList:MTMathList?, font:MTFont?, style:MTLineStyle, cramped:Bool) -> MTMathListDisplay? { + return self.createLineForMathList(mathList, font:font, style:style, cramped:cramped, spaced:false) + } + + // Internal + static func createLineForMathList(_ mathList:MTMathList?, font:MTFont?, style:MTLineStyle, cramped:Bool, spaced:Bool) -> MTMathListDisplay? { + assert(font != nil) + let preprocessedAtoms = self.preprocessMathList(mathList) + let typesetter = MTTypesetter(withFont:font, style:style, cramped:cramped, spaced:spaced) + typesetter.createDisplayAtoms(preprocessedAtoms) + let lastAtom = mathList!.atoms.last + let line = MTMathListDisplay(withDisplays: typesetter.displayAtoms, range: NSMakeRange(0, NSMaxRange(lastAtom!.indexRange))) + return line + } + + static var placeholderColor: MTColor { MTColor.blue } + + init(withFont font:MTFont?, style:MTLineStyle, cramped:Bool, spaced:Bool) { + self.font = font + self.displayAtoms = [MTDisplay]() + self.currentPosition = CGPoint.zero + self.cramped = cramped + self.spaced = spaced + self.currentLine = NSMutableAttributedString() + self.currentAtoms = [MTMathAtom]() + self.style = style + self.currentLineIndexRange = NSMakeRange(NSNotFound, NSNotFound); + } + + static func preprocessMathList(_ ml:MTMathList?) -> [MTMathAtom] { + // Note: Some of the preprocessing described by the TeX algorithm is done in the finalize method of MTMathList. + // Specifically rules 5 & 6 in Appendix G are handled by finalize. + // This function does not do a complete preprocessing as specified by TeX either. It removes any special atom types + // that are not included in TeX and applies Rule 14 to merge ordinary characters. + var preprocessed = [MTMathAtom]() // arrayWithCapacity:ml.atoms.count) + var prevNode:MTMathAtom! = nil + preprocessed.reserveCapacity(ml!.atoms.count) + for atom in ml!.atoms { + if atom.type == .variable || atom.type == .number { + // This is not a TeX type node. TeX does this during parsing the input. + // switch to using the italic math font + // We convert it to ordinary + let newFont = changeFont(atom.nucleus, fontStyle: atom.fontStyle) // mathItalicize(atom.nucleus) + atom.type = .ordinary + atom.nucleus = newFont + } else if atom.type == .unaryOperator { + // Neither of these are TeX nodes. TeX treats these as Ordinary. So will we. + atom.type = .ordinary + } + + if atom.type == .ordinary { + // This is Rule 14 to merge ordinary characters. + // combine ordinary atoms together + if prevNode != nil && prevNode.type == .ordinary && prevNode.subScript == nil && prevNode.superScript == nil { + prevNode.fuse(with: atom) + // skip the current node, we are done here. + continue + } + } + + // TODO: add italic correction here or in second pass? + prevNode = atom + preprocessed.append(atom) + } + return preprocessed + } + + // returns the size of the font in this style + static func getStyleSize(_ style:MTLineStyle, font:MTFont?) -> CGFloat { + let original = font!.fontSize + switch style { + case .display, .text: + return original; + + case .script: + return original * font!.mathTable!.scriptScaleDown; + + case .scriptOfScript: + return original * font!.mathTable!.scriptScriptScaleDown; + } + } + + func setStyle(_ style:MTLineStyle) { + self.style = style + self.styleFont = self.font.copy(withSize: Self.getStyleSize(self.style, font: self.font)) //copyFontWithSize:[self.class] getStyleSize:style font:font]) + } + + func addInterElementSpace(_ prevNode:MTMathAtom?, currentType type:MTMathAtomType) { + var interElementSpace = CGFloat(0) + if prevNode != nil { + interElementSpace = getInterElementSpace(prevNode!.type, right:type) + } else if self.spaced { + // For the first atom of a spaced list, treat it as if it is preceded by an open. + interElementSpace = getInterElementSpace(.open, right:type) + } + self.currentPosition.x += interElementSpace + } + + func createDisplayAtoms(_ preprocessed:[MTMathAtom]) { + // items should contain all the nodes that need to be layed out. + // convert to a list of DisplayAtoms + var prevNode:MTMathAtom? = nil; + var lastType:MTMathAtomType = .style + for atom in preprocessed { + switch atom.type { + case .number, .variable,. unaryOperator: + // These should never appear as they should have been removed by preprocessing + assertionFailure("These types should never show here as they are removed by preprocessing.") + + case .boundary: + assertionFailure("A boundary atom should never be inside a mathlist.") + + case .space: + // stash the existing layout + if currentLine.length > 0 { + self.addDisplayLine() + } + let space = atom as! MTMathSpace + // add the desired space + currentPosition.x += space.space * styleFont.mathTable!.muUnit; + // Since this is extra space, the desired interelement space between the prevAtom + // and the next node is still preserved. To avoid resetting the prevAtom and lastType + // we skip to the next node. + continue + + case .style: + // stash the existing layout + if currentLine.length > 0 { + self.addDisplayLine() + } + let style = atom as! MTMathStyle + self.style = style.style; + // We need to preserve the prevNode for any interelement space changes. + // so we skip to the next node. + continue + + case .color: + // stash the existing layout + if currentLine.length > 0 { + self.addDisplayLine() + } + let colorAtom = atom as! MTMathColor + let display = MTTypesetter.createLineForMathList(colorAtom.innerList, font: font, style: style) + display!.localTextColor = MTColor.color(fromHexString: colorAtom.colorString) + display!.position = currentPosition + currentPosition.x += display!.width + displayAtoms.append(display!) + + case .colorBox: + // stash the existing layout + if currentLine.length > 0 { + self.addDisplayLine() + } + let colorboxAtom = atom as! MTMathColorbox + let display = MTTypesetter.createLineForMathList(colorboxAtom.innerList, font:font, style:style) + + display!.localBackgroundColor = MTColor.color(fromHexString: colorboxAtom.colorString) + display!.position = currentPosition + currentPosition.x += display!.width; + displayAtoms.append(display!) + + case .radical: + // stash the existing layout + if currentLine.length > 0 { + self.addDisplayLine() + } + let rad = atom as! MTRadical + // Radicals are considered as Ord in rule 16. + self.addInterElementSpace(prevNode, currentType:.ordinary) + let displayRad = self.makeRadical(rad.radicand, range:rad.indexRange) + if rad.degree != nil { + // add the degree to the radical + let degree = MTTypesetter.createLineForMathList(rad.degree, font:font, style:.scriptOfScript) + displayRad!.setDegree(degree, fontMetrics:styleFont.mathTable) + } + displayAtoms.append(displayRad!) + currentPosition.x += displayRad!.width + + // add super scripts || subscripts + if atom.subScript != nil || atom.superScript != nil { + self.makeScripts(atom, display:displayRad, index:UInt(rad.indexRange.location), delta:0) + } + // change type to ordinary + //atom.type = .ordinary; + + case .fraction: + // stash the existing layout + if currentLine.length > 0 { + self.addDisplayLine() + } + let frac = atom as! MTFraction? + self.addInterElementSpace(prevNode, currentType:atom.type) + let display = self.makeFraction(frac) + displayAtoms.append(display!) + currentPosition.x += display!.width; + // add super scripts || subscripts + if atom.subScript != nil || atom.superScript != nil { + self.makeScripts(atom, display:display, index:UInt(frac!.indexRange.location), delta:0) + } + + case .largeOperator: + // stash the existing layout + if currentLine.length > 0 { + self.addDisplayLine() + } + self.addInterElementSpace(prevNode, currentType:atom.type) + let op = atom as! MTLargeOperator? + let display = self.makeLargeOp(op) + displayAtoms.append(display!) + + case .inner: + // stash the existing layout + if currentLine.length > 0 { + self.addDisplayLine() + } + self.addInterElementSpace(prevNode, currentType:atom.type) + let inner = atom as! MTInner? + var display : MTDisplay? = nil + if inner!.leftBoundary != nil || inner!.rightBoundary != nil { + display = self.makeLeftRight(inner) + } else { + display = MTTypesetter.createLineForMathList(inner!.innerList, font:font, style:style, cramped:cramped) + } + display!.position = currentPosition + currentPosition.x += display!.width + displayAtoms.append(display!) + // add super scripts || subscripts + if atom.subScript != nil || atom.superScript != nil { + self.makeScripts(atom, display:display, index:UInt(atom.indexRange.location), delta:0) + } + + case .underline: + // stash the existing layout + if currentLine.length > 0 { + self.addDisplayLine() + } + // Underline is considered as Ord in rule 16. + self.addInterElementSpace(prevNode, currentType:.ordinary) + atom.type = .ordinary; + + let under = atom as! MTUnderLine? + let display = self.makeUnderline(under) + displayAtoms.append(display!) + currentPosition.x += display!.width; + // add super scripts || subscripts + if atom.subScript != nil || atom.superScript != nil { + self.makeScripts(atom, display:display, index:UInt(atom.indexRange.location), delta:0) + } + + case .overline: + // stash the existing layout + if currentLine.length > 0 { + self.addDisplayLine() + } + // Overline is considered as Ord in rule 16. + self.addInterElementSpace(prevNode, currentType:.ordinary) + atom.type = .ordinary; + + let over = atom as! MTOverLine? + let display = self.makeOverline(over) + displayAtoms.append(display!) + currentPosition.x += display!.width; + // add super scripts || subscripts + if atom.subScript != nil || atom.superScript != nil { + self.makeScripts(atom, display:display, index:UInt(atom.indexRange.location), delta:0) + } + + case .accent: + // stash the existing layout + if currentLine.length > 0 { + self.addDisplayLine() + } + // Accent is considered as Ord in rule 16. + self.addInterElementSpace(prevNode, currentType:.ordinary) + atom.type = .ordinary; + + let accent = atom as! MTAccent? + let display = self.makeAccent(accent) + displayAtoms.append(display!) + currentPosition.x += display!.width; + + // add super scripts || subscripts + if atom.subScript != nil || atom.superScript != nil { + self.makeScripts(atom, display:display, index:UInt(atom.indexRange.location), delta:0) + } + + case .table: + // stash the existing layout + if currentLine.length > 0 { + self.addDisplayLine() + } + // We will consider tables as inner + self.addInterElementSpace(prevNode, currentType:.inner) + atom.type = .inner; + + let table = atom as! MTMathTable? + let display = self.makeTable(table) + displayAtoms.append(display!) + currentPosition.x += display!.width + // A table doesn't have subscripts or superscripts + + case .ordinary, .binaryOperator, .relation, .open, .close, .placeholder, .punctuation: + // the rendering for all the rest is pretty similar + // All we need is render the character and set the interelement space. + if prevNode != nil { + let interElementSpace = self.getInterElementSpace(prevNode!.type, right:atom.type) + if (currentLine.length > 0) { + if (interElementSpace > 0) { + // add a kerning of that space to the previous character + currentLine.addAttribute(kCTKernAttributeName as NSAttributedString.Key, + value:NSNumber(floatLiteral: interElementSpace), + range:currentLine.mutableString.rangeOfComposedCharacterSequence(at: currentLine.length-1)) + } + } else { + // increase the space + currentPosition.x += interElementSpace; + } + } + var current:NSAttributedString? = nil + if (atom.type == .placeholder) { + let color = MTTypesetter.placeholderColor + current = NSAttributedString(string: atom.nucleus, attributes:[kCTForegroundColorAttributeName as NSAttributedString.Key : color.cgColor]) + } else { + current = NSAttributedString(string:atom.nucleus) + } + currentLine.append(current!) + // add the atom to the current range + if (currentLineIndexRange.location == NSNotFound) { + currentLineIndexRange = atom.indexRange; + } else { + currentLineIndexRange.length += atom.indexRange.length + } + // add the fused atoms + if !atom.childAtoms.isEmpty { + currentAtoms.append(contentsOf: atom.childAtoms) //.addObjectsFromArray:atom.fusedAtoms) + } else { + currentAtoms.append(atom) + } + + // add super scripts || subscripts + if atom.subScript != nil || atom.superScript != nil { + // stash the existing line + // We don't check currentLine.length here since we want to allow empty lines with super/sub scripts. + let line = self.addDisplayLine() + var delta = CGFloat(0) + if !atom.nucleus.isEmpty { + // Use the italic correction of the last character. + let index = atom.nucleus.index(before: atom.nucleus.endIndex) + let glyph = self.findGlyphForCharacterAtIndex(index, inString:atom.nucleus) + delta = styleFont.mathTable!.getItalicCorrection(glyph) + } + if delta > 0 && atom.subScript == nil { + // Add a kern of delta + currentPosition.x += delta; + } + self.makeScripts(atom, display:line, index:UInt(NSMaxRange(atom.indexRange) - 1), delta:delta) + } + } + lastType = atom.type; + prevNode = atom; + } + if (currentLine.length > 0) { + self.addDisplayLine() + } + if spaced && !lastType.rawValue.isEmpty { + // If spaced then add an interelement space between the last type and close + let display = displayAtoms.last + let interElementSpace = self.getInterElementSpace(lastType, right:.close) + display?.width += interElementSpace + } + } + + @discardableResult + func addDisplayLine() -> MTCTLineDisplay? { + // add the font + currentLine.addAttribute(kCTFontAttributeName as NSAttributedString.Key, value:styleFont.ctFont!, range:NSMakeRange(0, currentLine.length)) + /*assert(currentLineIndexRange.length == numCodePoints(currentLine.string), + "The length of the current line: %@ does not match the length of the range (%d, %d)", + currentLine, currentLineIndexRange.location, currentLineIndexRange.length);*/ + + let displayAtom = MTCTLineDisplay(withString:currentLine, position:currentPosition, range:currentLineIndexRange, font:styleFont, atoms:currentAtoms) + self.displayAtoms.append(displayAtom) + // update the position + currentPosition.x += displayAtom.width; + // clear the string and the range + currentLine = NSMutableAttributedString() + currentAtoms = [MTMathAtom]() + currentLineIndexRange = NSMakeRange(NSNotFound, NSNotFound) + return displayAtom + } + + // Mark: Spacing + + // Returned in units of mu = 1/18 em. + func getSpacingInMu(_ type: InterElementSpaceType) -> Int { + // let valid = [MTLineStyle.display, .text] + switch type { + case .invalid: + return -1; + case .none: + return 0; + case .thin: + return 3; + case .nsThin: + return style.isNotScript ? 3 : 0; + case .nsMedium: + return style.isNotScript ? 4 : 0; + case .nsThick: + return style.isNotScript ? 5 : 0; + } + } + + func getInterElementSpace(_ left: MTMathAtomType, right:MTMathAtomType) -> CGFloat { + let leftIndex = getInterElementSpaceArrayIndexForType(left, row: true); + let rightIndex = getInterElementSpaceArrayIndexForType(right, row: false); + let spaceArray = getInterElementSpaces()[Int(leftIndex)] + let spaceTypeObj = spaceArray[Int(rightIndex)] + let spaceType = spaceTypeObj + assert(spaceType != .invalid, "Invalid space between \(left) and \(right)") + + let spaceMultipler = self.getSpacingInMu(spaceType) + if spaceMultipler > 0 { + // 1 em = size of font in pt. space multipler is in multiples mu or 1/18 em + return CGFloat(spaceMultipler) * styleFont.mathTable!.muUnit + } + return 0 + } + + + // Mark: Subscript/Superscript + + func scriptStyle() -> MTLineStyle { + switch style { + case .display, .text: + return .script + case .script, .scriptOfScript: + return .scriptOfScript + } + } + + // subscript is always cramped + func subscriptCramped() -> Bool { + return true; + } + + // superscript is cramped only if the current style is cramped + func superScriptCramped() -> Bool { + return cramped; + } + + func superScriptShiftUp() -> CGFloat { + if (cramped) { + return styleFont.mathTable!.superscriptShiftUpCramped; + } else { + return styleFont.mathTable!.superscriptShiftUp; + } + } + + // make scripts for the last atom + // index is the index of the element which is getting the sub/super scripts. + func makeScripts(_ atom: MTMathAtom?, display:MTDisplay?, index:UInt, delta:CGFloat) { + assert(atom!.subScript != nil || atom!.superScript != nil) + + var superScriptShiftUp = 0.0 + var subscriptShiftDown = 0.0 + + display?.hasScript = true + // let classy = display is MTCTLineDisplay + if !(display is MTCTLineDisplay) { + // get the font in script style + let scriptFontSize = Self.getStyleSize(self.scriptStyle(), font:font) + let scriptFont = font.copy(withSize: scriptFontSize) + let scriptFontMetrics = scriptFont.mathTable + + // if it is not a simple line then + superScriptShiftUp = display!.ascent - scriptFontMetrics!.superscriptBaselineDropMax + subscriptShiftDown = display!.descent + scriptFontMetrics!.subscriptBaselineDropMin + } + + if atom!.superScript == nil { + assert(atom!.subScript != nil) + let _subscript = MTTypesetter.createLineForMathList(atom!.subScript, font:font, style:self.scriptStyle(), cramped:self.subscriptCramped()) + _subscript?.type = .ssubscript + _subscript?.index = Int(index) + + subscriptShiftDown = fmax(subscriptShiftDown, styleFont.mathTable!.subscriptShiftDown); + subscriptShiftDown = fmax(subscriptShiftDown, _subscript!.ascent - styleFont.mathTable!.subscriptTopMax); + // add the subscript + _subscript?.position = CGPointMake(currentPosition.x, currentPosition.y - subscriptShiftDown); + displayAtoms.append(_subscript!) + // update the position + currentPosition.x += _subscript!.width + styleFont.mathTable!.spaceAfterScript; + return; + } + + let superScript = MTTypesetter.createLineForMathList(atom!.superScript, font:font, style:self.scriptStyle(), cramped:self.superScriptCramped()) + superScript!.type = .superscript + superScript!.index = Int(index); + superScriptShiftUp = fmax(superScriptShiftUp, self.superScriptShiftUp()); + superScriptShiftUp = fmax(superScriptShiftUp, superScript!.descent + styleFont.mathTable!.superscriptBottomMin); + + if atom!.subScript == nil { + superScript!.position = CGPointMake(currentPosition.x, currentPosition.y + superScriptShiftUp); + displayAtoms.append(superScript!) + // update the position + currentPosition.x += superScript!.width + styleFont.mathTable!.spaceAfterScript; + return; + } + let ssubscript = MTTypesetter.createLineForMathList(atom!.subScript, font:font, style:self.scriptStyle(), cramped:self.subscriptCramped()) + ssubscript!.type = .ssubscript + ssubscript!.index = Int(index) + subscriptShiftDown = fmax(subscriptShiftDown, styleFont.mathTable!.subscriptShiftDown); + + // joint positioning of subscript & superscript + let subSuperScriptGap = (superScriptShiftUp - superScript!.descent) + (subscriptShiftDown - ssubscript!.ascent); + if (subSuperScriptGap < styleFont.mathTable!.subSuperscriptGapMin) { + // Set the gap to atleast as much + subscriptShiftDown += styleFont.mathTable!.subSuperscriptGapMin - subSuperScriptGap; + let superscriptBottomDelta = styleFont.mathTable!.superscriptBottomMaxWithSubscript - (superScriptShiftUp - superScript!.descent); + if (superscriptBottomDelta > 0) { + // superscript is lower than the max allowed by the font with a subscript. + superScriptShiftUp += superscriptBottomDelta; + subscriptShiftDown -= superscriptBottomDelta; + } + } + // The delta is the italic correction above that shift superscript position + superScript?.position = CGPointMake(currentPosition.x + delta, currentPosition.y + superScriptShiftUp); + displayAtoms.append(superScript!) + ssubscript?.position = CGPointMake(currentPosition.x, currentPosition.y - subscriptShiftDown); + displayAtoms.append(ssubscript!) + currentPosition.x += max(superScript!.width + delta, ssubscript!.width) + styleFont.mathTable!.spaceAfterScript; + } + + // Mark: - Fractions + + func numeratorShiftUp(_ hasRule:Bool) -> CGFloat { + if hasRule { + if style == .display { + return styleFont.mathTable!.fractionNumeratorDisplayStyleShiftUp; + } else { + return styleFont.mathTable!.fractionNumeratorShiftUp; + } + } else { + if (style == .display) { + return styleFont.mathTable!.stackTopDisplayStyleShiftUp; + } else { + return styleFont.mathTable!.stackTopShiftUp; + } + } + } + + func numeratorGapMin() -> CGFloat { + if (style == .display) { + return styleFont.mathTable!.fractionNumeratorDisplayStyleGapMin; + } else { + return styleFont.mathTable!.fractionNumeratorGapMin; + } + } + + func denominatorShiftDown(_ hasRule:Bool) -> CGFloat { + if hasRule { + if (style == .display) { + return styleFont.mathTable!.fractionDenominatorDisplayStyleShiftDown; + } else { + return styleFont.mathTable!.fractionDenominatorShiftDown; + } + } else { + if (style == .display) { + return styleFont.mathTable!.stackBottomDisplayStyleShiftDown; + } else { + return styleFont.mathTable!.stackBottomShiftDown; + } + } + } + + func denominatorGapMin() -> CGFloat { + if style == .display { + return styleFont.mathTable!.fractionDenominatorDisplayStyleGapMin; + } else { + return styleFont.mathTable!.fractionDenominatorGapMin; + } + } + + func stackGapMin() -> CGFloat { + if style == .display { + return styleFont.mathTable!.stackDisplayStyleGapMin; + } else { + return styleFont.mathTable!.stackGapMin; + } + } + + func fractionDelimiterHeight()-> CGFloat { + if style == .display { + return styleFont.mathTable!.fractionDelimiterDisplayStyleSize; + } else { + return styleFont.mathTable!.fractionDelimiterSize; + } + } + + func fractionStyle() -> MTLineStyle { + if style == .scriptOfScript { + return .scriptOfScript + } + return style.inc() + } + + func makeFraction(_ frac:MTFraction?) -> MTDisplay? { + // lay out the parts of the fraction + let fractionStyle = self.fractionStyle; + let numeratorDisplay = MTTypesetter.createLineForMathList(frac!.numerator, font:font, style:fractionStyle(), cramped:false) + let denominatorDisplay = MTTypesetter.createLineForMathList(frac!.denominator, font:font, style:fractionStyle(), cramped:true) + + // determine the location of the numerator + var numeratorShiftUp = self.numeratorShiftUp(frac!.hasRule) + var denominatorShiftDown = self.denominatorShiftDown(frac!.hasRule) + let barLocation = styleFont.mathTable!.axisHeight + let barThickness = frac!.hasRule ? styleFont.mathTable!.fractionRuleThickness : 0; + + if frac!.hasRule { + // This is the difference between the lowest edge of the numerator and the top edge of the fraction bar + let distanceFromNumeratorToBar = (numeratorShiftUp - numeratorDisplay!.descent) - (barLocation + barThickness/2); + // The distance should at least be displayGap + let minNumeratorGap = self.numeratorGapMin; + if distanceFromNumeratorToBar < minNumeratorGap() { + // This makes the distance between the bottom of the numerator and the top edge of the fraction bar + // at least minNumeratorGap. + numeratorShiftUp += (minNumeratorGap() - distanceFromNumeratorToBar); + } + + // Do the same for the denominator + // This is the difference between the top edge of the denominator and the bottom edge of the fraction bar + let distanceFromDenominatorToBar = (barLocation - barThickness/2) - (denominatorDisplay!.ascent - denominatorShiftDown); + // The distance should at least be denominator gap + let minDenominatorGap = self.denominatorGapMin; + if distanceFromDenominatorToBar < minDenominatorGap() { + // This makes the distance between the top of the denominator and the bottom of the fraction bar to be exactly + // minDenominatorGap + denominatorShiftDown += (minDenominatorGap() - distanceFromDenominatorToBar); + } + } else { + // This is the distance between the numerator and the denominator + let clearance = (numeratorShiftUp - numeratorDisplay!.descent) - (denominatorDisplay!.ascent - denominatorShiftDown); + // This is the minimum clearance between the numerator and denominator. + let minGap = self.stackGapMin() + if clearance < minGap { + numeratorShiftUp += (minGap - clearance)/2; + denominatorShiftDown += (minGap - clearance)/2; + } + } + + let display = MTFractionDisplay(withNumerator: numeratorDisplay, denominator: denominatorDisplay, position: currentPosition, range: frac!.indexRange) + + display.numeratorUp = numeratorShiftUp; + display.denominatorDown = denominatorShiftDown; + display.lineThickness = barThickness; + display.linePosition = barLocation; + if frac!.leftDelimiter == nil && frac!.rightDelimiter == nil { + return display + } else { + return self.addDelimitersToFractionDisplay(display, forFraction:frac) + } + } + + func addDelimitersToFractionDisplay(_ display:MTFractionDisplay?, forFraction frac:MTFraction?) -> MTDisplay? { + assert(frac!.leftDelimiter != nil || frac!.rightDelimiter != nil, "Fraction should have a delimiters to call this function"); + + var innerElements = [MTDisplay]() + let glyphHeight = self.fractionDelimiterHeight + var position = CGPoint.zero + if !frac!.leftDelimiter!.isEmpty { + let leftGlyph = self.findGlyphForBoundary(frac!.leftDelimiter!, withHeight:glyphHeight()) + leftGlyph!.position = position + position.x += leftGlyph!.width + innerElements.append(leftGlyph!) + } + + display!.position = position + position.x += display!.width + innerElements.append(display!) + + if !frac!.rightDelimiter!.isEmpty { + let rightGlyph = self.findGlyphForBoundary(frac!.rightDelimiter!, withHeight:glyphHeight()) + rightGlyph!.position = position + position.x += rightGlyph!.width + innerElements.append(rightGlyph!) + } + let innerDisplay = MTMathListDisplay(withDisplays: innerElements, range: frac!.indexRange) + innerDisplay.position = currentPosition + return innerDisplay + } + + // Mark: Radicals + + func radicalVerticalGap() -> CGFloat { + if style == .display { + return styleFont.mathTable!.radicalDisplayStyleVerticalGap + } else { + return styleFont.mathTable!.radicalVerticalGap + } + } + + func getRadicalGlyphWithHeight(_ radicalHeight:CGFloat) -> MTDisplayDS? { + var glyphAscent=CGFloat(0), glyphDescent=CGFloat(0), glyphWidth=CGFloat(0) + + let radicalGlyph = self.findGlyphForCharacterAtIndex("\u{221A}".startIndex, inString:"\u{221A}") + let glyph = self.findGlyph(radicalGlyph, withHeight:radicalHeight, glyphAscent:&glyphAscent, glyphDescent:&glyphDescent, glyphWidth:&glyphWidth) + + var glyphDisplay:MTDisplayDS? + if glyphAscent + glyphDescent < radicalHeight { + // the glyphs is not as large as required. A glyph needs to be constructed using the extenders. + glyphDisplay = self.constructGlyph(radicalGlyph, withHeight:radicalHeight) + } + + if glyphDisplay != nil { + // No constructed display so use the glyph we got. + glyphDisplay = MTGlyphDisplay(withGlpyh: glyph, range: NSMakeRange(NSNotFound, 0), font:styleFont) + glyphDisplay!.ascent = glyphAscent; + glyphDisplay!.descent = glyphDescent; + glyphDisplay!.width = glyphWidth; + } + return glyphDisplay; + } + + func makeRadical(_ radicand:MTMathList?, range:NSRange) -> MTRadicalDisplay? { + let innerDisplay = MTTypesetter.createLineForMathList(radicand, font:font, style:style, cramped:true) + var clearance : CGFloat = self.radicalVerticalGap() + let radicalRuleThickness : CGFloat = styleFont.mathTable!.radicalRuleThickness + let radicalHeight = innerDisplay!.ascent + innerDisplay!.descent + clearance + radicalRuleThickness; + + let glyph = self.getRadicalGlyphWithHeight(radicalHeight) + + + // Note this is a departure from Latex. Latex assumes that glyphAscent == thickness. + // Open type math makes no such assumption, and ascent and descent are independent of the thickness. + // Latex computes delta as descent - (h(inner) + d(inner) + clearance) + // but since we may not have ascent == thickness, we modify the delta calculation slightly. + // If the font designer followes Latex conventions, it will be identical. + let delta : CGFloat = (glyph!.descent + glyph!.ascent) - (innerDisplay!.ascent + innerDisplay!.descent + clearance + radicalRuleThickness); + if delta > 0 { + clearance += delta/2; // increase the clearance to center the radicand inside the sign. + } + + // we need to shift the radical glyph up, to coincide with the baseline of inner. + // The new ascent of the radical glyph should be thickness + adjusted clearance + h(inner) + let radicalAscent = radicalRuleThickness + clearance + innerDisplay!.ascent; + let shiftUp = radicalAscent - glyph!.ascent; // Note: if the font designer followed latex conventions, this is the same as glyphAscent == thickness. + glyph!.shiftDown = -shiftUp; + + let radical = MTRadicalDisplay(withRadicand: innerDisplay, glyph: glyph!, position: currentPosition, range: range) + radical.ascent = radicalAscent + styleFont.mathTable!.radicalExtraAscender; + radical.topKern = styleFont.mathTable!.radicalExtraAscender; + radical.lineThickness = radicalRuleThickness; + // Note: Until we have radical construction from parts, it is possible that glyphAscent+glyphDescent is less + // than the requested height of the glyph (i.e. radicalHeight), so in the case the innerDisplay has a larger + // descent we use the innerDisplay's descent. + radical.descent = max(glyph!.ascent + glyph!.descent - radicalAscent, innerDisplay!.descent); + radical.width = glyph!.width + innerDisplay!.width; + return radical; + } + + // Mark: Glyphs + + func findGlyph(_ glyph:CGGlyph, withHeight height:CGFloat, glyphAscent:inout CGFloat, glyphDescent:inout CGFloat, glyphWidth:inout CGFloat) -> CGGlyph { + let variants = styleFont.mathTable!.getVerticalVariantsForGlyph(glyph) + let numVariants = variants.count; + var glyphs = [CGGlyph]()// numVariants) + glyphs.reserveCapacity(numVariants) + for i in 0 ..< numVariants { + let glyph = variants[i]!.uint16Value + glyphs[i] = glyph + } + + var bboxes = [CGRect]() // = [numVariants) + var advances = [CGSize]() // [numVariants) + bboxes.reserveCapacity(numVariants) + advances.reserveCapacity(numVariants) + + // Get the bounds for these glyphs + CTFontGetBoundingRectsForGlyphs(styleFont.ctFont, .horizontal, glyphs, &bboxes, numVariants) + CTFontGetAdvancesForGlyphs(styleFont.ctFont, .horizontal, glyphs, &advances, numVariants); + var ascent=CGFloat(0), descent=CGFloat(0), width=CGFloat(0) + for i in 0..= height) { + glyphAscent = ascent; + glyphDescent = descent; + glyphWidth = width; + return glyphs[i] + } + } + glyphAscent = ascent; + glyphDescent = descent; + glyphWidth = width; + return glyphs[numVariants - 1] + } + + func constructGlyph(_ glyph:CGGlyph, withHeight glyphHeight:CGFloat) -> MTGlyphConstructionDisplay? { + let parts = styleFont.mathTable!.getVerticalGlyphAssembly(forGlyph: glyph) + if parts.count == 0 { + return nil + } + var glyphs = [NSNumber](), offsets = [NSNumber]() + var height:CGFloat=0 + self.constructGlyphWithParts(parts, glyphHeight:glyphHeight, glyphs:&glyphs, offsets:&offsets, height:&height) + var first = glyphs[0].uint16Value + let width = CTFontGetAdvancesForGlyphs(styleFont.ctFont, .horizontal, &first, nil, 1); + let display = MTGlyphConstructionDisplay(withGlyphs: glyphs, offsets: offsets, font: styleFont) + display.width = width; + display.ascent = height; + display.descent = 0; // it's upto the rendering to adjust the display up or down. + return display; + } + + func constructGlyphWithParts(_ parts:[GlyphPart], glyphHeight:CGFloat, glyphs:inout [NSNumber], offsets:inout [NSNumber], height:inout CGFloat) { + assert(!glyphs.isEmpty) + assert(!offsets.isEmpty) + + for numExtenders in 0...1 { + var glyphsRv = [NSNumber]() + var offsetsRv = [NSNumber]() + + var prev:GlyphPart? = nil; + let minDistance = styleFont.mathTable!.minConnectorOverlap; + var minOffset = CGFloat(0) + var maxDelta = CGFloat.greatestFiniteMagnitude // the maximum amount we can increase the offsets by + + for part in parts { + var repeats = 1; + if part.isExtender { + repeats = numExtenders; + } + // add the extender num extender times + for _ in 0 ..< repeats { + glyphsRv.append(NSNumber(value: part.glyph)) // addObject:[NSNumber numberWithShort:part.glyph]) + if prev != nil { + let maxOverlap = min(prev!.endConnectorLength, part.startConnectorLength); + // the minimum amount we can add to the offset + let minOffsetDelta = prev!.fullAdvance - maxOverlap; + // The maximum amount we can add to the offset. + let maxOffsetDelta = prev!.fullAdvance - minDistance; + // we can increase the offsets by at most max - min. + maxDelta = min(maxDelta, maxOffsetDelta - minOffsetDelta); + minOffset = minOffset + minOffsetDelta; + } + offsetsRv.append(NSNumber(floatLiteral: minOffset)) // addObject:[NSNumber numberWithFloat:minOffset]) + prev = part + } + } + + assert(glyphsRv.count == offsetsRv.count, "Offsets should match the glyphs"); + if prev == nil { + continue; // maybe only extenders + } + let minHeight = minOffset + prev!.fullAdvance + let maxHeight = minHeight + maxDelta * CGFloat(glyphsRv.count - 1) + if (minHeight >= glyphHeight) { + // we are done + glyphs = glyphsRv; + offsets = offsetsRv; + height = minHeight; + return; + } else if (glyphHeight <= maxHeight) { + // spread the delta equally between all the connectors + let delta = glyphHeight - minHeight; + let deltaIncrease = Float(delta) / Float(glyphsRv.count - 1) + var lastOffset = CGFloat(0) + for i in 0.. CGGlyph { + // Get the character at index taking into account UTF-32 characters + let range = str.rangeOfComposedCharacterSequence(at: index) //.rangeOfComposedCharacterSequenceAtIndex(index) + var chars = str[range].unicodeScalars.map { UInt16($0.value) } + + // Get the glyph from the font + var glyph = [CGGlyph](repeating: CGGlyph.zero, count: chars.count) // [range.length) + let found = CTFontGetGlyphsForCharacters(styleFont.ctFont, &chars, &glyph, chars.count) + if !found { + // the font did not contain a glyph for our character, so we just return 0 (notdef) + return 0 + } + return glyph[0] + } + + // Mark: Large Operators + + func makeLargeOp(_ op:MTLargeOperator!) -> MTDisplay? { + let limits = op.limits && style == .display + var delta = CGFloat(0) + if op.nucleus.count == 1 { + var glyph = self.findGlyphForCharacterAtIndex(op.nucleus.startIndex, inString:op.nucleus) + if style == .display && glyph != 0 { + // Enlarge the character in display style. + glyph = styleFont.mathTable!.getLargerGlyph(glyph) + } + // This is be the italic correction of the character. + delta = styleFont.mathTable!.getItalicCorrection(glyph) + + // vertically center + let bbox = CTFontGetBoundingRectsForGlyphs(styleFont.ctFont, .horizontal, &glyph, nil, 1); + let width = CTFontGetAdvancesForGlyphs(styleFont.ctFont, .horizontal, &glyph, nil, 1); + var ascent=CGFloat(0), descent=CGFloat(0) + getBboxDetails(bbox, ascent: &ascent, descent: &descent) + let shiftDown = 0.5*(ascent - descent) - styleFont.mathTable!.axisHeight; + let glyphDisplay = MTGlyphDisplay(withGlpyh: glyph, range: op.indexRange, font: styleFont) //initWithGlpyh:glyph range:op.indexRange font:styleFont) + glyphDisplay.ascent = ascent; + glyphDisplay.descent = descent; + glyphDisplay.width = width; + if (op.subScript != nil) && !limits { + // Remove italic correction from the width of the glyph if + // there is a subscript and limits is not set. + glyphDisplay.width -= delta; + } + glyphDisplay.shiftDown = shiftDown; + glyphDisplay.position = currentPosition; + return self.addLimitsToDisplay(glyphDisplay, forOperator:op, delta:delta) + } else { + // Create a regular node + let line = NSMutableAttributedString(string: op.nucleus) + // add the font + line.addAttribute(kCTFontAttributeName as NSAttributedString.Key, value:styleFont.ctFont!, range:NSMakeRange(0, line.length)) + let displayAtom = MTCTLineDisplay(withString: line, position: currentPosition, range: op.indexRange, font: styleFont, atoms: [op]) + return self.addLimitsToDisplay(displayAtom, forOperator:op, delta:0) + } + } + + func addLimitsToDisplay(_ display:MTDisplay?, forOperator op:MTLargeOperator, delta:CGFloat) -> MTDisplay? { + // If there is no subscript or superscript, just return the current display + if op.subScript == nil && op.superScript == nil { + currentPosition.x += display!.width + return display; + } + if (op.limits && style == .display) { + // make limits + var superScript:MTMathListDisplay? = nil, subScript:MTMathListDisplay? = nil + if op.superScript != nil { + superScript = MTTypesetter.createLineForMathList(op.superScript, font:font, style:self.scriptStyle(), cramped:self.superScriptCramped()) + } + if op.subScript != nil { + subScript = MTTypesetter.createLineForMathList(op.subScript, font:font, style:self.scriptStyle(), cramped:self.subscriptCramped()) + } + assert((superScript != nil) || (subScript != nil), "Atleast one of superscript or subscript should have been present."); + let opsDisplay = MTLargeOpLimitsDisplay(withNucleus:display, upperLimit:superScript, lowerLimit:subScript, limitShift:delta/2, extraPadding:0) + if superScript != nil { + let upperLimitGap = max(styleFont.mathTable!.upperLimitGapMin, styleFont.mathTable!.upperLimitBaselineRiseMin - superScript!.descent); + opsDisplay.upperLimitGap = upperLimitGap; + } + if subScript != nil { + let lowerLimitGap = max(styleFont.mathTable!.lowerLimitGapMin, styleFont.mathTable!.lowerLimitBaselineDropMin - subScript!.ascent); + opsDisplay.lowerLimitGap = lowerLimitGap; + } + opsDisplay.position = currentPosition; + opsDisplay.range = op.indexRange; + currentPosition.x += opsDisplay.width; + return opsDisplay; + } else { + currentPosition.x += display!.width; + self.makeScripts(op, display:display, index:UInt(op.indexRange.location), delta:delta) + return display; + } + } + + // Mark: Large delimiters + + // Delimiter shortfall from plain.tex + static let kDelimiterFactor = CGFloat(901) + static let kDelimiterShortfallPoints = CGFloat(5) + + func makeLeftRight(_ inner: MTInner?) -> MTDisplay? { + assert(inner!.leftBoundary != nil || inner!.rightBoundary != nil, "Inner should have a boundary to call this function"); + + let innerListDisplay = MTTypesetter.createLineForMathList(inner!.innerList, font:font, style:style, cramped:cramped, spaced:true) + let axisHeight = styleFont.mathTable!.axisHeight + // delta is the max distance from the axis + let delta = max(innerListDisplay!.ascent - axisHeight, innerListDisplay!.descent + axisHeight); + let d1 = (delta / 500) * MTTypesetter.kDelimiterFactor; // This represents atleast 90% of the formula + let d2 = 2 * delta - MTTypesetter.kDelimiterShortfallPoints; // This represents a shortfall of 5pt + // The size of the delimiter glyph should cover at least 90% of the formula or + // be at most 5pt short. + let glyphHeight = max(d1, d2); + + var innerElements = [MTDisplay]() // NSMutableArray() + var position = CGPoint.zero + if inner!.leftBoundary != nil && !inner!.leftBoundary!.nucleus.isEmpty { + let leftGlyph = self.findGlyphForBoundary(inner!.leftBoundary!.nucleus, withHeight:glyphHeight) + leftGlyph!.position = position + position.x += leftGlyph!.width + innerElements.append(leftGlyph!) + } + + innerListDisplay!.position = position; + position.x += innerListDisplay!.width; + innerElements.append(innerListDisplay!) + + if inner!.rightBoundary != nil && !inner!.rightBoundary!.nucleus.isEmpty { + let rightGlyph = self.findGlyphForBoundary(inner!.rightBoundary!.nucleus, withHeight:glyphHeight) + rightGlyph!.position = position; + position.x += rightGlyph!.width; + innerElements.append(rightGlyph!) + } + let innerDisplay = MTMathListDisplay(withDisplays: innerElements, range: inner!.indexRange) + return innerDisplay + } + + func findGlyphForBoundary(_ delimiter:String, withHeight glyphHeight:CGFloat) -> MTDisplay? { + var glyphAscent=CGFloat(0), glyphDescent=CGFloat(0), glyphWidth=CGFloat(0) + let leftGlyph = self.findGlyphForCharacterAtIndex(delimiter.startIndex, inString:delimiter) + let glyph = self.findGlyph(leftGlyph, withHeight:glyphHeight, glyphAscent:&glyphAscent, glyphDescent:&glyphDescent, glyphWidth:&glyphWidth) + + var glyphDisplay:MTDisplayDS? + if (glyphAscent + glyphDescent < glyphHeight) { + // we didn't find a pre-built glyph that is large enough + glyphDisplay = self.constructGlyph(leftGlyph, withHeight:glyphHeight) + } + + if glyphDisplay != nil { + // Create a glyph display + glyphDisplay = MTGlyphDisplay(withGlpyh: glyph, range: NSMakeRange(NSNotFound, 0), font:styleFont) + glyphDisplay!.ascent = glyphAscent; + glyphDisplay!.descent = glyphDescent; + glyphDisplay!.width = glyphWidth; + } + // Center the glyph on the axis + let shiftDown = 0.5*(glyphDisplay!.ascent - glyphDisplay!.descent) - styleFont.mathTable!.axisHeight; + glyphDisplay!.shiftDown = shiftDown; + return glyphDisplay; + } + + // Mark: Underline/Overline + + func makeUnderline(_ under:MTUnderLine?) -> MTDisplay? { + let innerListDisplay = MTTypesetter.createLineForMathList(under!.innerList, font:font, style:style, cramped:cramped) + let underDisplay = MTLineDisplay(withInner: innerListDisplay, position: currentPosition, range: under!.indexRange) + // Move the line down by the vertical gap. + underDisplay.lineShiftUp = -(innerListDisplay!.descent + styleFont.mathTable!.underbarVerticalGap); + underDisplay.lineThickness = styleFont.mathTable!.underbarRuleThickness; + underDisplay.ascent = innerListDisplay!.ascent + underDisplay.descent = innerListDisplay!.descent + styleFont.mathTable!.underbarVerticalGap + styleFont.mathTable!.underbarRuleThickness + styleFont.mathTable!.underbarExtraDescender; + underDisplay.width = innerListDisplay!.width; + return underDisplay; + } + + func makeOverline(_ over:MTOverLine?) -> MTDisplay? { + let innerListDisplay = MTTypesetter.createLineForMathList(over!.innerList, font:font, style:style, cramped:true) + let overDisplay = MTLineDisplay(withInner:innerListDisplay, position:currentPosition, range:over!.indexRange) + overDisplay.lineShiftUp = innerListDisplay!.ascent + styleFont.mathTable!.overbarVerticalGap; + overDisplay.lineThickness = styleFont.mathTable!.underbarRuleThickness; + overDisplay.ascent = innerListDisplay!.ascent + styleFont.mathTable!.overbarVerticalGap + styleFont.mathTable!.overbarRuleThickness + styleFont.mathTable!.overbarExtraAscender; + overDisplay.descent = innerListDisplay!.descent; + overDisplay.width = innerListDisplay!.width; + return overDisplay; + } + + // Mark: Accents + + func isSingleCharAccentee(_ accent:MTAccent?) -> Bool { + if (accent!.innerList!.atoms.count != 1) { + // Not a single char list. + return false + } + let innerAtom = accent!.innerList!.atoms[0] + if innerAtom.nucleus.count != 1 { + // A complex atom, not a simple char. + return false + } + if (innerAtom.subScript != nil || innerAtom.superScript != nil) { + return false + } + return true + } + + // The distance the accent must be moved from the beginning. + func getSkew(_ accent: MTAccent?, accenteeWidth width:CGFloat, accentGlyph:CGGlyph) -> CGFloat { + if accent!.nucleus.isEmpty { + // No accent + return 0; + } + let accentAdjustment = styleFont.mathTable!.getTopAccentAdjustment(accentGlyph) + var accenteeAdjustment = CGFloat(0) + if !self.isSingleCharAccentee(accent) { + // use the center of the accentee + accenteeAdjustment = width/2; + } else { + let innerAtom = accent!.innerList!.atoms[0] + let accenteeGlyph = self.findGlyphForCharacterAtIndex(innerAtom.nucleus.index(innerAtom.nucleus.endIndex, offsetBy:-1), inString:innerAtom.nucleus) + accenteeAdjustment = styleFont.mathTable!.getTopAccentAdjustment(accenteeGlyph) + } + // The adjustments need to aligned, so skew is just the difference. + return (accenteeAdjustment - accentAdjustment); + } + + // Find the largest horizontal variant if exists, with width less than max width. + func findVariantGlyph(_ glyph:CGGlyph, withMaxWidth maxWidth:CGFloat, maxWidth glyphAscent:inout CGFloat, glyphDescent:inout CGFloat, glyphWidth:inout CGFloat) -> CGGlyph { + let variants = styleFont.mathTable!.getHorizontalVariantsForGlyph(glyph) + let numVariants = variants.count + assert(numVariants > 0, "A glyph is always it's own variant, so number of variants should be > 0"); + var glyphs = [CGGlyph]() // [numVariants) + glyphs.reserveCapacity(numVariants) + for i in 0 ..< numVariants { + let glyph = variants[i]!.uint16Value + glyphs.append(glyph) + } + + var curGlyph = glyphs[0] // if no other glyph is found, we'll return the first one. + var bboxes = [CGRect](repeating: CGRect.zero, count: numVariants) // [numVariants) + var advances = [CGSize](repeating: CGSize.zero, count:numVariants) + // Get the bounds for these glyphs + CTFontGetBoundingRectsForGlyphs(styleFont.ctFont, .horizontal, &glyphs, &bboxes, numVariants); + CTFontGetAdvancesForGlyphs(styleFont.ctFont, .horizontal, &glyphs, &advances, numVariants); + for i in 0.. maxWidth) { + if (i == 0) { + // glyph dimensions are not yet set + glyphWidth = advances[i].width; + glyphAscent = ascent; + glyphDescent = descent; + } + return curGlyph; + } else { + curGlyph = glyphs[i] + glyphWidth = advances[i].width; + glyphAscent = ascent; + glyphDescent = descent; + } + } + // We exhausted all the variants and none was larger than the width, so we return the largest + return curGlyph; + } + + func makeAccent(_ accent:MTAccent?) -> MTDisplay? { + var accentee = MTTypesetter.createLineForMathList(accent!.innerList, font:font, style:style, cramped:true) + if accent!.nucleus.isEmpty { + // no accent! + return accentee + } + let end = accent!.nucleus.index(before: accent!.nucleus.endIndex) + var accentGlyph = self.findGlyphForCharacterAtIndex(end, inString:accent!.nucleus) + let accenteeWidth = accentee!.width; + var glyphAscent=CGFloat(0), glyphDescent=CGFloat(0), glyphWidth=CGFloat(0) + accentGlyph = self.findVariantGlyph(accentGlyph, withMaxWidth:accenteeWidth, maxWidth:&glyphAscent, glyphDescent:&glyphDescent, glyphWidth:&glyphWidth) + let delta = min(accentee!.ascent, styleFont.mathTable!.accentBaseHeight); + let skew = self.getSkew(accent, accenteeWidth:accenteeWidth, accentGlyph:accentGlyph) + let height = accentee!.ascent - delta; // This is always positive since delta <= height. + let accentPosition = CGPointMake(skew, height); + let accentGlyphDisplay = MTGlyphDisplay(withGlpyh: accentGlyph, range: accent!.indexRange, font: styleFont) + accentGlyphDisplay.ascent = glyphAscent; + accentGlyphDisplay.descent = glyphDescent; + accentGlyphDisplay.width = glyphWidth; + accentGlyphDisplay.position = accentPosition; + + if self.isSingleCharAccentee(accent) && (accent!.subScript != nil || accent!.superScript != nil) { + // Attach the super/subscripts to the accentee instead of the accent. + let innerAtom = accent!.innerList!.atoms[0] + innerAtom.superScript = accent!.superScript; + innerAtom.subScript = accent!.subScript; + accent?.superScript = nil; + accent?.subScript = nil; + // Remake the accentee (now with sub/superscripts) + // Note: Latex adjusts the heights in case the height of the char is different in non-cramped mode. However this shouldn't be the case since cramping + // only affects fractions and superscripts. We skip adjusting the heights. + accentee = MTTypesetter.createLineForMathList(accent!.innerList, font:font, style:style, cramped:cramped) + } + + let display = MTAccentDisplay(withAccent:accentGlyphDisplay, accentee:accentee, range:accent!.indexRange) + display.width = accentee!.width; + display.descent = accentee!.descent; + let ascent = accentee!.ascent - delta + glyphAscent; + display.ascent = max(accentee!.ascent, ascent); + display.position = currentPosition; + + return display; + } + + // Mark: - Table + + let kBaseLineSkipMultiplier = CGFloat(1.2) // default base line stretch is 12 pt for 10pt font. + let kLineSkipMultiplier = CGFloat(0.1) // default is 1pt for 10pt font. + let kLineSkipLimitMultiplier = CGFloat(0) + let kJotMultiplier = CGFloat(0.3) // A jot is 3pt for a 10pt font. + + func makeTable(_ table:MTMathTable?) -> MTDisplay? { + let numColumns = table!.numColumns; + if numColumns == 0 || table!.numRows == 0 { + // Empty table + return MTMathListDisplay(withDisplays: [MTDisplay](), range: table!.indexRange) + } + + var columnWidths = [CGFloat](repeating: 0, count: numColumns) + let displays = self.typesetCells(table, columnWidths:&columnWidths) + + // Position all the columns in each row + var rowDisplays = [MTDisplay]() // NSMutableArray(capacity: table!.cells.count) + rowDisplays.reserveCapacity(table!.cells.count) + for row in displays { + let rowDisplay = self.makeRowWithColumns(row, forTable:table, columnWidths:columnWidths) + rowDisplays.append(rowDisplay!) + } + + // Position all the rows + self.positionRows(rowDisplays, forTable:table) + let tableDisplay = MTMathListDisplay(withDisplays: rowDisplays, range: table!.indexRange) + tableDisplay.position = currentPosition; + return tableDisplay; + } + + // Typeset every cell in the table. As a side-effect calculate the max column width of each column. + func typesetCells(_ table:MTMathTable?, columnWidths: inout [CGFloat]) -> [[MTDisplay]] { + var displays = [[MTDisplay]]() + displays.reserveCapacity(table!.numRows) + for row in table!.cells { + var colDisplays = [MTDisplay]() //NSMutableArray(capacity: row.count) + colDisplays.reserveCapacity(row.count) + displays.append(colDisplays) + for i in 0.. MTMathListDisplay? { + var columnStart = CGFloat(0) + var rowRange = NSMakeRange(NSNotFound, 0); + for i in 0..= "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 + } + +} diff --git a/Tests/SwiftMathRenderTests/SwiftMathRenderTests.swift b/Tests/SwiftMathRenderTests/SwiftMathRenderTests.swift index 913524f..3ae16d3 100644 --- a/Tests/SwiftMathRenderTests/SwiftMathRenderTests.swift +++ b/Tests/SwiftMathRenderTests/SwiftMathRenderTests.swift @@ -1,11 +1,468 @@ import XCTest @testable import SwiftMathRender +// +// MathRenderSwiftTests.swift +// MathRenderSwiftTests +// +// Created by Mike Griebling on 2023-01-02. +// + final class SwiftMathRenderTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(SwiftMathRender().text, "Hello, World!") + + func checkAtomTypes(_ list:MTMathList?, types:[MTMathAtomType], desc:String) { + if let list = list { + XCTAssertEqual(list.atoms.count, types.count, desc) + for i in 0.. [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. +// } +// } + } +