Added color and font styles.
This commit is contained in:
33
Sources/SwiftMathRender/MathRender/MTBezierPath.swift
Normal file
33
Sources/SwiftMathRender/MathRender/MTBezierPath.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// MTBezierPath.swift
|
||||
// MathRenderSwift
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
extension MTBezierPath {
|
||||
func addLine(to point: CGPoint) {
|
||||
self.line(to: point)
|
||||
}
|
||||
}
|
||||
|
||||
extension MTView {
|
||||
|
||||
var backgroundColor:MTColor? {
|
||||
get {
|
||||
MTColor(cgColor: self.layer?.backgroundColor ?? MTColor.clear.cgColor)
|
||||
}
|
||||
set {
|
||||
self.layer?.backgroundColor = MTColor.clear.cgColor
|
||||
self.wantsLayer = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
25
Sources/SwiftMathRender/MathRender/MTColor.swift
Normal file
25
Sources/SwiftMathRender/MathRender/MTColor.swift
Normal file
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// MTColor.swift
|
||||
// MathRenderSwift
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension MTColor {
|
||||
|
||||
static func color(fromHexString hexString:String) -> MTColor? {
|
||||
if hexString.isEmpty { return nil }
|
||||
if !hexString.hasPrefix("#") { return nil }
|
||||
|
||||
var rgbValue = UInt64(0)
|
||||
let scanner = Scanner(string: hexString)
|
||||
scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#")
|
||||
scanner.scanHexInt64(&rgbValue)
|
||||
return MTColor(red: CGFloat((rgbValue & 0xFF0000))/255.0,
|
||||
green: CGFloat((rgbValue & 0xFF00))/255.0,
|
||||
blue: CGFloat((rgbValue & 0xFF))/255.0, alpha: 1.0)
|
||||
}
|
||||
|
||||
}
|
||||
36
Sources/SwiftMathRender/MathRender/MTConfig.swift
Normal file
36
Sources/SwiftMathRender/MathRender/MTConfig.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// MTConfig.swift
|
||||
// MathRenderSwift
|
||||
//
|
||||
// Created by Mike Griebling on 2023-01-01.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if os(iOS)
|
||||
|
||||
import UIKit
|
||||
|
||||
typealias MTView = UIView
|
||||
typealias MTColor = UIColor
|
||||
typealias MTBezierPath = UIBezierPath
|
||||
typealias MTLabel = UILabel
|
||||
typealias MTRect = CGRect
|
||||
|
||||
let MTEdgeInsetsZero = UIEdgeInsets.zero
|
||||
func MTGraphicsGetCurrentContext() -> CGContext? { UIGraphicsGetCurrentContext() }
|
||||
|
||||
#else
|
||||
|
||||
import AppKit
|
||||
|
||||
typealias MTView = NSView
|
||||
typealias MTColor = NSColor
|
||||
typealias MTBezierPath = NSBezierPath
|
||||
typealias MTEdgeInsets = NSEdgeInsets
|
||||
typealias MTRect = NSRect
|
||||
|
||||
let MTEdgeInsetsZero = NSEdgeInsets.init(top: 0, left: 0, bottom: 0, right: 0)
|
||||
func MTGraphicsGetCurrentContext() -> CGContext? { NSGraphicsContext.current?.cgContext }
|
||||
|
||||
#endif
|
||||
78
Sources/SwiftMathRender/MathRender/MTFont.swift
Normal file
78
Sources/SwiftMathRender/MathRender/MTFont.swift
Normal file
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// MTFont.swift
|
||||
// MathRenderSwift
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
import CoreText
|
||||
|
||||
//
|
||||
// Font.swift
|
||||
// iosMath
|
||||
//
|
||||
// Created by Kostub Deshmukh on 5/18/16.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
public class MTFont {
|
||||
|
||||
var defaultCGFont: CGFont!
|
||||
var ctFont: CTFont!
|
||||
var mathTable: MTFontMathTable?
|
||||
var rawMathTable: NSDictionary?
|
||||
|
||||
init() {}
|
||||
|
||||
convenience init(fontWithName name: String, size:CGFloat) {
|
||||
// CTFontCreateWithName does not load the complete math font, it only has about half the glyphs of the full math font.
|
||||
// In particular it does not have the math italic characters which breaks our variable rendering.
|
||||
// So we first load a CGFont from the file and then convert it to a CTFont.
|
||||
self.init()
|
||||
print("Loading font %@", name);
|
||||
let bundle = MTFont.fontBundle
|
||||
let fontPath = bundle.path(forResource: name, ofType: "otf")
|
||||
let fontDataProvider = CGDataProvider(filename: fontPath!)
|
||||
self.defaultCGFont = CGFont(fontDataProvider!)!
|
||||
print("Num glyphs: %zd", self.defaultCGFont.numberOfGlyphs)
|
||||
|
||||
self.ctFont = CTFontCreateWithGraphicsFont(self.defaultCGFont, size, nil, nil);
|
||||
|
||||
let mathTablePlist = bundle.url(forResource:name, withExtension:"plist")
|
||||
let dict = NSDictionary(contentsOf: mathTablePlist!) // dictionaryWithContentsOfFile:mathTablePlist];
|
||||
self.rawMathTable = dict
|
||||
self.mathTable = MTFontMathTable(withFont:self, mathTable:rawMathTable!)
|
||||
}
|
||||
|
||||
static var fontBundle:Bundle {
|
||||
// Uses bundle for class so that this can be access by the unit tests.
|
||||
return Bundle(url: Bundle(for: self).url(forResource: "iosMathFonts", withExtension: "bundle")!)!
|
||||
}
|
||||
|
||||
func copy(withSize size: CGFloat) -> MTFont {
|
||||
let newFont = MTFont()
|
||||
newFont.defaultCGFont = self.defaultCGFont
|
||||
newFont.ctFont = CTFontCreateWithGraphicsFont(self.defaultCGFont, size, nil, nil)
|
||||
newFont.rawMathTable = self.rawMathTable
|
||||
newFont.mathTable = MTFontMathTable(withFont: newFont, mathTable: newFont.rawMathTable!)
|
||||
return newFont
|
||||
}
|
||||
|
||||
func get(nameForGlyph glyph:CGGlyph) -> String {
|
||||
let name = self.defaultCGFont.name(for: glyph)
|
||||
return name! as String
|
||||
}
|
||||
|
||||
func get(glyphWithName name:String) -> CGGlyph {
|
||||
return self.defaultCGFont.getGlyphWithGlyphName(name: name as CFString)
|
||||
}
|
||||
|
||||
var fontSize:CGFloat {
|
||||
return CTFontGetSize(self.ctFont)
|
||||
}
|
||||
|
||||
}
|
||||
52
Sources/SwiftMathRender/MathRender/MTFontManager.swift
Normal file
52
Sources/SwiftMathRender/MathRender/MTFontManager.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// MTFontManager.swift
|
||||
// MathRenderSwift
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class MTFontManager {
|
||||
|
||||
static var manager:MTFontManager? = nil
|
||||
let kDefaultFontSize = CGFloat(20)
|
||||
|
||||
static var fontManager : MTFontManager {
|
||||
if manager == nil {
|
||||
manager = MTFontManager()
|
||||
}
|
||||
return manager!
|
||||
}
|
||||
|
||||
var nameToFontMap = [String: MTFont]()
|
||||
|
||||
public func font(withName name:String, size:CGFloat) -> MTFont? {
|
||||
var f = self.nameToFontMap[name]
|
||||
if f == nil {
|
||||
f = MTFont(fontWithName: name, size: size)
|
||||
self.nameToFontMap[name] = f
|
||||
}
|
||||
|
||||
if f!.fontSize == size { return f }
|
||||
else { return f!.copy(withSize: size) }
|
||||
}
|
||||
|
||||
public func latinModernFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "latinmodern-math", size: size)
|
||||
}
|
||||
|
||||
public func xitsFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "xits-math", size: size)
|
||||
}
|
||||
|
||||
public func termesFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "texgyretermes-math", size: size)
|
||||
}
|
||||
|
||||
public var defaultFont: MTFont? {
|
||||
MTFontManager.fontManager.latinModernFont(withSize: kDefaultFontSize)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
323
Sources/SwiftMathRender/MathRender/MTFontMathTable.swift
Normal file
323
Sources/SwiftMathRender/MathRender/MTFontMathTable.swift
Normal file
@@ -0,0 +1,323 @@
|
||||
//
|
||||
// MTFontMathTable.swift
|
||||
// MathRenderSwift
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
import CoreText
|
||||
|
||||
class GlyphPart {
|
||||
/// The glyph that represents this part
|
||||
var glyph: CGGlyph!
|
||||
|
||||
/// Full advance width/height for this part, in the direction of the extension in points.
|
||||
var fullAdvance: CGFloat = 0
|
||||
|
||||
/// Advance width/ height of the straight bar connector material at the beginning of the glyph in points.
|
||||
var startConnectorLength: CGFloat = 0
|
||||
|
||||
/// Advance width/ height of the straight bar connector material at the end of the glyph in points.
|
||||
var endConnectorLength: CGFloat = 0
|
||||
|
||||
/// If this part is an extender. If set, the part can be skipped or repeated.
|
||||
var isExtender: Bool = false
|
||||
}
|
||||
|
||||
/** This class represents the Math table of an open type font.
|
||||
|
||||
The math table is documented here: https://www.microsoft.com/typography/otspec/math.htm
|
||||
|
||||
How the constants in this class affect the display is documented here:
|
||||
http://www.tug.org/TUGboat/tb30-1/tb94vieth.pdf
|
||||
|
||||
@note We don't parse the math table from the open type font. Rather we parse it
|
||||
in python and convert it to a .plist file which is easily consumed by this class.
|
||||
This approach is preferable to spending an inordinate amount of time figuring out
|
||||
how to parse the returned NSData object using the open type rules.
|
||||
|
||||
@remark This class is not meant to be used outside of this library.
|
||||
*/
|
||||
|
||||
class MTFontMathTable {
|
||||
|
||||
// The font for this math table.
|
||||
weak var font:MTFont? // @property (nonatomic, readonly, weak) MTFont* font;
|
||||
|
||||
var _unitsPerEm: UInt
|
||||
var _fontSize: CGFloat
|
||||
var _mathTable: NSDictionary!
|
||||
|
||||
let kConstants = "constants"
|
||||
|
||||
/** MU unit in points */
|
||||
var muUnit:CGFloat { _fontSize/18 }
|
||||
|
||||
func fontUnitsToPt(_ fontUnits:Int) -> CGFloat {
|
||||
return CGFloat(fontUnits) * _fontSize / CGFloat(_unitsPerEm)
|
||||
}
|
||||
|
||||
init(withFont font: MTFont?, mathTable:NSDictionary) {
|
||||
assert(font != nil, "font has nil value")
|
||||
assert(font!.ctFont != nil, "font.ctFont has nil value")
|
||||
self.font = font;
|
||||
// do domething with font
|
||||
_unitsPerEm = UInt(CTFontGetUnitsPerEm(font!.ctFont))
|
||||
_fontSize = font!.fontSize;
|
||||
_mathTable = mathTable
|
||||
let version = _mathTable["version"] as! String
|
||||
if version != "1.3" {
|
||||
NSException(name: NSExceptionName.internalInconsistencyException, reason: "Invalid version of math table plist: \(version)").raise()
|
||||
}
|
||||
}
|
||||
|
||||
func constantFromTable(_ constName:String) -> CGFloat {
|
||||
let consts = _mathTable[kConstants] as! NSDictionary?
|
||||
let val = consts![constName] as! NSNumber?
|
||||
return fontUnitsToPt(val!.intValue)
|
||||
}
|
||||
|
||||
func percentFromTable(_ percentName:String) -> CGFloat {
|
||||
let consts = _mathTable[kConstants] as! NSDictionary?
|
||||
let val = consts![percentName] as! NSNumber?
|
||||
return CGFloat(val!.floatValue) / 100
|
||||
}
|
||||
|
||||
/// Math Font Metrics from the opentype specification
|
||||
// MARK: - Fractions
|
||||
var fractionNumeratorDisplayStyleShiftUp:CGFloat { constantFromTable("FractionNumeratorDisplayStyleShiftUp") } // \sigma_8 in TeX
|
||||
var fractionNumeratorShiftUp:CGFloat { constantFromTable("FractionNumeratorShiftUp") } // \sigma_9 in TeX
|
||||
var fractionDenominatorDisplayStyleShiftDown:CGFloat { constantFromTable("FractionDenominatorDisplayStyleShiftDown") } // \sigma_11 in TeX
|
||||
var fractionDenominatorShiftDown:CGFloat { constantFromTable("FractionDenominatorShiftDown") } // \sigma_12 in TeX
|
||||
var fractionNumeratorDisplayStyleGapMin:CGFloat { constantFromTable("FractionNumDisplayStyleGapMin") } // 3 * \xi_8 in TeX
|
||||
var fractionNumeratorGapMin:CGFloat { constantFromTable("FractionNumeratorGapMin") } // \xi_8 in TeX
|
||||
var fractionDenominatorDisplayStyleGapMin:CGFloat { constantFromTable("FractionDenomDisplayStyleGapMin") } // 3 * \xi_8 in TeX
|
||||
var fractionDenominatorGapMin:CGFloat { constantFromTable("FractionDenominatorGapMin") } // \xi_8 in TeX
|
||||
var fractionRuleThickness:CGFloat { constantFromTable("FractionRuleThickness") } // \xi_8 in TeX
|
||||
var skewedFractionHorizonalGap:CGFloat { constantFromTable("SkewedFractionHorizontalGap") } // \sigma_20 in TeX
|
||||
var skewedFractionVerticalGap:CGFloat { constantFromTable("SkewedFractionVerticalGap") } // \sigma_21 in TeX
|
||||
|
||||
// MARK: - Non-standard
|
||||
// FractionDelimiterSize and FractionDelimiterDisplayStyleSize are not constants
|
||||
// specified in the OpenType Math specification. Rather these are proposed LuaTeX extensions
|
||||
// for the TeX parameters \sigma_20 (delim1) and \sigma_21 (delim2). Since these do not
|
||||
// exist in the fonts that we have, we use the same approach as LuaTeX and use the fontSize
|
||||
// to determine these values. The constants used are the same as LuaTeX and KaTeX and match the
|
||||
// metrics values of the original TeX fonts.
|
||||
// Note: An alternative approach is to use DelimitedSubFormulaMinHeight for \sigma21 and use a factor
|
||||
// of 2 to get \sigma 20 as proposed in Vieth paper.
|
||||
// The XeTeX implementation sets \sigma21 = fontSize and \sigma20 = DelimitedSubFormulaMinHeight which
|
||||
// will produce smaller delimiters.
|
||||
// Of all the approaches we've implemented LuaTeX's approach since it mimics LaTeX most accurately.
|
||||
var fractionDelimiterSize: CGFloat { return 1.01 * _fontSize }
|
||||
|
||||
/// Modified constant from 2.4 to 2.39, it matches KaTeX and looks better.
|
||||
var fractionDelimiterDisplayStyleSize: CGFloat { return 2.39 * _fontSize }
|
||||
|
||||
// MARK: - Stacks
|
||||
var stackTopDisplayStyleShiftUp:CGFloat { constantFromTable("StackTopDisplayStyleShiftUp") } // \sigma_8 in TeX
|
||||
var stackTopShiftUp:CGFloat { constantFromTable("StackTopShiftUp") } // \sigma_10 in TeX
|
||||
var stackDisplayStyleGapMin:CGFloat { constantFromTable("StackDisplayStyleGapMin") } // 7 \xi_8 in TeX
|
||||
var stackGapMin:CGFloat { constantFromTable("StackGapMin") } // 3 \xi_8 in TeX
|
||||
var stackBottomDisplayStyleShiftDown:CGFloat { constantFromTable("StackBottomDisplayStyleShiftDown") } // \sigma_11 in TeX
|
||||
var stackBottomShiftDown:CGFloat { constantFromTable("StackBottomShiftDown") } // \sigma_12 in TeX
|
||||
|
||||
var stretchStackBottomShiftDown:CGFloat { constantFromTable("StretchStackBottomShiftDown") }
|
||||
var stretchStackGapAboveMin:CGFloat { constantFromTable("StretchStackGapAboveMin") }
|
||||
var stretchStackGapBelowMin:CGFloat { constantFromTable("StretchStackGapBelowMin") }
|
||||
var stretchStackTopShiftUp:CGFloat { constantFromTable("StretchStackTopShiftUp") }
|
||||
|
||||
// MARK: - super/sub scripts
|
||||
|
||||
var superscriptShiftUp:CGFloat { constantFromTable("SuperscriptShiftUp") } // \sigma_13, \sigma_14 in TeX
|
||||
var superscriptShiftUpCramped:CGFloat { constantFromTable("SuperscriptShiftUpCramped") } // \sigma_15 in TeX
|
||||
var subscriptShiftDown:CGFloat { constantFromTable("SubscriptShiftDown") } // \sigma_16, \sigma_17 in TeX
|
||||
var superscriptBaselineDropMax:CGFloat { constantFromTable("SuperscriptBaselineDropMax") } // \sigma_18 in TeX
|
||||
var subscriptBaselineDropMin:CGFloat { constantFromTable("SubscriptBaselineDropMin") } // \sigma_19 in TeX
|
||||
var superscriptBottomMin:CGFloat { constantFromTable("SuperscriptBottomMin") } // 1/4 \sigma_5 in TeX
|
||||
var subscriptTopMax:CGFloat { constantFromTable("SubscriptTopMax") } // 4/5 \sigma_5 in TeX
|
||||
var subSuperscriptGapMin:CGFloat { constantFromTable("SubSuperscriptGapMin") } // 4 \xi_8 in TeX
|
||||
var superscriptBottomMaxWithSubscript:CGFloat { constantFromTable("SuperscriptBottomMaxWithSubscript") } // 4/5 \sigma_5 in TeX
|
||||
|
||||
var spaceAfterScript:CGFloat { constantFromTable("SpaceAfterScript") }
|
||||
|
||||
// MARK: - radicals
|
||||
var radicalExtraAscender:CGFloat { constantFromTable("RadicalExtraAscender") } // \xi_8 in Tex
|
||||
var radicalRuleThickness:CGFloat { constantFromTable("RadicalRuleThickness") } // \xi_8 in Tex
|
||||
var radicalDisplayStyleVerticalGap:CGFloat { constantFromTable("RadicalDisplayStyleVerticalGap") } // \xi_8 + 1/4 \sigma_5 in Tex
|
||||
var radicalVerticalGap:CGFloat { constantFromTable("RadicalVerticalGap") } // 5/4 \xi_8 in Tex
|
||||
var radicalKernBeforeDegree:CGFloat { constantFromTable("RadicalKernBeforeDegree") } // 5 mu in Tex
|
||||
var radicalKernAfterDegree:CGFloat { constantFromTable("RadicalKernAfterDegree") } // -10 mu in Tex
|
||||
var radicalDegreeBottomRaisePercent:CGFloat { percentFromTable("RadicalDegreeBottomRaisePercent") } // 60% in Tex
|
||||
|
||||
// MARK: - Limits
|
||||
var upperLimitBaselineRiseMin:CGFloat { constantFromTable("UpperLimitBaselineRiseMin") } // \xi_11 in TeX
|
||||
var upperLimitGapMin:CGFloat { constantFromTable("UpperLimitGapMin") } // \xi_9 in TeX
|
||||
var lowerLimitGapMin:CGFloat { constantFromTable("LowerLimitGapMin") } // \xi_10 in TeX
|
||||
var lowerLimitBaselineDropMin:CGFloat { constantFromTable("LowerLimitBaselineDropMin") } // \xi_12 in TeX
|
||||
var limitExtraAscenderDescender:CGFloat { 0 } // \xi_13 in TeX, not present in OpenType so we always set it to 0.
|
||||
|
||||
// MARK: - Underline
|
||||
var underbarVerticalGap:CGFloat { constantFromTable("UnderbarVerticalGap") } // 3 \xi_8 in TeX
|
||||
var underbarRuleThickness:CGFloat { constantFromTable("UnderbarRuleThickness") } // \xi_8 in TeX
|
||||
var underbarExtraDescender:CGFloat { constantFromTable("UnderbarExtraDescender") } // \xi_8 in TeX
|
||||
|
||||
// MARK: - Overline
|
||||
var overbarVerticalGap:CGFloat { constantFromTable("OverbarVerticalGap") } // 3 \xi_8 in TeX
|
||||
var overbarRuleThickness:CGFloat { constantFromTable("OverbarRuleThickness") } // \xi_8 in TeX
|
||||
var overbarExtraAscender:CGFloat { constantFromTable("OverbarExtraAscender") } // \xi_8 in TeX
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
var axisHeight:CGFloat { constantFromTable("AxisHeight") } // \sigma_22 in TeX
|
||||
var scriptScaleDown:CGFloat { percentFromTable("ScriptPercentScaleDown") }
|
||||
var scriptScriptScaleDown:CGFloat { percentFromTable("ScriptScriptPercentScaleDown") }
|
||||
var mathLeading:CGFloat { constantFromTable("MathLeading") }
|
||||
var delimitedSubFormulaMinHeight:CGFloat { constantFromTable("DelimitedSubFormulaMinHeight") }
|
||||
|
||||
// MARK: - Accent
|
||||
|
||||
var accentBaseHeight:CGFloat { constantFromTable("AccentBaseHeight") } // \fontdimen5 in TeX (x-height)
|
||||
var flattenedAccentBaseHeight:CGFloat { constantFromTable("FlattenedAccentBaseHeight") }
|
||||
|
||||
// MARK: - Variants
|
||||
|
||||
let kVertVariants = "v_variants"
|
||||
let kHorizVariants = "h_variants"
|
||||
|
||||
/** Returns an Array of all the vertical variants of the glyph if any. If
|
||||
there are no variants for the glyph, the array contains the given glyph. */
|
||||
func getVerticalVariantsForGlyph( _ glyph:CGGlyph) -> [NSNumber?] {
|
||||
let variants = _mathTable[kVertVariants] as! NSDictionary?
|
||||
return self.getVariantsForGlyph(glyph, inDictionary: variants)
|
||||
}
|
||||
|
||||
/** Returns an Array of all the horizontal variants of the glyph if any. If
|
||||
there are no variants for the glyph, the array contains the given glyph. */
|
||||
func getHorizontalVariantsForGlyph( _ glyph:CGGlyph) -> [NSNumber?] {
|
||||
let variants = _mathTable[kHorizVariants] as! NSDictionary?
|
||||
return self.getVariantsForGlyph(glyph, inDictionary:variants)
|
||||
}
|
||||
|
||||
func getVariantsForGlyph(_ glyph: CGGlyph, inDictionary variants:NSDictionary?) -> [NSNumber?] {
|
||||
let glyphName = self.font?.get(nameForGlyph: glyph)
|
||||
let variantGlyphs = variants![glyphName!] as! NSArray?
|
||||
let glyphArray = NSMutableArray(capacity: variantGlyphs!.count)
|
||||
if variantGlyphs == nil {
|
||||
// There are no extra variants, so just add the current glyph to it.
|
||||
let glyph = self.font?.get(glyphWithName: glyphName!)
|
||||
glyphArray.add(glyph as Any)
|
||||
return glyphArray as! [NSNumber?]
|
||||
}
|
||||
for gvn in variantGlyphs! {
|
||||
let glyphVariantName = gvn as! String?
|
||||
let variantGlyph = self.font?.get(glyphWithName: glyphVariantName!)
|
||||
glyphArray.add(variantGlyph as Any)
|
||||
}
|
||||
return glyphArray as! [NSNumber?]
|
||||
}
|
||||
|
||||
/** Returns a larger vertical variant of the given glyph if any.
|
||||
If there is no larger version, this returns the current glyph.
|
||||
*/
|
||||
func getLargerGlyph(_ glyph:CGGlyph) -> CGGlyph {
|
||||
let variants = _mathTable[kVertVariants] as! NSDictionary?
|
||||
let glyphName = self.font?.get(nameForGlyph: glyph)
|
||||
let variantGlyphs = variants![glyphName!] as! NSArray?
|
||||
if variantGlyphs == nil {
|
||||
// There are no extra variants, so just returnt the current glyph.
|
||||
return glyph
|
||||
}
|
||||
// Find the first variant with a different name.
|
||||
for gvn in variantGlyphs! {
|
||||
let glyphVariantName = gvn as! String?
|
||||
if glyphVariantName != glyphName {
|
||||
let variantGlyph = self.font?.get(glyphWithName: glyphVariantName!)
|
||||
return variantGlyph!
|
||||
}
|
||||
}
|
||||
// We did not find any variants of this glyph so return it.
|
||||
return glyph;
|
||||
}
|
||||
|
||||
// MARK: - Italic Correction
|
||||
|
||||
let kItalic = "italic"
|
||||
|
||||
/** Returns the italic correction for the given glyph if any. If there
|
||||
isn't any this returns 0. */
|
||||
func getItalicCorrection(_ glyph: CGGlyph) -> CGFloat {
|
||||
let italics = _mathTable[kItalic] as! NSDictionary?
|
||||
let glyphName = self.font?.get(nameForGlyph: glyph)
|
||||
let val = italics![glyphName!] as! NSNumber?
|
||||
// if val is nil, this returns 0.
|
||||
return self.fontUnitsToPt(val?.intValue ?? 0)
|
||||
}
|
||||
|
||||
// MARK: - Accents
|
||||
|
||||
let kAccents = "accents"
|
||||
|
||||
/** Returns the adjustment to the top accent for the given glyph if any.
|
||||
If there isn't any this returns -1. */
|
||||
func getTopAccentAdjustment(_ glyph: CGGlyph) -> CGFloat {
|
||||
var glyph = glyph
|
||||
let accents = _mathTable[kAccents] as! NSDictionary?
|
||||
let glyphName = self.font?.get(nameForGlyph: glyph)
|
||||
let val = accents![glyphName!] as! NSNumber?
|
||||
if let val = val {
|
||||
return self.fontUnitsToPt(val.intValue)
|
||||
} else {
|
||||
// If no top accent is defined then it is the center of the advance width.
|
||||
var advances = CGSize.zero
|
||||
CTFontGetAdvancesForGlyphs(self.font!.ctFont, .horizontal, &glyph, &advances, 1)
|
||||
return advances.width/2
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Glyph Construction
|
||||
|
||||
/** Minimum overlap of connecting glyphs during glyph construction */
|
||||
var minConnectorOverlap:CGFloat { constantFromTable("MinConnectorOverlap") }
|
||||
|
||||
let kVertAssembly = "v_assembly"
|
||||
let kAssemblyParts = "parts"
|
||||
|
||||
/** Returns an array of the glyph parts to be used for constructing vertical variants
|
||||
of this glyph. If there is no glyph assembly defined, returns nil. */
|
||||
func getVerticalGlyphAssembly(forGlyph glyph:CGGlyph) -> [GlyphPart] {
|
||||
let assemblyTable = _mathTable[kVertAssembly] as! NSDictionary?
|
||||
let glyphName = self.font?.get(nameForGlyph: glyph) // getGlyphName:glyph];
|
||||
let assemblyInfo = assemblyTable![glyphName!] as! NSDictionary?
|
||||
if assemblyInfo == nil {
|
||||
// No vertical assembly defined for glyph
|
||||
return []
|
||||
}
|
||||
let parts = assemblyInfo![kAssemblyParts] as! NSArray?
|
||||
if parts == nil {
|
||||
// parts should always have been defined, but if it isn't return nil
|
||||
return []
|
||||
}
|
||||
var rv = [GlyphPart]()
|
||||
for part in parts! {
|
||||
let partInfo = part as! NSDictionary?
|
||||
let part = GlyphPart()
|
||||
let adv = partInfo!["advance"] as! NSNumber?
|
||||
part.fullAdvance = self.fontUnitsToPt(adv!.intValue)
|
||||
let end = partInfo!["endConnector"] as! NSNumber?
|
||||
part.endConnectorLength = self.fontUnitsToPt(end!.intValue)
|
||||
let start = partInfo!["startConnector"] as! NSNumber?
|
||||
part.startConnectorLength = self.fontUnitsToPt(start!.intValue)
|
||||
let ext = partInfo!["extender"] as! NSNumber?
|
||||
part.isExtender = ext!.boolValue
|
||||
let glyphName = partInfo!["glyph"] as! String?
|
||||
part.glyph = self.font?.get(glyphWithName: glyphName!)
|
||||
rv.append(part)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
35
Sources/SwiftMathRender/MathRender/MTLabel.swift
Normal file
35
Sources/SwiftMathRender/MathRender/MTLabel.swift
Normal file
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// MTLabel.swift
|
||||
// MathRenderSwift
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
class MTLabel : NSTextField {
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
self.stringValue = ""
|
||||
self.isBezeled = false
|
||||
self.drawsBackground = false
|
||||
self.isEditable = false
|
||||
self.isSelectable = false
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
var text:String? {
|
||||
get { super.stringValue }
|
||||
set { super.stringValue = newValue! }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
815
Sources/SwiftMathRender/MathRender/MTMathAtomFactory.swift
Normal file
815
Sources/SwiftMathRender/MathRender/MTMathAtomFactory.swift
Normal file
@@ -0,0 +1,815 @@
|
||||
//
|
||||
// MTMathAtomFactory.swift
|
||||
// MathRenderSwift
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class MTMathAtomFactory {
|
||||
|
||||
public static let aliases = [
|
||||
"lnot" : "neg",
|
||||
"land" : "wedge",
|
||||
"lor" : "vee",
|
||||
"ne" : "neq",
|
||||
"le" : "leq",
|
||||
"ge" : "geq",
|
||||
"lbrace" : "{",
|
||||
"rbrace" : "}",
|
||||
"Vert" : "|",
|
||||
"gets" : "leftarrow",
|
||||
"to" : "rightarrow",
|
||||
"iff" : "Longleftrightarrow",
|
||||
"AA" : "angstrom"
|
||||
]
|
||||
|
||||
public static let delimiters = [
|
||||
"." : "", // . means no delimiter
|
||||
"(" : "(",
|
||||
")" : ")",
|
||||
"[" : "[",
|
||||
"]" : "]",
|
||||
"<" : "\u{2329}",
|
||||
">" : "\u{232A}",
|
||||
"/" : "/",
|
||||
"\\" : "\\",
|
||||
"|" : "|",
|
||||
"lgroup" : "\u{27EE}",
|
||||
"rgroup" : "\u{27EF}",
|
||||
"||" : "\u{2016}",
|
||||
"Vert" : "\u{2016}",
|
||||
"vert" : "|",
|
||||
"uparrow" : "\u{2191}",
|
||||
"downarrow" : "\u{2193}",
|
||||
"updownarrow" : "\u{2195}",
|
||||
"Uparrow" : "\u{21D1}",
|
||||
"Downarrow" : "\u{21D3}",
|
||||
"Updownarrow" : "\u{21D5}",
|
||||
"backslash" : "\\",
|
||||
"rangle" : "\u{232A}",
|
||||
"langle" : "\u{2329}",
|
||||
"rbrace" : "}",
|
||||
"}" : "}",
|
||||
"{" : "{",
|
||||
"lbrace" : "{",
|
||||
"lceil" : "\u{2308}",
|
||||
"rceil" : "\u{2309}",
|
||||
"lfloor" : "\u{230A}",
|
||||
"rfloor" : "\u{230B}"
|
||||
]
|
||||
|
||||
var _delimValueToName: [String: String]? = nil
|
||||
public var delimValueToName: [String: String] {
|
||||
if _delimValueToName == nil {
|
||||
var output = [String: String]()
|
||||
|
||||
for (key, value) in Self.delimiters {
|
||||
if let existingValue = output[value] {
|
||||
if key.count > existingValue.count {
|
||||
continue
|
||||
} else if key.count == existingValue.count {
|
||||
if key.compare(existingValue) == .orderedDescending {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output[value] = key
|
||||
}
|
||||
_delimValueToName = output
|
||||
}
|
||||
return _delimValueToName!
|
||||
}
|
||||
|
||||
public static let accents = [
|
||||
"grave" : "\u{0300}",
|
||||
"acute" : "\u{0301}",
|
||||
"hat" : "\u{0302}", // In our implementation hat and widehat behave the same.
|
||||
"tilde" : "\u{0303}", // In our implementation tilde and widetilde behave the same.
|
||||
"bar" : "\u{0304}",
|
||||
"breve" : "\u{0306}",
|
||||
"dot" : "\u{0307}",
|
||||
"ddot" : "\u{0308}",
|
||||
"check" : "\u{030C}",
|
||||
"vec" : "\u{20D7}",
|
||||
"widehat" : "\u{0302}",
|
||||
"widetilde" : "\u{0303}"
|
||||
]
|
||||
|
||||
var _accentValueToName: [String: String]? = nil
|
||||
public var accentValueToName: [String: String] {
|
||||
if _accentValueToName == nil {
|
||||
var output = [String: String]()
|
||||
|
||||
for (key, value) in Self.accents {
|
||||
if let existingValue = output[value] {
|
||||
if key.count > existingValue.count {
|
||||
continue
|
||||
} else if key.count == existingValue.count {
|
||||
if key.compare(existingValue) == .orderedDescending {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output[value] = key
|
||||
}
|
||||
|
||||
_accentValueToName = output
|
||||
}
|
||||
|
||||
return _accentValueToName!
|
||||
}
|
||||
|
||||
public var supportedLatexSymbols: [String: MTMathAtom] = [
|
||||
"square" : MTMathAtomFactory.placeholder(),
|
||||
|
||||
// Greek characters
|
||||
"alpha" : MTMathAtom(type: .variable, value: "\u{03B1}"),
|
||||
"beta" : MTMathAtom(type: .variable, value: "\u{03B2}"),
|
||||
"gamma" : MTMathAtom(type: .variable, value: "\u{03B3}"),
|
||||
"delta" : MTMathAtom(type: .variable, value: "\u{03B4}"),
|
||||
"varepsilon" : MTMathAtom(type: .variable, value: "\u{03B5}"),
|
||||
"zeta" : MTMathAtom(type: .variable, value: "\u{03B6}"),
|
||||
"eta" : MTMathAtom(type: .variable, value: "\u{03B7}"),
|
||||
"theta" : MTMathAtom(type: .variable, value: "\u{03B8}"),
|
||||
"iota" : MTMathAtom(type: .variable, value: "\u{03B9}"),
|
||||
"kappa" : MTMathAtom(type: .variable, value: "\u{03BA}"),
|
||||
"lambda" : MTMathAtom(type: .variable, value: "\u{03BB}"),
|
||||
"mu" : MTMathAtom(type: .variable, value: "\u{03BC}"),
|
||||
"nu" : MTMathAtom(type: .variable, value: "\u{03BD}"),
|
||||
"xi" : MTMathAtom(type: .variable, value: "\u{03BE}"),
|
||||
"omicron" : MTMathAtom(type: .variable, value: "\u{03BF}"),
|
||||
"pi" : MTMathAtom(type: .variable, value: "\u{03C0}"),
|
||||
"rho" : MTMathAtom(type: .variable, value: "\u{03C1}"),
|
||||
"varsigma" : MTMathAtom(type: .variable, value: "\u{03C1}"),
|
||||
"sigma" : MTMathAtom(type: .variable, value: "\u{03C3}"),
|
||||
"tau" : MTMathAtom(type: .variable, value: "\u{03C4}"),
|
||||
"upsilon" : MTMathAtom(type: .variable, value: "\u{03C5}"),
|
||||
"varphi" : MTMathAtom(type: .variable, value: "\u{03C6}"),
|
||||
"chi" : MTMathAtom(type: .variable, value: "\u{03C7}"),
|
||||
"psi" : MTMathAtom(type: .variable, value: "\u{03C8}"),
|
||||
"omega" : MTMathAtom(type: .variable, value: "\u{03C9}"),
|
||||
// We mark the following greek chars as ordinary so that we don't try
|
||||
// to automatically italicize them as we do with variables.
|
||||
// These characters fall outside the rules of italicization that we have defined.
|
||||
"epsilon" : MTMathAtom(type: .ordinary, value: "\u{0001D716}"),
|
||||
"vartheta" : MTMathAtom(type: .ordinary, value: "\u{0001D717}"),
|
||||
"phi" : MTMathAtom(type: .ordinary, value: "\u{0001D719}"),
|
||||
"varrho" : MTMathAtom(type: .ordinary, value: "\u{0001D71A}"),
|
||||
"varpi" : MTMathAtom(type: .ordinary, value: "\u{0001D71B}"),
|
||||
|
||||
// Capital greek characters
|
||||
"Gamma" : MTMathAtom(type: .variable, value: "\u{0393}"),
|
||||
"Delta" : MTMathAtom(type: .variable, value: "\u{0394}"),
|
||||
"Theta" : MTMathAtom(type: .variable, value: "\u{0398}"),
|
||||
"Lambda" : MTMathAtom(type: .variable, value: "\u{039B}"),
|
||||
"Xi" : MTMathAtom(type: .variable, value: "\u{039E}"),
|
||||
"Pi" : MTMathAtom(type: .variable, value: "\u{03A0}"),
|
||||
"Sigma" : MTMathAtom(type: .variable, value: "\u{03A3}"),
|
||||
"Upsilon" : MTMathAtom(type: .variable, value: "\u{03A5}"),
|
||||
"Phi" : MTMathAtom(type: .variable, value: "\u{03A6}"),
|
||||
"Psi" : MTMathAtom(type: .variable, value: "\u{03A8}"),
|
||||
"Omega" : MTMathAtom(type: .variable, value: "\u{03A9}"),
|
||||
|
||||
// Open
|
||||
"lceil" : MTMathAtom(type: .open, value: "\u{2308}"),
|
||||
"lfloor" : MTMathAtom(type: .open, value: "\u{230A}"),
|
||||
"langle" : MTMathAtom(type: .open, value: "\u{27E8}"),
|
||||
"lgroup" : MTMathAtom(type: .open, value: "\u{27EE}"),
|
||||
|
||||
// Close
|
||||
"rceil" : MTMathAtom(type: .close, value: "\u{2309}"),
|
||||
"rfloor" : MTMathAtom(type: .close, value: "\u{230B}"),
|
||||
"rangle" : MTMathAtom(type: .close, value: "\u{27E9}"),
|
||||
"rgroup" : MTMathAtom(type: .close, value: "\u{27EF}"),
|
||||
|
||||
// Arrows
|
||||
"leftarrow" : MTMathAtom(type: .relation, value: "\u{2190}"),
|
||||
"uparrow" : MTMathAtom(type: .relation, value: "\u{2191}"),
|
||||
"rightarrow" : MTMathAtom(type: .relation, value: "\u{2192}"),
|
||||
"downarrow" : MTMathAtom(type: .relation, value: "\u{2193}"),
|
||||
"leftrightarrow" : MTMathAtom(type: .relation, value: "\u{2194}"),
|
||||
"updownarrow" : MTMathAtom(type: .relation, value: "\u{2195}"),
|
||||
"nwarrow" : MTMathAtom(type: .relation, value: "\u{2196}"),
|
||||
"nearrow" : MTMathAtom(type: .relation, value: "\u{2197}"),
|
||||
"searrow" : MTMathAtom(type: .relation, value: "\u{2198}"),
|
||||
"swarrow" : MTMathAtom(type: .relation, value: "\u{2199}"),
|
||||
"mapsto" : MTMathAtom(type: .relation, value: "\u{21A6}"),
|
||||
"Leftarrow" : MTMathAtom(type: .relation, value: "\u{21D0}"),
|
||||
"Uparrow" : MTMathAtom(type: .relation, value: "\u{21D1}"),
|
||||
"Rightarrow" : MTMathAtom(type: .relation, value: "\u{21D2}"),
|
||||
"Downarrow" : MTMathAtom(type: .relation, value: "\u{21D3}"),
|
||||
"Leftrightarrow" : MTMathAtom(type: .relation, value: "\u{21D4}"),
|
||||
"Updownarrow" : MTMathAtom(type: .relation, value: "\u{21D5}"),
|
||||
"longleftarrow" : MTMathAtom(type: .relation, value: "\u{27F5}"),
|
||||
"longrightarrow" : MTMathAtom(type: .relation, value: "\u{27F6}"),
|
||||
"longleftrightarrow" : MTMathAtom(type: .relation, value: "\u{27F7}"),
|
||||
"Longleftarrow" : MTMathAtom(type: .relation, value: "\u{27F8}"),
|
||||
"Longrightarrow" : MTMathAtom(type: .relation, value: "\u{27F9}"),
|
||||
"Longleftrightarrow" : MTMathAtom(type: .relation, value: "\u{27FA}"),
|
||||
|
||||
|
||||
// Relations
|
||||
"leq" : MTMathAtom(type: .relation, value: UnicodeSymbol.lessEqual),
|
||||
"geq" : MTMathAtom(type: .relation, value: UnicodeSymbol.greaterEqual),
|
||||
"neq" : MTMathAtom(type: .relation, value: UnicodeSymbol.notEqual),
|
||||
"in" : MTMathAtom(type: .relation, value: "\u{2208}"),
|
||||
"notin" : MTMathAtom(type: .relation, value: "\u{2209}"),
|
||||
"ni" : MTMathAtom(type: .relation, value: "\u{220B}"),
|
||||
"propto" : MTMathAtom(type: .relation, value: "\u{221D}"),
|
||||
"mid" : MTMathAtom(type: .relation, value: "\u{2223}"),
|
||||
"parallel" : MTMathAtom(type: .relation, value: "\u{2225}"),
|
||||
"sim" : MTMathAtom(type: .relation, value: "\u{223C}"),
|
||||
"simeq" : MTMathAtom(type: .relation, value: "\u{2243}"),
|
||||
"cong" : MTMathAtom(type: .relation, value: "\u{2245}"),
|
||||
"approx" : MTMathAtom(type: .relation, value: "\u{2248}"),
|
||||
"asymp" : MTMathAtom(type: .relation, value: "\u{224D}"),
|
||||
"doteq" : MTMathAtom(type: .relation, value: "\u{2250}"),
|
||||
"equiv" : MTMathAtom(type: .relation, value: "\u{2261}"),
|
||||
"gg" : MTMathAtom(type: .relation, value: "\u{226A}"),
|
||||
"ll" : MTMathAtom(type: .relation, value: "\u{226B}"),
|
||||
"prec" : MTMathAtom(type: .relation, value: "\u{227A}"),
|
||||
"succ" : MTMathAtom(type: .relation, value: "\u{227B}"),
|
||||
"subset" : MTMathAtom(type: .relation, value: "\u{2282}"),
|
||||
"supset" : MTMathAtom(type: .relation, value: "\u{2283}"),
|
||||
"subseteq" : MTMathAtom(type: .relation, value: "\u{2286}"),
|
||||
"supseteq" : MTMathAtom(type: .relation, value: "\u{2287}"),
|
||||
"sqsubset" : MTMathAtom(type: .relation, value: "\u{228F}"),
|
||||
"sqsupset" : MTMathAtom(type: .relation, value: "\u{2290}"),
|
||||
"sqsubseteq" : MTMathAtom(type: .relation, value: "\u{2291}"),
|
||||
"sqsupseteq" : MTMathAtom(type: .relation, value: "\u{2292}"),
|
||||
"models" : MTMathAtom(type: .relation, value: "\u{22A7}"),
|
||||
"perp" : MTMathAtom(type: .relation, value: "\u{27C2}"),
|
||||
|
||||
// operators
|
||||
"times" : MTMathAtomFactory.times(),
|
||||
"div" : MTMathAtomFactory.divide(),
|
||||
"pm" : MTMathAtom(type: .binaryOperator, value: "\u{00B1}"),
|
||||
"dagger" : MTMathAtom(type: .binaryOperator, value: "\u{2020}"),
|
||||
"ddagger" : MTMathAtom(type: .binaryOperator, value: "\u{2021}"),
|
||||
"mp" : MTMathAtom(type: .binaryOperator, value: "\u{2213}"),
|
||||
"setminus" : MTMathAtom(type: .binaryOperator, value: "\u{2216}"),
|
||||
"ast" : MTMathAtom(type: .binaryOperator, value: "\u{2217}"),
|
||||
"circ" : MTMathAtom(type: .binaryOperator, value: "\u{2218}"),
|
||||
"bullet" : MTMathAtom(type: .binaryOperator, value: "\u{2219}"),
|
||||
"wedge" : MTMathAtom(type: .binaryOperator, value: "\u{2227}"),
|
||||
"vee" : MTMathAtom(type: .binaryOperator, value: "\u{2228}"),
|
||||
"cap" : MTMathAtom(type: .binaryOperator, value: "\u{2229}"),
|
||||
"cup" : MTMathAtom(type: .binaryOperator, value: "\u{222A}"),
|
||||
"wr" : MTMathAtom(type: .binaryOperator, value: "\u{2240}"),
|
||||
"uplus" : MTMathAtom(type: .binaryOperator, value: "\u{228E}"),
|
||||
"sqcap" : MTMathAtom(type: .binaryOperator, value: "\u{2293}"),
|
||||
"sqcup" : MTMathAtom(type: .binaryOperator, value: "\u{2294}"),
|
||||
"oplus" : MTMathAtom(type: .binaryOperator, value: "\u{2295}"),
|
||||
"ominus" : MTMathAtom(type: .binaryOperator, value: "\u{2296}"),
|
||||
"otimes" : MTMathAtom(type: .binaryOperator, value: "\u{2297}"),
|
||||
"oslash" : MTMathAtom(type: .binaryOperator, value: "\u{2298}"),
|
||||
"odot" : MTMathAtom(type: .binaryOperator, value: "\u{2299}"),
|
||||
"star" : MTMathAtom(type: .binaryOperator, value: "\u{22C6}"),
|
||||
"cdot" : MTMathAtom(type: .binaryOperator, value: "\u{22C5}"),
|
||||
"amalg" : MTMathAtom(type: .binaryOperator, value: "\u{2A3F}"),
|
||||
|
||||
// No limit operators
|
||||
"log" : MTMathAtomFactory.getOperator(withName: "log", limits: false),
|
||||
"lg" : MTMathAtomFactory.getOperator(withName: "lg", limits: false),
|
||||
"ln" : MTMathAtomFactory.getOperator(withName: "ln", limits: false),
|
||||
"sin" : MTMathAtomFactory.getOperator(withName: "sin", limits: false),
|
||||
"arcsin" : MTMathAtomFactory.getOperator(withName: "arcsin", limits: false),
|
||||
"sinh" : MTMathAtomFactory.getOperator(withName: "sinh", limits: false),
|
||||
"cos" : MTMathAtomFactory.getOperator(withName: "cos", limits: false),
|
||||
"arccos" : MTMathAtomFactory.getOperator(withName: "arccos", limits: false),
|
||||
"cosh" : MTMathAtomFactory.getOperator(withName: "cosh", limits: false),
|
||||
"tan" : MTMathAtomFactory.getOperator(withName: "tan", limits: false),
|
||||
"arctan" : MTMathAtomFactory.getOperator(withName: "arctan", limits: false),
|
||||
"tanh" : MTMathAtomFactory.getOperator(withName: "tanh", limits: false),
|
||||
"cot" : MTMathAtomFactory.getOperator(withName: "cot", limits: false),
|
||||
"coth" : MTMathAtomFactory.getOperator(withName: "coth", limits: false),
|
||||
"sec" : MTMathAtomFactory.getOperator(withName: "sec", limits: false),
|
||||
"csc" : MTMathAtomFactory.getOperator(withName: "csc", limits: false),
|
||||
"arg" : MTMathAtomFactory.getOperator(withName: "arg", limits: false),
|
||||
"ker" : MTMathAtomFactory.getOperator(withName: "ker", limits: false),
|
||||
"dim" : MTMathAtomFactory.getOperator(withName: "dim", limits: false),
|
||||
"hom" : MTMathAtomFactory.getOperator(withName: "hom", limits: false),
|
||||
"exp" : MTMathAtomFactory.getOperator(withName: "exp", limits: false),
|
||||
"deg" : MTMathAtomFactory.getOperator(withName: "deg", limits: false),
|
||||
|
||||
// Limit operators
|
||||
"lim" : MTMathAtomFactory.getOperator(withName: "lim", limits: true),
|
||||
"limsup" : MTMathAtomFactory.getOperator(withName: "lim sup", limits: true),
|
||||
"liminf" : MTMathAtomFactory.getOperator(withName: "lim inf", limits: true),
|
||||
"max" : MTMathAtomFactory.getOperator(withName: "max", limits: true),
|
||||
"min" : MTMathAtomFactory.getOperator(withName: "min", limits: true),
|
||||
"sup" : MTMathAtomFactory.getOperator(withName: "sup", limits: true),
|
||||
"inf" : MTMathAtomFactory.getOperator(withName: "inf", limits: true),
|
||||
"det" : MTMathAtomFactory.getOperator(withName: "det", limits: true),
|
||||
"Pr" : MTMathAtomFactory.getOperator(withName: "Pr", limits: true),
|
||||
"gcd" : MTMathAtomFactory.getOperator(withName: "gcd", limits: true),
|
||||
|
||||
// Large operators
|
||||
"prod" : MTMathAtomFactory.getOperator(withName: "\u{220F}", limits: true),
|
||||
"coprod" : MTMathAtomFactory.getOperator(withName: "\u{2210}", limits: true),
|
||||
"sum" : MTMathAtomFactory.getOperator(withName: "\u{2211}", limits: true),
|
||||
"int" : MTMathAtomFactory.getOperator(withName: "\u{222B}", limits: false),
|
||||
"oint" : MTMathAtomFactory.getOperator(withName: "\u{222E}", limits: false),
|
||||
"bigwedge" : MTMathAtomFactory.getOperator(withName: "\u{22C0}", limits: true),
|
||||
"bigvee" : MTMathAtomFactory.getOperator(withName: "\u{22C1}", limits: true),
|
||||
"bigcap" : MTMathAtomFactory.getOperator(withName: "\u{22C2}", limits: true),
|
||||
"bigcup" : MTMathAtomFactory.getOperator(withName: "\u{22C3}", limits: true),
|
||||
"bigodot" : MTMathAtomFactory.getOperator(withName: "\u{2A00}", limits: true),
|
||||
"bigoplus" : MTMathAtomFactory.getOperator(withName: "\u{2A01}", limits: true),
|
||||
"bigotimes" : MTMathAtomFactory.getOperator(withName: "\u{2A02}", limits: true),
|
||||
"biguplus" : MTMathAtomFactory.getOperator(withName: "\u{2A04}", limits: true),
|
||||
"bigsqcup" : MTMathAtomFactory.getOperator(withName: "\u{2A06}", limits: true),
|
||||
|
||||
// Latex command characters
|
||||
"{" : MTMathAtom(type: .open, value: "{"),
|
||||
"}" : MTMathAtom(type: .close, value: "}"),
|
||||
"$" : MTMathAtom(type: .ordinary, value: "{"),
|
||||
"&" : MTMathAtom(type: .ordinary, value: "&"),
|
||||
"#" : MTMathAtom(type: .ordinary, value: "#"),
|
||||
"%" : MTMathAtom(type: .ordinary, value: "%"),
|
||||
"_" : MTMathAtom(type: .ordinary, value: "_"),
|
||||
" " : MTMathAtom(type: .ordinary, value: " "),
|
||||
"backslash" : MTMathAtom(type: .ordinary, value: "\\"),
|
||||
|
||||
// Punctuation
|
||||
// Note: \colon is different from : which is a relation
|
||||
"colon" : MTMathAtom(type: .punctuation, value: ":"),
|
||||
"cdotp" : MTMathAtom(type: .punctuation, value: "\u{00B7}"),
|
||||
|
||||
// Other symbols
|
||||
"degree" : MTMathAtom(type: .ordinary, value: "\u{00B0}"),
|
||||
"neg" : MTMathAtom(type: .ordinary, value: "\u{00AC}"),
|
||||
"angstrom" : MTMathAtom(type: .ordinary, value: "\u{00C5}"),
|
||||
"|" : MTMathAtom(type: .ordinary, value: "\u{2016}"),
|
||||
"vert" : MTMathAtom(type: .ordinary, value: "|"),
|
||||
"ldots" : MTMathAtom(type: .ordinary, value: "\u{2026}"),
|
||||
"prime" : MTMathAtom(type: .ordinary, value: "\u{2032}"),
|
||||
"hbar" : MTMathAtom(type: .ordinary, value: "\u{210F}"),
|
||||
"Im" : MTMathAtom(type: .ordinary, value: "\u{2111}"),
|
||||
"ell" : MTMathAtom(type: .ordinary, value: "\u{2113}"),
|
||||
"wp" : MTMathAtom(type: .ordinary, value: "\u{2118}"),
|
||||
"Re" : MTMathAtom(type: .ordinary, value: "\u{211C}"),
|
||||
"mho" : MTMathAtom(type: .ordinary, value: "\u{2127}"),
|
||||
"aleph" : MTMathAtom(type: .ordinary, value: "\u{2135}"),
|
||||
"forall" : MTMathAtom(type: .ordinary, value: "\u{2200}"),
|
||||
"exists" : MTMathAtom(type: .ordinary, value: "\u{2203}"),
|
||||
"emptyset" : MTMathAtom(type: .ordinary, value: "\u{2205}"),
|
||||
"nabla" : MTMathAtom(type: .ordinary, value: "\u{2207}"),
|
||||
"infty" : MTMathAtom(type: .ordinary, value: "\u{221E}"),
|
||||
"angle" : MTMathAtom(type: .ordinary, value: "\u{2220}"),
|
||||
"top" : MTMathAtom(type: .ordinary, value: "\u{22A4}"),
|
||||
"bot" : MTMathAtom(type: .ordinary, value: "\u{22A5}"),
|
||||
"vdots" : MTMathAtom(type: .ordinary, value: "\u{22EE}"),
|
||||
"cdots" : MTMathAtom(type: .ordinary, value: "\u{22EF}"),
|
||||
"ddots" : MTMathAtom(type: .ordinary, value: "\u{22F1}"),
|
||||
"triangle" : MTMathAtom(type: .ordinary, value: "\u{25B3}"),
|
||||
"imath" : MTMathAtom(type: .ordinary, value: "\u{0001D6A4}"),
|
||||
"jmath" : MTMathAtom(type: .ordinary, value: "\u{0001D6A5}"),
|
||||
"partial" : MTMathAtom(type: .ordinary, value: "\u{0001D715}"),
|
||||
|
||||
// Spacing
|
||||
"," : MTMathSpace(space: 3),
|
||||
">" : MTMathSpace(space: 4),
|
||||
";" : MTMathSpace(space: 5),
|
||||
"!" : MTMathSpace(space: -3),
|
||||
"quad" : MTMathSpace(space: 18), // quad = 1em = 18mu
|
||||
"qquad" : MTMathSpace(space: 36), // qquad = 2em
|
||||
|
||||
// Style
|
||||
"displaystyle" : MTMathStyle(style: .display),
|
||||
"textstyle" : MTMathStyle(style: .text),
|
||||
"scriptstyle" : MTMathStyle(style: .script),
|
||||
"scriptscriptstyle" : MTMathStyle(style: .scriptOfScript),
|
||||
]
|
||||
|
||||
var latexSymbolNames = [String]()
|
||||
|
||||
var _textToLatexSymbolName: [String: String]? = nil
|
||||
public var textToLatexSymbolName: [String: String] {
|
||||
get {
|
||||
if self._textToLatexSymbolName == nil {
|
||||
var output = [String: String]()
|
||||
for (key, atom) in self.supportedLatexSymbols {
|
||||
if atom.nucleus.count == 0 {
|
||||
continue
|
||||
}
|
||||
if let existingText = output[atom.nucleus] {
|
||||
// If there are 2 key for the same symbol, choose one deterministically.
|
||||
if key.count > existingText.count {
|
||||
// Keep the shorter command
|
||||
continue
|
||||
} else if key.count == existingText.count {
|
||||
// If the length is the same, keep the alphabetically first
|
||||
if key.compare(existingText) == .orderedDescending {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
output[atom.nucleus] = key
|
||||
}
|
||||
self._textToLatexSymbolName = output
|
||||
}
|
||||
return self._textToLatexSymbolName!
|
||||
}
|
||||
set {
|
||||
self._textToLatexSymbolName = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public static let sharedInstance = MTMathAtomFactory()
|
||||
|
||||
static let fontStyles : [String: MTFontStyle] = [
|
||||
"mathnormal" : (.defaultStyle),
|
||||
"mathrm": (.roman),
|
||||
"textrm": (.roman),
|
||||
"rm": (.roman),
|
||||
"mathbf": (.bold),
|
||||
"bf": (.bold),
|
||||
"textbf": (.bold),
|
||||
"mathcal": (.caligraphic),
|
||||
"cal": (.caligraphic),
|
||||
"mathtt": (.typewriter),
|
||||
"texttt": (.typewriter),
|
||||
"mathit": (.italic),
|
||||
"textit": (.italic),
|
||||
"mit": (.italic),
|
||||
"mathsf": (.sansSerif),
|
||||
"textsf": (.sansSerif),
|
||||
"mathfrak": (.fraktur),
|
||||
"frak": (.fraktur),
|
||||
"mathbb": (.blackboard),
|
||||
"mathbfit": (.boldItalic),
|
||||
"bm": (.boldItalic),
|
||||
"text": (.roman),
|
||||
]
|
||||
|
||||
public static func fontStyleWithName(_ fontName:String) -> MTFontStyle? {
|
||||
if let style = fontStyles[fontName] {
|
||||
return style
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return an atom for times sign \times or *
|
||||
public static func times() -> MTMathAtom {
|
||||
return MTMathAtom(type: .binaryOperator, value: UnicodeSymbol.multiplication)
|
||||
}
|
||||
|
||||
// Return an atom for division sign \div or /
|
||||
public static func divide() -> MTMathAtom {
|
||||
return MTMathAtom(type: .binaryOperator, value: UnicodeSymbol.division)
|
||||
}
|
||||
|
||||
// Return an atom aka placeholder square
|
||||
public static func placeholder() -> MTMathAtom {
|
||||
return MTMathAtom(type: .placeholder, value: UnicodeSymbol.whiteSquare)
|
||||
}
|
||||
|
||||
public static func placeholderFraction() -> MTFraction {
|
||||
let frac = MTFraction()
|
||||
|
||||
frac.numerator = MTMathList()
|
||||
frac.numerator?.add(placeholder())
|
||||
frac.denominator = MTMathList()
|
||||
frac.denominator?.add(placeholder())
|
||||
|
||||
return frac
|
||||
}
|
||||
|
||||
public static func placeholderSquareRoot() -> MTRadical {
|
||||
let rad = MTRadical()
|
||||
|
||||
rad.radicand = MTMathList()
|
||||
rad.radicand?.add(placeholder())
|
||||
|
||||
return rad
|
||||
}
|
||||
|
||||
public static func placeholderRadical() -> MTRadical {
|
||||
let rad = MTRadical()
|
||||
|
||||
rad.radicand = MTMathList()
|
||||
rad.degree = MTMathList()
|
||||
|
||||
rad.radicand?.add(placeholder())
|
||||
rad.degree?.add(placeholder())
|
||||
|
||||
return rad
|
||||
}
|
||||
|
||||
/** Gets the atom with the right type for the given character. If an atom
|
||||
cannot be determined for a given character this returns nil.
|
||||
This function follows latex conventions for assigning types to the atoms.
|
||||
The following characters are not supported and will return nil:
|
||||
- Any non-ascii character.
|
||||
- Any control character or spaces (< 0x21)
|
||||
- Latex control chars: $ % # & ~ '
|
||||
- Chars with special meaning in latex: ^ _ { } \
|
||||
All other characters will have a non-nil atom returned.
|
||||
*/
|
||||
public static func atom(for char: String) -> MTMathAtom? {
|
||||
let atomCharacterSet = CharacterSet(charactersIn: UnicodeScalar(0x21)!...UnicodeScalar(0x7E)!)
|
||||
if char.rangeOfCharacter(from: atomCharacterSet) != nil {
|
||||
switch char {
|
||||
case "$", "%", "#", "&", "~", "\'", "^", "_", "{", "}", "\\":
|
||||
return nil
|
||||
case "(", "[":
|
||||
return MTMathAtom(type: .open, value: char)
|
||||
case ")", "]", "!", "?":
|
||||
return MTMathAtom(type: .close, value: char)
|
||||
case ",", ";":
|
||||
return MTMathAtom(type: .punctuation, value: char)
|
||||
case "=", ">", "<":
|
||||
return MTMathAtom(type: .relation, value: char)
|
||||
case ":":
|
||||
// Math colon is ratio. Regular colon is \colon
|
||||
return MTMathAtom(type: .relation, value: "\u{2236}")
|
||||
case "-":
|
||||
return MTMathAtom(type: .binaryOperator, value: "\u{2212}")
|
||||
case "+", "*":
|
||||
return MTMathAtom(type: .binaryOperator, value: char)
|
||||
case ".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
|
||||
return MTMathAtom(type: .number, value: char)
|
||||
case _ where
|
||||
(char.rangeOfCharacter(from: CharacterSet.lowercaseLetters) != nil) ||
|
||||
(char.rangeOfCharacter(from: CharacterSet.lowercaseLetters) != nil) :
|
||||
return MTMathAtom(type: .variable, value: char)
|
||||
case "\"", "/", "@", "`", "|":
|
||||
return MTMathAtom(type: .ordinary, value: char)
|
||||
default:
|
||||
assert(false, "Unknown Character: \(char)")
|
||||
print("Unknown characters: \(char)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/** Returns a `MTMathList` with one atom per character in the given string. This function
|
||||
does not do any LaTeX conversion or interpretation. It simply uses `atomForCharacter` to
|
||||
convert the characters to atoms. Any character that cannot be converted is ignored. */
|
||||
public static func atomList(for string: String) -> MTMathList {
|
||||
let list = MTMathList()
|
||||
|
||||
for character in string {
|
||||
if let newAtom = atom(for: "\(character)") {
|
||||
list.add(newAtom)
|
||||
}
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
/** Returns an atom with the right type for a given latex symbol (e.g. theta)
|
||||
If the latex symbol is unknown this will return nil. This supports LaTeX aliases as well.
|
||||
*/
|
||||
public static func atom(forLatexSymbol name: String) -> MTMathAtom? {
|
||||
var _name = name
|
||||
|
||||
if let canonicalName = aliases[name] {
|
||||
_name = canonicalName
|
||||
}
|
||||
|
||||
if let atom = sharedInstance.supportedLatexSymbols[_name] {
|
||||
return atom
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/** Finds the name of the LaTeX symbol name for the given atom. This function is a reverse
|
||||
of the above function. If no latex symbol name corresponds to the atom, then this returns `nil`
|
||||
If nucleus of the atom is empty, then this will return `nil`.
|
||||
@note: This is not an exact reverse of the above in the case of aliases. If an LaTeX alias
|
||||
points to a given symbol, then this function will return the original symbol name and not the
|
||||
alias.
|
||||
@note: This function does not convert MathSpaces to latex command names either.
|
||||
*/
|
||||
public static func latexSymbolName(for atom: MTMathAtom) -> String? {
|
||||
if atom.nucleus.count == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Self.sharedInstance.textToLatexSymbolName[atom.nucleus]
|
||||
}
|
||||
|
||||
/** Define a latex symbol for rendering. This function allows defining custom symbols that are
|
||||
not already present in the default set, or override existing symbols with new meaning.
|
||||
e.g. to define a symbol for "lcm" one can call:
|
||||
`[MTMathAtomFactory addLatexSymbol:@"lcm" value:[MTMathAtomFactory operatorWithName:@"lcm" limits: false)]` */
|
||||
|
||||
public static func define(latexSymbol name: String, value: MTMathAtom) {
|
||||
Self.sharedInstance.supportedLatexSymbols[name] = value
|
||||
Self.sharedInstance.textToLatexSymbolName[value.nucleus] = name
|
||||
}
|
||||
|
||||
/** Returns a large opertor for the given name. If limits is true, limits are set up on
|
||||
the operator and displyed differently. */
|
||||
public static func getOperator(withName name: String, limits: Bool) -> MTLargeOperator {
|
||||
return MTLargeOperator(value: name, limits: limits)
|
||||
}
|
||||
|
||||
/** Returns an accent with the given name. The name of the accent is the LaTeX name
|
||||
such as `grave`, `hat` etc. If the name is not a recognized accent name, this
|
||||
returns nil. The `innerList` of the returned `MTAccent` is nil.
|
||||
*/
|
||||
public static func getAccent(withName name: String) -> MTAccent? {
|
||||
if let accentValue = Self.accents[name] {
|
||||
return MTAccent(value: accentValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/** Returns the accent name for the given accent. This is the reverse of the above
|
||||
function. */
|
||||
public static func getName(of accent: MTAccent) -> String? {
|
||||
return Self.sharedInstance.accentValueToName[accent.nucleus]
|
||||
}
|
||||
|
||||
/** Creates a new boundary atom for the given delimiter name. If the delimiter name
|
||||
is not recognized it returns nil. A delimiter name can be a single character such
|
||||
as '(' or a latex command such as 'uparrow'.
|
||||
@note In order to distinguish between the delimiter '|' and the delimiter '\|' the delimiter '\|'
|
||||
the has been renamed to '||'.
|
||||
*/
|
||||
public static func boundary(forDelimiter name: String) -> MTMathAtom? {
|
||||
if let delimValue = Self.delimiters[name] {
|
||||
return MTMathAtom(type: .boundary, value: delimValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/** Returns the delimiter name for a boundary atom. This is a reverse of the above function.
|
||||
If the atom is not a boundary atom or if the delimiter value is unknown this returns `nil`.
|
||||
@note This is not an exact reverse of the above function. Some delimiters have two names (e.g.
|
||||
`<` and `langle`) and this function always returns the shorter name.
|
||||
*/
|
||||
public static func getDelimiterName(of boundary: MTMathAtom) -> String? {
|
||||
if boundary.type != .boundary {
|
||||
return nil
|
||||
}
|
||||
return Self.sharedInstance.delimValueToName[boundary.nucleus]
|
||||
}
|
||||
|
||||
/** Returns a fraction with the given numerator and denominator. */
|
||||
public static func fraction(withNumerator num: MTMathList, denominator denom: MTMathList) -> MTFraction {
|
||||
let frac = MTFraction()
|
||||
|
||||
frac.numerator = num
|
||||
frac.denominator = denom
|
||||
|
||||
return frac
|
||||
}
|
||||
|
||||
/** Simplification of above function when numerator and denominator are simple strings.
|
||||
This function uses `mathListForCharacters` to convert the strings to `MTMathList`s. */
|
||||
public static func fraction(withNumeratorString numStr: String, denominatorString denomStr: String) -> MTFraction {
|
||||
let num = Self.atomList(for: numStr)
|
||||
let denom = Self.atomList(for: denomStr)
|
||||
|
||||
return Self.fraction(withNumerator: num, denominator: denom)
|
||||
}
|
||||
|
||||
/** Builds a table for a given environment with the given rows. Returns a `MTMathAtom` containing the
|
||||
table and any other atoms necessary for the given environment. Returns nil and sets error
|
||||
if the table could not be built.
|
||||
@param env The environment to use to build the table. If the env is nil, then the default table is built.
|
||||
@note The reason this function returns a `MTMathAtom` and not a `MTMathTable` is because some
|
||||
matrix environments are have builtin delimiters added to the table and hence are returned as inner atoms.
|
||||
*/
|
||||
public static func table(withEnvironment env: String?, rows: [[MTMathList]]) -> MTMathAtom? {
|
||||
let table = MTMathTable(environment: env)
|
||||
|
||||
for i in 0..<rows.count {
|
||||
let row = rows[i]
|
||||
for j in 0..<row.count {
|
||||
table.set(cell: row[j], forRow: i, column: j)
|
||||
}
|
||||
}
|
||||
|
||||
let matrixEnvs = [
|
||||
"matrix": [],
|
||||
"pmatrix": ["(", ")"],
|
||||
"bmatrix": ["[", "]"],
|
||||
"Bmatrix": ["{", "}"],
|
||||
"vmatrix": ["vert", "vert"],
|
||||
"Vmatrix": ["Vert", "Vert"]
|
||||
]
|
||||
|
||||
if env == nil {
|
||||
table.interColumnSpacing = 0
|
||||
table.interRowAdditionalSpacing = 1
|
||||
for i in 0..<table.numberOfCols() {
|
||||
table.set(alignment: .left, forCol: i)
|
||||
}
|
||||
return table
|
||||
} else {
|
||||
if let delims = matrixEnvs[env!] {
|
||||
table.environment = "matrix"
|
||||
table.interRowAdditionalSpacing = 0
|
||||
table.interColumnSpacing = 18
|
||||
|
||||
let style = MTMathStyle(style: .text)
|
||||
|
||||
for i in 0..<table.cells.count {
|
||||
for j in 0..<table.cells[i].count {
|
||||
table.cells[i][j].insert(style, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
if delims.count == 2 {
|
||||
let inner = MTInner()
|
||||
inner.leftBoundary = Self.boundary(forDelimiter: delims[0])
|
||||
inner.rightBoundary = Self.boundary(forDelimiter: delims[1])
|
||||
inner.innerList = MTMathList(atoms: [table])
|
||||
return inner
|
||||
} else {
|
||||
return table
|
||||
}
|
||||
} else if env == "eqalign" || env == "split" || env == "aligned" {
|
||||
if table.numberOfCols() != 2 {
|
||||
print("\(env!) environment can only have 2 columns")
|
||||
return nil
|
||||
}
|
||||
|
||||
let spacer = MTMathAtom(type: .ordinary, value: "")
|
||||
|
||||
for i in 0..<table.cells.count {
|
||||
if table.cells[i].count >= 1 {
|
||||
table.cells[i][1].insert(spacer, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
table.interRowAdditionalSpacing = 1
|
||||
table.interColumnSpacing = 0
|
||||
|
||||
table.set(alignment: .right, forCol: 0)
|
||||
table.set(alignment: .left, forCol: 1)
|
||||
|
||||
return table
|
||||
} else if env == "displaylines" || env == "gather" {
|
||||
if table.numberOfCols() != 1 {
|
||||
print("\(env!) environment can only have 1 columns")
|
||||
return nil
|
||||
}
|
||||
|
||||
table.interRowAdditionalSpacing = 1
|
||||
table.interColumnSpacing = 0
|
||||
|
||||
table.set(alignment: .center, forCol: 0)
|
||||
|
||||
return table
|
||||
} else if env == "eqnarray" {
|
||||
if table.numberOfCols() != 3 {
|
||||
print("\(env!) environment can only have 3 columns")
|
||||
return nil
|
||||
}
|
||||
|
||||
table.interRowAdditionalSpacing = 1
|
||||
table.interColumnSpacing = 18
|
||||
|
||||
table.set(alignment: .right, forCol: 0)
|
||||
table.set(alignment: .center, forCol: 1)
|
||||
table.set(alignment: .left, forCol: 2)
|
||||
|
||||
return table
|
||||
} else if env == "cases" {
|
||||
if table.numberOfCols() != 2 {
|
||||
print("\(env!) environment can only have 2 columns")
|
||||
return nil
|
||||
}
|
||||
|
||||
table.interRowAdditionalSpacing = 0
|
||||
table.interColumnSpacing = 18
|
||||
|
||||
table.set(alignment: .left, forCol: 0)
|
||||
table.set(alignment: .left, forCol: 1)
|
||||
|
||||
let style = MTMathStyle(style: .text)
|
||||
|
||||
|
||||
for i in 0..<table.cells.count {
|
||||
for j in 0..<table.cells[i].count {
|
||||
table.cells[i][j].insert(style, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
let inner = MTInner()
|
||||
inner.leftBoundary = Self.boundary(forDelimiter: "{")
|
||||
inner.rightBoundary = Self.boundary(forDelimiter: ".")
|
||||
let space = Self.atom(forLatexSymbol: ",")!
|
||||
|
||||
inner.innerList = MTMathList(atoms: [space, table])
|
||||
|
||||
return inner
|
||||
} else {
|
||||
print("Unknown table environment")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
702
Sources/SwiftMathRender/MathRender/MTMathList.swift
Normal file
702
Sources/SwiftMathRender/MathRender/MTMathList.swift
Normal file
@@ -0,0 +1,702 @@
|
||||
//
|
||||
// MTMathList.swift
|
||||
// MathRenderSwift
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// type defines spacing and how it is rendered
|
||||
public enum MTMathAtomType: String, CustomStringConvertible {
|
||||
case ordinary // number or text
|
||||
case number // number
|
||||
case variable // text in italic
|
||||
case largeOperator // sin/cos, integral
|
||||
case binaryOperator // \bin
|
||||
case unaryOperator //
|
||||
case relation // = < >
|
||||
case open // open bracket
|
||||
case close // close bracket
|
||||
case fraction // \frac
|
||||
case radical // \sqrt
|
||||
case punctuation // ,
|
||||
case placeholder // inner atom
|
||||
case inner // embedded list
|
||||
case underline // underlined atom
|
||||
case overline // overlined atom
|
||||
case accent // accented atom
|
||||
|
||||
// these atoms do not support subscripts/superscripts:
|
||||
case boundary
|
||||
case space
|
||||
|
||||
// Denotes style changes during randering
|
||||
case style
|
||||
case color
|
||||
case colorBox
|
||||
|
||||
case table
|
||||
|
||||
func isNotBinaryOperator() -> Bool {
|
||||
switch self {
|
||||
case .binaryOperator, .relation, .open, .punctuation, .largeOperator: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
func isScriptAllowed() -> Bool {
|
||||
return self != .boundary && self != .space && self != .style && self != .table
|
||||
}
|
||||
|
||||
// we want string representations to be capitalized
|
||||
public var description: String {
|
||||
self.rawValue.capitalized
|
||||
}
|
||||
}
|
||||
|
||||
public enum MTFontStyle:Int {
|
||||
/// The default latex rendering style. i.e. variables are italic and numbers are roman.
|
||||
case defaultStyle = 0,
|
||||
/// Roman font style i.e. \mathrm
|
||||
roman,
|
||||
/// Bold font style i.e. \mathbf
|
||||
bold,
|
||||
/// Caligraphic font style i.e. \mathcal
|
||||
caligraphic,
|
||||
/// Typewriter (monospace) style i.e. \mathtt
|
||||
typewriter,
|
||||
/// Italic style i.e. \mathit
|
||||
italic,
|
||||
/// San-serif font i.e. \mathss
|
||||
sansSerif,
|
||||
/// Fractur font i.e \mathfrak
|
||||
fraktur,
|
||||
/// Blackboard font i.e. \mathbb
|
||||
blackboard,
|
||||
/// Bold italic
|
||||
boldItalic
|
||||
}
|
||||
|
||||
public class MTMathAtom: Any, CustomStringConvertible {
|
||||
public var type: MTMathAtomType
|
||||
public var subScript: MTMathList?
|
||||
public var superScript: MTMathList?
|
||||
var fontStyle: MTFontStyle = .defaultStyle
|
||||
var fusedAtoms: MTMathList?
|
||||
|
||||
public static func atom(withType type:MTMathAtomType, value:String) -> MTMathAtom {
|
||||
switch type {
|
||||
case .largeOperator:
|
||||
return MTLargeOperator(value: value, limits: true)
|
||||
case .fraction:
|
||||
return MTFraction()
|
||||
case .radical:
|
||||
return MTRadical()
|
||||
case .placeholder:
|
||||
return MTMathAtom(type: type, value: UnicodeSymbol.whiteSquare)
|
||||
case .inner:
|
||||
return MTInner()
|
||||
case .underline:
|
||||
return MTUnderLine()
|
||||
case .overline:
|
||||
return MTOverLine()
|
||||
case .accent:
|
||||
return MTAccent(value: value)
|
||||
case .space:
|
||||
return MTMathSpace(space: 0)
|
||||
case .color:
|
||||
return MTMathColor()
|
||||
case .colorBox:
|
||||
return MTMathColorbox()
|
||||
default:
|
||||
return MTMathAtom(type: type, value: value)
|
||||
}
|
||||
}
|
||||
|
||||
public func setSuperScript(_ list: MTMathList?) {
|
||||
if self.isScriptAllowed() {
|
||||
self.superScript = list
|
||||
} else {
|
||||
print("superscripts not allowed for atom \(self.type.rawValue)")
|
||||
self.superScript = nil
|
||||
}
|
||||
}
|
||||
|
||||
public func setSubScript(_ list: MTMathList?) {
|
||||
if self.isScriptAllowed() {
|
||||
self.subScript = list
|
||||
} else {
|
||||
print("subscripts not allowed for atom \(self.type.rawValue)")
|
||||
self.subScript = nil
|
||||
}
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
var string = ""
|
||||
|
||||
string += self.nucleus
|
||||
if self.superScript != nil {
|
||||
string += "^{\(self.superScript!.description)}"
|
||||
}
|
||||
if self.subScript != nil {
|
||||
string += "_{\(self.subScript!.description)}"
|
||||
}
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
public var nucleus: String = ""
|
||||
public var finalized: MTMathAtom {
|
||||
let finalized = self
|
||||
if finalized.superScript != nil {
|
||||
finalized.superScript = finalized.superScript!.finalized
|
||||
}
|
||||
if finalized.subScript != nil {
|
||||
finalized.subScript = finalized.subScript!.finalized
|
||||
}
|
||||
return finalized
|
||||
}
|
||||
|
||||
// atoms that fused to create this one
|
||||
public var childAtoms = [MTMathAtom]()
|
||||
|
||||
// indexRange in list that this atom tracks:
|
||||
public var indexRange = NSRange(location: 0, length: 0)
|
||||
|
||||
public var string:String {
|
||||
var str = self.nucleus
|
||||
if let superScript = self.superScript {
|
||||
str.append("^{\(superScript.string)}")
|
||||
}
|
||||
if let subScript = self.subScript {
|
||||
str.append("_{\(subScript.string)}")
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func fuse(with atom: MTMathAtom) {
|
||||
assert(self.subScript == nil, "Cannot fuse into an atom which has a subscript: \(self)");
|
||||
assert(self.superScript == nil, "Cannot fuse into an atom which has a superscript: \(self)");
|
||||
assert(atom.type == self.type, "Only atoms of the same type can be fused. \(self), \(atom)");
|
||||
guard self.subScript == nil,
|
||||
self.superScript == nil,
|
||||
self.type == atom.type
|
||||
else {
|
||||
print("Can't fuse these 2 atom")
|
||||
return
|
||||
}
|
||||
|
||||
self.childAtoms.append(self)
|
||||
if atom.childAtoms.count > 0 {
|
||||
self.childAtoms += atom.childAtoms
|
||||
} else {
|
||||
self.childAtoms.append(atom)
|
||||
}
|
||||
|
||||
// Update nucleus:
|
||||
self.nucleus += atom.nucleus
|
||||
|
||||
// Update range:
|
||||
self.indexRange.length += atom.indexRange.length
|
||||
|
||||
// Update super/subscript:
|
||||
self.superScript = atom.superScript
|
||||
self.subScript = atom.subScript
|
||||
}
|
||||
|
||||
func isScriptAllowed() -> Bool { self.type.isScriptAllowed() }
|
||||
|
||||
public init(type: MTMathAtomType, value: String) {
|
||||
self.type = type
|
||||
self.nucleus = value
|
||||
}
|
||||
|
||||
func isNotBinaryOperator() -> Bool { self.type.isNotBinaryOperator() }
|
||||
|
||||
}
|
||||
|
||||
func isNotBinaryOperator(_ prevNode:MTMathAtom?) -> Bool {
|
||||
if prevNode == nil { return true }
|
||||
return prevNode!.type.isNotBinaryOperator()
|
||||
}
|
||||
|
||||
public class MTFraction: MTMathAtom {
|
||||
public var hasRule: Bool = true
|
||||
public var leftDelimiter: String?
|
||||
public var rightDelimiter: String?
|
||||
public var numerator: MTMathList? = MTMathList()
|
||||
public var denominator: MTMathList? = MTMathList()
|
||||
|
||||
override public var description: String {
|
||||
var string = ""
|
||||
if self.hasRule {
|
||||
string += "\\atop"
|
||||
} else {
|
||||
string += "\\frac"
|
||||
}
|
||||
if self.leftDelimiter != nil {
|
||||
string += "[\(self.leftDelimiter!)]"
|
||||
}
|
||||
if self.rightDelimiter != nil {
|
||||
string += "[\(self.rightDelimiter!)]"
|
||||
}
|
||||
|
||||
string += "{\(self.numerator?.description ?? "placeholder")}{\(self.denominator?.description ?? "placeholder")}"
|
||||
|
||||
if self.superScript != nil {
|
||||
string += "^{\(self.superScript!.description)}"
|
||||
}
|
||||
if self.subScript != nil {
|
||||
string += "_{\(self.subScript!.description)}"
|
||||
}
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
override public var finalized: MTMathAtom {
|
||||
let finalized: MTFraction = super.finalized as! MTFraction
|
||||
|
||||
finalized.numerator = finalized.numerator?.finalized
|
||||
finalized.denominator = finalized.denominator?.finalized
|
||||
|
||||
return finalized
|
||||
}
|
||||
|
||||
convenience init(hasRule: Bool = true) {
|
||||
self.init(type: .fraction, value: "")
|
||||
self.hasRule = hasRule
|
||||
}
|
||||
}
|
||||
|
||||
public class MTRadical: MTMathAtom {
|
||||
// Under the roof
|
||||
var radicand: MTMathList? = MTMathList()
|
||||
|
||||
// Value on radical sign
|
||||
var degree: MTMathList?
|
||||
|
||||
convenience init() {
|
||||
self.init(type: .radical, value: "")
|
||||
}
|
||||
|
||||
override public var description: String {
|
||||
var string = "\\sqrt"
|
||||
|
||||
if self.degree != nil {
|
||||
string += "[\(self.degree!.description)]"
|
||||
}
|
||||
|
||||
if self.radicand != nil {
|
||||
string += "{\(self.radicand?.description ?? "placeholder")}"
|
||||
}
|
||||
|
||||
if self.superScript != nil {
|
||||
string += "^{\(self.superScript!.description)}"
|
||||
}
|
||||
if self.subScript != nil {
|
||||
string += "_{\(self.subScript!.description)}"
|
||||
}
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
override public var finalized: MTMathAtom {
|
||||
let finalized: MTRadical = super.finalized as! MTRadical
|
||||
|
||||
finalized.radicand = finalized.radicand?.finalized
|
||||
finalized.degree = finalized.degree?.finalized
|
||||
|
||||
return finalized
|
||||
}
|
||||
}
|
||||
|
||||
public class MTLargeOperator: MTMathAtom {
|
||||
var limits: Bool = false
|
||||
|
||||
convenience init(value: String, limits: Bool = false) {
|
||||
self.init(type: .largeOperator, value: value)
|
||||
self.limits = limits
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MTInner
|
||||
|
||||
public class MTInner: MTMathAtom {
|
||||
var innerList: MTMathList?
|
||||
var leftBoundary: MTMathAtom? {
|
||||
didSet {
|
||||
if leftBoundary != nil && leftBoundary!.type != .boundary {
|
||||
assertionFailure("Left boundary must be of type .boundary")
|
||||
}
|
||||
}
|
||||
}
|
||||
var rightBoundary: MTMathAtom? {
|
||||
didSet {
|
||||
if rightBoundary != nil && rightBoundary!.type != .boundary {
|
||||
assertionFailure("Right boundary must be of type .boundary")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(type: .inner, value: "")
|
||||
}
|
||||
|
||||
public override convenience init(type: MTMathAtomType, value: String) {
|
||||
if type == .inner {
|
||||
self.init(); return
|
||||
}
|
||||
assertionFailure("MTInner(type:value:) cannot be called. Use MTInner() instead.")
|
||||
self.init()
|
||||
}
|
||||
|
||||
override public var description: String {
|
||||
var string = "\\inner"
|
||||
|
||||
if self.leftBoundary != nil {
|
||||
string += "[\(self.leftBoundary!.nucleus)]"
|
||||
}
|
||||
string += "{\(self.innerList!.description)}"
|
||||
|
||||
if self.rightBoundary != nil {
|
||||
string += "[\(self.rightBoundary!.nucleus)]"
|
||||
}
|
||||
|
||||
if self.superScript != nil {
|
||||
string += "^{\(self.superScript!.description)}"
|
||||
}
|
||||
if self.subScript != nil {
|
||||
string += "_{\(self.subScript!.description)}"
|
||||
}
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
override public var finalized: MTMathAtom {
|
||||
let finalized: MTInner = super.finalized as! MTInner
|
||||
|
||||
finalized.innerList = finalized.innerList?.finalized
|
||||
|
||||
return finalized
|
||||
}
|
||||
}
|
||||
|
||||
public class MTOverLine: MTMathAtom {
|
||||
var innerList: MTMathList?
|
||||
|
||||
override public var finalized: MTMathAtom {
|
||||
let finalized: MTOverLine = super.finalized as! MTOverLine
|
||||
|
||||
finalized.innerList = finalized.innerList?.finalized
|
||||
|
||||
return finalized
|
||||
}
|
||||
|
||||
convenience init() {
|
||||
self.init(type: .overline, value: "")
|
||||
}
|
||||
}
|
||||
|
||||
public class MTUnderLine: MTMathAtom {
|
||||
var innerList: MTMathList?
|
||||
|
||||
override public var finalized: MTMathAtom {
|
||||
let finalized: MTUnderLine = super.finalized as! MTUnderLine
|
||||
|
||||
finalized.innerList = finalized.innerList?.finalized
|
||||
|
||||
return finalized
|
||||
}
|
||||
|
||||
convenience init() {
|
||||
self.init(type: .underline, value: "")
|
||||
}
|
||||
}
|
||||
|
||||
public class MTAccent: MTMathAtom {
|
||||
var innerList: MTMathList?
|
||||
|
||||
override public var finalized: MTMathAtom {
|
||||
let finalized: MTAccent = super.finalized as! MTAccent
|
||||
|
||||
finalized.innerList = finalized.innerList?.finalized
|
||||
|
||||
return finalized
|
||||
}
|
||||
|
||||
convenience init(value: String) {
|
||||
self.init(type: .accent, value: value)
|
||||
}
|
||||
}
|
||||
|
||||
public class MTMathSpace: MTMathAtom {
|
||||
var space: CGFloat = 0
|
||||
|
||||
convenience init(space: CGFloat) {
|
||||
self.init(type: .space, value: "")
|
||||
self.space = space
|
||||
}
|
||||
}
|
||||
|
||||
public enum MTLineStyle {
|
||||
case display
|
||||
case text
|
||||
case script
|
||||
case scriptOfScript
|
||||
|
||||
public func inc() -> MTLineStyle {
|
||||
switch self {
|
||||
case .display: return .text
|
||||
case .text: return .script
|
||||
case .script: return .scriptOfScript
|
||||
case .scriptOfScript: return .display
|
||||
}
|
||||
}
|
||||
|
||||
public var isNotScript:Bool {
|
||||
self == .display || self == .text
|
||||
}
|
||||
}
|
||||
|
||||
public class MTMathStyle: MTMathAtom {
|
||||
var style: MTLineStyle = .display
|
||||
|
||||
convenience init(style: MTLineStyle = .display) {
|
||||
self.init(type: .space, value: "")
|
||||
self.style = style
|
||||
}
|
||||
}
|
||||
|
||||
public class MTMathColor: MTMathAtom {
|
||||
public var colorString:String=""
|
||||
public var innerList:MTMathList?
|
||||
|
||||
init() {
|
||||
super.init(type: .color, value: "")
|
||||
}
|
||||
|
||||
public override convenience init(type: MTMathAtomType, value: String) {
|
||||
if type == .color {
|
||||
self.init(); return
|
||||
}
|
||||
NSException(name: NSExceptionName("InvalidMethod"), reason: "MTMathColor(type:value) cannot be called. Use MTMathColor() instead.").raise()
|
||||
self.init()
|
||||
}
|
||||
|
||||
public override var string: String {
|
||||
"\\color{\(self.colorString)}{\(self.innerList!.string)}"
|
||||
}
|
||||
}
|
||||
|
||||
public class MTMathColorbox: MTMathAtom {
|
||||
public var colorString:String=""
|
||||
public var innerList:MTMathList?
|
||||
|
||||
init() {
|
||||
super.init(type: .color, value: "")
|
||||
}
|
||||
|
||||
public override convenience init(type: MTMathAtomType, value: String) {
|
||||
if type == .color {
|
||||
self.init(); return
|
||||
}
|
||||
NSException(name: NSExceptionName("InvalidMethod"), reason: "MTMathColorbox(type:value) cannot be called. Use MTMathColorbox() instead.").raise()
|
||||
self.init()
|
||||
}
|
||||
|
||||
public override var string: String {
|
||||
"\\colorbox{\(self.colorString)}{\(self.innerList!.string)}"
|
||||
}
|
||||
}
|
||||
|
||||
public enum MTColumnAlignment {
|
||||
case left
|
||||
case center
|
||||
case right
|
||||
}
|
||||
|
||||
public class MTMathTable: MTMathAtom {
|
||||
var alignments = [MTColumnAlignment]()
|
||||
var cells = [[MTMathList]]()
|
||||
|
||||
var environment: String?
|
||||
var interColumnSpacing: CGFloat = 0
|
||||
var interRowAdditionalSpacing: CGFloat = 0
|
||||
var numColumns = 0
|
||||
var numRows = 0
|
||||
|
||||
override public var finalized: MTMathAtom {
|
||||
let finalized: MTMathTable = super.finalized as! MTMathTable
|
||||
|
||||
for var row in finalized.cells {
|
||||
for i in 0..<row.count {
|
||||
row[i] = row[i].finalized
|
||||
}
|
||||
}
|
||||
|
||||
return finalized
|
||||
}
|
||||
|
||||
convenience init(environment: String? = nil) {
|
||||
self.init(type: .table, value: "")
|
||||
self.environment = environment
|
||||
}
|
||||
|
||||
func set(cell list: MTMathList, forRow row:Int, column:Int) {
|
||||
if self.cells.count <= row {
|
||||
for _ in self.cells.count...row {
|
||||
self.cells.append([])
|
||||
}
|
||||
}
|
||||
var rowArray = self.cells[row]
|
||||
if rowArray.count <= column {
|
||||
for _ in rowArray.count...column {
|
||||
rowArray.append(MTMathList())
|
||||
}
|
||||
}
|
||||
rowArray[column] = list
|
||||
}
|
||||
|
||||
func set(alignment: MTColumnAlignment, forCol col: Int) {
|
||||
if self.alignments.count <= col {
|
||||
for _ in self.alignments.count...col {
|
||||
self.alignments.append(MTColumnAlignment.center)
|
||||
}
|
||||
}
|
||||
|
||||
self.alignments[col] = alignment
|
||||
}
|
||||
|
||||
func getAlignmentOf(col: Int) -> MTColumnAlignment {
|
||||
if self.alignments.count <= col {
|
||||
return MTColumnAlignment.center
|
||||
} else {
|
||||
return self.alignments[col]
|
||||
}
|
||||
}
|
||||
|
||||
func numberOfCols() -> Int {
|
||||
var numberOfCols = 0
|
||||
|
||||
for row in self.cells {
|
||||
numberOfCols = max(numberOfCols, row.count)
|
||||
}
|
||||
|
||||
return numberOfCols
|
||||
}
|
||||
|
||||
func numberOfRows() -> Int {
|
||||
return self.cells.count
|
||||
}
|
||||
}
|
||||
|
||||
// represent list of math objects
|
||||
extension MTMathList: CustomStringConvertible {
|
||||
public var description: String { self.atoms.description }
|
||||
public var string: String { self.description }
|
||||
}
|
||||
|
||||
public class MTMathList {
|
||||
public var atoms = [MTMathAtom]()
|
||||
|
||||
public var finalized: MTMathList {
|
||||
let finalizedList = MTMathList()
|
||||
let zeroRange = NSMakeRange(0, 0)
|
||||
|
||||
var prevNode: MTMathAtom? = nil
|
||||
for atom in self.atoms {
|
||||
let newNode = atom.finalized
|
||||
|
||||
if NSEqualRanges(zeroRange, atom.indexRange) {
|
||||
let index = prevNode == nil ? 0 : prevNode!.indexRange.location + prevNode!.indexRange.length
|
||||
newNode.indexRange = NSMakeRange(index, 1)
|
||||
}
|
||||
|
||||
switch newNode.type {
|
||||
case .binaryOperator:
|
||||
if prevNode == nil || prevNode!.isNotBinaryOperator() {
|
||||
newNode.type = .unaryOperator
|
||||
}
|
||||
break
|
||||
case .relation, .punctuation, .close:
|
||||
if prevNode != nil &&
|
||||
prevNode!.type == .binaryOperator {
|
||||
prevNode!.type = .unaryOperator
|
||||
}
|
||||
break
|
||||
case .number:
|
||||
if prevNode != nil &&
|
||||
prevNode!.type == .number &&
|
||||
prevNode!.subScript == nil &&
|
||||
prevNode!.superScript == nil {
|
||||
prevNode!.fuse(with: newNode)
|
||||
continue
|
||||
}
|
||||
break
|
||||
default: break
|
||||
}
|
||||
|
||||
finalizedList.add(newNode)
|
||||
prevNode = newNode
|
||||
}
|
||||
|
||||
if prevNode != nil && prevNode!.type == .binaryOperator {
|
||||
prevNode!.type = .unaryOperator
|
||||
finalizedList.removeLastAtom()
|
||||
finalizedList.add(prevNode!)
|
||||
}
|
||||
|
||||
return finalizedList
|
||||
}
|
||||
|
||||
public init(atoms: [MTMathAtom]) {
|
||||
self.atoms.append(contentsOf: atoms)
|
||||
}
|
||||
|
||||
public init(atom: MTMathAtom) {
|
||||
self.atoms.append(atom)
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.atoms = []
|
||||
}
|
||||
|
||||
func add(_ atom: MTMathAtom) {
|
||||
if self.isAtomAllowed(atom) {
|
||||
self.atoms.append(atom)
|
||||
} else {
|
||||
print("error, cannot add atom of type \(atom.type.rawValue) into atomlist")
|
||||
}
|
||||
}
|
||||
|
||||
func insert(_ atom: MTMathAtom, at index: Int) {
|
||||
if self.isAtomAllowed(atom) {
|
||||
self.atoms.insert(atom, at: index)
|
||||
} else {
|
||||
print("error, cannot add atom of type \(atom.type.rawValue) into atomlist")
|
||||
}
|
||||
}
|
||||
|
||||
func append(_ list: MTMathList) {
|
||||
self.atoms += list.atoms
|
||||
}
|
||||
|
||||
func removeLastAtom() {
|
||||
if self.atoms.count > 0 {
|
||||
self.atoms.removeLast()
|
||||
}
|
||||
}
|
||||
|
||||
func removeAtom(at index: Int) {
|
||||
self.atoms.remove(at: index)
|
||||
}
|
||||
|
||||
func removeAtoms(in range: ClosedRange<Int>) {
|
||||
self.atoms.removeSubrange(range)
|
||||
}
|
||||
|
||||
func isAtomAllowed(_ atom: MTMathAtom) -> Bool {
|
||||
return atom.type != .boundary
|
||||
}
|
||||
}
|
||||
1016
Sources/SwiftMathRender/MathRender/MTMathListBuilder.swift
Normal file
1016
Sources/SwiftMathRender/MathRender/MTMathListBuilder.swift
Normal file
File diff suppressed because it is too large
Load Diff
816
Sources/SwiftMathRender/MathRender/MTMathListDisplay.swift
Normal file
816
Sources/SwiftMathRender/MathRender/MTMathListDisplay.swift
Normal file
@@ -0,0 +1,816 @@
|
||||
//
|
||||
// MTMathListDisplay.swift
|
||||
// MathRenderSwift
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import QuartzCore
|
||||
import CoreText
|
||||
import SwiftUI
|
||||
|
||||
func isIos6Supported() -> Bool {
|
||||
if !MTDisplay.initialized {
|
||||
#if os(iOS)
|
||||
let reqSysVer = "6.0"
|
||||
let currSysVer = UIDevice.current.systemVersion
|
||||
if currSysVer.compare(reqSysVer, options: .numeric) != .orderedAscending {
|
||||
MTDisplay.supported = true
|
||||
}
|
||||
#else
|
||||
MTDisplay.supported = true
|
||||
#endif
|
||||
MTDisplay.initialized = true
|
||||
}
|
||||
return MTDisplay.supported
|
||||
}
|
||||
|
||||
protocol DownShift {
|
||||
var shiftDown:CGFloat { set get }
|
||||
}
|
||||
|
||||
// MARK: - MTDisplay
|
||||
|
||||
/// The base class for rendering a math equation.
|
||||
class MTDisplay {
|
||||
|
||||
// needed for isIos6Supported() func above
|
||||
static var initialized = false
|
||||
static var supported = false
|
||||
|
||||
/// Draws itself in the given graphics context.
|
||||
func draw(_ context:CGContext) {
|
||||
if (self.localBackgroundColor != nil) {
|
||||
context.saveGState()
|
||||
context.setBlendMode(.normal)
|
||||
context.setFillColor(self.localBackgroundColor!.cgColor)
|
||||
context.fill(self.displayBounds())
|
||||
context.restoreGState()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Gets the bounding rectangle for the MTDisplay
|
||||
func displayBounds() -> CGRect {
|
||||
return CGRectMake(self.position.x, self.position.y - self.descent, self.width, self.ascent + self.descent)
|
||||
}
|
||||
|
||||
/// For debugging. Shows the object in quick look in Xcode.
|
||||
#if os(iOS)
|
||||
func debugQuickLookObject() -> Any {
|
||||
let size = CGSizeMake(self.width, self.ascent + self.descent);
|
||||
UIGraphicsBeginImageContext(size);
|
||||
|
||||
// get a reference to that context we created
|
||||
let context = UIGraphicsGetCurrentContext()!
|
||||
// translate/flip the graphics context (for transforming from CG* coords to UI* coords
|
||||
context.translateBy(x: 0, y: size.height);
|
||||
context.scaleBy(x: 1.0, y: -1.0);
|
||||
// move the position to (0,0)
|
||||
context.translateBy(x: -self.position.x, y: -self.position.y);
|
||||
|
||||
// Move the line up by self.descent
|
||||
context.translateBy(x: 0, y: self.descent);
|
||||
// Draw self on context
|
||||
self.draw(context)
|
||||
|
||||
// generate a new UIImage from the graphics context we drew onto
|
||||
let img = UIGraphicsGetImageFromCurrentImageContext()
|
||||
return img as Any
|
||||
}
|
||||
#endif
|
||||
|
||||
/// The distance from the axis to the top of the display
|
||||
var ascent:CGFloat = 0
|
||||
/// The distance from the axis to the bottom of the display
|
||||
var descent:CGFloat = 0
|
||||
/// The width of the display
|
||||
var width:CGFloat = 0
|
||||
/// Position of the display with respect to the parent view or display.
|
||||
var position=CGPoint.zero
|
||||
/// The range of characters supported by this item
|
||||
var range:NSRange=NSMakeRange(0, 0)
|
||||
/// Whether the display has a subscript/superscript following it.
|
||||
var hasScript:Bool = false
|
||||
/// The text color for this display
|
||||
var textColor: MTColor?
|
||||
/// The local color, if the color was mutated local with the color command
|
||||
var localTextColor: MTColor?
|
||||
/// The background color for this display
|
||||
var localBackgroundColor: MTColor?
|
||||
|
||||
}
|
||||
|
||||
class MTDisplayDS : MTDisplay, DownShift {
|
||||
|
||||
var shiftDown: CGFloat = 0
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MTCTLineDisplay
|
||||
|
||||
/// A rendering of a single CTLine as an MTDisplay
|
||||
class MTCTLineDisplay : MTDisplay {
|
||||
|
||||
/// The CTLine being displayed
|
||||
var line:CTLine!
|
||||
/// The attributed string used to generate the CTLineRef. Note setting this does not reset the dimensions of
|
||||
/// the display. So set only when
|
||||
var attributedString:NSAttributedString?
|
||||
|
||||
/// An array of MTMathAtoms that this CTLine displays. Used for indexing back into the MTMathList
|
||||
var atoms = [MTMathAtom]()
|
||||
|
||||
init(withString attrString:NSAttributedString?, position:CGPoint, range:NSRange, font:MTFont?, atoms:[MTMathAtom]) {
|
||||
super.init()
|
||||
self.position = position
|
||||
self.attributedString = attrString
|
||||
self.range = range
|
||||
self.atoms = atoms
|
||||
// We can't use typographic bounds here as the ascent and descent returned are for the font and not for the line.
|
||||
self.width = CTLineGetTypographicBounds(line, nil, nil, nil);
|
||||
if isIos6Supported() {
|
||||
let bounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
|
||||
self.ascent = max(0, CGRectGetMaxY(bounds) - 0);
|
||||
self.descent = max(0, 0 - CGRectGetMinY(bounds));
|
||||
// TODO: Should we use this width vs the typographic width? They are slightly different. Don't know why.
|
||||
// _width = CGRectGetMaxX(bounds);
|
||||
} else {
|
||||
// Our own implementation of the ios6 function to get glyph path bounds.
|
||||
self.computeDimensions(font)
|
||||
}
|
||||
}
|
||||
|
||||
func set(attrString: NSAttributedString?) {
|
||||
attributedString = attrString
|
||||
line = CTLineCreateWithAttributedString(attributedString!)
|
||||
}
|
||||
|
||||
func set(textColor:MTColor) {
|
||||
self.textColor = textColor
|
||||
let attrStr = NSMutableAttributedString(attributedString: self.attributedString!)
|
||||
let foregroundColor = NSAttributedString.Key(kCTForegroundColorAttributeName as String)
|
||||
attrStr.addAttribute(foregroundColor, value:self.textColor!.cgColor, range:NSMakeRange(0, attrStr.length))
|
||||
self.attributedString = attrStr
|
||||
}
|
||||
|
||||
func computeDimensions(_ font:MTFont?) {
|
||||
let runs = CTLineGetGlyphRuns(line) as NSArray
|
||||
for obj in runs {
|
||||
let run = obj as! CTRun?
|
||||
let numGlyphs = CTRunGetGlyphCount(run!)
|
||||
var glyphs = [CGGlyph]()
|
||||
glyphs.reserveCapacity(numGlyphs)
|
||||
CTRunGetGlyphs(run!, CFRangeMake(0, numGlyphs), &glyphs);
|
||||
let bounds = CTFontGetBoundingRectsForGlyphs(font!.ctFont, .horizontal, glyphs, nil, numGlyphs);
|
||||
let ascent = max(0, CGRectGetMaxY(bounds) - 0);
|
||||
// Descent is how much the line goes below the origin. However if the line is all above the origin, then descent can't be negative.
|
||||
let descent = max(0, 0 - CGRectGetMinY(bounds));
|
||||
if (ascent > self.ascent) {
|
||||
self.ascent = ascent;
|
||||
}
|
||||
if (descent > self.descent) {
|
||||
self.descent = descent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func draw(_ context: CGContext) {
|
||||
context.saveGState()
|
||||
|
||||
context.textPosition = self.position
|
||||
CTLineDraw(line, context)
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MTMathListDisplay
|
||||
|
||||
/// An MTLine is a rendered form of MTMathList in one line.
|
||||
/// It can render itself using the draw method.
|
||||
class MTMathListDisplay : MTDisplay {
|
||||
|
||||
/**
|
||||
@typedef MTLinePosition
|
||||
@brief The type of position for a line, i.e. subscript/superscript or regular.
|
||||
*/
|
||||
enum LinePosition : Int {
|
||||
/// Regular
|
||||
case regular
|
||||
/// Positioned at a subscript
|
||||
case ssubscript
|
||||
/// Positioned at a superscript
|
||||
case superscript
|
||||
}
|
||||
|
||||
/// Where the line is positioned
|
||||
var type:LinePosition = .regular
|
||||
/// An array of MTDisplays which are positioned relative to the position of the
|
||||
/// the current display.
|
||||
var subDisplays = [MTDisplay]()
|
||||
/// If a subscript or superscript this denotes the location in the parent MTList. For a
|
||||
/// regular list this is NSNotFound
|
||||
var index: Int = 0
|
||||
|
||||
init(withDisplays displays:[MTDisplay], range:NSRange) {
|
||||
super.init()
|
||||
self.subDisplays = displays
|
||||
self.position = CGPoint.zero
|
||||
self.type = .regular //kMTLinePositionRegular;
|
||||
self.index = NSNotFound
|
||||
self.range = range
|
||||
self.recomputeDimensions()
|
||||
}
|
||||
|
||||
func setType(_ type:LinePosition) {
|
||||
self.type = type
|
||||
}
|
||||
|
||||
func setIndex(_ index:Int) {
|
||||
self.index = index
|
||||
}
|
||||
|
||||
func setTextColor(_ textColor:MTColor) {
|
||||
// Set the color on all subdisplays
|
||||
self.textColor = textColor
|
||||
for displayAtom in self.subDisplays {
|
||||
displayAtom.textColor = textColor
|
||||
}
|
||||
}
|
||||
|
||||
func draw(context: CGContext) {
|
||||
context.saveGState()
|
||||
|
||||
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
|
||||
context.translateBy(x: self.position.x, y: self.position.y)
|
||||
context.textPosition = CGPoint.zero
|
||||
|
||||
// draw each atom separately
|
||||
for displayAtom in self.subDisplays {
|
||||
displayAtom.draw(context)
|
||||
}
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
func recomputeDimensions() {
|
||||
var max_ascent:CGFloat = 0
|
||||
var max_descent:CGFloat = 0
|
||||
var max_width:CGFloat = 0
|
||||
for atom in self.subDisplays {
|
||||
let ascent = max(0, atom.position.y + atom.ascent);
|
||||
if (ascent > max_ascent) {
|
||||
max_ascent = ascent;
|
||||
}
|
||||
|
||||
let descent = max(0, 0 - (atom.position.y - atom.descent));
|
||||
if (descent > max_descent) {
|
||||
max_descent = descent;
|
||||
}
|
||||
let width = atom.width + atom.position.x;
|
||||
if (width > max_width) {
|
||||
max_width = width;
|
||||
}
|
||||
}
|
||||
self.ascent = max_ascent;
|
||||
self.descent = max_descent;
|
||||
self.width = max_width;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MTFractionDisplay
|
||||
|
||||
/// Rendering of an MTFraction as an MTDisplay
|
||||
class MTFractionDisplay : MTDisplay {
|
||||
|
||||
/** A display representing the numerator of the fraction. Its position is relative
|
||||
to the parent and is not treated as a sub-display.
|
||||
*/
|
||||
var numerator:MTMathListDisplay?
|
||||
/** A display representing the denominator of the fraction. Its position is relative
|
||||
to the parent is not treated as a sub-display.
|
||||
*/
|
||||
var denominator:MTMathListDisplay?
|
||||
|
||||
var numeratorUp:CGFloat=0 {
|
||||
didSet { self.updateNumeratorPosition() }
|
||||
}
|
||||
var denominatorDown:CGFloat=0 {
|
||||
didSet { self.updateDenominatorPosition() }
|
||||
}
|
||||
var linePosition:CGFloat=0
|
||||
var lineThickness:CGFloat=0
|
||||
|
||||
init(withNumerator numerator:MTMathListDisplay?, denominator:MTMathListDisplay?, position:CGPoint, range:NSRange) {
|
||||
super.init()
|
||||
self.numerator = numerator;
|
||||
self.denominator = denominator;
|
||||
self.position = position;
|
||||
self.range = range;
|
||||
assert(self.range.length == 1, "Fraction range length not 1 - range (\(range.location), \(range.length)")
|
||||
}
|
||||
|
||||
override var ascent:CGFloat {
|
||||
set { super.ascent = newValue }
|
||||
get { numerator!.ascent + self.numeratorUp }
|
||||
}
|
||||
|
||||
override var descent:CGFloat {
|
||||
set { super.descent = newValue }
|
||||
get { denominator!.descent + self.denominatorDown }
|
||||
}
|
||||
|
||||
override var width:CGFloat {
|
||||
set { super.width = newValue }
|
||||
get { max(numerator!.width, denominator!.width) }
|
||||
}
|
||||
|
||||
func updateDenominatorPosition() {
|
||||
denominator?.position = CGPointMake(self.position.x + (self.width - denominator!.width)/2, self.position.y - self.denominatorDown)
|
||||
}
|
||||
|
||||
func updateNumeratorPosition() {
|
||||
numerator?.position = CGPointMake(self.position.x + (self.width - numerator!.width)/2, self.position.y + self.numeratorUp)
|
||||
}
|
||||
|
||||
func setPosition(_ position: CGPoint) {
|
||||
super.position = position
|
||||
self.updateDenominatorPosition()
|
||||
self.updateNumeratorPosition()
|
||||
}
|
||||
|
||||
func setTextColor(_ textColor:MTColor) {
|
||||
super.textColor = textColor
|
||||
numerator?.textColor = textColor
|
||||
denominator?.textColor = textColor
|
||||
}
|
||||
|
||||
override func draw(_ context:CGContext) {
|
||||
numerator?.draw(context)
|
||||
denominator?.draw(context)
|
||||
|
||||
context.saveGState()
|
||||
|
||||
self.textColor?.setStroke()
|
||||
|
||||
// draw the horizontal line
|
||||
let path = MTBezierPath()
|
||||
path.move(to: CGPointMake(self.position.x, self.position.y + self.linePosition))
|
||||
path.addLine(to: CGPointMake(self.position.x + self.width, self.position.y + self.linePosition))
|
||||
path.lineWidth = self.lineThickness
|
||||
path.stroke()
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MTRadicalDisplay
|
||||
|
||||
/// Rendering of an MTRadical as an MTDisplay
|
||||
class MTRadicalDisplay : MTDisplay {
|
||||
|
||||
/** A display representing the radicand of the radical. Its position is relative
|
||||
to the parent is not treated as a sub-display.
|
||||
*/
|
||||
var radicand:MTMathListDisplay?
|
||||
/** A display representing the degree of the radical. Its position is relative
|
||||
to the parent is not treated as a sub-display.
|
||||
*/
|
||||
var degree:MTMathListDisplay?
|
||||
|
||||
private var _radicalGlyph:MTDisplay?
|
||||
private var _radicalShift:CGFloat=0
|
||||
|
||||
var topKern:CGFloat=0
|
||||
var lineThickness:CGFloat=0
|
||||
|
||||
init(withRadicand radicand:MTMathListDisplay?, glyph:MTDisplay, position:CGPoint, range:NSRange) {
|
||||
super.init()
|
||||
self.radicand = radicand
|
||||
_radicalGlyph = glyph
|
||||
_radicalShift = 0
|
||||
|
||||
self.position = position
|
||||
self.range = range
|
||||
}
|
||||
|
||||
func setDegree(_ degree:MTMathListDisplay?, fontMetrics:MTFontMathTable?) {
|
||||
// sets up the degree of the radical
|
||||
var kernBefore = fontMetrics!.radicalKernBeforeDegree;
|
||||
let kernAfter = fontMetrics!.radicalKernAfterDegree;
|
||||
let raise = fontMetrics!.radicalDegreeBottomRaisePercent * (self.ascent - self.descent);
|
||||
|
||||
// The layout is:
|
||||
// kernBefore, raise, degree, kernAfter, radical
|
||||
self.degree = degree;
|
||||
|
||||
// the radical is now shifted by kernBefore + degree.width + kernAfter
|
||||
_radicalShift = kernBefore + degree!.width + kernAfter;
|
||||
if _radicalShift < 0 {
|
||||
// we can't have the radical shift backwards, so instead we increase the kernBefore such
|
||||
// that _radicalShift will be 0.
|
||||
kernBefore -= _radicalShift;
|
||||
_radicalShift = 0;
|
||||
}
|
||||
|
||||
// Note: position of degree is relative to parent.
|
||||
self.degree!.position = CGPointMake(self.position.x + kernBefore, self.position.y + raise);
|
||||
// Update the width by the _radicalShift
|
||||
self.width = _radicalShift + _radicalGlyph!.width + self.radicand!.width;
|
||||
// update the position of the radicand
|
||||
self.updateRadicandPosition()
|
||||
}
|
||||
|
||||
func setPosition(_ position:CGPoint) {
|
||||
super.position = position
|
||||
self.updateRadicandPosition()
|
||||
}
|
||||
|
||||
func updateRadicandPosition() {
|
||||
// The position of the radicand includes the position of the MTRadicalDisplay
|
||||
// This is to make the positioning of the radical consistent with fractions and
|
||||
// have the cursor position finding algorithm work correctly.
|
||||
// move the radicand by the width of the radical sign
|
||||
self.radicand!.position = CGPointMake(self.position.x + _radicalShift + _radicalGlyph!.width, self.position.y);
|
||||
}
|
||||
|
||||
func setTextColor(textColor:MTColor) {
|
||||
super.textColor = textColor
|
||||
self.radicand!.textColor = textColor
|
||||
self.degree!.textColor = textColor
|
||||
}
|
||||
|
||||
func draw(context: CGContext) {
|
||||
// draw the radicand & degree at its position
|
||||
self.radicand?.draw(context)
|
||||
self.degree?.draw(context)
|
||||
|
||||
context.saveGState();
|
||||
self.textColor?.setStroke()
|
||||
self.textColor?.setFill()
|
||||
|
||||
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
|
||||
context.translateBy(x: self.position.x + _radicalShift, y: self.position.y);
|
||||
context.textPosition = CGPoint.zero
|
||||
|
||||
// Draw the glyph.
|
||||
_radicalGlyph?.draw(context)
|
||||
|
||||
// Draw the VBOX
|
||||
// for the kern of, we don't need to draw anything.
|
||||
let heightFromTop = topKern;
|
||||
|
||||
// draw the horizontal line with the given thickness
|
||||
let path = MTBezierPath()
|
||||
let lineStart = CGPointMake(_radicalGlyph!.width, self.ascent - heightFromTop - self.lineThickness / 2); // subtract half the line thickness to center the line
|
||||
let lineEnd = CGPointMake(lineStart.x + self.radicand!.width, lineStart.y);
|
||||
path.move(to: lineStart)
|
||||
path.addLine(to: lineEnd)
|
||||
path.lineWidth = lineThickness
|
||||
path.lineCapStyle = .round
|
||||
path.stroke()
|
||||
|
||||
context.restoreGState();
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MTGlyphDisplay
|
||||
|
||||
/// Rendering a glyph as a display
|
||||
class MTGlyphDisplay : MTDisplayDS {
|
||||
|
||||
var glyph:CGGlyph!
|
||||
var font:MTFont?
|
||||
|
||||
init(withGlpyh glyph:CGGlyph, range:NSRange, font:MTFont?) {
|
||||
super.init()
|
||||
self.font = font
|
||||
self.glyph = glyph
|
||||
|
||||
self.position = CGPoint.zero
|
||||
self.range = range
|
||||
}
|
||||
|
||||
func draw(context: CGContext) {
|
||||
context.saveGState();
|
||||
|
||||
self.textColor?.setFill()
|
||||
|
||||
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
|
||||
|
||||
context.translateBy(x: self.position.x, y: self.position.y - self.shiftDown);
|
||||
context.textPosition = CGPoint.zero
|
||||
|
||||
var pos = CGPoint.zero
|
||||
CTFontDrawGlyphs(font!.ctFont, &glyph, &pos, 1, context);
|
||||
|
||||
context.restoreGState();
|
||||
}
|
||||
|
||||
override var ascent:CGFloat {
|
||||
set {
|
||||
super.ascent = newValue
|
||||
}
|
||||
get {
|
||||
return super.ascent - self.shiftDown;
|
||||
}
|
||||
}
|
||||
|
||||
override var descent:CGFloat {
|
||||
set {
|
||||
super.descent = newValue
|
||||
}
|
||||
get {
|
||||
return super.descent + self.shiftDown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MTGlyphConstructionDisplay
|
||||
|
||||
class MTGlyphConstructionDisplay:MTDisplayDS {
|
||||
var glyphs = [CGGlyph]()
|
||||
var positions = [CGPoint]()
|
||||
var font:MTFont?
|
||||
var numGlyphs:Int=0
|
||||
|
||||
init(withGlyphs glyphs:[NSNumber?], offsets:[NSNumber?], font:MTFont?) {
|
||||
super.init()
|
||||
assert(glyphs.count == offsets.count, "Glyphs and offsets need to match")
|
||||
self.numGlyphs = glyphs.count;
|
||||
self.glyphs = [CGGlyph](repeating: CGGlyph(), count: self.numGlyphs) //malloc(sizeof(CGGlyph) * _numGlyphs);
|
||||
self.positions = [CGPoint](repeating: CGPoint.zero, count: self.numGlyphs) //malloc(sizeof(CGPoint) * _numGlyphs);
|
||||
for i in 0 ..< self.numGlyphs {
|
||||
self.glyphs[i] = glyphs[i]!.uint16Value
|
||||
self.positions[i] = CGPointMake(0, CGFloat(offsets[i]!.floatValue))
|
||||
}
|
||||
self.font = font
|
||||
self.position = CGPoint.zero
|
||||
}
|
||||
|
||||
override func draw(_ context: CGContext) {
|
||||
context.saveGState()
|
||||
|
||||
self.textColor?.setFill()
|
||||
|
||||
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
|
||||
context.translateBy(x: self.position.x, y: self.position.y - self.shiftDown)
|
||||
context.textPosition = CGPoint.zero
|
||||
|
||||
// Draw the glyphs.
|
||||
CTFontDrawGlyphs(font!.ctFont, glyphs, positions, numGlyphs, context)
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
override var ascent:CGFloat {
|
||||
set {
|
||||
super.ascent = newValue
|
||||
}
|
||||
get {
|
||||
return super.ascent - self.shiftDown;
|
||||
}
|
||||
}
|
||||
|
||||
override var descent:CGFloat {
|
||||
set {
|
||||
super.descent = newValue
|
||||
}
|
||||
get {
|
||||
return super.descent + self.shiftDown;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MTLargeOpLimitsDisplay
|
||||
|
||||
/// Rendering a large operator with limits as an MTDisplay
|
||||
class MTLargeOpLimitsDisplay : MTDisplay {
|
||||
|
||||
/** A display representing the upper limit of the large operator. Its position is relative
|
||||
to the parent is not treated as a sub-display.
|
||||
*/
|
||||
var upperLimit:MTMathListDisplay?
|
||||
/** A display representing the lower limit of the large operator. Its position is relative
|
||||
to the parent is not treated as a sub-display.
|
||||
*/
|
||||
var lowerLimit:MTMathListDisplay?
|
||||
|
||||
var limitShift:CGFloat=0
|
||||
var upperLimitGap:CGFloat=0 {
|
||||
didSet {
|
||||
self.updateUpperLimitPosition()
|
||||
}
|
||||
}
|
||||
var lowerLimitGap:CGFloat=0 {
|
||||
didSet {
|
||||
self.updateUpperLimitPosition()
|
||||
}
|
||||
}
|
||||
var extraPadding:CGFloat=0
|
||||
|
||||
var nucleus:MTDisplay?
|
||||
|
||||
init(withNucleus nucleus:MTDisplay?, upperLimit:MTMathListDisplay?, lowerLimit:MTMathListDisplay?, limitShift:CGFloat, extraPadding:CGFloat) {
|
||||
super.init()
|
||||
self.upperLimit = upperLimit;
|
||||
self.lowerLimit = lowerLimit;
|
||||
self.nucleus = nucleus;
|
||||
|
||||
var maxWidth = max(nucleus!.width, upperLimit!.width);
|
||||
maxWidth = max(maxWidth, lowerLimit!.width);
|
||||
|
||||
self.limitShift = limitShift;
|
||||
self.upperLimitGap = 0;
|
||||
self.lowerLimitGap = 0;
|
||||
self.extraPadding = extraPadding; // corresponds to \xi_13 in TeX
|
||||
self.width = maxWidth;
|
||||
}
|
||||
|
||||
override var ascent:CGFloat {
|
||||
set {
|
||||
super.ascent = newValue
|
||||
}
|
||||
get {
|
||||
if self.upperLimit != nil {
|
||||
return nucleus!.ascent + extraPadding + self.upperLimit!.ascent + upperLimitGap + self.upperLimit!.descent
|
||||
} else {
|
||||
return nucleus!.ascent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var descent:CGFloat {
|
||||
set {
|
||||
super.descent = newValue
|
||||
}
|
||||
get {
|
||||
if self.lowerLimit != nil {
|
||||
return nucleus!.descent + extraPadding + lowerLimitGap + self.lowerLimit!.descent + self.lowerLimit!.ascent;
|
||||
} else {
|
||||
return nucleus!.descent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setPosition(_ position:CGPoint) {
|
||||
super.position = position;
|
||||
self.updateLowerLimitPosition()
|
||||
self.updateUpperLimitPosition()
|
||||
self.updateNucleusPosition()
|
||||
}
|
||||
|
||||
func updateLowerLimitPosition() {
|
||||
if self.lowerLimit != nil {
|
||||
// The position of the lower limit includes the position of the MTLargeOpLimitsDisplay
|
||||
// This is to make the positioning of the radical consistent with fractions and radicals
|
||||
// Move the starting point to below the nucleus leaving a gap of _lowerLimitGap and subtract
|
||||
// the ascent to to get the baseline. Also center and shift it to the left by _limitShift.
|
||||
self.lowerLimit!.position = CGPointMake(self.position.x - limitShift + (self.width - lowerLimit!.width)/2,
|
||||
self.position.y - nucleus!.descent - lowerLimitGap - self.lowerLimit!.ascent);
|
||||
}
|
||||
}
|
||||
|
||||
func updateUpperLimitPosition() {
|
||||
if self.upperLimit != nil {
|
||||
// The position of the upper limit includes the position of the MTLargeOpLimitsDisplay
|
||||
// This is to make the positioning of the radical consistent with fractions and radicals
|
||||
// Move the starting point to above the nucleus leaving a gap of _upperLimitGap and add
|
||||
// the descent to to get the baseline. Also center and shift it to the right by _limitShift.
|
||||
self.upperLimit!.position = CGPointMake(self.position.x + limitShift + (self.width - self.upperLimit!.width)/2,
|
||||
self.position.y + nucleus!.ascent + upperLimitGap + self.upperLimit!.descent);
|
||||
}
|
||||
}
|
||||
|
||||
func updateNucleusPosition() {
|
||||
// Center the nucleus
|
||||
nucleus?.position = CGPointMake(self.position.x + (self.width - nucleus!.width)/2, self.position.y);
|
||||
}
|
||||
|
||||
func setTextColor(_ textColor:MTColor) {
|
||||
super.textColor = textColor
|
||||
self.upperLimit?.textColor = textColor
|
||||
self.lowerLimit?.textColor = textColor
|
||||
nucleus?.textColor = textColor
|
||||
}
|
||||
|
||||
override func draw(_ context:CGContext) {
|
||||
// Draw the elements.
|
||||
self.upperLimit?.draw(context)
|
||||
self.lowerLimit?.draw(context)
|
||||
nucleus?.draw(context)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MTLineDisplay
|
||||
|
||||
/// Rendering of an list with an overline or underline
|
||||
class MTLineDisplay : MTDisplay {
|
||||
|
||||
/** A display representing the inner list that is underlined. Its position is relative
|
||||
to the parent is not treated as a sub-display.
|
||||
*/
|
||||
var inner:MTMathListDisplay?
|
||||
var lineShiftUp:CGFloat=0
|
||||
var lineThickness:CGFloat=0
|
||||
|
||||
init(withInner inner:MTMathListDisplay?, position:CGPoint, range:NSRange) {
|
||||
super.init()
|
||||
self.inner = inner;
|
||||
|
||||
self.position = position;
|
||||
self.range = range;
|
||||
}
|
||||
|
||||
func setTextColor(_ textColor:MTColor) {
|
||||
super.textColor = textColor
|
||||
inner?.textColor = textColor
|
||||
}
|
||||
|
||||
override func draw(_ context:CGContext) {
|
||||
self.inner?.draw(context)
|
||||
|
||||
context.saveGState();
|
||||
|
||||
self.textColor?.setStroke()
|
||||
|
||||
// draw the horizontal line
|
||||
let path = MTBezierPath()
|
||||
let lineStart = CGPointMake(self.position.x, self.position.y + self.lineShiftUp);
|
||||
let lineEnd = CGPointMake(lineStart.x + self.inner!.width, lineStart.y);
|
||||
path.move(to:lineStart)
|
||||
path.addLine(to: lineEnd)
|
||||
path.lineWidth = self.lineThickness;
|
||||
path.stroke()
|
||||
|
||||
context.restoreGState();
|
||||
}
|
||||
|
||||
func setPosition(_ position:CGPoint) {
|
||||
super.position = position;
|
||||
self.updateInnerPosition()
|
||||
}
|
||||
|
||||
func updateInnerPosition() {
|
||||
self.inner?.position = CGPointMake(self.position.x, self.position.y);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MTAccentDisplay
|
||||
|
||||
/// Rendering an accent as a display
|
||||
class MTAccentDisplay : MTDisplay {
|
||||
|
||||
/** A display representing the inner list that is accented. Its position is relative
|
||||
to the parent is not treated as a sub-display.
|
||||
*/
|
||||
var accentee:MTMathListDisplay?
|
||||
|
||||
/** A display representing the accent. Its position is relative to the current display.
|
||||
*/
|
||||
var accent:MTGlyphDisplay?
|
||||
|
||||
init(withAccent glyph:MTGlyphDisplay?, accentee:MTMathListDisplay?, range:NSRange) {
|
||||
super.init()
|
||||
self.accent = glyph
|
||||
self.accentee = accentee
|
||||
self.accentee?.position = CGPoint.zero
|
||||
self.range = range
|
||||
}
|
||||
|
||||
func setTextColor(_ textColor:MTColor) {
|
||||
super.textColor = textColor
|
||||
accentee?.textColor = textColor
|
||||
accent?.textColor = textColor
|
||||
}
|
||||
|
||||
func setPosition(_ position:CGPoint) {
|
||||
super.position = position
|
||||
self.updateAccenteePosition()
|
||||
}
|
||||
|
||||
func updateAccenteePosition() {
|
||||
self.accentee?.position = CGPointMake(self.position.x, self.position.y);
|
||||
}
|
||||
|
||||
override func draw(_ context:CGContext) {
|
||||
self.accentee?.draw(context)
|
||||
|
||||
context.saveGState();
|
||||
context.translateBy(x: self.position.x, y: self.position.y);
|
||||
context.textPosition = CGPoint.zero
|
||||
|
||||
self.accent?.draw(context: context)
|
||||
|
||||
context.restoreGState();
|
||||
}
|
||||
|
||||
}
|
||||
168
Sources/SwiftMathRender/MathRender/MTMathListIndex.swift
Normal file
168
Sources/SwiftMathRender/MathRender/MTMathListIndex.swift
Normal file
@@ -0,0 +1,168 @@
|
||||
//
|
||||
// MTMathListIndex.swift
|
||||
// MathRenderSwift
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class MTMathListIndex {
|
||||
|
||||
public enum MTMathListSubIndexType: Int {
|
||||
case none = 0
|
||||
case nucleus
|
||||
case superScript
|
||||
case subScript
|
||||
case numerator
|
||||
case denominator
|
||||
case radicand
|
||||
case degree
|
||||
}
|
||||
|
||||
/// The index of the associated atom.
|
||||
var atomIndex: Int
|
||||
|
||||
/// The type of subindex, e.g. superscript, numerator etc.
|
||||
var subIndexType: MTMathListSubIndexType = .none
|
||||
|
||||
/// The index into the sublist.
|
||||
var subIndex: MTMathListIndex?
|
||||
|
||||
var finalIndex: Int {
|
||||
if self.subIndexType == .none {
|
||||
return self.atomIndex
|
||||
} else {
|
||||
return self.subIndex?.finalIndex ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
func prevIndex() -> MTMathListIndex? {
|
||||
if self.subIndexType == .none {
|
||||
if self.atomIndex > 0 {
|
||||
return MTMathListIndex(level0Index: self.atomIndex - 1)
|
||||
}
|
||||
} else {
|
||||
if let prevSubIndex = self.subIndex?.prevIndex() {
|
||||
return MTMathListIndex(at: self.atomIndex, with: prevSubIndex, type: self.subIndexType)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func nextIndex() -> MTMathListIndex {
|
||||
if self.subIndexType == .none {
|
||||
return MTMathListIndex(level0Index: self.atomIndex + 1)
|
||||
} else if self.subIndexType == .nucleus {
|
||||
return MTMathListIndex(at: self.atomIndex + 1, with: self.subIndex, type: self.subIndexType)
|
||||
} else {
|
||||
return MTMathListIndex(at: self.atomIndex, with: self.subIndex?.nextIndex(), type: self.subIndexType)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this index represents the beginning of a line. Note there may be multiple lines in a MTMathList,
|
||||
* e.g. a superscript or a fraction numerator. This returns true if the innermost subindex points to the beginning of a
|
||||
* line.
|
||||
*/
|
||||
func isBeginningOfLine() -> Bool {
|
||||
return self.finalIndex == 0
|
||||
}
|
||||
|
||||
func isAtSameLevel(with index: MTMathListIndex?) -> Bool {
|
||||
if self.subIndexType != index?.subIndexType {
|
||||
return false
|
||||
} else if self.subIndexType == .none {
|
||||
// No subindexes, they are at the same level.
|
||||
return true
|
||||
} else if (self.atomIndex != index?.atomIndex) {
|
||||
return false
|
||||
} else {
|
||||
return self.subIndex?.isAtSameLevel(with: index?.subIndex) ?? false
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the type of the innermost sub index. */
|
||||
func finalSubIndexType() -> MTMathListSubIndexType {
|
||||
if self.subIndex?.subIndex != nil {
|
||||
return self.subIndex!.finalSubIndexType()
|
||||
} else {
|
||||
return self.subIndexType
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true if any of the subIndexes of this index have the given type. */
|
||||
func hasSubIndex(ofType type: MTMathListSubIndexType) -> Bool {
|
||||
if self.subIndexType == type {
|
||||
return true
|
||||
} else {
|
||||
return self.subIndex?.hasSubIndex(ofType: type) ?? false
|
||||
}
|
||||
}
|
||||
|
||||
func levelUp(with subIndex: MTMathListIndex?, type: MTMathListSubIndexType) -> MTMathListIndex {
|
||||
if self.subIndexType == .none {
|
||||
return MTMathListIndex(at: self.atomIndex, with: subIndex, type: type)
|
||||
}
|
||||
|
||||
return MTMathListIndex(at: self.atomIndex, with: self.subIndex?.levelUp(with: subIndex, type: type), type: self.subIndexType)
|
||||
}
|
||||
|
||||
func levelDown() -> MTMathListIndex? {
|
||||
if self.subIndexType == .none {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let subIndexDown = self.subIndex?.levelDown() {
|
||||
return MTMathListIndex(at: self.atomIndex, with: subIndexDown, type: self.subIndexType)
|
||||
} else {
|
||||
return MTMathListIndex(level0Index: self.atomIndex)
|
||||
}
|
||||
}
|
||||
|
||||
/** Factory function to create a `MTMathListIndex` with no subindexes.
|
||||
@param index The index of the atom that the `MTMathListIndex` points at.
|
||||
*/
|
||||
public init(level0Index: Int) {
|
||||
self.atomIndex = level0Index
|
||||
}
|
||||
|
||||
public convenience init(at location: Int, with subIndex: MTMathListIndex?, type: MTMathListSubIndexType) {
|
||||
self.init(level0Index: location)
|
||||
self.subIndexType = type
|
||||
self.subIndex = subIndex
|
||||
}
|
||||
}
|
||||
|
||||
extension MTMathListIndex: CustomStringConvertible {
|
||||
public var description: String {
|
||||
if self.subIndex != nil {
|
||||
return "[\(self.atomIndex), \(self.subIndexType.rawValue):\(self.subIndex!)]"
|
||||
}
|
||||
return "[\(self.atomIndex)]"
|
||||
}
|
||||
}
|
||||
|
||||
extension MTMathListIndex: Hashable {
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(self.atomIndex)
|
||||
hasher.combine(self.subIndexType)
|
||||
hasher.combine(self.subIndex)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MTMathListIndex: Equatable {
|
||||
public static func ==(lhs: MTMathListIndex, rhs: MTMathListIndex) -> Bool {
|
||||
if lhs.atomIndex != rhs.atomIndex || lhs.subIndexType != rhs.subIndexType {
|
||||
return false
|
||||
}
|
||||
|
||||
if rhs.subIndex != nil {
|
||||
return rhs.subIndex == lhs.subIndex
|
||||
} else {
|
||||
return lhs.subIndex == nil
|
||||
}
|
||||
}
|
||||
}
|
||||
250
Sources/SwiftMathRender/MathRender/MTMathUILabel.swift
Normal file
250
Sources/SwiftMathRender/MathRender/MTMathUILabel.swift
Normal file
@@ -0,0 +1,250 @@
|
||||
//
|
||||
// MTMathUILabel.swift
|
||||
// MathRenderSwift
|
||||
//
|
||||
// Created by Mike Griebling on 2023-01-01.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreText
|
||||
|
||||
/**
|
||||
Different display styles supported by the `MTMathUILabel`.
|
||||
|
||||
The only significant difference between the two modes is how fractions
|
||||
and limits on large operators are displayed.
|
||||
*/
|
||||
public enum MTMathUILabelMode {
|
||||
/// Display mode. Equivalent to $$ in TeX
|
||||
case display
|
||||
/// Text mode. Equivalent to $ in TeX.
|
||||
case text
|
||||
}
|
||||
|
||||
/**
|
||||
@typedef MTTextAlignment
|
||||
@brief Horizontal text alignment for `MTMathUILabel`.
|
||||
*/
|
||||
public enum MTTextAlignment : UInt {
|
||||
/// Align left.
|
||||
case left
|
||||
/// Align center.
|
||||
case center
|
||||
/// Align right.
|
||||
case right
|
||||
}
|
||||
|
||||
/** The main view for rendering math.
|
||||
|
||||
`MTMathLabel` accepts either a string in LaTeX or an `MTMathList` to display. Use
|
||||
`MTMathList` directly only if you are building it programmatically (e.g. using an
|
||||
editor), otherwise using LaTeX is the preferable method.
|
||||
|
||||
The math display is centered vertically in the label. The default horizontal alignment is
|
||||
is left. This can be changed by setting `textAlignment`. The math is default displayed in
|
||||
*Display* mode. This can be changed using `labelMode`.
|
||||
|
||||
When created it uses `[MTFontManager defaultFont]` as its font. This can be changed using
|
||||
the `font` parameter.
|
||||
*/
|
||||
class MTMathUILabel : MTView {
|
||||
|
||||
/** The `MTMathList` to render. Setting this will remove any
|
||||
`latex` that has already been set. If `latex` has been set, this will
|
||||
return the parsed `MTMathList` if the `latex` parses successfully. Use this
|
||||
setting if the `MTMathList` has been programmatically constructed, otherwise it
|
||||
is preferred to use `latex`.
|
||||
*/
|
||||
var mathList:MTMathList? {
|
||||
didSet {
|
||||
self.error = nil
|
||||
self.latex = MTMathListBuilder.mathListToString(mathList)
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
/** The latex string to be displayed. Setting this will remove any `mathList` that
|
||||
has been set. If latex has not been set, this will return the latex output for the
|
||||
`mathList` that is set.
|
||||
@see error */
|
||||
var latex = "" {
|
||||
didSet {
|
||||
self.error = nil
|
||||
var error: NSError? = nil
|
||||
self.mathList = MTMathListBuilder.build(fromString: latex, error: &error)
|
||||
if error != nil {
|
||||
self.mathList = nil
|
||||
self.error = error
|
||||
self.errorLabel?.text = error?.localizedDescription
|
||||
self.errorLabel?.frame = self.bounds
|
||||
self.errorLabel?.isHidden = !self.displayErrorInline
|
||||
} else {
|
||||
self.errorLabel?.isHidden = true
|
||||
}
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
/** This contains any error that occurred when parsing the latex. */
|
||||
var error:NSError?
|
||||
|
||||
/** If true, if there is an error it displays the error message inline. Default true. */
|
||||
var displayErrorInline = true
|
||||
|
||||
/** The MTFont to use for rendering. */
|
||||
var font = MTFontManager.fontManager.defaultFont {
|
||||
didSet {
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
/** Convenience method to just set the size of the font without changing the fontface. */
|
||||
var fontSize = MTFontManager.fontManager.kDefaultFontSize {
|
||||
didSet {
|
||||
self.font = font?.copy(withSize: fontSize)
|
||||
}
|
||||
}
|
||||
|
||||
/** This sets the text color of the rendered math formula. The default color is black. */
|
||||
var textColor:MTColor? = MTColor.black {
|
||||
didSet {
|
||||
self.displayList?.textColor = textColor
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
/** The minimum distance from the margin of the view to the rendered math. This value is
|
||||
`UIEdgeInsetsZero` by default. This is useful if you need some padding between the math and
|
||||
the border/background color. sizeThatFits: will have its returned size increased by these insets.
|
||||
*/
|
||||
var contentInsets = MTEdgeInsetsZero {
|
||||
didSet {
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
/** The Label mode for the label. The default mode is Display */
|
||||
var labelMode = MTMathUILabelMode.display {
|
||||
didSet {
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
/** Horizontal alignment for the text. The default is align left. */
|
||||
var textAlignment = MTTextAlignment.left {
|
||||
didSet {
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
/** The internal display of the MTMathUILabel. This is for advanced use only. */
|
||||
var displayList: MTMathListDisplay? = nil
|
||||
|
||||
var currentStyle:MTLineStyle {
|
||||
switch labelMode {
|
||||
case .display: return .display
|
||||
case .text: return .text
|
||||
}
|
||||
}
|
||||
|
||||
var errorLabel: MTLabel?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
self.initCommon()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
self.initCommon()
|
||||
}
|
||||
|
||||
func initCommon() {
|
||||
#if os(macOS)
|
||||
self.layer?.isGeometryFlipped = true
|
||||
errorLabel?.layer?.isGeometryFlipped = true
|
||||
#else
|
||||
self.layer.isGeometryFlipped = true
|
||||
errorLabel?.layer.isGeometryFlipped = true
|
||||
#endif
|
||||
self.backgroundColor = MTColor.clear
|
||||
errorLabel = MTLabel()
|
||||
errorLabel?.isHidden = true
|
||||
errorLabel?.textColor = MTColor.red
|
||||
self.addSubview(errorLabel!)
|
||||
}
|
||||
|
||||
override func draw(_ dirtyRect: MTRect) {
|
||||
super.draw(dirtyRect)
|
||||
if self.mathList == nil { return }
|
||||
|
||||
// drawing code
|
||||
let context = MTGraphicsGetCurrentContext()!
|
||||
context.saveGState()
|
||||
displayList!.draw(context)
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
func _layoutSubviews() {
|
||||
if mathList != nil {
|
||||
displayList = MTTypesetter.createLineForMathList(mathList, font: font, style: currentStyle)
|
||||
displayList?.textColor = textColor
|
||||
var textX = CGFloat(0)
|
||||
switch self.textAlignment {
|
||||
case .left:
|
||||
textX = self.contentInsets.left
|
||||
case .center:
|
||||
textX = (bounds.size.width - contentInsets.left - contentInsets.right - displayList!.width) / 2 +
|
||||
contentInsets.left
|
||||
case .right:
|
||||
textX = bounds.size.width - displayList!.width - contentInsets.right
|
||||
}
|
||||
let availableHeight = bounds.size.height - contentInsets.bottom - contentInsets.top
|
||||
|
||||
// center things vertically
|
||||
var height = displayList!.ascent + displayList!.descent
|
||||
if height < fontSize/2 {
|
||||
height = fontSize/2 // set height to half the font size
|
||||
}
|
||||
let textY = (availableHeight - height) / 2 + displayList!.descent + contentInsets.bottom
|
||||
displayList?.position = CGPointMake(textX, textY)
|
||||
} else {
|
||||
displayList = nil
|
||||
}
|
||||
errorLabel?.frame = self.bounds
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
|
||||
func _sizeThatFits(_ size:CGSize) -> CGSize {
|
||||
var size = size
|
||||
var displayList:MTMathListDisplay? = nil
|
||||
if mathList != nil {
|
||||
displayList = MTTypesetter.createLineForMathList(mathList, font: font, style: currentStyle)
|
||||
}
|
||||
size.width = displayList!.width + contentInsets.left + contentInsets.right
|
||||
size.height = displayList!.ascent + displayList!.descent + contentInsets.top + contentInsets.bottom
|
||||
return size
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize { _sizeThatFits(CGSizeZero) }
|
||||
|
||||
#if os(macOS)
|
||||
override var isFlipped: Bool { false }
|
||||
func setNeedsDisplay() { self.needsDisplay = true }
|
||||
func setNeedsLayout() { self.needsLayout = true }
|
||||
override func layout() {
|
||||
self._layoutSubviews()
|
||||
super.layout()
|
||||
}
|
||||
#else
|
||||
override func layoutSubviews() { self._layoutSubviews() }
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize { self._sizeThatFits(size) }
|
||||
#endif
|
||||
|
||||
}
|
||||
1704
Sources/SwiftMathRender/MathRender/MTTypesetter.swift
Normal file
1704
Sources/SwiftMathRender/MathRender/MTTypesetter.swift
Normal file
File diff suppressed because it is too large
Load Diff
96
Sources/SwiftMathRender/MathRender/MTUnicode.swift
Normal file
96
Sources/SwiftMathRender/MathRender/MTUnicode.swift
Normal file
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// MTUnicode.swift
|
||||
// MathRenderSwift
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct UnicodeSymbol {
|
||||
static let multiplication = "\u{00D7}"
|
||||
static let division = "\u{00F7}"
|
||||
static let fractionSlash = "\u{2044}"
|
||||
static let whiteSquare = "\u{25A1}"
|
||||
static let blackSquare = "\u{25A0}"
|
||||
static let lessEqual = "\u{2264}"
|
||||
static let greaterEqual = "\u{2265}"
|
||||
static let notEqual = "\u{2260}"
|
||||
static let squareRoot = "\u{221A}" // \sqrt
|
||||
static let cubeRoot = "\u{221B}"
|
||||
static let infinity = "\u{221E}" // \infty
|
||||
static let angle = "\u{2220}" // \angle
|
||||
static let degree = "\u{00B0}" // \circ
|
||||
|
||||
static let capitalGreekStart = UInt32(0x0391)
|
||||
static let capitalGreekEnd = UInt32(0x03A9)
|
||||
static let lowerGreekStart = UInt32(0x03B1)
|
||||
static let lowerGreekEnd = UInt32(0x03C9)
|
||||
static let planksConstant = UInt32(0x210e)
|
||||
static let lowerItalicStart = UInt32(0x1D44E)
|
||||
static let capitalItalicStart = UInt32(0x1D434)
|
||||
static let greekLowerItalicStart = UInt32(0x1D6FC)
|
||||
static let greekCapitalItalicStart = UInt32(0x1D6E2)
|
||||
static let greekSymbolItalicStart = UInt32(0x1D716)
|
||||
|
||||
static let mathCapitalBoldStart = UInt32(0x1D400)
|
||||
static let mathLowerBoldStart = UInt32(0x1D41A)
|
||||
static let greekCapitalBoldStart = UInt32(0x1D6A8)
|
||||
static let greekLowerBoldStart = UInt32(0x1D6C2)
|
||||
static let greekSymbolBoldStart = UInt32(0x1D6DC)
|
||||
static let numberBoldStart = UInt32(0x1D7CE)
|
||||
|
||||
static let mathCapitalBoldItalicStart = UInt32(0x1D468)
|
||||
static let mathLowerBoldItalicStart = UInt32(0x1D482)
|
||||
static let greekCapitalBoldItalicStart = UInt32(0x1D71C)
|
||||
static let greekLowerBoldItalicStart = UInt32(0x1D736)
|
||||
static let greekSymbolBoldItalicStart = UInt32(0x1D750)
|
||||
|
||||
static let mathCapitalScriptStart = UInt32(0x1D49C)
|
||||
static let mathCapitalTTStart = UInt32(0x1D670)
|
||||
static let mathLowerTTStart = UInt32(0x1D68A)
|
||||
static let numberTTStart = UInt32(0x1D7F6)
|
||||
static let mathCapitalSansSerifStart = UInt32(0x1D5A0)
|
||||
static let mathLowerSansSerifStart = UInt32(0x1D5BA)
|
||||
static let numberSansSerifStart = UInt32(0x1D7E2)
|
||||
static let mathCapitalFrakturStart = UInt32(0x1D504)
|
||||
static let mathLowerFrakturStart = UInt32(0x1D51E)
|
||||
static let mathCapitalBlackboardStart = UInt32(0x1D538)
|
||||
static let mathLowerBlackboardStart = UInt32(0x1D552)
|
||||
static let numberBlackboardStart = UInt32(0x1D7D8)
|
||||
}
|
||||
|
||||
extension Character {
|
||||
|
||||
var utf32Char: UTF32Char { self.unicodeScalars.map { $0.value }.reduce(0, +) }
|
||||
var isLowerEnglish : Bool { self >= "a" && self <= "z" }
|
||||
var isUpperEnglish : Bool { self >= "A" && self <= "Z" }
|
||||
var isNumber : Bool { self >= "0" && self <= "9" }
|
||||
|
||||
var isLowerGreek : Bool {
|
||||
let uch = self.utf32Char
|
||||
return uch >= UnicodeSymbol.lowerGreekStart && uch <= UnicodeSymbol.lowerGreekEnd
|
||||
}
|
||||
|
||||
var isCapitalGreek : Bool {
|
||||
let uch = self.utf32Char
|
||||
return uch >= UnicodeSymbol.capitalGreekStart && uch <= UnicodeSymbol.capitalGreekEnd
|
||||
}
|
||||
|
||||
var greekSymbolOrder : UInt32? {
|
||||
let greekSymbols : [UTF32Char] = [0x03F5, 0x03D1, 0x03F0, 0x03D5, 0x03F1, 0x03D6]
|
||||
let index = greekSymbols.firstIndex(of: self.utf32Char)
|
||||
if let pos = index { return UInt32(pos) }
|
||||
return nil
|
||||
}
|
||||
|
||||
var isGreekSymbol : Bool { self.greekSymbolOrder != nil }
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
||||
var unicodeLength:Int {
|
||||
self.lengthOfBytes(using: .utf32) / 4
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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..<list.atoms.count {
|
||||
let atom = list.atoms[i]
|
||||
XCTAssertNotNil(atom, desc)
|
||||
XCTAssertEqual(atom.type, types[i], desc)
|
||||
}
|
||||
} else {
|
||||
XCTAssert(types.count == 0, "MathList should have no atoms!")
|
||||
}
|
||||
}
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
struct TestRecord {
|
||||
let build : String
|
||||
let atomType : [MTMathAtomType]
|
||||
let types : [MTMathAtomType]
|
||||
let extra : [MTMathAtomType]
|
||||
let result : String
|
||||
|
||||
init(build: String, atomType: [MTMathAtomType], types: [MTMathAtomType], extra: [MTMathAtomType] = [MTMathAtomType](), result: String) {
|
||||
self.build = build
|
||||
self.atomType = atomType
|
||||
self.types = types
|
||||
self.extra = extra
|
||||
self.result = result
|
||||
}
|
||||
}
|
||||
|
||||
func getTestDataSuperScript() -> [TestRecord] {
|
||||
[
|
||||
TestRecord(build: "x^2", atomType: [.variable], types: [.number], result: "x^{2}"),
|
||||
TestRecord(build: "x^23", atomType: [ .variable, .number ], types: [ .number ], result: "x^{2}3"),
|
||||
TestRecord(build: "x^{23}", atomType: [ .variable ], types: [ .number, .number ], result: "x^{23}"),
|
||||
TestRecord(build: "x^2^3", atomType: [ .variable, .ordinary ], types: [ .number ], result: "x^{2}{}^{3}" ),
|
||||
TestRecord(build: "x^{2^3}", atomType: [ .variable ], types: [ .number], extra: [ .number ], result: "x^{2^{3}}"),
|
||||
TestRecord(build: "x^{^2*}", atomType: [ .variable ], types: [ .ordinary, .binaryOperator], extra:[ .number ], result:"x^{{}^{2}*}"),
|
||||
TestRecord(build: "^2", atomType: [ .ordinary], types: [ .number ], result: "{}^{2}"),
|
||||
TestRecord(build: "{}^2", atomType: [ .ordinary], types: [ .number ], result: "{}^{2}"),
|
||||
TestRecord(build: "x^^2", atomType: [ .variable, .ordinary ], types: [ ], result: "x^{}{}^{2}"),
|
||||
TestRecord(build: "5{x}^2", atomType: [ .number, .variable], types: [ ], result: "5x^{2}"),
|
||||
]
|
||||
}
|
||||
|
||||
func getTestDataSubScript() -> [TestRecord] {
|
||||
[
|
||||
TestRecord(build: "x_2", atomType: [.variable], types: [.number], result: "x_{2}"),
|
||||
TestRecord(build: "x_23", atomType: [ .variable, .number ], types: [ .number ], result: "x_{2}3"),
|
||||
TestRecord(build: "x_{23}", atomType: [ .variable ], types: [ .number, .number ], result: "x_{23}"),
|
||||
TestRecord(build: "x_2_3", atomType: [ .variable, .ordinary ], types: [ .number ], result: "x_{2}{}_{3}" ),
|
||||
TestRecord(build: "x_{2_3}", atomType: [ .variable ], types: [ .number], extra: [ .number ], result: "x_{2_{3}}"),
|
||||
TestRecord(build: "x_{_2*}", atomType: [ .variable ], types: [ .ordinary, .binaryOperator], extra:[ .number ], result:"x_{{}_{2}*}"),
|
||||
TestRecord(build: "_2", atomType: [ .ordinary], types: [ .number ], result: "{}_{2}"),
|
||||
TestRecord(build: "{}_2", atomType: [ .ordinary], types: [ .number ], result: "{}_{2}"),
|
||||
TestRecord(build: "x__2", atomType: [ .variable, .ordinary ], types: [ ], result: "x_{}{}_{2}"),
|
||||
TestRecord(build: "5{x}_2", atomType: [ .number, .variable], types: [ ], result: "5x_{2}"),
|
||||
]
|
||||
}
|
||||
|
||||
func getTestDataSuperSubScript() -> [TestRecord] {
|
||||
[
|
||||
TestRecord(build: "x_2^*", atomType: [.variable], types: [.number], extra: [.binaryOperator], result: "x^{*}_{2}"),
|
||||
TestRecord(build: "x^*_2", atomType: [.variable], types: [.number], extra: [.binaryOperator], result: "x^{*}_{2}"),
|
||||
TestRecord(build: "x_^*", atomType: [.variable], types: [ ], extra: [.binaryOperator], result: "x^{*}_{}"),
|
||||
TestRecord(build: "x^_2", atomType: [.variable], types: [.number], result: "x^{}_{2}"),
|
||||
TestRecord(build: "x_{2^*}", atomType: [.variable], types: [.number], result: "x_{2^{*}}"),
|
||||
TestRecord(build: "x^{*_2}", atomType: [.variable], types: [ ], extra: [.binaryOperator], result: "x^{*_{2}}"),
|
||||
TestRecord(build: "_2^*", atomType: [.ordinary], types: [.number], extra: [.binaryOperator], result: "{}^{*}_{2}")
|
||||
]
|
||||
}
|
||||
|
||||
struct TestRecord2 {
|
||||
let build : String
|
||||
let type1 : [MTMathAtomType]
|
||||
let number : Int
|
||||
let type2 : [MTMathAtomType]
|
||||
let left : String
|
||||
let right : String
|
||||
let result : String
|
||||
}
|
||||
|
||||
func getTestDataLeftRight() -> [TestRecord2] {
|
||||
[
|
||||
TestRecord2(build: "\\left( 2 \\right)", type1: [ .inner ], number: 0, type2: [ .number], left: "(", right: ")", result: "\\left( 2\\right) "),
|
||||
// spacing
|
||||
TestRecord2(build: "\\left ( 2 \\right )", type1: [ .inner ], number: 0, type2: [ .number], left: "(", right: ")", result: "\\left( 2\\right) "),
|
||||
// commands
|
||||
TestRecord2(build: "\\left\\{ 2 \\right\\}", type1: [ .inner ], number: 0, type2: [ .number], left: "{", right: "}", result: "\\left\\{ 2\\right\\} "),
|
||||
// complex commands
|
||||
TestRecord2(build: "\\left\\langle x \\right\\rangle", type1: [ .inner ], number: 0, type2: [ .variable], left: "\u{2329}", right: "\u{232A}", result: "\\left< x\\right> "),
|
||||
// bars
|
||||
TestRecord2(build: "\\left| x \\right\\|", type1: [ .inner ], number: 0, type2: [ .variable], left: "|", right: "\u{2016}", result: "\\left| x\\right\\| "),
|
||||
// inner in between
|
||||
TestRecord2(build: "5 + \\left( 2 \\right) - 2", type1: [ .number, .binaryOperator, .inner, .binaryOperator, .number ], number: 2, type2: [ .number], left: "(", right: ")", result: "5+\\left( 2\\right) -2"),
|
||||
// long inner
|
||||
TestRecord2(build: "\\left( 2 + \\frac12\\right)", type1: [ .inner ], number: 0, type2: [ .number, .binaryOperator, .fraction], left: "(", right: ")", result: "\\left( 2+\\frac{1}{2}\\right) "),
|
||||
// nested
|
||||
TestRecord2(build: "\\left[ 2 + \\left|\\frac{-x}{2}\\right| \\right]", type1: [ .inner ], number: 0, type2: [ .number, .binaryOperator, .inner], left: "[", right: "]", result: "\\left[ 2+\\left| \\frac{-x}{2}\\right| \\right] "),
|
||||
// With scripts
|
||||
TestRecord2(build: "\\left( 2 \\right)^2", type1: [ .inner ], number: 0, type2: [ .number], left: "(", right: ")", result: "\\left( 2\\right) ^{2}"),
|
||||
// Scripts on left
|
||||
TestRecord2(build: "\\left(^2 \\right )", type1: [ .inner], number: 0, type2: [ .ordinary], left: "(", right: ")", result: "\\left( {}^{2}\\right) "),
|
||||
// Dot
|
||||
TestRecord2(build: "\\left( 2 \\right.", type1: [ .inner], number: 0, type2: [ .number], left: "(", right: "", result: "\\left( 2\\right. ")
|
||||
]
|
||||
}
|
||||
|
||||
func testSuperScript() throws {
|
||||
let data = getTestDataSuperScript()
|
||||
for testCase in data {
|
||||
let str = testCase.build
|
||||
var error:NSError?
|
||||
let list = MTMathListBuilder.build(fromString: str, error:&error)
|
||||
XCTAssertNil(error)
|
||||
let desc = "Error for string:\(str)"
|
||||
let atomTypes = testCase.atomType
|
||||
checkAtomTypes(list, types:atomTypes, desc:desc)
|
||||
|
||||
// get the first atom
|
||||
let first = list!.atoms[0]
|
||||
// check it's superscript
|
||||
let types = testCase.types
|
||||
if types.count > 0 {
|
||||
XCTAssertNotNil(first.superScript, desc)
|
||||
}
|
||||
let superlist = first.superScript
|
||||
checkAtomTypes(superlist, types:types, desc:desc)
|
||||
|
||||
if !testCase.extra.isEmpty {
|
||||
// one more level
|
||||
let superFirst = superlist!.atoms[0]
|
||||
let supersuperList = superFirst.superScript
|
||||
checkAtomTypes(supersuperList, types:testCase.extra, desc:desc)
|
||||
}
|
||||
|
||||
// convert it back to latex
|
||||
let latex = MTMathListBuilder.mathListToString(list)
|
||||
XCTAssertEqual(latex, testCase.result, desc)
|
||||
}
|
||||
}
|
||||
|
||||
func testSubScript() throws {
|
||||
let data = getTestDataSubScript()
|
||||
for testCase in data {
|
||||
let str = testCase.build
|
||||
var error:NSError?
|
||||
let list = MTMathListBuilder.build(fromString: str, error:&error)
|
||||
XCTAssertNil(error)
|
||||
let desc = "Error for string:\(str)"
|
||||
let atomTypes = testCase.atomType
|
||||
checkAtomTypes(list, types:atomTypes, desc:desc)
|
||||
|
||||
// get the first atom
|
||||
let first = list!.atoms[0]
|
||||
// check it's superscript
|
||||
let types = testCase.types
|
||||
if (types.count > 0) {
|
||||
XCTAssertNotNil(first.subScript, desc);
|
||||
}
|
||||
let sublist = first.subScript
|
||||
checkAtomTypes(sublist, types:types, desc:desc)
|
||||
|
||||
if !testCase.extra.isEmpty {
|
||||
// one more level
|
||||
let subFirst = sublist!.atoms[0]
|
||||
let subsubList = subFirst.subScript
|
||||
checkAtomTypes(subsubList, types:testCase.extra, desc:desc)
|
||||
}
|
||||
|
||||
// convert it back to latex
|
||||
let latex = MTMathListBuilder.mathListToString(list)
|
||||
XCTAssertEqual(latex, testCase.result, desc)
|
||||
}
|
||||
}
|
||||
|
||||
func testSuperSubScript() throws {
|
||||
let data = getTestDataSuperSubScript()
|
||||
for testCase in data {
|
||||
let str = testCase.build
|
||||
var error:NSError?
|
||||
let list = MTMathListBuilder.build(fromString: str, error:&error)
|
||||
XCTAssertNil(error)
|
||||
let desc = "Error for string:\(str)"
|
||||
let atomTypes = testCase.atomType
|
||||
checkAtomTypes(list, types:atomTypes, desc:desc)
|
||||
|
||||
// get the first atom
|
||||
let first = list!.atoms[0]
|
||||
// check its subscript
|
||||
let sub = testCase.types
|
||||
if sub.count > 0 {
|
||||
XCTAssertNotNil(first.subScript, desc)
|
||||
let sublist = first.subScript
|
||||
checkAtomTypes(sublist, types: sub, desc: desc)
|
||||
}
|
||||
let sup = testCase.extra
|
||||
if sup.count > 0 {
|
||||
XCTAssertNotNil(first.superScript, desc)
|
||||
let sublist = first.superScript
|
||||
checkAtomTypes(sublist, types: sup, desc: desc)
|
||||
}
|
||||
|
||||
// convert it back to latex
|
||||
let latex = MTMathListBuilder.mathListToString(list)
|
||||
XCTAssertEqual(latex, testCase.result, desc)
|
||||
}
|
||||
}
|
||||
|
||||
func testSymbols() throws {
|
||||
let str = "5\\times3^{2\\div2}";
|
||||
let list = MTMathListBuilder.build(fromString: str)!
|
||||
let desc = "Error for string:\(str)"
|
||||
|
||||
XCTAssertNotNil(list, desc)
|
||||
XCTAssertEqual((list.atoms.count), 3, desc)
|
||||
var atom = list.atoms[0];
|
||||
XCTAssertEqual(atom.type, .number, desc)
|
||||
XCTAssertEqual(atom.nucleus, "5", desc)
|
||||
atom = list.atoms[1];
|
||||
XCTAssertEqual(atom.type, .binaryOperator, desc)
|
||||
XCTAssertEqual(atom.nucleus, "\u{00D7}", desc)
|
||||
atom = list.atoms[2];
|
||||
XCTAssertEqual(atom.type, .number, desc)
|
||||
XCTAssertEqual(atom.nucleus, "3", desc)
|
||||
|
||||
// super script
|
||||
let superList = atom.superScript!
|
||||
XCTAssertNotNil(superList, desc)
|
||||
XCTAssertEqual((superList.atoms.count), 3, desc)
|
||||
atom = superList.atoms[0];
|
||||
XCTAssertEqual(atom.type, .number, desc)
|
||||
XCTAssertEqual(atom.nucleus, "2", desc)
|
||||
atom = superList.atoms[1];
|
||||
XCTAssertEqual(atom.type, .binaryOperator, desc)
|
||||
XCTAssertEqual(atom.nucleus, "\u{00F7}", desc)
|
||||
atom = superList.atoms[2];
|
||||
XCTAssertEqual(atom.type, .number, desc)
|
||||
XCTAssertEqual(atom.nucleus, "2", desc)
|
||||
}
|
||||
|
||||
func testFrac() throws {
|
||||
let str = "\\frac1c";
|
||||
let list = MTMathListBuilder.build(fromString: str)!
|
||||
let desc = "Error for string:\(str)"
|
||||
|
||||
XCTAssertNotNil(list, desc)
|
||||
XCTAssertEqual((list.atoms.count), 1, desc)
|
||||
let frac = list.atoms[0] as! MTFraction
|
||||
XCTAssertEqual(frac.type, .fraction, desc)
|
||||
XCTAssertEqual(frac.nucleus, "", desc)
|
||||
XCTAssertTrue(frac.hasRule);
|
||||
XCTAssertNil(frac.rightDelimiter);
|
||||
XCTAssertNil(frac.leftDelimiter);
|
||||
|
||||
var subList = frac.numerator!
|
||||
XCTAssertNotNil(subList, desc)
|
||||
XCTAssertEqual((subList.atoms.count), 1, desc)
|
||||
var atom = subList.atoms[0];
|
||||
XCTAssertEqual(atom.type, .number, desc)
|
||||
XCTAssertEqual(atom.nucleus, "1", desc)
|
||||
|
||||
atom = list.atoms[0];
|
||||
subList = frac.denominator!
|
||||
XCTAssertNotNil(subList, desc)
|
||||
XCTAssertEqual((subList.atoms.count), 1, desc)
|
||||
atom = subList.atoms[0];
|
||||
XCTAssertEqual(atom.type, .variable, desc)
|
||||
XCTAssertEqual(atom.nucleus, "c", desc)
|
||||
|
||||
// convert it back to latex
|
||||
let latex = MTMathListBuilder.mathListToString(list)
|
||||
XCTAssertEqual(latex, "\\frac{1}{c}", desc)
|
||||
}
|
||||
|
||||
func testFracInFrac() throws {
|
||||
let str = "\\frac1\\frac23";
|
||||
let list = MTMathListBuilder.build(fromString: str)!
|
||||
let desc = "Error for string:\(str)"
|
||||
|
||||
XCTAssertNotNil(list, desc)
|
||||
XCTAssertEqual((list.atoms.count), 1, desc)
|
||||
var frac = list.atoms[0] as! MTFraction
|
||||
XCTAssertEqual(frac.type, .fraction, desc)
|
||||
XCTAssertEqual(frac.nucleus, "", desc)
|
||||
XCTAssertTrue(frac.hasRule);
|
||||
|
||||
var subList = frac.numerator!
|
||||
XCTAssertNotNil(subList, desc)
|
||||
XCTAssertEqual((subList.atoms.count), 1, desc)
|
||||
var atom = subList.atoms[0];
|
||||
XCTAssertEqual(atom.type, .number, desc)
|
||||
XCTAssertEqual(atom.nucleus, "1", desc)
|
||||
|
||||
subList = frac.denominator!
|
||||
XCTAssertNotNil(subList, desc)
|
||||
XCTAssertEqual((subList.atoms.count), 1, desc)
|
||||
frac = subList.atoms[0] as! MTFraction
|
||||
XCTAssertEqual(frac.type, .fraction, desc)
|
||||
XCTAssertEqual(frac.nucleus, "", desc)
|
||||
|
||||
subList = frac.numerator!
|
||||
XCTAssertNotNil(subList, desc)
|
||||
XCTAssertEqual((subList.atoms.count), 1, desc)
|
||||
atom = subList.atoms[0];
|
||||
XCTAssertEqual(atom.type, .number, desc)
|
||||
XCTAssertEqual(atom.nucleus, "2", desc)
|
||||
|
||||
subList = frac.denominator!
|
||||
XCTAssertNotNil(subList, desc)
|
||||
XCTAssertEqual((subList.atoms.count), 1, desc)
|
||||
atom = subList.atoms[0];
|
||||
XCTAssertEqual(atom.type, .number, desc)
|
||||
XCTAssertEqual(atom.nucleus, "3", desc)
|
||||
|
||||
// convert it back to latex
|
||||
let latex = MTMathListBuilder.mathListToString(list)
|
||||
XCTAssertEqual(latex, "\\frac{1}{\\frac{2}{3}}", desc)
|
||||
}
|
||||
|
||||
func testSqrt() throws {
|
||||
let str = "\\sqrt2";
|
||||
let list = MTMathListBuilder.build(fromString: str)!
|
||||
let desc = "Error for string:\(str)"
|
||||
|
||||
XCTAssertNotNil(list, desc)
|
||||
XCTAssertEqual((list.atoms.count), 1, desc)
|
||||
let rad = list.atoms[0] as! MTRadical
|
||||
XCTAssertEqual(rad.type, .radical, desc)
|
||||
XCTAssertEqual(rad.nucleus, "", desc)
|
||||
|
||||
let subList = rad.radicand!
|
||||
XCTAssertNotNil(subList, desc)
|
||||
XCTAssertEqual((subList.atoms.count), 1, desc)
|
||||
let atom = subList.atoms[0]
|
||||
XCTAssertEqual(atom.type, .number, desc)
|
||||
XCTAssertEqual(atom.nucleus, "2", desc)
|
||||
|
||||
// convert it back to latex
|
||||
let latex = MTMathListBuilder.mathListToString(list)
|
||||
XCTAssertEqual(latex, "\\sqrt{2}", desc)
|
||||
}
|
||||
|
||||
func testSqrtInSqrt() throws {
|
||||
let str = "\\sqrt\\sqrt2";
|
||||
let list = MTMathListBuilder.build(fromString: str)!
|
||||
let desc = "Error for string:\(str)"
|
||||
|
||||
XCTAssertNotNil(list, desc)
|
||||
XCTAssertEqual((list.atoms.count), 1, desc)
|
||||
var rad = list.atoms[0] as! MTRadical
|
||||
XCTAssertEqual(rad.type, .radical, desc)
|
||||
XCTAssertEqual(rad.nucleus, "", desc)
|
||||
|
||||
var subList = rad.radicand!
|
||||
XCTAssertNotNil(subList, desc)
|
||||
XCTAssertEqual((subList.atoms.count), 1, desc)
|
||||
rad = subList.atoms[0] as! MTRadical
|
||||
XCTAssertEqual(rad.type, .radical, desc)
|
||||
XCTAssertEqual(rad.nucleus, "", desc)
|
||||
|
||||
|
||||
subList = rad.radicand!
|
||||
XCTAssertNotNil(subList, desc)
|
||||
XCTAssertEqual((subList.atoms.count), 1, desc)
|
||||
let atom = subList.atoms[0];
|
||||
XCTAssertEqual(atom.type, .number, desc)
|
||||
XCTAssertEqual(atom.nucleus, "2", desc)
|
||||
|
||||
// convert it back to latex
|
||||
let latex = MTMathListBuilder.mathListToString(list)
|
||||
XCTAssertEqual(latex, "\\sqrt{\\sqrt{2}}", desc)
|
||||
}
|
||||
|
||||
func testRad() throws {
|
||||
let str = "\\sqrt[3]2";
|
||||
let list = MTMathListBuilder.build(fromString: str)!
|
||||
|
||||
XCTAssertNotNil(list);
|
||||
XCTAssertEqual((list.atoms.count), 1);
|
||||
let rad = list.atoms[0] as! MTRadical
|
||||
XCTAssertEqual(rad.type, .radical);
|
||||
XCTAssertEqual(rad.nucleus, "");
|
||||
|
||||
var subList = rad.radicand!
|
||||
XCTAssertNotNil(subList);
|
||||
XCTAssertEqual((subList.atoms.count), 1);
|
||||
var atom = subList.atoms[0];
|
||||
XCTAssertEqual(atom.type, .number);
|
||||
XCTAssertEqual(atom.nucleus, "2");
|
||||
|
||||
subList = rad.degree!
|
||||
XCTAssertNotNil(subList);
|
||||
XCTAssertEqual((subList.atoms.count), 1);
|
||||
atom = subList.atoms[0];
|
||||
XCTAssertEqual(atom.type, .number);
|
||||
XCTAssertEqual(atom.nucleus, "3");
|
||||
|
||||
// convert it back to latex
|
||||
let latex = MTMathListBuilder.mathListToString(list)
|
||||
XCTAssertEqual(latex, "\\sqrt[3]{2}");
|
||||
}
|
||||
|
||||
func testLeftRight() throws {
|
||||
let data = getTestDataLeftRight()
|
||||
for testCase in data {
|
||||
let str = testCase.build
|
||||
|
||||
var error:NSError?
|
||||
let list = MTMathListBuilder.build(fromString: str, error: &error)!
|
||||
|
||||
XCTAssertNotNil(list, str);
|
||||
XCTAssertNil(error, str);
|
||||
|
||||
checkAtomTypes(list, types:testCase.type1, desc:"\(str) outer")
|
||||
|
||||
let innerLoc = testCase.number
|
||||
let inner = list.atoms[innerLoc] as! MTInner
|
||||
XCTAssertEqual(inner.type, .inner, str);
|
||||
XCTAssertEqual(inner.nucleus, "", str);
|
||||
|
||||
let innerList = inner.innerList!
|
||||
XCTAssertNotNil(innerList, str);
|
||||
checkAtomTypes(innerList, types:testCase.type2, desc:"\(str) inner")
|
||||
|
||||
XCTAssertNotNil(inner.leftBoundary, str);
|
||||
XCTAssertEqual(inner.leftBoundary!.type, .boundary, str);
|
||||
XCTAssertEqual(inner.leftBoundary!.nucleus, testCase.left, str);
|
||||
|
||||
XCTAssertNotNil(inner.rightBoundary, str);
|
||||
XCTAssertEqual(inner.rightBoundary!.type, .boundary, str);
|
||||
XCTAssertEqual(inner.rightBoundary!.nucleus, testCase.right, str);
|
||||
|
||||
// convert it back to latex
|
||||
let latex = MTMathListBuilder.mathListToString(list)
|
||||
XCTAssertEqual(latex, testCase.result, str);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// func testPerformanceExample() throws {
|
||||
// // This is an example of a performance test case.
|
||||
// measure {
|
||||
// // Put the code you want to measure the time of here.
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user