Rename package and address warnings
This commit is contained in:
160
Sources/SwiftUIMath/MathBundle/MTFontMathTableV2.swift
Executable file
160
Sources/SwiftUIMath/MathBundle/MTFontMathTableV2.swift
Executable file
@@ -0,0 +1,160 @@
|
||||
//
|
||||
// MTFontMathTableV2.swift
|
||||
//
|
||||
//
|
||||
// Created by Peter Tang on 15/9/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
import CoreText
|
||||
|
||||
internal class MTFontMathTableV2: MTFontMathTable {
|
||||
private let mathFont: MathFont
|
||||
private let fontSize: CGFloat
|
||||
private let unitsPerEm: UInt
|
||||
private let mTable: NSDictionary
|
||||
init(mathFont: MathFont, size: CGFloat, unitsPerEm: UInt) {
|
||||
self.mathFont = mathFont
|
||||
self.fontSize = size
|
||||
self.unitsPerEm = unitsPerEm
|
||||
mTable = mathFont.rawMathTable()
|
||||
super.init(withFont: mathFont.mtfont(size: fontSize), mathTable: mTable)
|
||||
super._mathTable = nil
|
||||
// disable all possible access to _mathTable in superclass!
|
||||
}
|
||||
override var _mathTable: NSDictionary? {
|
||||
set { fatalError("\(#function) change to _mathTable \(mathFont.rawValue) not allowed.") }
|
||||
get { mTable }
|
||||
}
|
||||
override var muUnit: CGFloat { fontSize/18 }
|
||||
|
||||
override func fontUnitsToPt(_ fontUnits:Int) -> CGFloat {
|
||||
CGFloat(fontUnits) * fontSize / CGFloat(unitsPerEm)
|
||||
}
|
||||
override func constantFromTable(_ constName: String) -> CGFloat {
|
||||
guard let consts = mTable[kConstants] as? NSDictionary, let val = consts[constName] as? NSNumber else {
|
||||
return .zero
|
||||
}
|
||||
return fontUnitsToPt(val.intValue)
|
||||
}
|
||||
override func percentFromTable(_ percentName: String) -> CGFloat {
|
||||
guard let consts = mTable[kConstants] as? NSDictionary, let val = consts[percentName] as? NSNumber else {
|
||||
return .zero
|
||||
}
|
||||
return CGFloat(val.floatValue) / 100
|
||||
}
|
||||
/** 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. */
|
||||
override func getVerticalVariantsForGlyph(_ glyph: CGGlyph) -> [NSNumber?] {
|
||||
guard let variants = mTable[kVertVariants] as? NSDictionary else { return [] }
|
||||
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. */
|
||||
override func getHorizontalVariantsForGlyph(_ glyph: CGGlyph) -> [NSNumber?] {
|
||||
guard let variants = mTable[kHorizVariants] as? NSDictionary else { return [] }
|
||||
return self.getVariantsForGlyph(glyph, inDictionary:variants)
|
||||
}
|
||||
override func getVariantsForGlyph(_ glyph: CGGlyph, inDictionary variants: NSDictionary) -> [NSNumber?] {
|
||||
let font = mathFont.mtfont(size: fontSize)
|
||||
let glyphName = font.get(nameForGlyph: glyph)
|
||||
|
||||
var glyphArray = [NSNumber]()
|
||||
let variantGlyphs = variants[glyphName] as? NSArray
|
||||
|
||||
guard let variantGlyphs = variantGlyphs, variantGlyphs.count != .zero else {
|
||||
// There are no extra variants, so just add the current glyph to it.
|
||||
let glyph = font.get(glyphWithName: glyphName)
|
||||
glyphArray.append(NSNumber(value:glyph))
|
||||
return glyphArray
|
||||
}
|
||||
for gvn in variantGlyphs {
|
||||
if let glyphVariantName = gvn as? String {
|
||||
let variantGlyph = 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.
|
||||
*/
|
||||
override func getLargerGlyph(_ glyph: CGGlyph) -> CGGlyph {
|
||||
let font = mathFont.mtfont(size: fontSize)
|
||||
let glyphName = font.get(nameForGlyph: glyph)
|
||||
|
||||
guard let variants = mTable[kVertVariants] as? NSDictionary,
|
||||
let variantGlyphs = variants[glyphName] as? NSArray, variantGlyphs.count != .zero else {
|
||||
// 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 {
|
||||
if let glyphVariantName = gvn as? String, glyphVariantName != glyphName {
|
||||
let variantGlyph = font.get(glyphWithName: glyphVariantName)
|
||||
return variantGlyph
|
||||
}
|
||||
}
|
||||
// We did not find any variants of this glyph so return it.
|
||||
return glyph
|
||||
}
|
||||
/** Returns the italic correction for the given glyph if any. If there
|
||||
isn't any this returns 0. */
|
||||
override func getItalicCorrection(_ glyph: CGGlyph) -> CGFloat {
|
||||
let font = mathFont.mtfont(size: fontSize)
|
||||
let glyphName = font.get(nameForGlyph: glyph)
|
||||
|
||||
guard let italics = mTable[kItalic] as? NSDictionary, let val = italics[glyphName] as? NSNumber else {
|
||||
return .zero
|
||||
}
|
||||
// if val is nil, this returns 0.
|
||||
return fontUnitsToPt(val.intValue)
|
||||
}
|
||||
override func getTopAccentAdjustment(_ glyph: CGGlyph) -> CGFloat {
|
||||
let font = mathFont.mtfont(size: fontSize)
|
||||
let glyphName = font.get(nameForGlyph: glyph)
|
||||
|
||||
guard let accents = mTable[kAccents] as? NSDictionary, let val = accents[glyphName] as? NSNumber else {
|
||||
// If no top accent is defined then it is the center of the advance width.
|
||||
var glyph = glyph
|
||||
var advances = CGSize.zero
|
||||
CTFontGetAdvancesForGlyphs(font.ctFont, .horizontal, &glyph, &advances, 1)
|
||||
return advances.width/2
|
||||
}
|
||||
return fontUnitsToPt(val.intValue)
|
||||
}
|
||||
override func getVerticalGlyphAssembly(forGlyph glyph: CGGlyph) -> [GlyphPart] {
|
||||
let font = mathFont.mtfont(size: fontSize)
|
||||
let glyphName = font.get(nameForGlyph: glyph)
|
||||
|
||||
guard let assemblyTable = mTable[kVertAssembly] as? NSDictionary,
|
||||
let assemblyInfo = assemblyTable[glyphName] as? NSDictionary,
|
||||
let parts = assemblyInfo[kAssemblyParts] as? NSArray else {
|
||||
// No vertical assembly defined for glyph
|
||||
// parts should always have been defined, but if it isn't return nil
|
||||
return []
|
||||
}
|
||||
|
||||
var rv = [GlyphPart]()
|
||||
for part in parts {
|
||||
guard let partInfo = part as? NSDictionary,
|
||||
let adv = partInfo["advance"] as? NSNumber,
|
||||
let end = partInfo["endConnector"] as? NSNumber,
|
||||
let start = partInfo["startConnector"] as? NSNumber,
|
||||
let ext = partInfo["extender"] as? NSNumber,
|
||||
let glyphName = partInfo["glyph"] as? String else { continue }
|
||||
let fullAdvance = fontUnitsToPt(adv.intValue)
|
||||
let endConnectorLength = fontUnitsToPt(end.intValue)
|
||||
let startConnectorLength = fontUnitsToPt(start.intValue)
|
||||
let isExtender = ext.boolValue
|
||||
let glyph = font.get(glyphWithName: glyphName)
|
||||
let part = GlyphPart(glyph: glyph, fullAdvance: fullAdvance,
|
||||
startConnectorLength: startConnectorLength,
|
||||
endConnectorLength: endConnectorLength,
|
||||
isExtender: isExtender)
|
||||
rv.append(part)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
}
|
||||
68
Sources/SwiftUIMath/MathBundle/MTFontV2.swift
Executable file
68
Sources/SwiftUIMath/MathBundle/MTFontV2.swift
Executable file
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// MTFontV2.swift
|
||||
//
|
||||
//
|
||||
// Created by Peter Tang on 15/9/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
import CoreText
|
||||
|
||||
extension MathFont {
|
||||
public func mtfont(size: CGFloat) -> MTFontV2 {
|
||||
MTFontV2(font: self, size: size)
|
||||
}
|
||||
}
|
||||
public final class MTFontV2: MTFont {
|
||||
let font: MathFont
|
||||
let size: CGFloat
|
||||
private let _cgFont: CGFont
|
||||
private let _ctFont: CTFont
|
||||
private let unitsPerEm: UInt
|
||||
private var _mathTab: MTFontMathTableV2?
|
||||
init(font: MathFont = .latinModernFont, size: CGFloat) {
|
||||
self.font = font
|
||||
self.size = size
|
||||
// MathFont cgfont and ctfont are fast & threadsafe, keep a local copy is cheaper than
|
||||
// handling via NSLock
|
||||
self._cgFont = font.cgFont()
|
||||
self._ctFont = font.ctFont(withSize: size)
|
||||
self.unitsPerEm = self._ctFont.unitsPerEm
|
||||
super.init()
|
||||
|
||||
super.defaultCGFont = nil
|
||||
super.ctFont = nil
|
||||
super.mathTable = nil
|
||||
super.rawMathTable = nil
|
||||
}
|
||||
override var defaultCGFont: CGFont! {
|
||||
set { fatalError("\(#function): change to \(font.fontName) not allowed.") }
|
||||
get { _cgFont }
|
||||
}
|
||||
override var ctFont: CTFont! {
|
||||
set { fatalError("\(#function): change to \(font.fontName) not allowed.") }
|
||||
get { _ctFont }
|
||||
}
|
||||
private let mtfontV2LockOnMathTable = NSLock()
|
||||
override var mathTable: MTFontMathTable? {
|
||||
set { fatalError("\(#function): change to \(font.rawValue) not allowed.") }
|
||||
get {
|
||||
guard _mathTab == nil else { return _mathTab }
|
||||
//Note: lazy _mathTab initialization is now threadsafe.
|
||||
mtfontV2LockOnMathTable.lock()
|
||||
defer { mtfontV2LockOnMathTable.unlock() }
|
||||
if _mathTab == nil {
|
||||
_mathTab = MTFontMathTableV2(mathFont: font, size: size, unitsPerEm: unitsPerEm)
|
||||
}
|
||||
return _mathTab
|
||||
}
|
||||
}
|
||||
override var rawMathTable: NSDictionary? {
|
||||
set { fatalError("\(#function): change to \(font.rawValue) not allowed.") }
|
||||
get { fatalError("\(#function): access to \(font.rawValue) not allowed.") }
|
||||
}
|
||||
public override func copy(withSize size: CGFloat) -> MTFont {
|
||||
MTFontV2(font: font, size: size)
|
||||
}
|
||||
}
|
||||
225
Sources/SwiftUIMath/MathBundle/MathFont.swift
Normal file
225
Sources/SwiftUIMath/MathBundle/MathFont.swift
Normal file
@@ -0,0 +1,225 @@
|
||||
//
|
||||
// MathFont.swift
|
||||
//
|
||||
//
|
||||
// Created by Peter Tang on 10/9/2023.
|
||||
//
|
||||
|
||||
#if os(iOS) || os(visionOS)
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
/// Now available for everyone to use
|
||||
public enum MathFont: String, CaseIterable, Identifiable {
|
||||
|
||||
public var id: Self { self } // Makes things simpler for SwiftUI
|
||||
|
||||
case latinModernFont = "latinmodern-math"
|
||||
case kpMathLightFont = "KpMath-Light"
|
||||
case kpMathSansFont = "KpMath-Sans"
|
||||
case xitsFont = "xits-math"
|
||||
case termesFont = "texgyretermes-math"
|
||||
case asanaFont = "Asana-Math"
|
||||
case eulerFont = "Euler-Math"
|
||||
case firaFont = "FiraMath-Regular"
|
||||
case notoSansFont = "NotoSansMath-Regular"
|
||||
case libertinusFont = "LibertinusMath-Regular"
|
||||
case garamondFont = "Garamond-Math"
|
||||
case leteSansFont = "LeteSansMath"
|
||||
|
||||
var fontFamilyName: String {
|
||||
switch self {
|
||||
case .latinModernFont: "Latin Modern Math"
|
||||
case .kpMathLightFont: "KpMath"
|
||||
case .kpMathSansFont: "KpMath"
|
||||
case .xitsFont: "XITS Math"
|
||||
case .termesFont: "TeX Gyre Termes Math"
|
||||
case .asanaFont: "Asana Math"
|
||||
case .eulerFont: "Euler Math"
|
||||
case .firaFont: "Fira Math"
|
||||
case .notoSansFont: "Noto Sans Math"
|
||||
case .libertinusFont: "Libertinus Math"
|
||||
case .garamondFont: "Garamond-Math" // PostScript name is "Garamond-Math", not "Garamond Math"
|
||||
case .leteSansFont: "Lete Sans Math"
|
||||
}
|
||||
}
|
||||
|
||||
var postScriptName: String {
|
||||
switch self {
|
||||
case .latinModernFont: "LatinModernMath-Regular"
|
||||
case .kpMathLightFont: "KpMath-Light"
|
||||
case .kpMathSansFont: "KpMath-Sans"
|
||||
case .xitsFont: "XITSMath"
|
||||
case .termesFont: "TeXGyreTermesMath-Regular"
|
||||
case .asanaFont: "Asana-Math"
|
||||
case .eulerFont: "Euler-Math"
|
||||
case .firaFont: "FiraMath-Regular"
|
||||
case .notoSansFont: "NotoSansMath-Regular"
|
||||
case .libertinusFont: "LibertinusMath-Regular"
|
||||
case .garamondFont: "Garamond-Math"
|
||||
case .leteSansFont: "LeteSansMath"
|
||||
}
|
||||
}
|
||||
|
||||
var fontName: String { self.rawValue }
|
||||
|
||||
public func cgFont() -> CGFont {
|
||||
BundleManager.manager.obtainCGFont(font: self)
|
||||
}
|
||||
public func ctFont(withSize size: CGFloat) -> CTFont {
|
||||
BundleManager.manager.obtainCTFont(font: self, withSize: size)
|
||||
}
|
||||
internal func rawMathTable() -> NSDictionary {
|
||||
BundleManager.manager.obtainRawMathTable(font: self)
|
||||
}
|
||||
|
||||
//Note: Below code are no longer supported, unable to tell if UIFont/NSFont is threadsafe, not used in SwiftMath.
|
||||
// #if os(iOS) || os(visionOS)
|
||||
// public func uiFont(withSize size: CGFloat) -> UIFont? {
|
||||
// UIFont(name: fontName, size: size)
|
||||
// }
|
||||
// #endif
|
||||
// #if os(macOS)
|
||||
// public func nsFont(withSize size: CGFloat) -> NSFont? {
|
||||
// NSFont(name: fontName, size: size)
|
||||
// }
|
||||
// #endif
|
||||
}
|
||||
internal extension CTFont {
|
||||
/** The size of this font in points. */
|
||||
var fontSize: CGFloat {
|
||||
CTFontGetSize(self)
|
||||
}
|
||||
var unitsPerEm: UInt {
|
||||
return UInt(CTFontGetUnitsPerEm(self))
|
||||
}
|
||||
}
|
||||
private class BundleManager {
|
||||
//Note: below should be lightweight and without threadsafe problem.
|
||||
static internal let manager = BundleManager()
|
||||
|
||||
private var cgFonts = [MathFont: CGFont]()
|
||||
private var ctFonts = [CTFontSizePair: CTFont]()
|
||||
private var rawMathTables = [MathFont: NSDictionary]()
|
||||
|
||||
private let threadSafeQueue = DispatchQueue(label: "com.smartmath.mathfont.threadsafequeue",
|
||||
qos: .userInitiated,
|
||||
attributes: .concurrent)
|
||||
|
||||
private func registerCGFont(mathFont: MathFont) throws {
|
||||
guard let frameworkBundleURL = Bundle.module.url(forResource: "mathFonts", withExtension: "bundle"),
|
||||
let resourceBundleURL = Bundle(url: frameworkBundleURL)?.path(forResource: mathFont.rawValue, ofType: "otf") else {
|
||||
throw FontError.fontPathNotFound
|
||||
}
|
||||
guard let fontData = NSData(contentsOfFile: resourceBundleURL), let dataProvider = CGDataProvider(data: fontData) else {
|
||||
throw FontError.invalidFontFile
|
||||
}
|
||||
guard let defaultCGFont = CGFont(dataProvider) else {
|
||||
throw FontError.initFontError
|
||||
}
|
||||
|
||||
cgFonts[mathFont] = defaultCGFont
|
||||
|
||||
/// This 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.
|
||||
var errorRef: Unmanaged<CFError>? = nil
|
||||
guard CTFontManagerRegisterGraphicsFont(defaultCGFont, &errorRef) else {
|
||||
throw FontError.registerFailed
|
||||
}
|
||||
let postsript = (defaultCGFont.postScriptName as? String) ?? ""
|
||||
let cgfontName = (defaultCGFont.fullName as? String) ?? ""
|
||||
let threadName = Thread.isMainThread ? "main" : "global"
|
||||
debugPrint("mathFonts bundle resource: \(mathFont.rawValue), font: \(cgfontName), ps: \(postsript) registered on \(threadName).")
|
||||
}
|
||||
|
||||
private func registerMathTable(mathFont: MathFont) throws {
|
||||
guard let frameworkBundleURL = Bundle.module.url(forResource: "mathFonts", withExtension: "bundle"),
|
||||
let mathTablePlist = Bundle(url: frameworkBundleURL)?.url(forResource: mathFont.rawValue, withExtension:"plist") else {
|
||||
throw FontError.fontPathNotFound
|
||||
}
|
||||
guard let rawMathTable = NSDictionary(contentsOf: mathTablePlist),
|
||||
let version = rawMathTable["version"] as? String,
|
||||
version == "1.3" else {
|
||||
throw FontError.invalidMathTable
|
||||
}
|
||||
|
||||
rawMathTables[mathFont] = rawMathTable
|
||||
|
||||
let threadName = Thread.isMainThread ? "main" : "global"
|
||||
debugPrint("mathFonts bundle resource: \(mathFont.rawValue).plist registered on \(threadName).")
|
||||
}
|
||||
|
||||
private func onDemandRegistration(mathFont: MathFont) {
|
||||
guard threadSafeQueue.sync(execute: { cgFonts[mathFont] }) == nil else { return }
|
||||
// Note: resourceLoading is now serialized.
|
||||
threadSafeQueue.sync(flags: .barrier, execute: { [weak self] in
|
||||
if self?.cgFonts[mathFont] == nil {
|
||||
do {
|
||||
try BundleManager.manager.registerCGFont(mathFont: mathFont)
|
||||
try BundleManager.manager.registerMathTable(mathFont: mathFont)
|
||||
|
||||
} catch {
|
||||
fatalError("MTMathFonts:\(#function) ondemand loading failed, mathFont \(mathFont.rawValue), reason \(error)")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
fileprivate func obtainCGFont(font: MathFont) -> CGFont {
|
||||
onDemandRegistration(mathFont: font)
|
||||
guard let cgFont = threadSafeQueue.sync(execute: { cgFonts[font] }) else {
|
||||
fatalError("\(#function) unable to locate CGFont \(font.fontName)")
|
||||
}
|
||||
return cgFont
|
||||
}
|
||||
|
||||
fileprivate func obtainCTFont(font: MathFont, withSize size: CGFloat) -> CTFont {
|
||||
onDemandRegistration(mathFont: font)
|
||||
let fontSizePair = CTFontSizePair(font: font, size: size)
|
||||
let ctFont = threadSafeQueue.sync(execute: { ctFonts[fontSizePair] })
|
||||
guard ctFont == nil else { return ctFont! }
|
||||
guard let cgFont = threadSafeQueue.sync(execute: { cgFonts[font] }) else {
|
||||
fatalError("\(#function) unable to locate CGFont \(font.fontName) to create CTFont")
|
||||
}
|
||||
//Note: ctfont creation and caching is now threadsafe.
|
||||
guard threadSafeQueue.sync(execute: { ctFonts[fontSizePair] }) == nil else { return ctFonts[fontSizePair]! }
|
||||
return threadSafeQueue.sync(flags: .barrier, execute: {
|
||||
if let ctfont = ctFonts[fontSizePair] {
|
||||
return ctfont
|
||||
} else {
|
||||
let result = CTFontCreateWithGraphicsFont(cgFont, size, nil, nil)
|
||||
ctFonts[fontSizePair] = result
|
||||
return result
|
||||
}
|
||||
})
|
||||
}
|
||||
fileprivate func obtainRawMathTable(font: MathFont) -> NSDictionary {
|
||||
onDemandRegistration(mathFont: font)
|
||||
guard let mathTable = threadSafeQueue.sync(execute: { rawMathTables[font] } ) else {
|
||||
fatalError("\(#function) unable to locate mathTable: \(font.rawValue).plist")
|
||||
}
|
||||
return mathTable
|
||||
}
|
||||
deinit {
|
||||
ctFonts.removeAll()
|
||||
var errorRef: Unmanaged<CFError>? = nil
|
||||
cgFonts.values.forEach { cgFont in
|
||||
CTFontManagerUnregisterGraphicsFont(cgFont, &errorRef)
|
||||
}
|
||||
cgFonts.removeAll()
|
||||
}
|
||||
public enum FontError: Error {
|
||||
case invalidFontFile
|
||||
case fontPathNotFound
|
||||
case initFontError
|
||||
case registerFailed
|
||||
case invalidMathTable
|
||||
}
|
||||
|
||||
private struct CTFontSizePair: Hashable {
|
||||
let font: MathFont
|
||||
let size: CGFloat
|
||||
}
|
||||
}
|
||||
123
Sources/SwiftUIMath/MathBundle/MathImage.swift
Executable file
123
Sources/SwiftUIMath/MathBundle/MathImage.swift
Executable file
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// MathImage.swift
|
||||
//
|
||||
//
|
||||
// Created by Peter Tang on 15/9/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if os(iOS) || os(visionOS)
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
public struct MathImage {
|
||||
public var font: MathFont = .latinModernFont
|
||||
public var fontSize: CGFloat
|
||||
public var textColor: MTColor
|
||||
|
||||
public var labelMode: MTMathUILabelMode
|
||||
public var textAlignment: MTTextAlignment
|
||||
|
||||
public var contentInsets: MTEdgeInsets = MTEdgeInsetsZero
|
||||
|
||||
public let latex: String
|
||||
|
||||
private(set) var intrinsicContentSize = CGSize.zero
|
||||
|
||||
public init(latex: String, fontSize: CGFloat, textColor: MTColor, labelMode: MTMathUILabelMode = .display, textAlignment: MTTextAlignment = .center) {
|
||||
self.latex = latex
|
||||
self.fontSize = fontSize
|
||||
self.textColor = textColor
|
||||
self.labelMode = labelMode
|
||||
self.textAlignment = textAlignment
|
||||
}
|
||||
}
|
||||
extension MathImage {
|
||||
public var currentStyle: MTLineStyle {
|
||||
switch labelMode {
|
||||
case .display: return .display
|
||||
case .text: return .text
|
||||
}
|
||||
}
|
||||
private func intrinsicContentSize(_ displayList: MTMathListDisplay) -> CGSize {
|
||||
CGSize(width: displayList.width + contentInsets.left + contentInsets.right,
|
||||
height: displayList.ascent + displayList.descent + contentInsets.top + contentInsets.bottom)
|
||||
}
|
||||
public struct LayoutInfo {
|
||||
public var ascent: CGFloat = 0
|
||||
public var descent: CGFloat = 0
|
||||
|
||||
public init(ascent: CGFloat, descent: CGFloat) {
|
||||
self.ascent = ascent
|
||||
self.descent = descent
|
||||
}
|
||||
}
|
||||
public mutating func asImage() -> (NSError?, MTImage?, LayoutInfo?) {
|
||||
func layoutImage(size: CGSize, displayList: MTMathListDisplay) {
|
||||
var textX = CGFloat(0)
|
||||
switch self.textAlignment {
|
||||
case .left: textX = contentInsets.left
|
||||
case .center: textX = (size.width - contentInsets.left - contentInsets.right - displayList.width) / 2 + contentInsets.left
|
||||
case .right: textX = size.width - displayList.width - contentInsets.right
|
||||
}
|
||||
let availableHeight = 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 = CGPoint(x: textX, y: textY)
|
||||
}
|
||||
var error: NSError?
|
||||
let mtfont: MTFont? = font.mtfont(size: fontSize)
|
||||
|
||||
guard let mathList = MTMathListBuilder.build(fromString: latex, error: &error), error == nil,
|
||||
let displayList = MTTypesetter.createLineForMathList(mathList, font: mtfont, style: currentStyle) else {
|
||||
return (error, nil, nil)
|
||||
}
|
||||
|
||||
intrinsicContentSize = intrinsicContentSize(displayList)
|
||||
displayList.textColor = textColor
|
||||
|
||||
let size = intrinsicContentSize.regularized
|
||||
layoutImage(size: size, displayList: displayList)
|
||||
|
||||
#if os(iOS) || os(visionOS)
|
||||
let renderer = UIGraphicsImageRenderer(size: size)
|
||||
let image = renderer.image { rendererContext in
|
||||
rendererContext.cgContext.saveGState()
|
||||
rendererContext.cgContext.concatenate(.flippedVertically(size.height))
|
||||
displayList.draw(rendererContext.cgContext)
|
||||
rendererContext.cgContext.restoreGState()
|
||||
}
|
||||
return (nil, image, LayoutInfo(ascent: displayList.ascent, descent: displayList.descent))
|
||||
#endif
|
||||
#if os(macOS)
|
||||
let image = NSImage(size: size, flipped: false) { bounds in
|
||||
guard let context = NSGraphicsContext.current?.cgContext else { return false }
|
||||
context.saveGState()
|
||||
displayList.draw(context)
|
||||
context.restoreGState()
|
||||
return true
|
||||
}
|
||||
return (nil, image, LayoutInfo(ascent: displayList.ascent, descent: displayList.descent))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
private extension CGAffineTransform {
|
||||
static func flippedVertically(_ height: CGFloat) -> CGAffineTransform {
|
||||
var transform = CGAffineTransform(scaleX: 1, y: -1)
|
||||
transform = transform.translatedBy(x: 0, y: -height)
|
||||
return transform
|
||||
}
|
||||
}
|
||||
extension CGSize {
|
||||
fileprivate var regularized: CGSize {
|
||||
CGSize(width: ceil(width), height: ceil(height))
|
||||
}
|
||||
}
|
||||
35
Sources/SwiftUIMath/MathRender/MTBezierPath.swift
Executable file
35
Sources/SwiftUIMath/MathRender/MTBezierPath.swift
Executable file
@@ -0,0 +1,35 @@
|
||||
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by 安志钢.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
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
|
||||
|
||||
28
Sources/SwiftUIMath/MathRender/MTColor.swift
Executable file
28
Sources/SwiftUIMath/MathRender/MTColor.swift
Executable file
@@ -0,0 +1,28 @@
|
||||
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by Markus Sähn.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension MTColor {
|
||||
|
||||
public convenience init?(fromHexString hexString:String) {
|
||||
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)
|
||||
self.init(red: CGFloat((rgbValue & 0xFF0000) >> 16)/255.0,
|
||||
green: CGFloat((rgbValue & 0xFF00) >> 8)/255.0,
|
||||
blue: CGFloat((rgbValue & 0xFF))/255.0,
|
||||
alpha: 1.0)
|
||||
}
|
||||
|
||||
}
|
||||
40
Sources/SwiftUIMath/MathRender/MTConfig.swift
Executable file
40
Sources/SwiftUIMath/MathRender/MTConfig.swift
Executable file
@@ -0,0 +1,40 @@
|
||||
import Foundation
|
||||
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by 安志钢.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
#if os(iOS) || os(visionOS)
|
||||
|
||||
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
|
||||
public typealias MTImage = UIImage
|
||||
|
||||
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
|
||||
public typealias MTImage = NSImage
|
||||
|
||||
let MTEdgeInsetsZero = NSEdgeInsets.init(top: 0, left: 0, bottom: 0, right: 0)
|
||||
func MTGraphicsGetCurrentContext() -> CGContext? { NSGraphicsContext.current?.cgContext }
|
||||
|
||||
#endif
|
||||
80
Sources/SwiftUIMath/MathRender/MTFont.swift
Normal file
80
Sources/SwiftUIMath/MathRender/MTFont.swift
Normal file
@@ -0,0 +1,80 @@
|
||||
|
||||
import Foundation
|
||||
import CoreText
|
||||
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by Kostub Deshmukh.
|
||||
//
|
||||
// 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?
|
||||
|
||||
/// Fallback font for characters not supported by the main math font.
|
||||
/// Defaults to the system font at the same size. This is particularly useful
|
||||
/// for rendering text in \text{} commands with characters outside the math font's coverage
|
||||
/// (e.g., Chinese, Japanese, Korean, emoji, etc.)
|
||||
public var fallbackFont: CTFont?
|
||||
|
||||
init() {}
|
||||
|
||||
/// `MTFont(fontWithName:)` 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.
|
||||
convenience init(fontWithName name: String, size:CGFloat) {
|
||||
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")!)!
|
||||
}
|
||||
|
||||
/** Returns a copy of this font but with a different size. */
|
||||
public 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)
|
||||
}
|
||||
|
||||
/** The size of this font in points. */
|
||||
public var fontSize:CGFloat { CTFontGetSize(self.ctFont) }
|
||||
|
||||
deinit {
|
||||
self.ctFont = nil
|
||||
self.defaultCGFont = nil
|
||||
}
|
||||
|
||||
}
|
||||
93
Sources/SwiftUIMath/MathRender/MTFontManager.swift
Executable file
93
Sources/SwiftUIMath/MathRender/MTFontManager.swift
Executable file
@@ -0,0 +1,93 @@
|
||||
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by Kostub Deshmukh.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class MTFontManager {
|
||||
|
||||
static public private(set) var manager: MTFontManager = {
|
||||
MTFontManager()
|
||||
}()
|
||||
|
||||
let kDefaultFontSize = CGFloat(20)
|
||||
|
||||
static var fontManager : MTFontManager {
|
||||
return manager
|
||||
}
|
||||
|
||||
public init() { }
|
||||
|
||||
@RWLocked
|
||||
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 kpMathLightFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "KpMath-Light", size: size)
|
||||
}
|
||||
|
||||
public func kpMathSansFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "KpMath-Sans", 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 func asanaFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "Asana-Math", size: size)
|
||||
}
|
||||
|
||||
public func eulerFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "Euler-Math", size: size)
|
||||
}
|
||||
|
||||
public func firaRegularFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "FiraMath-Regular", size: size)
|
||||
}
|
||||
|
||||
public func notoSansRegularFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "NotoSansMath-Regular", size: size)
|
||||
}
|
||||
|
||||
public func libertinusRegularFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "LibertinusMath-Regular", size: size)
|
||||
}
|
||||
|
||||
public func garamondMathFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "Garamond-Math", size: size)
|
||||
}
|
||||
|
||||
public func leteSansFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "LeteSansMath", size: size)
|
||||
}
|
||||
|
||||
public var defaultFont: MTFont? {
|
||||
MTFontManager.fontManager.latinModernFont(withSize: kDefaultFontSize)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
322
Sources/SwiftUIMath/MathRender/MTFontMathTable.swift
Executable file
322
Sources/SwiftUIMath/MathRender/MTFontMathTable.swift
Executable file
@@ -0,0 +1,322 @@
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by Kostub Deshmukh.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreText
|
||||
|
||||
struct 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.
|
||||
public private(set) weak var font:MTFont? // @property (nonatomic, readonly, weak) MTFont* font;
|
||||
|
||||
var _unitsPerEm: UInt
|
||||
var _fontSize: CGFloat
|
||||
var _mathTable: NSDictionary!
|
||||
|
||||
let kConstants = "constants"
|
||||
|
||||
/** MU unit in points */
|
||||
var muUnit:CGFloat { _fontSize/18 }
|
||||
|
||||
func fontUnitsToPt(_ fontUnits:Int) -> CGFloat {
|
||||
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 { 1.01 * _fontSize }
|
||||
|
||||
/// Modified constant from 2.4 to 2.39, it matches KaTeX and looks better.
|
||||
var fractionDelimiterDisplayStyleSize: CGFloat { 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 || variantGlyphs?.count == 0 {
|
||||
// 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 || variantGlyphs?.count == 0 {
|
||||
// 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)
|
||||
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?
|
||||
var 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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
37
Sources/SwiftUIMath/MathRender/MTLabel.swift
Executable file
37
Sources/SwiftUIMath/MathRender/MTLabel.swift
Executable file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by 安志钢.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// MARK: - Customized getter and setter methods for property text.
|
||||
var text:String? {
|
||||
get { super.stringValue }
|
||||
set { super.stringValue = newValue! }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
960
Sources/SwiftUIMath/MathRender/MTMathAtomFactory.swift
Normal file
960
Sources/SwiftUIMath/MathRender/MTMathAtomFactory.swift
Normal file
@@ -0,0 +1,960 @@
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by Kostub Deshmukh.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/** A factory to create commonly used MTMathAtoms. */
|
||||
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}"
|
||||
]
|
||||
|
||||
private static let delimValueLock = NSLock()
|
||||
static var _delimValueToName = [String: String]()
|
||||
public static var delimValueToName: [String: String] {
|
||||
if _delimValueToName.isEmpty {
|
||||
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
|
||||
}
|
||||
// protect lazily loading table in a multi-thread concurrent environment
|
||||
delimValueLock.lock()
|
||||
defer { delimValueLock.unlock() }
|
||||
if _delimValueToName.isEmpty {
|
||||
_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}"
|
||||
]
|
||||
|
||||
private static let accentValueLock = NSLock()
|
||||
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
|
||||
}
|
||||
// protect lazily loading table in a multi-thread concurrent environment
|
||||
accentValueLock.lock()
|
||||
defer { accentValueLock.unlock() }
|
||||
if _accentValueToName == nil {
|
||||
_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{226B}"),
|
||||
"ll" : MTMathAtom(type: .relation, value: "\u{226A}"),
|
||||
"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}"),
|
||||
"implies" : MTMathAtom(type: .relation, value: "\u{27F9}"),
|
||||
|
||||
// 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),
|
||||
"mod" : MTMathAtomFactory.operatorWithName("mod", 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),
|
||||
"iint" : MTMathAtomFactory.operatorWithName( "\u{222C}", limits: false),
|
||||
"iiint" : MTMathAtomFactory.operatorWithName( "\u{222D}", limits: false),
|
||||
"iiiint" : MTMathAtomFactory.operatorWithName( "\u{2A0C}", 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}"),
|
||||
"aa" : MTMathAtom(type: .ordinary, value: "\u{00E5}"), // NEW å
|
||||
"ae" : MTMathAtom(type: .ordinary, value: "\u{00E6}"), // NEW æ
|
||||
"o" : MTMathAtom(type: .ordinary, value: "\u{00F8}"), // NEW ø
|
||||
"oe" : MTMathAtom(type: .ordinary, value: "\u{0153}"), // NEW œ
|
||||
"ss" : MTMathAtom(type: .ordinary, value: "\u{00DF}"), // NEW ß
|
||||
"cc" : MTMathAtom(type: .ordinary, value: "\u{00E7}"), // NEW ç
|
||||
"CC" : MTMathAtom(type: .ordinary, value: "\u{00C7}"), // NEW Ç
|
||||
"O" : MTMathAtom(type: .ordinary, value: "\u{00D8}"), // NEW Ø
|
||||
"AE" : MTMathAtom(type: .ordinary, value: "\u{00C6}"), // NEW Æ
|
||||
"OE" : MTMathAtom(type: .ordinary, value: "\u{0152}"), // NEW Œ
|
||||
"|" : 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}"),
|
||||
"nexists" : MTMathAtom(type: .ordinary, value: "\u{2204}"),
|
||||
"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}"),
|
||||
"upquote" : MTMathAtom(type: .ordinary, value: "\u{0027}"),
|
||||
"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),
|
||||
]
|
||||
|
||||
static var supportedAccentedCharacters: [Character: (String, String)] = [
|
||||
// Acute accents
|
||||
"á": ("acute", "a"), "é": ("acute", "e"), "í": ("acute", "i"),
|
||||
"ó": ("acute", "o"), "ú": ("acute", "u"), "ý": ("acute", "y"),
|
||||
|
||||
// Grave accents
|
||||
"à": ("grave", "a"), "è": ("grave", "e"), "ì": ("grave", "i"),
|
||||
"ò": ("grave", "o"), "ù": ("grave", "u"),
|
||||
|
||||
// Circumflex
|
||||
"â": ("hat", "a"), "ê": ("hat", "e"), "î": ("hat", "i"),
|
||||
"ô": ("hat", "o"), "û": ("hat", "u"),
|
||||
|
||||
// Umlaut/dieresis
|
||||
"ä": ("ddot", "a"), "ë": ("ddot", "e"), "ï": ("ddot", "i"),
|
||||
"ö": ("ddot", "o"), "ü": ("ddot", "u"), "ÿ": ("ddot", "y"),
|
||||
|
||||
// Tilde
|
||||
"ã": ("tilde", "a"), "ñ": ("tilde", "n"), "õ": ("tilde", "o"),
|
||||
|
||||
// Special characters
|
||||
"ç": ("cc", ""), "ø": ("o", ""), "å": ("aa", ""), "æ": ("ae", ""),
|
||||
"œ": ("oe", ""), "ß": ("ss", ""),
|
||||
"'": ("upquote", ""), // this may be dangerous in math mode
|
||||
|
||||
// Upper case variants
|
||||
"Á": ("acute", "A"), "É": ("acute", "E"), "Í": ("acute", "I"),
|
||||
"Ó": ("acute", "O"), "Ú": ("acute", "U"), "Ý": ("acute", "Y"),
|
||||
"À": ("grave", "A"), "È": ("grave", "E"), "Ì": ("grave", "I"),
|
||||
"Ò": ("grave", "O"), "Ù": ("grave", "U"),
|
||||
"Â": ("hat", "A"), "Ê": ("hat", "E"), "Î": ("hat", "I"),
|
||||
"Ô": ("hat", "O"), "Û": ("hat", "U"),
|
||||
"Ä": ("ddot", "A"), "Ë": ("ddot", "E"), "Ï": ("ddot", "I"),
|
||||
"Ö": ("ddot", "O"), "Ü": ("ddot", "U"),
|
||||
"Ã": ("tilde", "A"), "Ñ": ("tilde", "N"), "Õ": ("tilde", "O"),
|
||||
"Ç": ("CC", ""),
|
||||
"Ø": ("O", ""),
|
||||
"Å": ("AA", ""),
|
||||
"Æ": ("AE", ""),
|
||||
"Œ": ("OE", ""),
|
||||
]
|
||||
|
||||
private static let textToLatexLock = NSLock()
|
||||
static var _textToLatexSymbolName: [String: String]? = nil
|
||||
public static 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
|
||||
}
|
||||
// protect lazily loading table in a multi-thread concurrent environment
|
||||
textToLatexLock.lock()
|
||||
defer { textToLatexLock.unlock() }
|
||||
if self._textToLatexSymbolName == nil {
|
||||
self._textToLatexSymbolName = output
|
||||
}
|
||||
}
|
||||
return self._textToLatexSymbolName!
|
||||
}
|
||||
// make textToLatexSymbolName readonly (allows internal load)
|
||||
// entries can be lazily added with NSLock protection.
|
||||
// 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? {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an atom for the multiplication sign (i.e., \times or "*")
|
||||
public static func times() -> MTMathAtom {
|
||||
MTMathAtom(type: .binaryOperator, value: UnicodeSymbol.multiplication)
|
||||
}
|
||||
|
||||
/// Returns an atom for the division sign (i.e., \div or "/")
|
||||
public static func divide() -> MTMathAtom {
|
||||
MTMathAtom(type: .binaryOperator, value: UnicodeSymbol.division)
|
||||
}
|
||||
|
||||
/// Returns an atom which is a placeholder square
|
||||
public static func placeholder() -> MTMathAtom {
|
||||
MTMathAtom(type: .placeholder, value: UnicodeSymbol.whiteSquare)
|
||||
}
|
||||
|
||||
/** Returns a fraction with a placeholder for the numerator and denominator */
|
||||
public static func placeholderFraction() -> MTFraction {
|
||||
let frac = MTFraction()
|
||||
frac.numerator = MTMathList()
|
||||
frac.numerator?.add(placeholder())
|
||||
frac.denominator = MTMathList()
|
||||
frac.denominator?.add(placeholder())
|
||||
return frac
|
||||
}
|
||||
|
||||
/** Returns a square root with a placeholder as the radicand. */
|
||||
public static func placeholderSquareRoot() -> MTRadical {
|
||||
let rad = MTRadical()
|
||||
rad.radicand = MTMathList()
|
||||
rad.radicand?.add(placeholder())
|
||||
return rad
|
||||
}
|
||||
|
||||
/** Returns a radical with a placeholder as the radicand. */
|
||||
public static func placeholderRadical() -> MTRadical {
|
||||
let rad = MTRadical()
|
||||
rad.radicand = MTMathList()
|
||||
rad.degree = MTMathList()
|
||||
rad.radicand?.add(placeholder())
|
||||
rad.degree?.add(placeholder())
|
||||
return rad
|
||||
}
|
||||
|
||||
public static func atom(fromAccentedCharacter ch: Character) -> MTMathAtom? {
|
||||
if let symbol = supportedAccentedCharacters[ch] {
|
||||
// first handle any special characters
|
||||
if let atom = atom(forLatexSymbol: symbol.0) {
|
||||
return atom
|
||||
}
|
||||
|
||||
if let accent = MTMathAtomFactory.accent(withName: symbol.0) {
|
||||
// The command is an accent
|
||||
let list = MTMathList()
|
||||
let ch = Array(symbol.1)[0]
|
||||
list.add(atom(forCharacter: ch))
|
||||
accent.innerList = list
|
||||
return accent
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
/** 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, including those with accents, 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}":
|
||||
// Cyrillic alphabet
|
||||
return MTMathAtom(type: .ordinary, value: chStr)
|
||||
case _ where supportedAccentedCharacters.keys.contains(ch):
|
||||
// support for áéíóúýàèìòùâêîôûäëïöüÿãñõçøåæœß'ÁÉÍÓÚÝÀÈÌÒÙÂÊÎÔÛÄËÏÖÜÃÑÕÇØÅÆŒ
|
||||
return atom(fromAccentedCharacter: ch)
|
||||
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 `atom(forCharacter:)` 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? {
|
||||
guard !atom.nucleus.isEmpty else { return nil }
|
||||
return Self.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.add(latexSymbol:"lcm", value:MTMathAtomFactory.operatorWithName("lcm", limits: false))` */
|
||||
public static func add(latexSymbol name: String, value: MTMathAtom) {
|
||||
let _ = Self.textToLatexSymbolName
|
||||
// above force textToLatexSymbolName to initialise first, _textToLatexSymbolName also initialized.
|
||||
// protect lazily loading table in a multi-thread concurrent environment
|
||||
textToLatexLock.lock()
|
||||
defer { textToLatexLock.unlock() }
|
||||
supportedLatexSymbols[name] = value
|
||||
Self._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? {
|
||||
guard boundary.type == .boundary else { return nil }
|
||||
return Self.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 converts the strings to a `MTFraction`. */
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
static let matrixEnvs = [
|
||||
"matrix": [],
|
||||
"pmatrix": ["(", ")"],
|
||||
"bmatrix": ["[", "]"],
|
||||
"Bmatrix": ["{", "}"],
|
||||
"vmatrix": ["vert", "vert"],
|
||||
"Vmatrix": ["Vert", "Vert"],
|
||||
"smallmatrix": [],
|
||||
// Starred versions with optional alignment
|
||||
"matrix*": [],
|
||||
"pmatrix*": ["(", ")"],
|
||||
"bmatrix*": ["[", "]"],
|
||||
"Bmatrix*": ["{", "}"],
|
||||
"vmatrix*": ["vert", "vert"],
|
||||
"Vmatrix*": ["Vert", "Vert"]
|
||||
]
|
||||
|
||||
/** Builds a table for a given environment with the given rows. Returns a `MTMathAtom` containing the
|
||||
table and any other atoms necessary for the given environment. Returns nil and sets error
|
||||
if the table could not be built.
|
||||
@param env The environment to use to build the table. If the env is nil, then the default table is built.
|
||||
@note The reason this function returns a `MTMathAtom` and not a `MTMathTable` is because some
|
||||
matrix environments are have builtin delimiters added to the table and hence are returned as inner atoms.
|
||||
*/
|
||||
public static func table(withEnvironment env: String?, alignment: MTColumnAlignment? = nil, 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"
|
||||
|
||||
// smallmatrix uses script style and tighter spacing for inline use
|
||||
let isSmallMatrix = (env == "smallmatrix")
|
||||
|
||||
table.interRowAdditionalSpacing = 0
|
||||
table.interColumnSpacing = isSmallMatrix ? 6 : 18
|
||||
|
||||
let style = MTMathStyle(style: isSmallMatrix ? .script : .text)
|
||||
|
||||
for i in 0..<table.cells.count {
|
||||
for j in 0..<table.cells[i].count {
|
||||
table.cells[i][j].insert(style, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply alignment for starred matrix environments
|
||||
if let align = alignment {
|
||||
for col in 0..<table.numColumns {
|
||||
table.set(alignment: align, forColumn: col)
|
||||
}
|
||||
}
|
||||
|
||||
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 >= 2 {
|
||||
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 != 1 && table.numColumns != 2 {
|
||||
let message = "cases environment can have 1 or 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)
|
||||
if table.numColumns == 2 {
|
||||
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
|
||||
}
|
||||
}
|
||||
117
Sources/SwiftUIMath/MathRender/MTMathImage.swift
Executable file
117
Sources/SwiftUIMath/MathRender/MTMathImage.swift
Executable file
@@ -0,0 +1,117 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Peter Tang on 12/9/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if os(iOS) || os(visionOS)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
public class MTMathImage {
|
||||
public var font: MTFont? = MTFontManager.fontManager.defaultFont
|
||||
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
|
||||
public let textColor: MTColor
|
||||
|
||||
public let labelMode: MTMathUILabelMode
|
||||
public let textAlignment: MTTextAlignment
|
||||
|
||||
public var contentInsets: MTEdgeInsets = MTEdgeInsetsZero
|
||||
|
||||
public let latex: String
|
||||
private(set) var intrinsicContentSize = CGSize.zero
|
||||
|
||||
public init(latex: String, fontSize: CGFloat, textColor: MTColor, labelMode: MTMathUILabelMode = .display, textAlignment: MTTextAlignment = .center) {
|
||||
self.latex = latex
|
||||
self.textColor = textColor
|
||||
self.labelMode = labelMode
|
||||
self.textAlignment = textAlignment
|
||||
self.fontSize = fontSize
|
||||
}
|
||||
}
|
||||
extension MTMathImage {
|
||||
public var currentStyle: MTLineStyle {
|
||||
switch labelMode {
|
||||
case .display: return .display
|
||||
case .text: return .text
|
||||
}
|
||||
}
|
||||
private func intrinsicContentSize(_ displayList: MTMathListDisplay) -> CGSize {
|
||||
CGSize(width: displayList.width + contentInsets.left + contentInsets.right,
|
||||
height: displayList.ascent + displayList.descent + contentInsets.top + contentInsets.bottom)
|
||||
}
|
||||
public func asImage() -> (NSError?, MTImage?) {
|
||||
func layoutImage(size: CGSize, displayList: MTMathListDisplay) {
|
||||
var textX = CGFloat(0)
|
||||
switch self.textAlignment {
|
||||
case .left: textX = contentInsets.left
|
||||
case .center: textX = (size.width - contentInsets.left - contentInsets.right - displayList.width) / 2 + contentInsets.left
|
||||
case .right: textX = size.width - displayList.width - contentInsets.right
|
||||
}
|
||||
let availableHeight = 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 = CGPoint(x: textX, y: textY)
|
||||
}
|
||||
|
||||
var error: NSError?
|
||||
guard let mathList = MTMathListBuilder.build(fromString: latex, error: &error), error == nil,
|
||||
let displayList = MTTypesetter.createLineForMathList(mathList, font: font, style: currentStyle) else {
|
||||
return (error, nil)
|
||||
}
|
||||
|
||||
intrinsicContentSize = intrinsicContentSize(displayList)
|
||||
displayList.textColor = textColor
|
||||
|
||||
let size = intrinsicContentSize
|
||||
layoutImage(size: size, displayList: displayList)
|
||||
|
||||
#if os(iOS) || os(visionOS)
|
||||
let renderer = UIGraphicsImageRenderer(size: size)
|
||||
let image = renderer.image { rendererContext in
|
||||
rendererContext.cgContext.saveGState()
|
||||
rendererContext.cgContext.concatenate(.flippedVertically(size.height))
|
||||
displayList.draw(rendererContext.cgContext)
|
||||
rendererContext.cgContext.restoreGState()
|
||||
}
|
||||
return (nil, image)
|
||||
#endif
|
||||
#if os(macOS)
|
||||
let image = NSImage(size: size, flipped: false) { bounds in
|
||||
guard let context = NSGraphicsContext.current?.cgContext else { return false }
|
||||
context.saveGState()
|
||||
displayList.draw(context)
|
||||
context.restoreGState()
|
||||
return true
|
||||
}
|
||||
return (nil, image)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
private extension CGAffineTransform {
|
||||
static func flippedVertically(_ height: CGFloat) -> CGAffineTransform {
|
||||
var transform = CGAffineTransform(scaleX: 1, y: -1)
|
||||
transform = transform.translatedBy(x: 0, y: -height)
|
||||
return transform
|
||||
}
|
||||
}
|
||||
1027
Sources/SwiftUIMath/MathRender/MTMathList.swift
Normal file
1027
Sources/SwiftUIMath/MathRender/MTMathList.swift
Normal file
File diff suppressed because it is too large
Load Diff
1364
Sources/SwiftUIMath/MathRender/MTMathListBuilder.swift
Normal file
1364
Sources/SwiftUIMath/MathRender/MTMathListBuilder.swift
Normal file
File diff suppressed because it is too large
Load Diff
831
Sources/SwiftUIMath/MathRender/MTMathListDisplay.swift
Executable file
831
Sources/SwiftUIMath/MathRender/MTMathListDisplay.swift
Executable file
@@ -0,0 +1,831 @@
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by Kostub Deshmukh.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import QuartzCore
|
||||
import CoreText
|
||||
import SwiftUI
|
||||
|
||||
func isIos6Supported() -> Bool {
|
||||
if !MTDisplay.initialized {
|
||||
#if os(iOS) || os(visionOS)
|
||||
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
|
||||
}
|
||||
|
||||
// The Downshift protocol allows an MTDisplay to be shifted down by a given amount.
|
||||
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 {
|
||||
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) || os(visionOS)
|
||||
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
|
||||
public var ascent:CGFloat = 0
|
||||
/// The distance from the axis to the bottom of the display
|
||||
public var descent:CGFloat = 0
|
||||
/// The width of the display
|
||||
public 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
|
||||
public var range:NSRange=NSMakeRange(0, 0)
|
||||
/// Whether the display has a subscript/superscript following it.
|
||||
public 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?
|
||||
|
||||
}
|
||||
|
||||
/// Special class to be inherited from that implements the DownShift protocol
|
||||
class MTDisplayDS : MTDisplay, DownShift {
|
||||
|
||||
var shiftDown: CGFloat = 0
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MTCTLineDisplay
|
||||
|
||||
/// A rendering of a single CTLine as an MTDisplay
|
||||
public class MTCTLineDisplay : MTDisplay {
|
||||
|
||||
/// The CTLine being displayed
|
||||
public 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
|
||||
public fileprivate(set) 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 {
|
||||
|
||||
/**
|
||||
The type of position for a line, i.e. subscript/superscript or regular.
|
||||
*/
|
||||
public enum LinePosition : Int {
|
||||
/// Regular
|
||||
case regular
|
||||
/// Positioned at a subscript
|
||||
case ssubscript
|
||||
/// Positioned at a superscript
|
||||
case superscript
|
||||
}
|
||||
|
||||
/// Where the line is positioned
|
||||
public var type:LinePosition = .regular
|
||||
/// An array of MTDisplays which are positioned relative to the position of the
|
||||
/// the current display.
|
||||
public fileprivate(set) var subDisplays = [MTDisplay]()
|
||||
/// If a subscript or superscript this denotes the location in the parent MTList. For a
|
||||
/// regular list this is NSNotFound
|
||||
public 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
|
||||
public 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.
|
||||
*/
|
||||
public fileprivate(set) 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.
|
||||
*/
|
||||
public fileprivate(set) 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 public var ascent:CGFloat {
|
||||
set { super.ascent = newValue }
|
||||
get { numerator!.ascent + self.numeratorUp }
|
||||
}
|
||||
|
||||
override public var descent:CGFloat {
|
||||
set { super.descent = newValue }
|
||||
get { denominator!.descent + self.denominatorDown }
|
||||
}
|
||||
|
||||
override public 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
|
||||
// Note: line thickness of 0 draws the thinnest possible line - we want no line so check for 0s
|
||||
if self.lineThickness > 0 {
|
||||
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.
|
||||
*/
|
||||
public fileprivate(set) 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.
|
||||
*/
|
||||
public fileprivate(set) 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 { super.ascent - self.shiftDown }
|
||||
}
|
||||
|
||||
override var descent:CGFloat {
|
||||
set { super.descent = newValue }
|
||||
get { 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 { super.ascent - self.shiftDown }
|
||||
}
|
||||
|
||||
override var descent:CGFloat {
|
||||
set { super.descent = newValue }
|
||||
get { 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.updateLowerLimitPosition() } }
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
197
Sources/SwiftUIMath/MathRender/MTMathListIndex.swift
Executable file
197
Sources/SwiftUIMath/MathRender/MTMathListIndex.swift
Executable file
@@ -0,0 +1,197 @@
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by Kostub Deshmukh.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* An index that points to a particular character in the MTMathList. The index is a LinkedList that represents
|
||||
* a path from the beginning of the MTMathList to reach a particular atom in the list. The next node of the path
|
||||
* is represented by the subIndex. The path terminates when the subIndex is nil.
|
||||
*
|
||||
* If there is a subIndex, the subIndexType denotes what branch the path takes (i.e. superscript, subscript,
|
||||
* numerator, denominator etc.).
|
||||
* e.g in the expression 25^{2/4} the index of the character 4 is represented as:
|
||||
* (1, superscript) -> (0, denominator) -> (0, none)
|
||||
* This can be interpreted as start at index 1 (i.e. the 5) go up to the superscript.
|
||||
* Then look at index 0 (i.e. 2/4) and go to the denominator. Then look up index 0 (i.e. the 4) which this final
|
||||
* index.
|
||||
*
|
||||
* The level of an index is the number of nodes in the LinkedList to get to the final path.
|
||||
*/
|
||||
public class MTMathListIndex {
|
||||
|
||||
/**
|
||||
The type of the subindex.
|
||||
|
||||
The type of the subindex denotes what branch the path to the atom that this index points to takes.
|
||||
*/
|
||||
public enum MTMathListSubIndexType: Int {
|
||||
/// The index denotes the whole atom, subIndex is nil.
|
||||
case none = 0
|
||||
/// The position in the subindex is an index into the nucleus
|
||||
case nucleus
|
||||
/// The subindex indexes into the superscript.
|
||||
case superscript
|
||||
/// The subindex indexes into the subscript
|
||||
case ssubscript
|
||||
/// The subindex indexes into the numerator (only valid for fractions)
|
||||
case numerator
|
||||
/// The subindex indexes into the denominator (only valid for fractions)
|
||||
case denominator
|
||||
/// The subindex indexes into the radicand (only valid for radicals)
|
||||
case radicand
|
||||
/// The subindex indexes into the degree (only valid for radicals)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the previous index if present. Returns `nil` if there is no previous index.
|
||||
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
|
||||
}
|
||||
|
||||
/// Returns the next index.
|
||||
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 { 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
|
||||
}
|
||||
}
|
||||
}
|
||||
383
Sources/SwiftUIMath/MathRender/MTMathUILabel.swift
Normal file
383
Sources/SwiftUIMath/MathRender/MTMathUILabel.swift
Normal file
@@ -0,0 +1,383 @@
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by Kostub Deshmukh.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
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?
|
||||
|
||||
/** The preferred maximum width (in points) for a multiline label.
|
||||
Set this property to enable line wrapping based on available width. */
|
||||
public var preferredMaxLayoutWidth: CGFloat {
|
||||
set {
|
||||
_preferredMaxLayoutWidth = newValue
|
||||
_displayList = nil // Clear cached display list when width constraint changes
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
get { _preferredMaxLayoutWidth }
|
||||
}
|
||||
private var _preferredMaxLayoutWidth: CGFloat = 0
|
||||
|
||||
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
|
||||
self.clipsToBounds = 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 }
|
||||
if self.font == nil { return }
|
||||
|
||||
// Ensure display list is created before drawing
|
||||
if _displayList == nil {
|
||||
_layoutSubviews()
|
||||
}
|
||||
|
||||
guard let displayList = _displayList else { return }
|
||||
|
||||
// drawing code
|
||||
let context = MTGraphicsGetCurrentContext()!
|
||||
context.saveGState()
|
||||
|
||||
displayList.draw(context)
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
func _layoutSubviews() {
|
||||
guard _mathList != nil && self.font != nil else {
|
||||
_displayList = nil
|
||||
errorLabel?.frame = self.bounds
|
||||
self.setNeedsDisplay()
|
||||
return
|
||||
}
|
||||
// Ensure we have a valid font before attempting to typeset
|
||||
if self.font == nil {
|
||||
// No valid font - try to get default font
|
||||
if let defaultFont = MTFontManager.fontManager.defaultFont {
|
||||
self._font = defaultFont
|
||||
} else {
|
||||
// Cannot typeset without a font, clear display list
|
||||
_displayList = nil
|
||||
errorLabel?.frame = self.bounds
|
||||
self.setNeedsDisplay()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Use the effective width for layout
|
||||
let effectiveWidth = _preferredMaxLayoutWidth > 0 ? _preferredMaxLayoutWidth : bounds.size.width
|
||||
let availableWidth = effectiveWidth - contentInsets.left - contentInsets.right
|
||||
|
||||
_displayList = MTTypesetter.createLineForMathList(_mathList, font: self.font, style: currentStyle, maxWidth: availableWidth)
|
||||
|
||||
_displayList!.textColor = textColor
|
||||
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)
|
||||
errorLabel?.frame = self.bounds
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
|
||||
func _sizeThatFits(_ size:CGSize) -> CGSize {
|
||||
guard _mathList != nil else {
|
||||
// No content - return no-intrinsic-size marker
|
||||
return CGSize(width: -1, height: -1)
|
||||
}
|
||||
|
||||
// Ensure we have a valid font before attempting to typeset
|
||||
if self.font == nil {
|
||||
// No valid font - try to get default font
|
||||
if let defaultFont = MTFontManager.fontManager.defaultFont {
|
||||
self._font = defaultFont
|
||||
} else {
|
||||
// Cannot typeset without a font
|
||||
return CGSize(width: -1, height: -1)
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the maximum width to use
|
||||
var maxWidth: CGFloat = 0
|
||||
if _preferredMaxLayoutWidth > 0 {
|
||||
maxWidth = _preferredMaxLayoutWidth - contentInsets.left - contentInsets.right
|
||||
} else if size.width > 0 {
|
||||
maxWidth = size.width - contentInsets.left - contentInsets.right
|
||||
}
|
||||
|
||||
var displayList:MTMathListDisplay? = nil
|
||||
displayList = MTTypesetter.createLineForMathList(_mathList, font: self.font, style: currentStyle, maxWidth: maxWidth)
|
||||
|
||||
guard displayList != nil else {
|
||||
// Failed to create display list
|
||||
return CGSize(width: -1, height: -1)
|
||||
}
|
||||
|
||||
var resultWidth = displayList!.width + contentInsets.left + contentInsets.right
|
||||
let resultHeight = displayList!.ascent + displayList!.descent + contentInsets.top + contentInsets.bottom
|
||||
|
||||
// Ensure we don't exceed the width constraints
|
||||
if _preferredMaxLayoutWidth > 0 && resultWidth > _preferredMaxLayoutWidth {
|
||||
resultWidth = _preferredMaxLayoutWidth
|
||||
} else if _preferredMaxLayoutWidth == 0 && size.width > 0 && resultWidth > size.width {
|
||||
resultWidth = size.width
|
||||
}
|
||||
|
||||
return CGSize(width: resultWidth, height: resultHeight)
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
public func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
return _sizeThatFits(size)
|
||||
}
|
||||
#else
|
||||
public override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
return _sizeThatFits(size)
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
func setNeedsDisplay() { self.needsDisplay = true }
|
||||
func setNeedsLayout() { self.needsLayout = true }
|
||||
public override var fittingSize: CGSize { _sizeThatFits(CGSizeZero) }
|
||||
public override var intrinsicContentSize: CGSize { _sizeThatFits(CGSizeZero) }
|
||||
override public func layout() {
|
||||
self._layoutSubviews()
|
||||
super.layout()
|
||||
}
|
||||
#else
|
||||
public override var intrinsicContentSize: CGSize { _sizeThatFits(CGSizeZero) }
|
||||
override public func layoutSubviews() { _layoutSubviews() }
|
||||
#endif
|
||||
|
||||
}
|
||||
2644
Sources/SwiftUIMath/MathRender/MTTypesetter.swift
Normal file
2644
Sources/SwiftUIMath/MathRender/MTTypesetter.swift
Normal file
File diff suppressed because it is too large
Load Diff
89
Sources/SwiftUIMath/MathRender/MTUnicode.swift
Executable file
89
Sources/SwiftUIMath/MathRender/MTUnicode.swift
Executable file
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by Kostub Deshmukh.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
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 }
|
||||
}
|
||||
58
Sources/SwiftUIMath/MathRender/RWLock.swift
Normal file
58
Sources/SwiftUIMath/MathRender/RWLock.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
import Foundation
|
||||
|
||||
final class RWLock {
|
||||
init() {
|
||||
pthread_rwlock_init(&lock, nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
pthread_rwlock_destroy(&lock)
|
||||
}
|
||||
|
||||
func read<T>(_ block: () -> T) -> T {
|
||||
pthread_rwlock_rdlock(&lock)
|
||||
defer { pthread_rwlock_unlock(&lock) }
|
||||
return block()
|
||||
}
|
||||
|
||||
func readWrite<T>(_ block: () -> T) -> T {
|
||||
pthread_rwlock_wrlock(&lock)
|
||||
defer { pthread_rwlock_unlock(&lock) }
|
||||
return block()
|
||||
}
|
||||
|
||||
private var lock = pthread_rwlock_t()
|
||||
}
|
||||
|
||||
@propertyWrapper
|
||||
struct RWLocked<T> {
|
||||
init(wrappedValue: T) {
|
||||
value = wrappedValue
|
||||
}
|
||||
|
||||
var wrappedValue: T {
|
||||
get {
|
||||
lock.read {
|
||||
value
|
||||
}
|
||||
}
|
||||
set {
|
||||
lock.readWrite {
|
||||
value = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
mutating func readWrite(_ block: (inout T) -> Void) -> (oldValue: T, newValue: T) {
|
||||
lock.readWrite {
|
||||
let old = value
|
||||
block(&value)
|
||||
return (old, value)
|
||||
}
|
||||
}
|
||||
|
||||
private var value: T
|
||||
private let lock = RWLock()
|
||||
}
|
||||
|
||||
BIN
Sources/SwiftUIMath/mathFonts.bundle/Asana-Math.otf
Executable file
BIN
Sources/SwiftUIMath/mathFonts.bundle/Asana-Math.otf
Executable file
Binary file not shown.
4343
Sources/SwiftUIMath/mathFonts.bundle/Asana-Math.plist
Executable file
4343
Sources/SwiftUIMath/mathFonts.bundle/Asana-Math.plist
Executable file
File diff suppressed because it is too large
Load Diff
BIN
Sources/SwiftUIMath/mathFonts.bundle/Euler-Math.otf
Executable file
BIN
Sources/SwiftUIMath/mathFonts.bundle/Euler-Math.otf
Executable file
Binary file not shown.
2165
Sources/SwiftUIMath/mathFonts.bundle/Euler-Math.plist
Executable file
2165
Sources/SwiftUIMath/mathFonts.bundle/Euler-Math.plist
Executable file
File diff suppressed because it is too large
Load Diff
BIN
Sources/SwiftUIMath/mathFonts.bundle/FiraMath-Regular.otf
Executable file
BIN
Sources/SwiftUIMath/mathFonts.bundle/FiraMath-Regular.otf
Executable file
Binary file not shown.
3064
Sources/SwiftUIMath/mathFonts.bundle/FiraMath-Regular.plist
Executable file
3064
Sources/SwiftUIMath/mathFonts.bundle/FiraMath-Regular.plist
Executable file
File diff suppressed because it is too large
Load Diff
30
Sources/SwiftUIMath/mathFonts.bundle/GUST-FONT-LICENSE.txt
Executable file
30
Sources/SwiftUIMath/mathFonts.bundle/GUST-FONT-LICENSE.txt
Executable file
@@ -0,0 +1,30 @@
|
||||
% This is a preliminary version (2006-09-30), barring acceptance from
|
||||
% the LaTeX Project Team and other feedback, of the GUST Font License.
|
||||
% (GUST is the Polish TeX Users Group, http://www.gust.org.pl)
|
||||
%
|
||||
% For the most recent version of this license see
|
||||
% http://www.gust.org.pl/fonts/licenses/GUST-FONT-LICENSE.txt
|
||||
% or
|
||||
% http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt
|
||||
%
|
||||
% This work may be distributed and/or modified under the conditions
|
||||
% of the LaTeX Project Public License, either version 1.3c of this
|
||||
% license or (at your option) any later version.
|
||||
%
|
||||
% Please also observe the following clause:
|
||||
% 1) it is requested, but not legally required, that derived works be
|
||||
% distributed only after changing the names of the fonts comprising this
|
||||
% work and given in an accompanying "manifest", and that the
|
||||
% files comprising the Work, as listed in the manifest, also be given
|
||||
% new names. Any exceptions to this request are also given in the
|
||||
% manifest.
|
||||
%
|
||||
% We recommend the manifest be given in a separate file named
|
||||
% MANIFEST-<fontid>.txt, where <fontid> is some unique identification
|
||||
% of the font family. If a separate "readme" file accompanies the Work,
|
||||
% we recommend a name of the form README-<fontid>.txt.
|
||||
%
|
||||
% The latest version of the LaTeX Project Public License is in
|
||||
% http://www.latex-project.org/lppl.txt and version 1.3c or later
|
||||
% is part of all distributions of LaTeX version 2006/05/20 or later.
|
||||
|
||||
BIN
Sources/SwiftUIMath/mathFonts.bundle/Garamond-Math.otf
Executable file
BIN
Sources/SwiftUIMath/mathFonts.bundle/Garamond-Math.otf
Executable file
Binary file not shown.
7910
Sources/SwiftUIMath/mathFonts.bundle/Garamond-Math.plist
Executable file
7910
Sources/SwiftUIMath/mathFonts.bundle/Garamond-Math.plist
Executable file
File diff suppressed because it is too large
Load Diff
BIN
Sources/SwiftUIMath/mathFonts.bundle/KpMath-Light.otf
Executable file
BIN
Sources/SwiftUIMath/mathFonts.bundle/KpMath-Light.otf
Executable file
Binary file not shown.
6501
Sources/SwiftUIMath/mathFonts.bundle/KpMath-Light.plist
Executable file
6501
Sources/SwiftUIMath/mathFonts.bundle/KpMath-Light.plist
Executable file
File diff suppressed because it is too large
Load Diff
BIN
Sources/SwiftUIMath/mathFonts.bundle/KpMath-Sans.otf
Executable file
BIN
Sources/SwiftUIMath/mathFonts.bundle/KpMath-Sans.otf
Executable file
Binary file not shown.
4539
Sources/SwiftUIMath/mathFonts.bundle/KpMath-Sans.plist
Executable file
4539
Sources/SwiftUIMath/mathFonts.bundle/KpMath-Sans.plist
Executable file
File diff suppressed because it is too large
Load Diff
21
Sources/SwiftUIMath/mathFonts.bundle/LICENSE
Executable file
21
Sources/SwiftUIMath/mathFonts.bundle/LICENSE
Executable file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 MathChat
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
BIN
Sources/SwiftUIMath/mathFonts.bundle/LeteSansMath.otf
Executable file
BIN
Sources/SwiftUIMath/mathFonts.bundle/LeteSansMath.otf
Executable file
Binary file not shown.
5393
Sources/SwiftUIMath/mathFonts.bundle/LeteSansMath.plist
Executable file
5393
Sources/SwiftUIMath/mathFonts.bundle/LeteSansMath.plist
Executable file
File diff suppressed because it is too large
Load Diff
BIN
Sources/SwiftUIMath/mathFonts.bundle/LibertinusMath-Regular.otf
Executable file
BIN
Sources/SwiftUIMath/mathFonts.bundle/LibertinusMath-Regular.otf
Executable file
Binary file not shown.
4276
Sources/SwiftUIMath/mathFonts.bundle/LibertinusMath-Regular.plist
Executable file
4276
Sources/SwiftUIMath/mathFonts.bundle/LibertinusMath-Regular.plist
Executable file
File diff suppressed because it is too large
Load Diff
BIN
Sources/SwiftUIMath/mathFonts.bundle/NotoSansMath-Regular.otf
Executable file
BIN
Sources/SwiftUIMath/mathFonts.bundle/NotoSansMath-Regular.otf
Executable file
Binary file not shown.
9356
Sources/SwiftUIMath/mathFonts.bundle/NotoSansMath-Regular.plist
Executable file
9356
Sources/SwiftUIMath/mathFonts.bundle/NotoSansMath-Regular.plist
Executable file
File diff suppressed because it is too large
Load Diff
103
Sources/SwiftUIMath/mathFonts.bundle/OFL.txt
Executable file
103
Sources/SwiftUIMath/mathFonts.bundle/OFL.txt
Executable file
@@ -0,0 +1,103 @@
|
||||
STIX Font License
|
||||
|
||||
24 May 2010
|
||||
|
||||
Copyright (c) 2001-2010 by the STI Pub Companies, consisting of the American
|
||||
Institute of Physics, the American Chemical Society, the American Mathematical
|
||||
Society, the American Physical Society, Elsevier, Inc., and The Institute of
|
||||
Electrical and Electronic Engineers, Inc. (www.stixfonts.org), with Reserved
|
||||
Font Name STIX Fonts, STIX Fonts (TM) is a trademark of The Institute of
|
||||
Electrical and Electronics Engineers, Inc.
|
||||
|
||||
Portions copyright (c) 1998-2003 by MicroPress, Inc. (www.micropress-inc.com),
|
||||
with Reserved Font Name TM Math. To obtain additional mathematical fonts, please
|
||||
contact MicroPress, Inc., 68-30 Harrow Street, Forest Hills, NY 11375, USA,
|
||||
Phone: (718) 575-1816.
|
||||
|
||||
Portions copyright (c) 1990 by Elsevier, Inc.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide development
|
||||
of collaborative font projects, to support the font creation efforts of academic
|
||||
and linguistic communities, and to provide a free and open framework in which
|
||||
fonts may be shared and improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and redistributed
|
||||
freely as long as they are not sold by themselves. The fonts, including any
|
||||
derivative works, can be bundled, embedded, redistributed and/or sold with any
|
||||
software provided that any reserved names are not used by derivative works. The
|
||||
fonts and derivatives, however, cannot be released under any other type of license.
|
||||
The requirement for fonts to remain under this license does not apply to any
|
||||
document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
|
||||
"Font Software" refers to the set of files released by the Copyright Holder(s) under
|
||||
this license and clearly marked as such. This may include source files, build
|
||||
scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the copyright
|
||||
statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting, or
|
||||
substituting -- in part or in whole -- any of the components of the Original Version,
|
||||
by changing formats or by porting the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical writer or other
|
||||
person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of the
|
||||
Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell
|
||||
modified and unmodified copies of the Font Software, subject to the following
|
||||
conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in Original or
|
||||
Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled, redistributed
|
||||
and/or sold with any software, provided that each copy contains the above copyright
|
||||
notice and this license. These can be included either as stand-alone text files,
|
||||
human-readable headers or in the appropriate machine-readable metadata fields within
|
||||
text or binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless
|
||||
explicit written permission is granted by the corresponding Copyright Holder. This
|
||||
restriction only applies to the primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall
|
||||
not be used to promote, endorse or advertise any Modified Version, except to
|
||||
acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with
|
||||
their explicit written permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole, must be distributed
|
||||
entirely under this license, and must not be distributed under any other license. The
|
||||
requirement for fonts to remain under this license does not apply to any document
|
||||
created using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
|
||||
This license becomes null and void if any of the above conditions are not met.
|
||||
|
||||
DISCLAIMER
|
||||
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER
|
||||
RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR
|
||||
INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
Sources/SwiftUIMath/mathFonts.bundle/latinmodern-math.otf
Executable file
BIN
Sources/SwiftUIMath/mathFonts.bundle/latinmodern-math.otf
Executable file
Binary file not shown.
BIN
Sources/SwiftUIMath/mathFonts.bundle/latinmodern-math.plist
Executable file
BIN
Sources/SwiftUIMath/mathFonts.bundle/latinmodern-math.plist
Executable file
Binary file not shown.
205
Sources/SwiftUIMath/mathFonts.bundle/math_table_to_plist.py
Executable file
205
Sources/SwiftUIMath/mathFonts.bundle/math_table_to_plist.py
Executable file
@@ -0,0 +1,205 @@
|
||||
#!/usr/local/bin/python3
|
||||
import plistlib
|
||||
import sys
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
def usage(code):
|
||||
print('Usage math_table_to_plist.py <fontfile> <plistfile>')
|
||||
sys.exit(code)
|
||||
|
||||
def process_font(font_file, out_file):
|
||||
font = TTFont(font_file)
|
||||
math_table = font['MATH'].table
|
||||
constants = get_constants(math_table)
|
||||
italic_c = get_italic_correction(math_table)
|
||||
v_variants = get_v_variants(math_table)
|
||||
h_variants = get_h_variants(math_table)
|
||||
assembly = get_v_assembly(math_table)
|
||||
accents = get_accent_attachments(math_table)
|
||||
pl = {
|
||||
"version" : "1.3",
|
||||
"constants": constants,
|
||||
"v_variants" : v_variants,
|
||||
"h_variants" : h_variants,
|
||||
"italic" : italic_c,
|
||||
"accents" : accents,
|
||||
"v_assembly" : assembly }
|
||||
ofile = open(out_file, 'w+b')
|
||||
plistlib.dump(pl, ofile)
|
||||
ofile.close()
|
||||
|
||||
def get_constants(math_table):
|
||||
constants = math_table.MathConstants
|
||||
if constants is None:
|
||||
raise 'Cannot find MathConstants in MATH table'
|
||||
|
||||
int_consts = [ 'ScriptPercentScaleDown',
|
||||
'ScriptScriptPercentScaleDown',
|
||||
'DelimitedSubFormulaMinHeight',
|
||||
'DisplayOperatorMinHeight',
|
||||
'RadicalDegreeBottomRaisePercent']
|
||||
consts = { c : getattr(constants, c) for c in int_consts }
|
||||
|
||||
record_consts = [ 'MathLeading',
|
||||
'AxisHeight',
|
||||
'AccentBaseHeight',
|
||||
'FlattenedAccentBaseHeight',
|
||||
'SubscriptShiftDown',
|
||||
'SubscriptTopMax',
|
||||
'SubscriptBaselineDropMin',
|
||||
'SuperscriptShiftUp',
|
||||
'SuperscriptShiftUpCramped',
|
||||
'SuperscriptBottomMin',
|
||||
'SuperscriptBaselineDropMax',
|
||||
'SubSuperscriptGapMin',
|
||||
'SuperscriptBottomMaxWithSubscript',
|
||||
'SpaceAfterScript',
|
||||
'UpperLimitGapMin',
|
||||
'UpperLimitBaselineRiseMin',
|
||||
'LowerLimitGapMin',
|
||||
'LowerLimitBaselineDropMin',
|
||||
'StackTopShiftUp',
|
||||
'StackTopDisplayStyleShiftUp',
|
||||
'StackBottomShiftDown',
|
||||
'StackBottomDisplayStyleShiftDown',
|
||||
'StackGapMin',
|
||||
'StackDisplayStyleGapMin',
|
||||
'StretchStackTopShiftUp',
|
||||
'StretchStackBottomShiftDown',
|
||||
'StretchStackGapAboveMin',
|
||||
'StretchStackGapBelowMin',
|
||||
'FractionNumeratorShiftUp',
|
||||
'FractionNumeratorDisplayStyleShiftUp',
|
||||
'FractionDenominatorShiftDown',
|
||||
'FractionDenominatorDisplayStyleShiftDown',
|
||||
'FractionNumeratorGapMin',
|
||||
'FractionNumDisplayStyleGapMin',
|
||||
'FractionRuleThickness',
|
||||
'FractionDenominatorGapMin',
|
||||
'FractionDenomDisplayStyleGapMin',
|
||||
'SkewedFractionHorizontalGap',
|
||||
'SkewedFractionVerticalGap',
|
||||
'OverbarVerticalGap',
|
||||
'OverbarRuleThickness',
|
||||
'OverbarExtraAscender',
|
||||
'UnderbarVerticalGap',
|
||||
'UnderbarRuleThickness',
|
||||
'UnderbarExtraDescender',
|
||||
'RadicalVerticalGap',
|
||||
'RadicalDisplayStyleVerticalGap',
|
||||
'RadicalRuleThickness',
|
||||
'RadicalExtraAscender',
|
||||
'RadicalKernBeforeDegree',
|
||||
'RadicalKernAfterDegree',
|
||||
]
|
||||
consts_2 = { c : getattr(constants, c).Value for c in record_consts }
|
||||
consts.update(consts_2)
|
||||
|
||||
variants = math_table.MathVariants
|
||||
consts['MinConnectorOverlap'] = variants.MinConnectorOverlap
|
||||
return consts
|
||||
|
||||
def get_italic_correction(math_table):
|
||||
glyph_info = math_table.MathGlyphInfo
|
||||
if glyph_info is None:
|
||||
raise "Cannot find MathGlyphInfo in MATH table."
|
||||
italic = glyph_info.MathItalicsCorrectionInfo
|
||||
if italic is None:
|
||||
raise "Cannot find Italic Correction in GlyphInfo"
|
||||
|
||||
glyphs = italic.Coverage.glyphs
|
||||
count = italic.ItalicsCorrectionCount
|
||||
records = italic.ItalicsCorrection
|
||||
italic_dict = {}
|
||||
for i in range(count):
|
||||
name = glyphs[i]
|
||||
record = records[i]
|
||||
if record.DeviceTable is not None:
|
||||
raise "Don't know how to process device table for italic correction."
|
||||
italic_dict[name] = record.Value
|
||||
return italic_dict
|
||||
|
||||
def get_accent_attachments(math_table):
|
||||
glyph_info = math_table.MathGlyphInfo
|
||||
if glyph_info is None:
|
||||
raise "Cannot find MathGlyphInfo in MATH table."
|
||||
attach = glyph_info.MathTopAccentAttachment
|
||||
if attach is None:
|
||||
raise "Cannot find Top Accent Attachment in GlyphInfo"
|
||||
|
||||
glyphs = attach.TopAccentCoverage.glyphs
|
||||
count = attach.TopAccentAttachmentCount
|
||||
records = attach.TopAccentAttachment
|
||||
attach_dict = {}
|
||||
for i in range(count):
|
||||
name = glyphs[i]
|
||||
record = records[i]
|
||||
if record.DeviceTable is not None:
|
||||
raise "Don't know how to process device table for accent attachment."
|
||||
attach_dict[name] = record.Value
|
||||
return attach_dict
|
||||
|
||||
def get_v_variants(math_table):
|
||||
variants = math_table.MathVariants
|
||||
vglyphs = variants.VertGlyphCoverage.glyphs
|
||||
vconstruction = variants.VertGlyphConstruction
|
||||
count = variants.VertGlyphCount
|
||||
variant_dict = {}
|
||||
for i in range(count):
|
||||
name = vglyphs[i]
|
||||
record = vconstruction[i]
|
||||
glyph_variants = [x.VariantGlyph for x in
|
||||
record.MathGlyphVariantRecord]
|
||||
variant_dict[name] = glyph_variants
|
||||
return variant_dict
|
||||
|
||||
def get_h_variants(math_table):
|
||||
variants = math_table.MathVariants
|
||||
hglyphs = variants.HorizGlyphCoverage.glyphs
|
||||
hconstruction = variants.HorizGlyphConstruction
|
||||
count = variants.HorizGlyphCount
|
||||
variant_dict = {}
|
||||
for i in range(count):
|
||||
name = hglyphs[i]
|
||||
record = hconstruction[i]
|
||||
glyph_variants = [x.VariantGlyph for x in
|
||||
record.MathGlyphVariantRecord]
|
||||
variant_dict[name] = glyph_variants
|
||||
return variant_dict
|
||||
|
||||
def get_v_assembly(math_table):
|
||||
variants = math_table.MathVariants
|
||||
vglyphs = variants.VertGlyphCoverage.glyphs
|
||||
vconstruction = variants.VertGlyphConstruction
|
||||
count = variants.VertGlyphCount
|
||||
assembly_dict = {}
|
||||
for i in range(count):
|
||||
name = vglyphs[i]
|
||||
record = vconstruction[i]
|
||||
assembly = record.GlyphAssembly
|
||||
if assembly is not None:
|
||||
# There is an assembly for this glyph
|
||||
italic = assembly.ItalicsCorrection.Value
|
||||
parts = [part_dict(part) for part in assembly.PartRecords]
|
||||
assembly_dict[name] = {
|
||||
"italic" : assembly.ItalicsCorrection.Value,
|
||||
"parts" : parts }
|
||||
return assembly_dict
|
||||
|
||||
def part_dict(part):
|
||||
return {
|
||||
"glyph": part.glyph,
|
||||
"startConnector" : part.StartConnectorLength,
|
||||
"endConnector" : part.EndConnectorLength,
|
||||
"advance" : part.FullAdvance,
|
||||
"extender" : (part.PartFlags == 1) }
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
usage(1)
|
||||
font_file = sys.argv[1]
|
||||
plist_file = sys.argv[2]
|
||||
process_font(font_file, plist_file)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
BIN
Sources/SwiftUIMath/mathFonts.bundle/texgyretermes-math.otf
Executable file
BIN
Sources/SwiftUIMath/mathFonts.bundle/texgyretermes-math.otf
Executable file
Binary file not shown.
BIN
Sources/SwiftUIMath/mathFonts.bundle/texgyretermes-math.plist
Executable file
BIN
Sources/SwiftUIMath/mathFonts.bundle/texgyretermes-math.plist
Executable file
Binary file not shown.
BIN
Sources/SwiftUIMath/mathFonts.bundle/xits-math.otf
Executable file
BIN
Sources/SwiftUIMath/mathFonts.bundle/xits-math.otf
Executable file
Binary file not shown.
6599
Sources/SwiftUIMath/mathFonts.bundle/xits-math.plist
Executable file
6599
Sources/SwiftUIMath/mathFonts.bundle/xits-math.plist
Executable file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user