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