From f23a5b9fb562397efdc959b6e9fc7cb91fb396a9 Mon Sep 17 00:00:00 2001 From: Michael Griebling Date: Fri, 20 Jan 2023 10:54:45 -0500 Subject: [PATCH] Updated comments and minor fixes. --- README.md | 31 +- .../SwiftMath/MathRender/MTBezierPath.swift | 8 +- Sources/SwiftMath/MathRender/MTColor.swift | 11 +- Sources/SwiftMath/MathRender/MTConfig.swift | 15 +- Sources/SwiftMath/MathRender/MTFont.swift | 22 +- .../SwiftMath/MathRender/MTFontManager.swift | 8 +- .../MathRender/MTFontMathTable.swift | 42 +- Sources/SwiftMath/MathRender/MTLabel.swift | 8 +- .../MathRender/MTMathAtomFactory.swift | 77 ++- Sources/SwiftMath/MathRender/MTMathList.swift | 299 +++++++--- .../MathRender/MTMathListBuilder.swift | 520 +++++++++--------- .../MathRender/MTMathListDisplay.swift | 116 ++-- .../MathRender/MTMathListIndex.swift | 47 +- .../SwiftMath/MathRender/MTMathUILabel.swift | 7 +- .../SwiftMath/MathRender/MTTypesetter.swift | 29 +- Sources/SwiftMath/MathRender/MTUnicode.swift | 97 ++-- Sources/SwiftMath/mathFonts.bundle/Info.plist | Bin 1047 -> 0 bytes .../en.lproj/InfoPlist.strings | Bin 42 -> 0 bytes .../MTMathListBuilderTests.swift | 24 +- 19 files changed, 765 insertions(+), 596 deletions(-) delete mode 100644 Sources/SwiftMath/mathFonts.bundle/Info.plist delete mode 100644 Sources/SwiftMath/mathFonts.bundle/en.lproj/InfoPlist.strings diff --git a/README.md b/README.md index 8677b81..d16b9dd 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,19 @@ for displaying beautifully rendered math equations in iOS and MacOS applications. It typesets formulae written using LaTeX in a `UILabel` equivalent class. It uses the same typesetting rules as LaTeX and so the equations are rendered exactly as LaTeX would render them. -` -SwiftMath` is a Swift translation of the latest `iosMath` v0.9.5 release but includes bug fixes + +`SwiftMath` is similar to [MathJax](https://www.mathjax.org) or +[KaTeX](https://github.com/Khan/KaTeX) for the web but for native iOS or MacOS +applications without having to use a `UIWebView` and Javascript. More +importantly, it is significantly faster than using a `UIWebView`. + +`SwiftMath` is a Swift translation of the latest `iosMath` v0.9.5 release but includes bug fixes and enhancements like a new \lbar (lambda bar) character and cyrillic alphabet support. The original `iosMath` test suites have also been translated to Swift and run without errors. Note: Error test conditions are ignored to avoid tagging everything with silly `throw`s. Please let me know of any bugs or bug fixes that you find. `SwiftMath` prepackages everything needed for direct access via the Swift Package Manager. -No need for complicated alien pods that never seem to work quite right. - -It is similar to [MathJax](https://www.mathjax.org) or -[KaTeX](https://github.com/Khan/KaTeX) for the web but for native iOS or MacOS -applications without having to use a `UIWebView` and Javascript. More -importantly, it is significantly faster than using a `UIWebView`. ## Examples Here are screenshots of some formulae that were rendered with this library: @@ -50,7 +49,7 @@ f(x) = \int\limits_{-\infty}^\infty\!\hat f(\xi)\,e^{2 \pi i \xi x}\,\mathrm{d}\ More examples are included in [EXAMPLES](EXAMPLES.md) ## Requirements -`SwiftMath` works on iOS 6+ or MacOS 10.8+ and requires ARC to build. It depends +`SwiftMath` works on iOS 11+ or MacOS 11+. It depends on the following Apple frameworks: * Foundation.framework @@ -97,8 +96,8 @@ import SwiftMath struct MathView: UIViewRepresentable { - @Binding var equation: String - @Binding var fontSize: CGFloat + var equation: String + var fontSize: CGFloat func makeUIView(context: Context) -> MTMathUILabel { let view = MTMathUILabel() @@ -115,7 +114,7 @@ struct MathView: UIViewRepresentable { } ``` -If you need code that works with SwiftUI running natively under MacOS you'll need the following: +For code that works with SwiftUI running natively under MacOS use the following: ```swift import SwiftUI @@ -123,8 +122,8 @@ import SwiftMath struct MathView: NSViewRepresentable { - @Binding var equation: String - @Binding var fontSize: CGFloat + var equation: String + var fontSize: CGFloat func makeNSView(context: Context) -> MTMathUILabel { let view = MTMathUILabel() @@ -168,7 +167,7 @@ This is a list of formula types that the library currently supports: ### Example -The [SwiftMathDemo](https://github.com/mgriebling/SwiftMathDemo) is a Swift version +The [SwiftMathDemo](https://github.com/mgriebling/SwiftMathDemo) is a SwiftUI version of the Objective-C demo included in `iosMath` that uses `SwiftMath` as a Swift package dependency. ### Advanced configuration @@ -204,7 +203,7 @@ label.fontSize = 30 The default font is *Latin Modern Math*. This can be changed as: ```swift -label.font = MTFontManager().termesFont(withSize:20) +label.font = MTFontManager.fontmanager.termesFont(withSize:20) ``` This project has 3 fonts bundled with it, but you can use any OTF math diff --git a/Sources/SwiftMath/MathRender/MTBezierPath.swift b/Sources/SwiftMath/MathRender/MTBezierPath.swift index 754a785..fb5e082 100644 --- a/Sources/SwiftMath/MathRender/MTBezierPath.swift +++ b/Sources/SwiftMath/MathRender/MTBezierPath.swift @@ -1,8 +1,10 @@ -// -// MTBezierPath.swift -// MathRenderSwift + // // 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 diff --git a/Sources/SwiftMath/MathRender/MTColor.swift b/Sources/SwiftMath/MathRender/MTColor.swift index 6ecd4b3..a753f40 100644 --- a/Sources/SwiftMath/MathRender/MTColor.swift +++ b/Sources/SwiftMath/MathRender/MTColor.swift @@ -1,8 +1,10 @@ -// -// MTColor.swift -// MathRenderSwift + // // 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 @@ -19,7 +21,8 @@ extension MTColor { scanner.scanHexInt64(&rgbValue) return MTColor(red: CGFloat((rgbValue & 0xFF0000) >> 16)/255.0, green: CGFloat((rgbValue & 0xFF00) >> 8)/255.0, - blue: CGFloat((rgbValue & 0xFF))/255.0, alpha: 1.0) + blue: CGFloat((rgbValue & 0xFF))/255.0, + alpha: 1.0) } } diff --git a/Sources/SwiftMath/MathRender/MTConfig.swift b/Sources/SwiftMath/MathRender/MTConfig.swift index d3efa85..1ef5c70 100644 --- a/Sources/SwiftMath/MathRender/MTConfig.swift +++ b/Sources/SwiftMath/MathRender/MTConfig.swift @@ -1,12 +1,13 @@ -// -// MTConfig.swift -// MathRenderSwift -// -// Created by Mike Griebling on 2023-01-01. -// - 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) import UIKit diff --git a/Sources/SwiftMath/MathRender/MTFont.swift b/Sources/SwiftMath/MathRender/MTFont.swift index 718f685..dd5de6d 100644 --- a/Sources/SwiftMath/MathRender/MTFont.swift +++ b/Sources/SwiftMath/MathRender/MTFont.swift @@ -1,17 +1,11 @@ -// -// MTFont.swift -// MathRenderSwift -// -// Created by Mike Griebling on 2022-12-31. -// import Foundation import CoreGraphics import CoreText // -// Created by Kostub Deshmukh on 5/18/16. -// Modified by Michael Griebling on 17 Jan 2023. +// 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. @@ -26,10 +20,10 @@ public class MTFont { 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) { - // CTFontCreateWithName does not load the complete math font, it only has about half the glyphs of the full math font. - // In particular it does not have the math italic characters which breaks our variable rendering. - // So we first load a CGFont from the file and then convert it to a CTFont. self.init() print("Loading font \(name)") let bundle = MTFont.fontBundle @@ -51,7 +45,8 @@ public class MTFont { Bundle(url: Bundle.module.url(forResource: "mathFonts", withExtension: "bundle")!)! } - func copy(withSize size: CGFloat) -> MTFont { + /** 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) @@ -69,6 +64,7 @@ public class MTFont { defaultCGFont.getGlyphWithGlyphName(name: name as CFString) } - var fontSize:CGFloat { CTFontGetSize(self.ctFont) } + /** The size of this font in points. */ + public var fontSize:CGFloat { CTFontGetSize(self.ctFont) } } diff --git a/Sources/SwiftMath/MathRender/MTFontManager.swift b/Sources/SwiftMath/MathRender/MTFontManager.swift index 4b5a082..12fb822 100644 --- a/Sources/SwiftMath/MathRender/MTFontManager.swift +++ b/Sources/SwiftMath/MathRender/MTFontManager.swift @@ -1,8 +1,10 @@ -// -// MTFontManager.swift -// MathRenderSwift + // // 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 diff --git a/Sources/SwiftMath/MathRender/MTFontMathTable.swift b/Sources/SwiftMath/MathRender/MTFontMathTable.swift index c536243..545e731 100644 --- a/Sources/SwiftMath/MathRender/MTFontMathTable.swift +++ b/Sources/SwiftMath/MathRender/MTFontMathTable.swift @@ -1,15 +1,16 @@ // -// MTFontMathTable.swift -// MathRenderSwift -// // 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 CoreGraphics import CoreText -class GlyphPart { +struct GlyphPart { /// The glyph that represents this part var glyph: CGGlyph! @@ -33,14 +34,13 @@ class GlyphPart { 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 + 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. + Remark: This class is not meant to be used outside of this library. */ - class MTFontMathTable { // The font for this math table. @@ -100,21 +100,21 @@ class MTFontMathTable { var skewedFractionVerticalGap:CGFloat { constantFromTable("SkewedFractionVerticalGap") } // \sigma_21 in TeX // MARK: - Non-standard - // FractionDelimiterSize and FractionDelimiterDisplayStyleSize are not constants - // specified in the OpenType Math specification. Rather these are proposed LuaTeX extensions - // for the TeX parameters \sigma_20 (delim1) and \sigma_21 (delim2). Since these do not - // exist in the fonts that we have, we use the same approach as LuaTeX and use the fontSize - // to determine these values. The constants used are the same as LuaTeX and KaTeX and match the - // metrics values of the original TeX fonts. - // Note: An alternative approach is to use DelimitedSubFormulaMinHeight for \sigma21 and use a factor - // of 2 to get \sigma 20 as proposed in Vieth paper. - // The XeTeX implementation sets \sigma21 = fontSize and \sigma20 = DelimitedSubFormulaMinHeight which - // will produce smaller delimiters. - // Of all the approaches we've implemented LuaTeX's approach since it mimics LaTeX most accurately. - var fractionDelimiterSize: CGFloat { return 1.01 * _fontSize } + /// 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 { return 2.39 * _fontSize } + var fractionDelimiterDisplayStyleSize: CGFloat { 2.39 * _fontSize } // MARK: - Stacks var stackTopDisplayStyleShiftUp:CGFloat { constantFromTable("StackTopDisplayStyleShiftUp") } // \sigma_8 in TeX @@ -303,7 +303,7 @@ class MTFontMathTable { var rv = [GlyphPart]() for part in parts! { let partInfo = part as! NSDictionary? - let part = GlyphPart() + var part = GlyphPart() let adv = partInfo!["advance"] as! NSNumber? part.fullAdvance = self.fontUnitsToPt(adv!.intValue) let end = partInfo!["endConnector"] as! NSNumber? diff --git a/Sources/SwiftMath/MathRender/MTLabel.swift b/Sources/SwiftMath/MathRender/MTLabel.swift index d68cda1..8964a90 100644 --- a/Sources/SwiftMath/MathRender/MTLabel.swift +++ b/Sources/SwiftMath/MathRender/MTLabel.swift @@ -1,8 +1,9 @@ // -// MTLabel.swift -// MathRenderSwift -// // 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 @@ -25,6 +26,7 @@ public class MTLabel : NSTextField { super.init(coder: coder) } + // MARK: - Customized getter and setter methods for property text. var text:String? { get { super.stringValue } set { super.stringValue = newValue! } diff --git a/Sources/SwiftMath/MathRender/MTMathAtomFactory.swift b/Sources/SwiftMath/MathRender/MTMathAtomFactory.swift index 6594df1..319515f 100644 --- a/Sources/SwiftMath/MathRender/MTMathAtomFactory.swift +++ b/Sources/SwiftMath/MathRender/MTMathAtomFactory.swift @@ -1,12 +1,14 @@ // -// MTMathAtomFactory.swift -// MathRenderSwift -// // 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 = [ @@ -60,11 +62,10 @@ public class MTMathAtomFactory { "rfloor" : "\u{230B}" ] - var _delimValueToName: [String: String]? = nil - public var delimValueToName: [String: String] { - if _delimValueToName == nil { + 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 { @@ -75,12 +76,11 @@ public class MTMathAtomFactory { } } } - output[value] = key } _delimValueToName = output } - return _delimValueToName! + return _delimValueToName } public static let accents = [ @@ -389,8 +389,8 @@ public class MTMathAtomFactory { "scriptscriptstyle" : MTMathStyle(style: .scriptOfScript), ] - var _textToLatexSymbolName: [String: String]? = nil - public var textToLatexSymbolName: [String: String] { + static var _textToLatexSymbolName: [String: String]? = nil + public static var textToLatexSymbolName: [String: String] { get { if self._textToLatexSymbolName == nil { var output = [String: String]() @@ -421,7 +421,7 @@ public class MTMathAtomFactory { } } - public static let sharedInstance = MTMathAtomFactory() + // public static let sharedInstance = MTMathAtomFactory() static let fontStyles : [String: MTFontStyle] = [ "mathnormal" : .defaultStyle, @@ -449,7 +449,7 @@ public class MTMathAtomFactory { ] public static func fontStyleWithName(_ fontName:String) -> MTFontStyle? { - return fontStyles[fontName] + fontStyles[fontName] } public static func fontNameForStyle(_ fontStyle:MTFontStyle) -> String { @@ -467,21 +467,22 @@ public class MTMathAtomFactory { } } - // Return an atom for times sign \times or * + /// Returns an atom for the multiplication sign (i.e., \times or "*") public static func times() -> MTMathAtom { MTMathAtom(type: .binaryOperator, value: UnicodeSymbol.multiplication) } - // Return an atom for division sign \div or / + /// Returns an atom for the division sign (i.e., \div or "/") public static func divide() -> MTMathAtom { MTMathAtom(type: .binaryOperator, value: UnicodeSymbol.division) } - // Return an atom aka placeholder square + /// 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() @@ -491,6 +492,7 @@ public class MTMathAtomFactory { return frac } + /** Returns a square root with a placeholder as the radicand. */ public static func placeholderSquareRoot() -> MTRadical { let rad = MTRadical() rad.radicand = MTMathList() @@ -498,6 +500,7 @@ public class MTMathAtomFactory { return rad } + /** Returns a radical with a placeholder as the radicand. */ public static func placeholderRadical() -> MTRadical { let rad = MTRadical() rad.radicand = MTMathList() @@ -507,6 +510,7 @@ public class MTMathAtomFactory { return rad } + // 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. @@ -554,7 +558,7 @@ public class MTMathAtomFactory { } /** Returns a `MTMathList` with one atom per character in the given string. This function - does not do any LaTeX conversion or interpretation. It simply uses `atomForCharacter` to + 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() @@ -583,26 +587,23 @@ public class MTMathAtomFactory { /** 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 + 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. + Note: This function does not convert MathSpaces to latex command names either. */ public static func latexSymbolName(for atom: MTMathAtom) -> String? { - if atom.nucleus.count == 0 { - return nil - } - return sharedInstance.textToLatexSymbolName[atom.nucleus] + 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 addLatexSymbol:@"lcm" value:[MTMathAtomFactory operatorWithName:@"lcm" limits: false)]` */ - + `MTMathAtomFactory.add(latexSymbol:"lcm", value:MTMathAtomFactory.operatorWithName("lcm", limits: false))` */ public static func add(latexSymbol name: String, value: MTMathAtom) { supportedLatexSymbols[name] = value - sharedInstance.textToLatexSymbolName[value.nucleus] = name + Self.textToLatexSymbolName[value.nucleus] = name } /** Returns a large opertor for the given name. If limits is true, limits are set up on @@ -647,10 +648,8 @@ public class MTMathAtomFactory { `<` and `langle`) and this function always returns the shorter name. */ public static func getDelimiterName(of boundary: MTMathAtom) -> String? { - if boundary.type != .boundary { - return nil - } - return Self.sharedInstance.delimValueToName[boundary.nucleus] + guard boundary.type == .boundary else { return nil } + return Self.delimValueToName[boundary.nucleus] } /** Returns a fraction with the given numerator and denominator. */ @@ -672,21 +671,14 @@ public class MTMathAtomFactory { } /** Simplification of above function when numerator and denominator are simple strings. - This function uses `mathListForCharacters` to convert the strings to `MTMathList`s. */ + 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) } - /** Builds a table for a given environment with the given rows. Returns a `MTMathAtom` containing the - table and any other atoms necessary for the given environment. Returns nil and sets error - if the table could not be built. - @param env The environment to use to build the table. If the env is nil, then the default table is built. - @note The reason this function returns a `MTMathAtom` and not a `MTMathTable` is because some - matrix environments are have builtin delimiters added to the table and hence are returned as inner atoms. - */ + static let matrixEnvs = [ "matrix": [], "pmatrix": ["(", ")"], @@ -696,6 +688,13 @@ public class MTMathAtomFactory { "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?, rows: [[MTMathList]], error:inout NSError?) -> MTMathAtom? { let table = MTMathTable(environment: env) diff --git a/Sources/SwiftMath/MathRender/MTMathList.swift b/Sources/SwiftMath/MathRender/MTMathList.swift index c4fd896..cadd5d6 100644 --- a/Sources/SwiftMath/MathRender/MTMathList.swift +++ b/Sources/SwiftMath/MathRender/MTMathList.swift @@ -1,42 +1,76 @@ // -// MTMathList.swift -// MathRenderSwift -// // 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 -// type defines spacing and how it is rendered -public enum MTMathAtomType: Int, CustomStringConvertible, Comparable { +/** + The type of atom in a `MTMathList`. - case ordinary = 1 // number or text - case number // number - case variable // text in italic - case largeOperator // sin/cos, integral - case binaryOperator // \bin - case unaryOperator // - case relation // = < > - case open // open bracket - case close // close bracket - case fraction // \frac - case radical // \sqrt - case punctuation // , - case placeholder // inner atom - case inner // embedded list - case underline // underlined atom - case overline // overlined atom - case accent // accented atom + The type of the atom determines how it is rendered, and spacing between the atoms. + */ +public enum MTMathAtomType: Int, CustomStringConvertible, Comparable { + /// A number or text in ordinary format - Ord in TeX + case ordinary = 1 + /// A number - Does not exist in TeX + case number + /// A variable (i.e. text in italic format) - Does not exist in TeX + case variable + /// A large operator such as (sin/cos, integral etc.) - Op in TeX + case largeOperator + /// A binary operator - Bin in TeX + case binaryOperator + /// A unary operator - Does not exist in TeX. + case unaryOperator + /// A relation, e.g. = > < etc. - Rel in TeX + case relation + /// Open brackets - Open in TeX + case open + /// Close brackets - Close in TeX + case close + /// A fraction e.g 1/2 - generalized fraction node in TeX + case fraction + /// A radical operator e.g. sqrt(2) + case radical + /// Punctuation such as , - Punct in TeX + case punctuation + /// A placeholder square for future input. Does not exist in TeX + case placeholder + /// An inner atom, i.e. an embedded math list - Inner in TeX + case inner + /// An underlined atom - Under in TeX + case underline + /// An overlined atom - Over in TeX + case overline + /// An accented atom - Accent in TeX + case accent - // these atoms do not support subscripts/superscripts: + // Atoms after this point do not support subscripts or superscripts + + /// A left atom - Left & Right in TeX. We don't need two since we track boundaries separately. case boundary = 101 + + // Atoms after this are non-math TeX nodes that are still useful in math mode. They do not have + // the usual structure. + + /// Spacing between math atoms. This denotes both glue and kern for TeX. We do not + /// distinguish between glue and kern. case space = 201 - // Denotes style changes during randering + /// Denotes style changes during rendering. case style case color case colorBox + // Atoms after this point are not part of TeX and do not have the usual structure. + + /// An table atom. This atom does not exist in TeX. It is equivalent to the TeX command + /// halign which is handled outside of the TeX math rendering engine. We bring it into our + /// math typesetting to handle matrices and other tables. case table = 1001 func isNotBinaryOperator() -> Bool { @@ -79,9 +113,14 @@ public enum MTMathAtomType: Int, CustomStringConvertible, Comparable { // comparable support public static func < (lhs: MTMathAtomType, rhs: MTMathAtomType) -> Bool { lhs.rawValue < rhs.rawValue } - } +/** + The font style of a character. + + The fontstyle of the atom determines what style the character is rendered in. This only applies to atoms + of type kMTMathAtomVariable and kMTMathAtomNumber. None of the other atom types change their font style. + */ public enum MTFontStyle:Int { /// The default latex rendering style. i.e. variables are italic and numbers are roman. case defaultStyle = 0, @@ -107,9 +146,19 @@ public enum MTFontStyle:Int { // MARK: - MTMathAtom +/** A `MTMathAtom` is the basic unit of a math list. Each atom represents a single character + or mathematical operator in a list. However certain atoms can represent more complex structures + such as fractions and radicals. Each atom has a type which determines how the atom is rendered and + a nucleus. The nucleus contains the character(s) that need to be rendered. However the nucleus may + be empty for certain types of atoms. An atom has an optional subscript or superscript which represents + the subscript or superscript that is to be rendered. + + Certain types of atoms inherit from `MTMathAtom` and may have additional fields. + */ public class MTMathAtom: NSObject { - + /** The type of the atom. */ public var type = MTMathAtomType.ordinary + /** An optional subscript. */ public var subScript: MTMathList? { didSet { if subScript != nil && !self.isScriptAllowed() { @@ -118,6 +167,7 @@ public class MTMathAtom: NSObject { } } } + /** An optional superscript. */ public var superScript: MTMathList? { didSet { if superScript != nil && !self.isScriptAllowed() { @@ -127,11 +177,19 @@ public class MTMathAtom: NSObject { } } + /** The nucleus of the atom. */ public var nucleus: String = "" + + /// The index range in the MTMathList this MTMathAtom tracks. This is used by the finalizing and preprocessing steps + /// which fuse MTMathAtoms to track the position of the current MTMathAtom in the original list. public var indexRange = NSRange(location: 0, length: 0) // indexRange in list that this atom tracks: + /** The font style to be used for the atom. */ var fontStyle: MTFontStyle = .defaultStyle - var fusedAtoms = [MTMathAtom]() // atoms that fused to create this one + + /// If this atom was formed by fusion of multiple atoms, then this stores the list of atoms that were fused to create this one. + /// This is used in the finalizing and preprocessing steps. + var fusedAtoms = [MTMathAtom]() init(_ atom:MTMathAtom?) { guard let atom = atom else { return } @@ -146,11 +204,15 @@ public class MTMathAtom: NSObject { override init() { } + /// Factory function to create an atom with a given type and value. + /// - parameter type: The type of the atom to instantiate. + /// - parameter value: The value of the atoms nucleus. The value is ignored for fractions and radicals. init(type:MTMathAtomType, value:String) { self.type = type self.nucleus = type == .radical ? "" : value } + /// Returns a copy of `self`. public func copy() -> MTMathAtom { switch self.type { case .largeOperator: @@ -194,6 +256,7 @@ public class MTMathAtom: NSObject { return string } + /// Returns a finalized copy of the atom public var finalized: MTMathAtom { let finalized : MTMathAtom = self.copy() finalized.superScript = finalized.superScript?.finalized @@ -212,6 +275,7 @@ public class MTMathAtom: NSObject { return str } + // Fuse the given atom with this one by combining their nucleii. func fuse(with atom: MTMathAtom) { assert(self.subScript == nil, "Cannot fuse into an atom which has a subscript: \(self)"); assert(self.superScript == nil, "Cannot fuse into an atom which has a superscript: \(self)"); @@ -240,7 +304,9 @@ public class MTMathAtom: NSObject { self.subScript = atom.subScript } + /** Returns true if this atom allows scripts (sub or super). */ func isScriptAllowed() -> Bool { self.type.isScriptAllowed() } + func isNotBinaryOperator() -> Bool { self.type.isNotBinaryOperator() } } @@ -254,19 +320,21 @@ func isNotBinaryOperator(_ prevNode:MTMathAtom?) -> Bool { public class MTFraction: MTMathAtom { public var hasRule: Bool = true - public var leftDelimiter: String? - public var rightDelimiter: String? + public var leftDelimiter = "" + public var rightDelimiter = "" public var numerator: MTMathList? public var denominator: MTMathList? init(_ frac: MTFraction?) { super.init(frac) self.type = .fraction - self.numerator = MTMathList(frac!.numerator) - self.denominator = MTMathList(frac!.denominator) - self.hasRule = frac!.hasRule - self.leftDelimiter = frac!.leftDelimiter - self.rightDelimiter = frac!.rightDelimiter + if let frac = frac { + self.numerator = MTMathList(frac.numerator) + self.denominator = MTMathList(frac.denominator) + self.hasRule = frac.hasRule + self.leftDelimiter = frac.leftDelimiter + self.rightDelimiter = frac.rightDelimiter + } } init(hasRule rule:Bool = true) { @@ -276,28 +344,20 @@ public class MTFraction: MTMathAtom { } override public var description: String { - var string = "" - if self.hasRule { - string += "\\atop" - } else { - string += "\\frac" + var string = self.hasRule ? "\\frac" : "\\atop" + if !self.leftDelimiter.isEmpty { + string += "[\(self.leftDelimiter)]" } - if self.leftDelimiter != nil { - string += "[\(self.leftDelimiter!)]" + if !self.rightDelimiter.isEmpty { + string += "[\(self.rightDelimiter)]" } - if self.rightDelimiter != nil { - string += "[\(self.rightDelimiter!)]" - } - string += "{\(self.numerator?.description ?? "placeholder")}{\(self.denominator?.description ?? "placeholder")}" - if self.superScript != nil { string += "^{\(self.superScript!.description)}" } if self.subScript != nil { string += "_{\(self.subScript!.description)}" } - return string } @@ -311,12 +371,13 @@ public class MTFraction: MTMathAtom { } // MARK: - MTRadical - +/** An atom of type radical (square root). */ public class MTRadical: MTMathAtom { - // Under the roof + /// Denotes the term under the square root sign public var radicand: MTMathList? - // Value on radical sign + /// Denotes the degree of the radical, i.e. the value to the top left of the radical sign + /// This can be null if there is no degree. public var degree: MTMathList? init(_ rad:MTRadical?) { @@ -335,22 +396,18 @@ public class MTRadical: MTMathAtom { override public var description: String { var string = "\\sqrt" - if self.degree != nil { string += "[\(self.degree!.description)]" } - if self.radicand != nil { string += "{\(self.radicand?.description ?? "placeholder")}" } - if self.superScript != nil { string += "^{\(self.superScript!.description)}" } if self.subScript != nil { string += "_{\(self.subScript!.description)}" } - return string } @@ -363,8 +420,13 @@ public class MTRadical: MTMathAtom { } // MARK: - MTLargeOperator - +/** A `MTMathAtom` of type `kMTMathAtom.largeOperator`. */ public class MTLargeOperator: MTMathAtom { + + /** Indicates whether the limits (if present) should be displayed + above and below the operator in display mode. If limits is false + then the limits (if present) are displayed like a regular subscript/superscript. + */ public var limits: Bool = false init(_ op:MTLargeOperator?) { @@ -380,20 +442,25 @@ public class MTLargeOperator: MTMathAtom { } // MARK: - MTInner - +/** An inner atom. This denotes an atom which contains a math list inside it. An inner atom + has optional boundaries. Note: Only one boundary may be present, it is not required to have + both. */ public class MTInner: MTMathAtom { + /// The inner math list public var innerList: MTMathList? + /// The left boundary atom. This must be a node of type kMTMathAtomBoundary public var leftBoundary: MTMathAtom? { didSet { - if leftBoundary != nil && leftBoundary!.type != .boundary { + if let left = leftBoundary, left.type != .boundary { leftBoundary = nil NSException(name: NSExceptionName(rawValue: "Error"), reason: "Left boundary must be of type .boundary").raise() } } } + /// The right boundary atom. This must be a node of type kMTMathAtomBoundary public var rightBoundary: MTMathAtom? { didSet { - if rightBoundary != nil && rightBoundary!.type != .boundary { + if let right = rightBoundary, right.type != .boundary { rightBoundary = nil NSException(name: NSExceptionName(rawValue: "Error"), reason: "Right boundary must be of type .boundary").raise() } @@ -415,23 +482,19 @@ public class MTInner: MTMathAtom { override public var description: String { var string = "\\inner" - if self.leftBoundary != nil { string += "[\(self.leftBoundary!.nucleus)]" } string += "{\(self.innerList!.description)}" - if self.rightBoundary != nil { string += "[\(self.rightBoundary!.nucleus)]" } - if self.superScript != nil { string += "^{\(self.superScript!.description)}" } if self.subScript != nil { string += "_{\(self.subScript!.description)}" } - return string } @@ -443,7 +506,7 @@ public class MTInner: MTMathAtom { } // MARK: - MTOverLIne - +/** An atom with a line over the contained math list. */ public class MTOverLine: MTMathAtom { public var innerList: MTMathList? @@ -466,7 +529,7 @@ public class MTOverLine: MTMathAtom { } // MARK: - MTUnderLine - +/** An atom with a line under the contained math list. */ public class MTUnderLine: MTMathAtom { public var innerList: MTMathList? @@ -513,10 +576,16 @@ public class MTAccent: MTMathAtom { } // MARK: - MTMathSpace - +/** An atom representing space. + Note: None of the usual fields of the `MTMathAtom` apply even though this + class inherits from `MTMathAtom`. i.e. it is meaningless to have a value + in the nucleus, subscript or superscript fields. */ public class MTMathSpace: MTMathAtom { + /** The amount of space represented by this object in mu units. */ public var space: CGFloat = 0 + /// Creates a new `MTMathSpace` with the given spacing. + /// - parameter space: The amount of space in mu units. init(_ space: MTMathSpace?) { super.init(space) self.type = .space @@ -530,11 +599,17 @@ public class MTMathSpace: MTMathAtom { } } +/** + Styling of a line of math + */ public enum MTLineStyle:Int, Comparable { - + /// Display style case display + /// Text style (inline) case text + /// Script style (for sub/super scripts) case script + /// Script script style (for scripts of scripts) case scriptOfScript public func inc() -> MTLineStyle { @@ -548,7 +623,10 @@ public enum MTLineStyle:Int, Comparable { } // MARK: - MTMathStyle - +/** An atom representing a style change. + Note: None of the usual fields of the `MTMathAtom` apply even though this + class inherits from `MTMathAtom`. i.e. it is meaningless to have a value + in the nucleus, subscript or superscript fields. */ public class MTMathStyle: MTMathAtom { public var style: MTLineStyle = .display @@ -566,7 +644,10 @@ public class MTMathStyle: MTMathAtom { } // MARK: - MTMathColor - +/** An atom representing an color element. + Note: None of the usual fields of the `MTMathAtom` apply even though this + class inherits from `MTMathAtom`. i.e. it is meaningless to have a value + in the nucleus, subscript or superscript fields. */ public class MTMathColor: MTMathAtom { public var colorString:String="" public var innerList:MTMathList? @@ -595,9 +676,12 @@ public class MTMathColor: MTMathAtom { } // MARK: - MTMathColorbox - +/** An atom representing an colorbox element. + Note: None of the usual fields of the `MTMathAtom` apply even though this + class inherits from `MTMathAtom`. i.e. it is meaningless to have a value + in the nucleus, subscript or superscript fields. */ public class MTMathColorbox: MTMathAtom { - public var colorString:String="" + public var colorString="" public var innerList:MTMathList? init(_ cbox: MTMathColorbox?) { @@ -623,6 +707,9 @@ public class MTMathColorbox: MTMathAtom { } } +/** + Alignment for a column of MTMathTable + */ public enum MTColumnAlignment { case left case center @@ -630,13 +717,28 @@ public enum MTColumnAlignment { } // MARK: - MTMathTable - +/** An atom representing an table element. This atom is not like other + atoms and is not present in TeX. We use it to represent the `\halign` command + in TeX with some simplifications. This is used for matrices, equation + alignments and other uses of multiline environments. + + The cells in the table are represented as a two dimensional array of + `MTMathList` objects. The `MTMathList`s could be empty to denote a missing + value in the cell. Additionally an array of alignments indicates how each + column will be aligned. + */ public class MTMathTable: MTMathAtom { + /// The alignment for each column (left, right, center). The default alignment + /// for a column (if not set) is center. public var alignments = [MTColumnAlignment]() + /// The cells in the table as a two dimensional array. public var cells = [[MTMathList]]() - - public var environment: String? + /// The name of the environment that this table denotes. + public var environment = "" + /// Spacing between each column in mu units. public var interColumnSpacing: CGFloat = 0 + /// Additional spacing between rows in jots (one jot is 0.3 times font size). + /// If the additional spacing is 0, then normal row spacing is used are used. public var interRowAdditionalSpacing: CGFloat = 0 override public var finalized: MTMathAtom { @@ -652,7 +754,7 @@ public class MTMathTable: MTMathAtom { init(environment: String?) { super.init() self.type = .table - self.environment = environment + self.environment = environment ?? "" } init(_ table:MTMathTable) { @@ -678,6 +780,7 @@ public class MTMathTable: MTMathAtom { self.type = .table } + /// Set the value of a given cell. The table is automatically resized to contain this cell. public func set(cell list: MTMathList, forRow row:Int, column:Int) { if self.cells.count <= row { for _ in self.cells.count...row { @@ -693,6 +796,8 @@ public class MTMathTable: MTMathAtom { self.cells[row][column] = list } + /// Set the alignment of a particular column. The table is automatically resized to + /// contain this column and any new columns added have their alignment set to center. public func set(alignment: MTColumnAlignment, forColumn col: Int) { if self.alignments.count <= col { for _ in self.alignments.count...col { @@ -703,6 +808,8 @@ public class MTMathTable: MTMathAtom { self.alignments[col] = alignment } + /// Gets the alignment for a given column. If the alignment is not specified it defaults + /// to center. public func get(alignmentForColumn col: Int) -> MTColumnAlignment { if self.alignments.count <= col { return MTColumnAlignment.center @@ -724,12 +831,21 @@ public class MTMathTable: MTMathAtom { // MARK: - MTMathList -// represent list of math objects extension MTMathList { public override var description: String { self.atoms.description } + /// converts the MTMathList to a string form. Note: This is not the LaTeX form. public var string: String { self.description } } +/** A representation of a list of math objects. + + This list can be constructed directly or built with + the help of the MTMathListBuilder. It is not required that the mathematics represented make sense + (i.e. this can represent something like "x 2 = +". This list can be used for display using MTLine + or can be a list of tokens to be used by a parser after finalizedMathList is called. + + Note: This class is for **advanced** usage only. + */ public class MTMathList : NSObject { init?(_ list:MTMathList?) { @@ -739,8 +855,12 @@ public class MTMathList : NSObject { } } + /// A list of MathAtoms public var atoms = [MTMathAtom]() + /// Create a new math list as a final expression and update atoms + /// by combining like atoms that occur together and converting unary operators to binary operators. + /// This function does not modify the current MTMathList public var finalized: MTMathList { let finalizedList = MTMathList() let zeroRange = NSMakeRange(0, 0) @@ -800,7 +920,10 @@ public class MTMathList : NSObject { NSException(name: NSExceptionName(rawValue: "Error"), reason: "Index \(index) out of bounds").raise() } - func add(_ atom: MTMathAtom?) { + /// Add an atom to the end of the list. + /// - parameter atom: The atom to be inserted. This cannot be `nil` and cannot have the type `kMTMathAtomBoundary`. + /// - throws NSException if the atom is of type `kMTMathAtomBoundary` + public func add(_ atom: MTMathAtom?) { guard let atom = atom else { return } if self.isAtomAllowed(atom) { self.atoms.append(atom) @@ -809,7 +932,14 @@ public class MTMathList : NSObject { } } - func insert(_ atom: MTMathAtom?, at index: Int) { + /// Inserts an atom at the given index. If index is already occupied, the objects at index and beyond are + /// shifted by adding 1 to their indices to make room. An insert to an `index` greater than the number of atoms + /// is ignored. Insertions of nil atoms is ignored. + /// - parameter atom: The atom to be inserted. This cannot be `nil` and cannot have the type `kMTMathAtom.boundary`. + /// - parameter index: The index where the atom is to be inserted. The index should be less than or equal to the + /// number of elements in the math list. + /// - throws NSException if the atom is of type kMTMathAtomBoundary + public func insert(_ atom: MTMathAtom?, at index: Int) { // NSParamException(atom) guard let atom = atom else { return } guard self.atoms.indices.contains(index) || index == self.atoms.endIndex else { return } @@ -822,23 +952,30 @@ public class MTMathList : NSObject { } } - func append(_ list: MTMathList?) { + /// Append the given list to the end of the current list. + /// - parameter list: The list to append. + public func append(_ list: MTMathList?) { guard let list = list else { return } self.atoms += list.atoms } - func removeLastAtom() { + /** Removes the last atom from the math list. If there are no atoms in the list this does nothing. */ + public func removeLastAtom() { if !self.atoms.isEmpty { self.atoms.removeLast() } } - func removeAtom(at index: Int) { + /// Removes the atom at the given index. + /// - parameter index: The index at which to remove the atom. Must be less than the number of atoms + /// in the list. + public func removeAtom(at index: Int) { NSIndexException(self.atoms, index:index) self.atoms.remove(at: index) } - func removeAtoms(in range: ClosedRange) { + /** Removes all the atoms within the given range. */ + public func removeAtoms(in range: ClosedRange) { NSIndexException(self.atoms, index: range.lowerBound) NSIndexException(self.atoms, index: range.upperBound) self.atoms.removeSubrange(range) diff --git a/Sources/SwiftMath/MathRender/MTMathListBuilder.swift b/Sources/SwiftMath/MathRender/MTMathListBuilder.swift index 08e338b..69a117d 100644 --- a/Sources/SwiftMath/MathRender/MTMathListBuilder.swift +++ b/Sources/SwiftMath/MathRender/MTMathListBuilder.swift @@ -1,8 +1,9 @@ // -// MTMathListBuilder.swift -// MathRenderSwift -// // 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 @@ -23,10 +24,9 @@ struct MTEnvProperties { } /** - @typedef case s - @brief The error encountered when parsing a LaTeX string. + The error encountered when parsing a LaTeX string. - The `code` in the `NSError` is one of the following indiciating why the LaTeX string + The `code` in the `NSError` is one of the following indicating why the LaTeX string could not be parsed. */ enum MTParseErrors:Int { @@ -62,7 +62,10 @@ enum MTParseErrors:Int { let MTParseError = "ParseError" -public class MTMathListBuilder { +/** `MTMathListBuilder` is a class for parsing LaTeX into an `MTMathList` that + can be rendered and processed mathematically. + */ +public struct MTMathListBuilder { var string: String var currentCharIndex: String.Index var currentInnerAtom: MTInner? @@ -73,8 +76,42 @@ public class MTMathListBuilder { /** Contains any error that occurred during parsing. */ var error:NSError? + // MARK: - Character-handling routines + var hasCharacters: Bool { currentCharIndex < string.endIndex } + // gets the next character and increments the index + mutating func getNextCharacter() -> Character { + assert(self.hasCharacters, "Retrieving character at index \(self.currentCharIndex) beyond length \(self.string.count)") + let ch = string[currentCharIndex] + currentCharIndex = string.index(after: currentCharIndex) + return ch + } + + mutating func unlookCharacter() { + assert(currentCharIndex > string.startIndex, "Unlooking when at the first character.") + if currentCharIndex > string.startIndex { + currentCharIndex = string.index(before: currentCharIndex) + } + } + + mutating func expectCharacter(_ ch: Character) -> Bool { + MTAssertNotSpace(ch) + self.skipSpaces() + + if self.hasCharacters { + let nextChar = self.getNextCharacter() + MTAssertNotSpace(nextChar) + if nextChar == ch { + return true + } else { + self.unlookCharacter() + return false + } + } + return false + } + public static let spaceToCommands: [CGFloat: String] = [ 3 : ",", 4 : ">", @@ -99,7 +136,10 @@ public class MTMathListBuilder { self.spacesAllowed = false } - public func build() -> MTMathList? { + // MARK: - MTMathList builder functions + + /// Builds a mathlist from the internal `string`. Returns nil if there is an error. + public mutating func build() -> MTMathList? { let list = self.buildInternal(false) if self.hasCharacters && error == nil { self.setError(.mismatchBraces, message: "Mismatched braces: \(self.string)") @@ -111,13 +151,20 @@ public class MTMathListBuilder { return list } + /** Construct a math list from a given string. If there is parse error, returns + nil. To retrieve the error use the function `MTMathListBuilder.build(fromString:error:)`. + */ public static func build(fromString string: String) -> MTMathList? { - let builder = MTMathListBuilder(string: string) + var builder = MTMathListBuilder(string: string) return builder.build() } + /** Construct a math list from a given string. If there is an error while + constructing the string, this returns nil. The error is returned in the + `error` parameter. + */ public static func build(fromString string: String, error:inout NSError?) -> MTMathList? { - let builder = MTMathListBuilder(string: string) + var builder = MTMathListBuilder(string: string) let output = builder.build() if builder.error != nil { error = builder.error @@ -126,227 +173,29 @@ public class MTMathListBuilder { return output } - public static func mathListToString(_ ml: MTMathList?) -> String { - var str = "" - var currentfontStyle = MTFontStyle.defaultStyle - if let atomList = ml { - for atom in atomList.atoms { - if currentfontStyle != atom.fontStyle { - if currentfontStyle != .defaultStyle { - str += "}" - } - if atom.fontStyle != .defaultStyle { - let fontStyleName = MTMathAtomFactory.fontNameForStyle(atom.fontStyle) - str += "\\\(fontStyleName){" - } - currentfontStyle = atom.fontStyle - } - if atom.type == .fraction { - if let frac = atom as? MTFraction { - if frac.hasRule { - str += "\\frac{\(mathListToString(frac.numerator!))}{\(mathListToString(frac.denominator!))}" - } else { - var command: String? = nil - if frac.leftDelimiter == nil && frac.rightDelimiter == nil { - command = "atop" - } else if frac.leftDelimiter == "(" && frac.rightDelimiter == ")" { - command = "choose" - } else if frac.leftDelimiter == "{" && frac.rightDelimiter == "}" { - command = "brace" - } else if frac.leftDelimiter == "[" && frac.rightDelimiter == "]" { - command = "brack" - } else { - command = "atopwithdelims\(frac.leftDelimiter!)\(frac.rightDelimiter!)" - } - str += "{\(mathListToString(frac.numerator!)) \\\(command!) \(mathListToString(frac.denominator!))}" - } - } - } else if atom.type == .radical { - str += "\\sqrt" - if let rad = atom as? MTRadical { - if rad.degree != nil { - str += "[\(mathListToString(rad.degree!))]" - } - str += "{\(mathListToString(rad.radicand!))}" - } - } else if atom.type == .inner { - if let inner = atom as? MTInner { - if inner.leftBoundary != nil || inner.rightBoundary != nil { - if inner.leftBoundary != nil { - str += "\\left\(delimToString(delim: inner.leftBoundary!)) " - } else { - str += "\\left. " - } - - str += mathListToString(inner.innerList!) - - if inner.rightBoundary != nil { - str += "\\right\(delimToString(delim: inner.rightBoundary!)) " - } else { - str += "\\right. " - } - } else { - str += "{\(mathListToString(inner.innerList!))}" - } - } - } else if atom.type == .table { - if let table = atom as? MTMathTable { - if table.environment != nil { - str += "\\begin{\(table.environment!)}" - } - - for i in 0..= 1 && cell.atoms[0].type == .style { - // remove first atom - cell.atoms.removeFirst() - } - } - if table.environment == "eqalign" || table.environment == "aligned" || table.environment == "split" { - if j == 1 && cell.atoms.count >= 1 && cell.atoms[0].type == .ordinary && cell.atoms[0].nucleus.count == 0 { - // remove empty nucleus added for spacing - cell.atoms.removeFirst() - } - } - str += mathListToString(cell) - if j < row.count - 1 { - str += "&" - } - } - if i < table.numRows - 1 { - str += "\\\\ " - } - } - if table.environment != nil { - str += "\\end{\(table.environment!)}" - } - } - } else if atom.type == .overline { - if let overline = atom as? MTOverLine { - str += "\\overline" - str += "{\(mathListToString(overline.innerList!))}" - } - } else if atom.type == .underline { - if let underline = atom as? MTUnderLine { - str += "\\underline" - str += "{\(mathListToString(underline.innerList!))}" - } - } else if atom.type == .accent { - if let accent = atom as? MTAccent { - str += "\\\(MTMathAtomFactory.accentName(accent)!){\(mathListToString(accent.innerList!))}" - } - } else if atom.type == .largeOperator { - let op = atom as! MTLargeOperator - let command = MTMathAtomFactory.latexSymbolName(for: atom) - let originalOp = MTMathAtomFactory.atom(forLatexSymbol: command!) as! MTLargeOperator - str += "\\\(command!) " - if originalOp.limits != op.limits { - if op.limits { - str += "\\limits " - } else { - str += "\\nolimits " - } - } - } else if atom.type == .space { - if let space = atom as? MTMathSpace { - if let command = MTMathListBuilder.spaceToCommands[space.space] { - str += "\\\(command) " - } else { - str += String(format: "\\mkern%.1fmu", space.space) - } - } - } else if atom.type == .style { - if let style = atom as? MTMathStyle { - if let command = MTMathListBuilder.styleToCommands[style.style] { - str += "\\\(command) " - } - } - } else if atom.nucleus.isEmpty { - str += "{}" - } else if atom.nucleus == "\u{2236}" { - // math colon - str += ":" - } else if atom.nucleus == "\u{2212}" { - // math minus - str += "-" - } else { - if let command = MTMathAtomFactory.latexSymbolName(for: atom) { - str += "\\\(command) " - } else { - str += "\(atom.nucleus)" - } - } - - if atom.superScript != nil { - str += "^{\(mathListToString(atom.superScript!))}" - } - - if atom.subScript != nil { - str += "_{\(mathListToString(atom.subScript!))}" - } - } - } - if currentfontStyle != .defaultStyle { - str += "}" - } - return str + public mutating func buildInternal(_ oneCharOnly: Bool) -> MTMathList? { + self.buildInternal(oneCharOnly, stopChar: nil) } - public static func delimToString(delim: MTMathAtom) -> String { - if let command = MTMathAtomFactory.getDelimiterName(of: delim) { - let singleChars = [ "(", ")", "[", "]", "<", ">", "|", ".", "/"] - if singleChars.contains(command) { - return command - } else if command == "||" { - return "\\|" - } else { - return "\\\(command)" - } - } - - return "" - } - - func getNextCharacter() -> Character { - assert(self.hasCharacters, "Retrieving character at index \(self.currentCharIndex) beyond length \(self.string.count)") - let ch = string[currentCharIndex] - currentCharIndex = string.index(after: currentCharIndex) - return ch - } - - func unlookCharacter() { - assert(currentCharIndex > string.startIndex, "Unlooking when at the first character.") - if currentCharIndex > string.startIndex { - currentCharIndex = string.index(before: currentCharIndex) - } else { - print("unlooking at first character") - } - } - - public func buildInternal(_ oneCharOnly: Bool) -> MTMathList? { - return self.buildInternal(oneCharOnly, stopChar: nil) - } - - public func buildInternal(_ oneCharOnly: Bool, stopChar stop: Character?) -> MTMathList? { + public mutating func buildInternal(_ oneCharOnly: Bool, stopChar stop: Character?) -> MTMathList? { let list = MTMathList() assert(!(oneCharOnly && stop != nil), "Cannot set both oneCharOnly and stopChar.") var prevAtom: MTMathAtom? = nil while self.hasCharacters { - if error != nil { return nil } + if error != nil { return nil } // If there is an error thus far then bail out. var atom: MTMathAtom? = nil let char = self.getNextCharacter() if oneCharOnly { if char == "^" || char == "}" || char == "_" || char == "&" { + // this is not the character we are looking for. + // They are meant for the caller to look at. self.unlookCharacter() return list } } - + // If there is a stop character, keep scanning 'til we find it if stop != nil && char == stop! { return list } @@ -359,7 +208,8 @@ public class MTMathListBuilder { prevAtom = MTMathAtom(type: .ordinary, value: "") list.add(prevAtom!) } - + // this is a superscript for the previous atom + // note: if the next char is the stopChar it will be consumed by the ^ and so it doesn't count as stop prevAtom!.superScript = self.buildInternal(true) continue } else if char == "_" { @@ -370,6 +220,8 @@ public class MTMathListBuilder { prevAtom = MTMathAtom(type: .ordinary, value: "") list.add(prevAtom!) } + // this is a subscript for the previous atom + // note: if the next char is the stopChar it will be consumed by the _ and so it doesn't count as stop prevAtom!.subScript = self.buildInternal(true) continue } else if char == "{" { @@ -382,6 +234,7 @@ public class MTMathListBuilder { } continue } else if char == "}" { + // \ means a command assert(!oneCharOnly, "This should have been handled before") assert(stop == nil, "This should have been handled before") // We encountered a closing brace when there is no stop set, that means there was no @@ -428,18 +281,22 @@ public class MTMathListBuilder { return nil } } else if char == "&" { + // used for column separation in tables assert(!oneCharOnly, "This should have been handled before") if self.currentEnv != nil { return list } else { + // Create a new table with the current list and a default env let table = self.buildTable(env: nil, firstList: list, isRow: false) return MTMathList(atom: table!) } } else if spacesAllowed && char == " " { + // If spaces are allowed then spaces do not need escaping with a \ before being used. atom = MTMathAtomFactory.atom(forLatexSymbol: " ") } else { atom = MTMathAtomFactory.atom(forCharacter: char) if atom == nil { + // Not a recognized character continue } } @@ -453,7 +310,6 @@ public class MTMathListBuilder { return list } } - if stop != nil { if stop == "}" { // We did not find a corresponding closing brace. @@ -467,7 +323,194 @@ public class MTMathListBuilder { return list } - func atomForCommand(_ command:String) -> MTMathAtom? { + + // MARK: - MTMathList to LaTeX conversion + + /// This converts the MTMathList to LaTeX. + public static func mathListToString(_ ml: MTMathList?) -> String { + var str = "" + var currentfontStyle = MTFontStyle.defaultStyle + if let atomList = ml { + for atom in atomList.atoms { + if currentfontStyle != atom.fontStyle { + if currentfontStyle != .defaultStyle { + str += "}" + } + if atom.fontStyle != .defaultStyle { + let fontStyleName = MTMathAtomFactory.fontNameForStyle(atom.fontStyle) + str += "\\\(fontStyleName){" + } + currentfontStyle = atom.fontStyle + } + if atom.type == .fraction { + if let frac = atom as? MTFraction { + if frac.hasRule { + str += "\\frac{\(mathListToString(frac.numerator!))}{\(mathListToString(frac.denominator!))}" + } else { + let command: String + if frac.leftDelimiter.isEmpty && frac.rightDelimiter.isEmpty { + command = "atop" + } else if frac.leftDelimiter == "(" && frac.rightDelimiter == ")" { + command = "choose" + } else if frac.leftDelimiter == "{" && frac.rightDelimiter == "}" { + command = "brace" + } else if frac.leftDelimiter == "[" && frac.rightDelimiter == "]" { + command = "brack" + } else { + command = "atopwithdelims\(frac.leftDelimiter)\(frac.rightDelimiter)" + } + str += "{\(mathListToString(frac.numerator!)) \\\(command) \(mathListToString(frac.denominator!))}" + } + } + } else if atom.type == .radical { + str += "\\sqrt" + if let rad = atom as? MTRadical { + if rad.degree != nil { + str += "[\(mathListToString(rad.degree!))]" + } + str += "{\(mathListToString(rad.radicand!))}" + } + } else if atom.type == .inner { + if let inner = atom as? MTInner { + if inner.leftBoundary != nil || inner.rightBoundary != nil { + if inner.leftBoundary != nil { + str += "\\left\(delimToString(delim: inner.leftBoundary!)) " + } else { + str += "\\left. " + } + + str += mathListToString(inner.innerList!) + + if inner.rightBoundary != nil { + str += "\\right\(delimToString(delim: inner.rightBoundary!)) " + } else { + str += "\\right. " + } + } else { + str += "{\(mathListToString(inner.innerList!))}" + } + } + } else if atom.type == .table { + if let table = atom as? MTMathTable { + if !table.environment.isEmpty { + str += "\\begin{\(table.environment)}" + } + + for i in 0..= 1 && cell.atoms[0].type == .style { + // remove first atom + cell.atoms.removeFirst() + } + } + if table.environment == "eqalign" || table.environment == "aligned" || table.environment == "split" { + if j == 1 && cell.atoms.count >= 1 && cell.atoms[0].type == .ordinary && cell.atoms[0].nucleus.count == 0 { + // remove empty nucleus added for spacing + cell.atoms.removeFirst() + } + } + str += mathListToString(cell) + if j < row.count - 1 { + str += "&" + } + } + if i < table.numRows - 1 { + str += "\\\\ " + } + } + if !table.environment.isEmpty { + str += "\\end{\(table.environment)}" + } + } + } else if atom.type == .overline { + if let overline = atom as? MTOverLine { + str += "\\overline" + str += "{\(mathListToString(overline.innerList!))}" + } + } else if atom.type == .underline { + if let underline = atom as? MTUnderLine { + str += "\\underline" + str += "{\(mathListToString(underline.innerList!))}" + } + } else if atom.type == .accent { + if let accent = atom as? MTAccent { + str += "\\\(MTMathAtomFactory.accentName(accent)!){\(mathListToString(accent.innerList!))}" + } + } else if atom.type == .largeOperator { + let op = atom as! MTLargeOperator + let command = MTMathAtomFactory.latexSymbolName(for: atom) + let originalOp = MTMathAtomFactory.atom(forLatexSymbol: command!) as! MTLargeOperator + str += "\\\(command!) " + if originalOp.limits != op.limits { + if op.limits { + str += "\\limits " + } else { + str += "\\nolimits " + } + } + } else if atom.type == .space { + if let space = atom as? MTMathSpace { + if let command = Self.spaceToCommands[space.space] { + str += "\\\(command) " + } else { + str += String(format: "\\mkern%.1fmu", space.space) + } + } + } else if atom.type == .style { + if let style = atom as? MTMathStyle { + if let command = Self.styleToCommands[style.style] { + str += "\\\(command) " + } + } + } else if atom.nucleus.isEmpty { + str += "{}" + } else if atom.nucleus == "\u{2236}" { + // math colon + str += ":" + } else if atom.nucleus == "\u{2212}" { + // math minus + str += "-" + } else { + if let command = MTMathAtomFactory.latexSymbolName(for: atom) { + str += "\\\(command) " + } else { + str += "\(atom.nucleus)" + } + } + + if atom.superScript != nil { + str += "^{\(mathListToString(atom.superScript!))}" + } + + if atom.subScript != nil { + str += "_{\(mathListToString(atom.subScript!))}" + } + } + } + if currentfontStyle != .defaultStyle { + str += "}" + } + return str + } + + public static func delimToString(delim: MTMathAtom) -> String { + if let command = MTMathAtomFactory.getDelimiterName(of: delim) { + let singleChars = [ "(", ")", "[", "]", "<", ">", "|", ".", "/"] + if singleChars.contains(command) { + return command + } else if command == "||" { + return "\\|" + } else { + return "\\\(command)" + } + } + return "" + } + + mutating func atomForCommand(_ command:String) -> MTMathAtom? { if let atom = MTMathAtomFactory.atom(forLatexSymbol: command) { return atom } @@ -557,7 +600,7 @@ public class MTMathListBuilder { } } - func readColor() -> String? { + mutating func readColor() -> String? { if !self.expectCharacter("{") { // We didn't find an opening brace, so no env found. self.setError(.characterNotFound, message:"Missing {") @@ -588,7 +631,7 @@ public class MTMathListBuilder { return mutable; } - func skipSpaces() { + mutating func skipSpaces() { while self.hasCharacters { let ch = self.getNextCharacter().utf32Char if ch < 0x21 || ch > 0x7E { @@ -611,7 +654,7 @@ public class MTMathListBuilder { ] } - func stopCommand(_ command: String, list:MTMathList, stopChar:Character?) -> MTMathList? { + mutating func stopCommand(_ command: String, list:MTMathList, stopChar:Character?) -> MTMathList? { if command == "right" { if currentInnerAtom == nil { let errorMessage = "Missing \\left"; @@ -676,7 +719,7 @@ public class MTMathListBuilder { } // Applies the modifier to the atom. Returns true if modifier applied. - func applyModifier(_ modifier:String, atom:MTMathAtom?) -> Bool { + mutating func applyModifier(_ modifier:String, atom:MTMathAtom?) -> Bool { if modifier == "limits" { if atom?.type != .largeOperator { let errorMessage = "Limits can only be applied to an operator." @@ -699,14 +742,14 @@ public class MTMathListBuilder { return false } - func setError(_ code:MTParseErrors, message:String) { + mutating func setError(_ code:MTParseErrors, message:String) { // Only record the first error. if error == nil { error = NSError(domain: MTParseError, code: code.rawValue, userInfo: [ NSLocalizedDescriptionKey : message ]) } } - func atom(forCommand command: String) -> MTMathAtom? { + mutating func atom(forCommand command: String) -> MTMathAtom? { if let atom = MTMathAtomFactory.atom(forLatexSymbol: command) { return atom } @@ -786,7 +829,7 @@ public class MTMathListBuilder { } } - func readEnvironment() -> String? { + mutating func readEnvironment() -> String? { if !self.expectCharacter("{") { // We didn't find an opening brace, so no env found. self.setError(.characterNotFound, message: "Missing {") @@ -808,24 +851,7 @@ public class MTMathListBuilder { assert(ch >= "\u{21}" && ch <= "\u{7E}", "Expected non-space character \(ch)") } - func expectCharacter(_ ch: Character) -> Bool { - MTAssertNotSpace(ch) - self.skipSpaces() - - if self.hasCharacters { - let nextChar = self.getNextCharacter() - MTAssertNotSpace(nextChar) - if nextChar == ch { - return true - } else { - self.unlookCharacter() - return false - } - } - return false - } - - func buildTable(env: String?, firstList: MTMathList?, isRow: Bool) -> MTMathAtom? { + mutating func buildTable(env: String?, firstList: MTMathList?, isRow: Bool) -> MTMathAtom? { // Save the current env till an new one gets built. let oldEnv = self.currentEnv @@ -876,7 +902,7 @@ public class MTMathListBuilder { return table } - func getBoundaryAtom(_ delimiterType: String) -> MTMathAtom? { + mutating func getBoundaryAtom(_ delimiterType: String) -> MTMathAtom? { let delim = self.readDelimiter() if delim == nil { let errorMessage = "Missing delimiter for \\\(delimiterType)" @@ -892,7 +918,7 @@ public class MTMathListBuilder { return boundary } - func readDelimiter() -> String? { + mutating func readDelimiter() -> String? { self.skipSpaces() while self.hasCharacters { let char = self.getNextCharacter() @@ -910,7 +936,7 @@ public class MTMathListBuilder { return nil } - func readCommand() -> String { + mutating func readCommand() -> String { let singleChars = "{}$#%_| ,>;!\\" if self.hasCharacters { let char = self.getNextCharacter() @@ -923,7 +949,7 @@ public class MTMathListBuilder { return self.readString() } - func readString() -> String { + mutating func readString() -> String { // a string of all upper and lower case characters. var output = "" while self.hasCharacters { diff --git a/Sources/SwiftMath/MathRender/MTMathListDisplay.swift b/Sources/SwiftMath/MathRender/MTMathListDisplay.swift index cd4e6a7..7fe8ee5 100644 --- a/Sources/SwiftMath/MathRender/MTMathListDisplay.swift +++ b/Sources/SwiftMath/MathRender/MTMathListDisplay.swift @@ -1,8 +1,9 @@ // -// MTMathListDisplay.swift -// MathRenderSwift -// // 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 @@ -26,6 +27,7 @@ func isIos6Supported() -> Bool { return MTDisplay.supported } +// The Downshift protocol allows an MTDisplay to be shifted down by a given amount. protocol DownShift { var shiftDown:CGFloat { set get } } @@ -52,7 +54,7 @@ public class MTDisplay:NSObject { /// Gets the bounding rectangle for the MTDisplay func displayBounds() -> CGRect { - return CGRectMake(self.position.x, self.position.y - self.descent, self.width, self.ascent + self.descent) + 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. @@ -81,17 +83,17 @@ public class MTDisplay:NSObject { #endif /// The distance from the axis to the top of the display - var ascent:CGFloat = 0 + public var ascent:CGFloat = 0 /// The distance from the axis to the bottom of the display - var descent:CGFloat = 0 + public var descent:CGFloat = 0 /// The width of the display - var width:CGFloat = 0 + public var width:CGFloat = 0 /// Position of the display with respect to the parent view or display. - var position=CGPoint.zero + var position = CGPoint.zero /// The range of characters supported by this item - var range:NSRange=NSMakeRange(0, 0) + public var range:NSRange=NSMakeRange(0, 0) /// Whether the display has a subscript/superscript following it. - var hasScript:Bool = false + 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 @@ -101,6 +103,7 @@ public class MTDisplay:NSObject { } +/// Special class to be inherited from that implements the DownShift protocol class MTDisplayDS : MTDisplay, DownShift { var shiftDown: CGFloat = 0 @@ -110,10 +113,10 @@ class MTDisplayDS : MTDisplay, DownShift { // MARK: - MTCTLineDisplay /// A rendering of a single CTLine as an MTDisplay -class MTCTLineDisplay : MTDisplay { +public class MTCTLineDisplay : MTDisplay { /// The CTLine being displayed - var line:CTLine! + 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? { @@ -123,7 +126,7 @@ class MTCTLineDisplay : MTDisplay { } /// An array of MTMathAtoms that this CTLine displays. Used for indexing back into the MTMathList - var atoms = [MTMathAtom]() + public fileprivate(set) var atoms = [MTMathAtom]() init(withString attrString:NSAttributedString?, position:CGPoint, range:NSRange, font:MTFont?, atoms:[MTMathAtom]) { super.init() @@ -197,10 +200,9 @@ class MTCTLineDisplay : MTDisplay { public class MTMathListDisplay : MTDisplay { /** - @typedef MTLinePosition - @brief The type of position for a line, i.e. subscript/superscript or regular. + The type of position for a line, i.e. subscript/superscript or regular. */ - enum LinePosition : Int { + public enum LinePosition : Int { /// Regular case regular /// Positioned at a subscript @@ -210,13 +212,13 @@ public class MTMathListDisplay : MTDisplay { } /// Where the line is positioned - var type:LinePosition = .regular + public var type:LinePosition = .regular /// An array of MTDisplays which are positioned relative to the position of the /// the current display. - var subDisplays = [MTDisplay]() + 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 - var index: Int = 0 + public var index: Int = 0 init(withDisplays displays:[MTDisplay], range:NSRange) { super.init() @@ -287,23 +289,19 @@ public class MTMathListDisplay : MTDisplay { // MARK: - MTFractionDisplay /// Rendering of an MTFraction as an MTDisplay -class MTFractionDisplay : 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. */ - var numerator:MTMathListDisplay? + 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. */ - var denominator:MTMathListDisplay? + public fileprivate(set) var denominator:MTMathListDisplay? - var numeratorUp:CGFloat=0 { - didSet { self.updateNumeratorPosition() } - } - var denominatorDown:CGFloat=0 { - didSet { self.updateDenominatorPosition() } - } + var numeratorUp:CGFloat=0 { didSet { self.updateNumeratorPosition() } } + var denominatorDown:CGFloat=0 { didSet { self.updateDenominatorPosition() } } var linePosition:CGFloat=0 var lineThickness:CGFloat=0 @@ -316,17 +314,17 @@ class MTFractionDisplay : MTDisplay { assert(self.range.length == 1, "Fraction range length not 1 - range (\(range.location), \(range.length)") } - override var ascent:CGFloat { + override public var ascent:CGFloat { set { super.ascent = newValue } get { numerator!.ascent + self.numeratorUp } } - override var descent:CGFloat { + override public var descent:CGFloat { set { super.descent = newValue } get { denominator!.descent + self.denominatorDown } } - override var width:CGFloat { + override public var width:CGFloat { set { super.width = newValue } get { max(numerator!.width, denominator!.width) } } @@ -391,11 +389,11 @@ class MTRadicalDisplay : MTDisplay { /** A display representing the radicand of the radical. Its position is relative to the parent is not treated as a sub-display. */ - var radicand:MTMathListDisplay? + 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. */ - var degree:MTMathListDisplay? + public fileprivate(set) var degree:MTMathListDisplay? override var position: CGPoint { set { @@ -536,21 +534,13 @@ class MTGlyphDisplay : MTDisplayDS { } override var ascent:CGFloat { - set { - super.ascent = newValue - } - get { - return super.ascent - self.shiftDown; - } + set { super.ascent = newValue } + get { super.ascent - self.shiftDown } } override var descent:CGFloat { - set { - super.descent = newValue - } - get { - return super.descent + self.shiftDown; - } + set { super.descent = newValue } + get { super.descent + self.shiftDown } } } @@ -593,21 +583,13 @@ class MTGlyphConstructionDisplay:MTDisplayDS { } override var ascent:CGFloat { - set { - super.ascent = newValue - } - get { - return super.ascent - self.shiftDown; - } + set { super.ascent = newValue } + get { super.ascent - self.shiftDown } } override var descent:CGFloat { - set { - super.descent = newValue - } - get { - return super.descent + self.shiftDown; - } + set { super.descent = newValue } + get { super.descent + self.shiftDown } } } @@ -627,16 +609,8 @@ class MTLargeOpLimitsDisplay : MTDisplay { var lowerLimit:MTMathListDisplay? var limitShift:CGFloat=0 - var upperLimitGap:CGFloat=0 { - didSet { - self.updateUpperLimitPosition() - } - } - var lowerLimitGap:CGFloat=0 { - didSet { - self.updateUpperLimitPosition() - } - } + var upperLimitGap:CGFloat=0 { didSet { self.updateUpperLimitPosition() } } + var lowerLimitGap:CGFloat=0 { didSet { self.updateLowerLimitPosition() } } var extraPadding:CGFloat=0 var nucleus:MTDisplay? @@ -658,9 +632,7 @@ class MTLargeOpLimitsDisplay : MTDisplay { } override var ascent:CGFloat { - set { - super.ascent = newValue - } + set { super.ascent = newValue } get { if self.upperLimit != nil { return nucleus!.ascent + extraPadding + self.upperLimit!.ascent + upperLimitGap + self.upperLimit!.descent @@ -671,9 +643,7 @@ class MTLargeOpLimitsDisplay : MTDisplay { } override var descent:CGFloat { - set { - super.descent = newValue - } + set { super.descent = newValue } get { if self.lowerLimit != nil { return nucleus!.descent + extraPadding + lowerLimitGap + self.lowerLimit!.descent + self.lowerLimit!.ascent; diff --git a/Sources/SwiftMath/MathRender/MTMathListIndex.swift b/Sources/SwiftMath/MathRender/MTMathListIndex.swift index 1a49be6..34bd6bf 100644 --- a/Sources/SwiftMath/MathRender/MTMathListIndex.swift +++ b/Sources/SwiftMath/MathRender/MTMathListIndex.swift @@ -1,22 +1,51 @@ // -// MTMathListIndex.swift -// MathRenderSwift -// // 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 { - case none = 0 + /// 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 - case superScript - case subScript + /// 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 } @@ -37,6 +66,7 @@ public class MTMathListIndex { } } + /// 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 { @@ -50,6 +80,7 @@ public class MTMathListIndex { return nil } + /// Returns the next index. func nextIndex() -> MTMathListIndex { if self.subIndexType == .none { return MTMathListIndex(level0Index: self.atomIndex + 1) @@ -65,9 +96,7 @@ public class MTMathListIndex { * e.g. a superscript or a fraction numerator. This returns true if the innermost subindex points to the beginning of a * line. */ - func isBeginningOfLine() -> Bool { - return self.finalIndex == 0 - } + func isBeginningOfLine() -> Bool { self.finalIndex == 0 } func isAtSameLevel(with index: MTMathListIndex?) -> Bool { if self.subIndexType != index?.subIndexType { diff --git a/Sources/SwiftMath/MathRender/MTMathUILabel.swift b/Sources/SwiftMath/MathRender/MTMathUILabel.swift index dc9f4a2..ea4324f 100644 --- a/Sources/SwiftMath/MathRender/MTMathUILabel.swift +++ b/Sources/SwiftMath/MathRender/MTMathUILabel.swift @@ -1,8 +1,9 @@ // -// MTMathUILabel.swift -// MathRenderSwift +// Created by Mike Griebling on 2022-12-31. +// Translated from an Objective-C implementation by Kostub Deshmukh. // -// Created by Mike Griebling on 2023-01-01. +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. // import Foundation diff --git a/Sources/SwiftMath/MathRender/MTTypesetter.swift b/Sources/SwiftMath/MathRender/MTTypesetter.swift index 2504d2a..e80dfc5 100644 --- a/Sources/SwiftMath/MathRender/MTTypesetter.swift +++ b/Sources/SwiftMath/MathRender/MTTypesetter.swift @@ -1,15 +1,16 @@ // -// MTTypesetter.swift -// MathRenderSwift +// Created by Mike Griebling on 2022-12-31. +// Translated from an Objective-C implementation by Kostub Deshmukh. // -// Created by Mike Griebling on 2023-01-01. +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. // import Foundation import CoreGraphics import CoreText -// MARK: - - Inter Element Spacing +// MARK: - Inter Element Spacing enum InterElementSpaceType : Int { case invalid = -1 @@ -76,7 +77,7 @@ func getInterElementSpaceArrayIndexForType(_ type:MTMathAtomType, row:Bool) -> I } } -// MARK: - - Italics +// MARK: - Italics // mathit func getItalicized(_ ch:Character) -> UTF32Char { var unicode = ch.utf32Char @@ -235,7 +236,7 @@ func getSansSerif(_ ch:Character) -> UTF32Char { // mathfrak func getFraktur(_ ch:Character) -> UTF32Char { // Fraktur has exceptions: - switch(ch) { + switch ch { case "C": return 0x212D; // C Fraktur case "H": @@ -337,7 +338,7 @@ func getBboxDetails(_ bbox:CGRect, ascent:inout CGFloat, descent:inout CGFloat) descent = max(0, 0 - CGRectGetMinY(bbox)) } -// MARK: - - MTTypesetter +// MARK: - MTTypesetter class MTTypesetter { var font:MTFont! @@ -879,7 +880,7 @@ class MTTypesetter { currentPosition.x += max(superScript!.width + delta, ssubscript!.width) + styleFont.mathTable!.spaceAfterScript; } - // MARK: - - Fractions + // MARK: - Fractions func numeratorShiftUp(_ hasRule:Bool) -> CGFloat { if hasRule { @@ -1002,7 +1003,7 @@ class MTTypesetter { display.denominatorDown = denominatorShiftDown; display.lineThickness = barThickness; display.linePosition = barLocation; - if frac!.leftDelimiter == nil && frac!.rightDelimiter == nil { + if frac!.leftDelimiter.isEmpty && frac!.rightDelimiter.isEmpty { return display } else { return self.addDelimitersToFractionDisplay(display, forFraction:frac) @@ -1010,13 +1011,13 @@ class MTTypesetter { } func addDelimitersToFractionDisplay(_ display:MTFractionDisplay?, forFraction frac:MTFraction?) -> MTDisplay? { - assert(frac!.leftDelimiter != nil || frac!.rightDelimiter != nil, "Fraction should have a delimiters to call this function"); + assert(!frac!.leftDelimiter.isEmpty || !frac!.rightDelimiter.isEmpty, "Fraction should have a delimiters to call this function"); var innerElements = [MTDisplay]() let glyphHeight = self.fractionDelimiterHeight var position = CGPoint.zero - if !frac!.leftDelimiter!.isEmpty { - let leftGlyph = self.findGlyphForBoundary(frac!.leftDelimiter!, withHeight:glyphHeight()) + if !frac!.leftDelimiter.isEmpty { + let leftGlyph = self.findGlyphForBoundary(frac!.leftDelimiter, withHeight:glyphHeight()) leftGlyph!.position = position position.x += leftGlyph!.width innerElements.append(leftGlyph!) @@ -1026,8 +1027,8 @@ class MTTypesetter { position.x += display!.width innerElements.append(display!) - if !frac!.rightDelimiter!.isEmpty { - let rightGlyph = self.findGlyphForBoundary(frac!.rightDelimiter!, withHeight:glyphHeight()) + if !frac!.rightDelimiter.isEmpty { + let rightGlyph = self.findGlyphForBoundary(frac!.rightDelimiter, withHeight:glyphHeight()) rightGlyph!.position = position position.x += rightGlyph!.width innerElements.append(rightGlyph!) diff --git a/Sources/SwiftMath/MathRender/MTUnicode.swift b/Sources/SwiftMath/MathRender/MTUnicode.swift index 57a7124..a1fc48d 100644 --- a/Sources/SwiftMath/MathRender/MTUnicode.swift +++ b/Sources/SwiftMath/MathRender/MTUnicode.swift @@ -1,63 +1,64 @@ // -// MTUnicode.swift -// MathRenderSwift -// // 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 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 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 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 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 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) + 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 { diff --git a/Sources/SwiftMath/mathFonts.bundle/Info.plist b/Sources/SwiftMath/mathFonts.bundle/Info.plist deleted file mode 100644 index 03fb083601bc4e17ecb3676627e420712e8e5968..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1047 zcmZuvOHUL*5bo~rfiD<9c_R-2U6gTVmuEsk!mf)1c3F330SPi`W}BUk^Ky2UWD_+S zqZbceJa{n%V~mePWBdgky~)j+7vsrZS5VMr@Oj)de8I^oIQ7bU`V(q3Q{ySJTj{C zjngG+fd_x$U%`=p4P~xh5=Pqb% z*a@n6EET)uMdB8jwHij#*d(lH=w`5X9T$jFb{N_gqOxwgUbRy8Txu0mzJ51I3l{cD z?!JPOMVh?^`~!MG~l{@)B2 z`^y9)RO*vh^K6%r;gcRqPRs?_R|C^eTuLa*nw=-BRb9O}K1D5Rmdpitckh<8e7R&|D@$-5uNx$8J7t#^ zbZ;#;%dVGdB+2M7%8OBPCMFFpU9t-7P3epwkV>jM1GcSBNf$JZ?uW zY(-SQSF=q4JB~pFW(Bks1;f}h1OBJSX5e+ zVsS|n<#9o@`8E&-HgG^0xPXEJ(18cK!E5$Kfe0ip%&Zi^4Aoht9$3Qjv2h;q2-JWn zkOMcs1Mm`j1fRhV@CP=-2%Liy-hvO|3-}&>f?wcwxC4J9h-y&}nnZIbi&oH0bO${` zkI@tK6g@+)&>Qp?y+a?+SM(F@pkG`Q*TMC3gWMQ5$;G)D?lyOqy9WY6u%f|4RaX^v F|1U{0KQI6Q diff --git a/Sources/SwiftMath/mathFonts.bundle/en.lproj/InfoPlist.strings b/Sources/SwiftMath/mathFonts.bundle/en.lproj/InfoPlist.strings deleted file mode 100644 index 3967e063f94f2b9de2fdbeb4d90be9963443c793..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42 dcmYc)$jK}&F)+Bm!2kw~j1ZauMnky_oB)p~1JeKi diff --git a/Tests/SwiftMathTests/MTMathListBuilderTests.swift b/Tests/SwiftMathTests/MTMathListBuilderTests.swift index 44408f0..e6d1270 100644 --- a/Tests/SwiftMathTests/MTMathListBuilderTests.swift +++ b/Tests/SwiftMathTests/MTMathListBuilderTests.swift @@ -352,8 +352,8 @@ final class MTMathListBuilderTests: XCTestCase { XCTAssertEqual(frac.type, .fraction, desc) XCTAssertEqual(frac.nucleus, "", desc) XCTAssertTrue(frac.hasRule); - XCTAssertNil(frac.rightDelimiter); - XCTAssertNil(frac.leftDelimiter); + XCTAssertTrue(frac.rightDelimiter.isEmpty) + XCTAssertTrue(frac.leftDelimiter.isEmpty) var subList = frac.numerator! XCTAssertNotNil(subList, desc) @@ -550,8 +550,8 @@ final class MTMathListBuilderTests: XCTestCase { XCTAssertEqual(frac.type, .fraction, desc); XCTAssertEqual(frac.nucleus, "", desc); XCTAssertTrue(frac.hasRule); - XCTAssertNil(frac.rightDelimiter); - XCTAssertNil(frac.leftDelimiter); + XCTAssertTrue(frac.rightDelimiter.isEmpty) + XCTAssertTrue(frac.leftDelimiter.isEmpty) var subList = frac.numerator! XCTAssertNotNil(subList, desc); @@ -587,8 +587,8 @@ final class MTMathListBuilderTests: XCTestCase { XCTAssertEqual(frac.type, .fraction, desc); XCTAssertEqual(frac.nucleus, "", desc); XCTAssertTrue(frac.hasRule); - XCTAssertNil(frac.rightDelimiter); - XCTAssertNil(frac.leftDelimiter); + XCTAssertTrue(frac.rightDelimiter.isEmpty) + XCTAssertTrue(frac.leftDelimiter.isEmpty) var subList = frac.numerator! XCTAssertNotNil(subList, desc); @@ -621,8 +621,8 @@ final class MTMathListBuilderTests: XCTestCase { XCTAssertEqual(frac.type, .fraction, desc); XCTAssertEqual(frac.nucleus, "", desc); XCTAssertFalse(frac.hasRule); - XCTAssertNil(frac.rightDelimiter); - XCTAssertNil(frac.leftDelimiter); + XCTAssertTrue(frac.rightDelimiter.isEmpty) + XCTAssertTrue(frac.leftDelimiter.isEmpty) var subList = frac.numerator! XCTAssertNotNil(subList, desc); @@ -658,8 +658,8 @@ final class MTMathListBuilderTests: XCTestCase { XCTAssertEqual(frac.type, .fraction, desc); XCTAssertEqual(frac.nucleus, "", desc); XCTAssertFalse(frac.hasRule); - XCTAssertNil(frac.rightDelimiter); - XCTAssertNil(frac.leftDelimiter); + XCTAssertTrue(frac.rightDelimiter.isEmpty) + XCTAssertTrue(frac.leftDelimiter.isEmpty) var subList = frac.numerator! XCTAssertNotNil(subList, desc); @@ -1020,7 +1020,7 @@ final class MTMathListBuilderTests: XCTestCase { let table = list.atoms[0] as! MTMathTable XCTAssertEqual(table.type, .table); XCTAssertEqual(table.nucleus, ""); - XCTAssertNil(table.environment); + XCTAssertTrue(table.environment.isEmpty); XCTAssertEqual(table.interRowAdditionalSpacing, 1); XCTAssertEqual(table.interColumnSpacing, 0); XCTAssertEqual(table.numRows, 2); @@ -1051,7 +1051,7 @@ final class MTMathListBuilderTests: XCTestCase { let table = list.atoms[0] as! MTMathTable XCTAssertEqual(table.type, .table); XCTAssertEqual(table.nucleus, ""); - XCTAssertNil(table.environment); + XCTAssertTrue(table.environment.isEmpty); XCTAssertEqual(table.interRowAdditionalSpacing, 1); XCTAssertEqual(table.interColumnSpacing, 0); XCTAssertEqual(table.numRows, 2);