From 9afc6970d4b31d2a126b897996c7d0b1e301d7ca Mon Sep 17 00:00:00 2001 From: Michael Griebling Date: Sat, 7 Jan 2023 07:53:46 -0500 Subject: [PATCH] Passed first set of builder tests. --- .../MathRender/MTMathAtomFactory.swift | 234 +++-- .../MathRender/MTMathList.swift | 60 +- .../MathRender/MTMathListBuilder.swift | 612 ++++++------ .../MathRender/MTMathUILabel.swift | 6 +- .../MathRender/MTTypesetter.swift | 2 +- .../SwiftMathRenderTests.swift | 915 +++++++++++++++++- 6 files changed, 1354 insertions(+), 475 deletions(-) diff --git a/Sources/SwiftMathRender/MathRender/MTMathAtomFactory.swift b/Sources/SwiftMathRender/MathRender/MTMathAtomFactory.swift index c617d6c..9db0e77 100644 --- a/Sources/SwiftMathRender/MathRender/MTMathAtomFactory.swift +++ b/Sources/SwiftMathRender/MathRender/MTMathAtomFactory.swift @@ -273,56 +273,56 @@ public class MTMathAtomFactory { "amalg" : MTMathAtom(type: .binaryOperator, value: "\u{2A3F}"), // No limit operators - "log" : MTMathAtomFactory.getOperator(withName: "log", limits: false), - "lg" : MTMathAtomFactory.getOperator(withName: "lg", limits: false), - "ln" : MTMathAtomFactory.getOperator(withName: "ln", limits: false), - "sin" : MTMathAtomFactory.getOperator(withName: "sin", limits: false), - "arcsin" : MTMathAtomFactory.getOperator(withName: "arcsin", limits: false), - "sinh" : MTMathAtomFactory.getOperator(withName: "sinh", limits: false), - "cos" : MTMathAtomFactory.getOperator(withName: "cos", limits: false), - "arccos" : MTMathAtomFactory.getOperator(withName: "arccos", limits: false), - "cosh" : MTMathAtomFactory.getOperator(withName: "cosh", limits: false), - "tan" : MTMathAtomFactory.getOperator(withName: "tan", limits: false), - "arctan" : MTMathAtomFactory.getOperator(withName: "arctan", limits: false), - "tanh" : MTMathAtomFactory.getOperator(withName: "tanh", limits: false), - "cot" : MTMathAtomFactory.getOperator(withName: "cot", limits: false), - "coth" : MTMathAtomFactory.getOperator(withName: "coth", limits: false), - "sec" : MTMathAtomFactory.getOperator(withName: "sec", limits: false), - "csc" : MTMathAtomFactory.getOperator(withName: "csc", limits: false), - "arg" : MTMathAtomFactory.getOperator(withName: "arg", limits: false), - "ker" : MTMathAtomFactory.getOperator(withName: "ker", limits: false), - "dim" : MTMathAtomFactory.getOperator(withName: "dim", limits: false), - "hom" : MTMathAtomFactory.getOperator(withName: "hom", limits: false), - "exp" : MTMathAtomFactory.getOperator(withName: "exp", limits: false), - "deg" : MTMathAtomFactory.getOperator(withName: "deg", limits: false), + "log" : MTMathAtomFactory.operatorWithName( "log", limits: false), + "lg" : MTMathAtomFactory.operatorWithName( "lg", limits: false), + "ln" : MTMathAtomFactory.operatorWithName( "ln", limits: false), + "sin" : MTMathAtomFactory.operatorWithName( "sin", limits: false), + "arcsin" : MTMathAtomFactory.operatorWithName( "arcsin", limits: false), + "sinh" : MTMathAtomFactory.operatorWithName( "sinh", limits: false), + "cos" : MTMathAtomFactory.operatorWithName( "cos", limits: false), + "arccos" : MTMathAtomFactory.operatorWithName( "arccos", limits: false), + "cosh" : MTMathAtomFactory.operatorWithName( "cosh", limits: false), + "tan" : MTMathAtomFactory.operatorWithName( "tan", limits: false), + "arctan" : MTMathAtomFactory.operatorWithName( "arctan", limits: false), + "tanh" : MTMathAtomFactory.operatorWithName( "tanh", limits: false), + "cot" : MTMathAtomFactory.operatorWithName( "cot", limits: false), + "coth" : MTMathAtomFactory.operatorWithName( "coth", limits: false), + "sec" : MTMathAtomFactory.operatorWithName( "sec", limits: false), + "csc" : MTMathAtomFactory.operatorWithName( "csc", limits: false), + "arg" : MTMathAtomFactory.operatorWithName( "arg", limits: false), + "ker" : MTMathAtomFactory.operatorWithName( "ker", limits: false), + "dim" : MTMathAtomFactory.operatorWithName( "dim", limits: false), + "hom" : MTMathAtomFactory.operatorWithName( "hom", limits: false), + "exp" : MTMathAtomFactory.operatorWithName( "exp", limits: false), + "deg" : MTMathAtomFactory.operatorWithName( "deg", limits: false), // Limit operators - "lim" : MTMathAtomFactory.getOperator(withName: "lim", limits: true), - "limsup" : MTMathAtomFactory.getOperator(withName: "lim sup", limits: true), - "liminf" : MTMathAtomFactory.getOperator(withName: "lim inf", limits: true), - "max" : MTMathAtomFactory.getOperator(withName: "max", limits: true), - "min" : MTMathAtomFactory.getOperator(withName: "min", limits: true), - "sup" : MTMathAtomFactory.getOperator(withName: "sup", limits: true), - "inf" : MTMathAtomFactory.getOperator(withName: "inf", limits: true), - "det" : MTMathAtomFactory.getOperator(withName: "det", limits: true), - "Pr" : MTMathAtomFactory.getOperator(withName: "Pr", limits: true), - "gcd" : MTMathAtomFactory.getOperator(withName: "gcd", limits: true), + "lim" : MTMathAtomFactory.operatorWithName( "lim", limits: true), + "limsup" : MTMathAtomFactory.operatorWithName( "lim sup", limits: true), + "liminf" : MTMathAtomFactory.operatorWithName( "lim inf", limits: true), + "max" : MTMathAtomFactory.operatorWithName( "max", limits: true), + "min" : MTMathAtomFactory.operatorWithName( "min", limits: true), + "sup" : MTMathAtomFactory.operatorWithName( "sup", limits: true), + "inf" : MTMathAtomFactory.operatorWithName( "inf", limits: true), + "det" : MTMathAtomFactory.operatorWithName( "det", limits: true), + "Pr" : MTMathAtomFactory.operatorWithName( "Pr", limits: true), + "gcd" : MTMathAtomFactory.operatorWithName( "gcd", limits: true), // Large operators - "prod" : MTMathAtomFactory.getOperator(withName: "\u{220F}", limits: true), - "coprod" : MTMathAtomFactory.getOperator(withName: "\u{2210}", limits: true), - "sum" : MTMathAtomFactory.getOperator(withName: "\u{2211}", limits: true), - "int" : MTMathAtomFactory.getOperator(withName: "\u{222B}", limits: false), - "oint" : MTMathAtomFactory.getOperator(withName: "\u{222E}", limits: false), - "bigwedge" : MTMathAtomFactory.getOperator(withName: "\u{22C0}", limits: true), - "bigvee" : MTMathAtomFactory.getOperator(withName: "\u{22C1}", limits: true), - "bigcap" : MTMathAtomFactory.getOperator(withName: "\u{22C2}", limits: true), - "bigcup" : MTMathAtomFactory.getOperator(withName: "\u{22C3}", limits: true), - "bigodot" : MTMathAtomFactory.getOperator(withName: "\u{2A00}", limits: true), - "bigoplus" : MTMathAtomFactory.getOperator(withName: "\u{2A01}", limits: true), - "bigotimes" : MTMathAtomFactory.getOperator(withName: "\u{2A02}", limits: true), - "biguplus" : MTMathAtomFactory.getOperator(withName: "\u{2A04}", limits: true), - "bigsqcup" : MTMathAtomFactory.getOperator(withName: "\u{2A06}", limits: true), + "prod" : MTMathAtomFactory.operatorWithName( "\u{220F}", limits: true), + "coprod" : MTMathAtomFactory.operatorWithName( "\u{2210}", limits: true), + "sum" : MTMathAtomFactory.operatorWithName( "\u{2211}", limits: true), + "int" : MTMathAtomFactory.operatorWithName( "\u{222B}", limits: false), + "oint" : MTMathAtomFactory.operatorWithName( "\u{222E}", limits: false), + "bigwedge" : MTMathAtomFactory.operatorWithName( "\u{22C0}", limits: true), + "bigvee" : MTMathAtomFactory.operatorWithName( "\u{22C1}", limits: true), + "bigcap" : MTMathAtomFactory.operatorWithName( "\u{22C2}", limits: true), + "bigcup" : MTMathAtomFactory.operatorWithName( "\u{22C3}", limits: true), + "bigodot" : MTMathAtomFactory.operatorWithName( "\u{2A00}", limits: true), + "bigoplus" : MTMathAtomFactory.operatorWithName( "\u{2A01}", limits: true), + "bigotimes" : MTMathAtomFactory.operatorWithName( "\u{2A02}", limits: true), + "biguplus" : MTMathAtomFactory.operatorWithName( "\u{2A04}", limits: true), + "bigsqcup" : MTMathAtomFactory.operatorWithName( "\u{2A06}", limits: true), // Latex command characters "{" : MTMathAtom(type: .open, value: "{"), @@ -454,6 +454,40 @@ public class MTMathAtomFactory { return nil } + public static func fontNameForStyle(_ fontStyle:MTFontStyle) -> String { + switch fontStyle { + case .defaultStyle: + return "mathnormal"; + + case .roman: + return "mathrm"; + + case .bold: + return "mathbf"; + + case .fraktur: + return "mathfrak"; + + case .caligraphic: + return "mathcal"; + + case .italic: + return "mathit"; + + case .sansSerif: + return "mathsf"; + + case .blackboard: + return "mathbb"; + + case .typewriter: + return "mathtt"; + + case .boldItalic: + return "bm"; + } + } + // Return an atom for times sign \times or * public static func times() -> MTMathAtom { return MTMathAtom(type: .binaryOperator, value: UnicodeSymbol.multiplication) @@ -511,42 +545,40 @@ public class MTMathAtomFactory { - Chars with special meaning in latex: ^ _ { } \ All other characters will have a non-nil atom returned. */ - public static func atom(for char: String) -> MTMathAtom? { - let atomCharacterSet = CharacterSet(charactersIn: UnicodeScalar(0x21)!...UnicodeScalar(0x7E)!) - if char.rangeOfCharacter(from: atomCharacterSet) != nil { - switch char { + public static func atom(forCharacter ch: Character) -> MTMathAtom? { + let chStr = String(ch) + switch chStr { + case "\\u{0410}"..."\\u{044F}": + return MTMathAtom(type: .ordinary, value: chStr) + case _ where ch.utf32Char < 0x0021 || ch.utf32Char > 0x007E: + return nil case "$", "%", "#", "&", "~", "\'", "^", "_", "{", "}", "\\": return nil case "(", "[": - return MTMathAtom(type: .open, value: char) + return MTMathAtom(type: .open, value: chStr) case ")", "]", "!", "?": - return MTMathAtom(type: .close, value: char) + return MTMathAtom(type: .close, value: chStr) case ",", ";": - return MTMathAtom(type: .punctuation, value: char) + return MTMathAtom(type: .punctuation, value: chStr) case "=", ">", "<": - return MTMathAtom(type: .relation, value: char) + return MTMathAtom(type: .relation, value: chStr) case ":": // Math colon is ratio. Regular colon is \colon return MTMathAtom(type: .relation, value: "\u{2236}") case "-": return MTMathAtom(type: .binaryOperator, value: "\u{2212}") case "+", "*": - return MTMathAtom(type: .binaryOperator, value: char) - case ".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9": - return MTMathAtom(type: .number, value: char) - case _ where - (char.rangeOfCharacter(from: CharacterSet.lowercaseLetters) != nil) || - (char.rangeOfCharacter(from: CharacterSet.lowercaseLetters) != nil) : - return MTMathAtom(type: .variable, value: char) + return MTMathAtom(type: .binaryOperator, value: chStr) + case ".", "0"..."9": + return MTMathAtom(type: .number, value: chStr) + case "a"..."z", "A"..."Z": + return MTMathAtom(type: .variable, value: chStr) case "\"", "/", "@", "`", "|": - return MTMathAtom(type: .ordinary, value: char) + return MTMathAtom(type: .ordinary, value: chStr) default: - assert(false, "Unknown Character: \(char)") - print("Unknown characters: \(char)") + assertionFailure("Unknown ASCII character '\(ch)'. Should have been handled earlier.") return nil - } } - return nil } /** Returns a `MTMathList` with one atom per character in the given string. This function @@ -556,7 +588,7 @@ public class MTMathAtomFactory { let list = MTMathList() for character in string { - if let newAtom = atom(for: "\(character)") { + if let newAtom = atom(forCharacter: character) { list.add(newAtom) } } @@ -602,14 +634,14 @@ public class MTMathAtomFactory { e.g. to define a symbol for "lcm" one can call: `[MTMathAtomFactory addLatexSymbol:@"lcm" value:[MTMathAtomFactory operatorWithName:@"lcm" limits: false)]` */ - public static func define(latexSymbol name: String, value: MTMathAtom) { + public static func add(latexSymbol name: String, value: MTMathAtom) { Self.sharedInstance.supportedLatexSymbols[name] = value Self.sharedInstance.textToLatexSymbolName[value.nucleus] = name } /** Returns a large opertor for the given name. If limits is true, limits are set up on the operator and displyed differently. */ - public static func getOperator(withName name: String, limits: Bool) -> MTLargeOperator { + public static func operatorWithName(_ name: String, limits: Bool) -> MTLargeOperator { return MTLargeOperator(value: name, limits: limits) } @@ -617,7 +649,7 @@ public class MTMathAtomFactory { such as `grave`, `hat` etc. If the name is not a recognized accent name, this returns nil. The `innerList` of the returned `MTAccent` is nil. */ - public static func getAccent(withName name: String) -> MTAccent? { + public static func accent(withName name: String) -> MTAccent? { if let accentValue = Self.accents[name] { return MTAccent(value: accentValue) } @@ -626,7 +658,7 @@ public class MTMathAtomFactory { /** Returns the accent name for the given accent. This is the reverse of the above function. */ - public static func getName(of accent: MTAccent) -> String? { + public static func accentName(_ accent: MTAccent) -> String? { return Self.sharedInstance.accentValueToName[accent.nucleus] } @@ -681,7 +713,7 @@ public class MTMathAtomFactory { @note The reason this function returns a `MTMathAtom` and not a `MTMathTable` is because some matrix environments are have builtin delimiters added to the table and hence are returned as inner atoms. */ - public static func table(withEnvironment env: String?, rows: [[MTMathList]]) -> MTMathAtom? { + public static func table(withEnvironment env: String?, rows: [[MTMathList]], error:inout NSError?) -> MTMathAtom? { let table = MTMathTable(environment: env) for i in 0.. MTColumnAlignment { + public func get(alignmentForColumn col: Int) -> MTColumnAlignment { if self.alignments.count <= col { return MTColumnAlignment.center } else { @@ -576,7 +576,7 @@ public class MTMathTable: MTMathAtom { } } - func numberOfCols() -> Int { + public var numColumns: Int { var numberOfCols = 0 for row in self.cells { @@ -586,7 +586,7 @@ public class MTMathTable: MTMathAtom { return numberOfCols } - func numberOfRows() -> Int { + public var numRows: Int { return self.cells.count } } diff --git a/Sources/SwiftMathRender/MathRender/MTMathListBuilder.swift b/Sources/SwiftMathRender/MathRender/MTMathListBuilder.swift index 14145f7..11e3c10 100644 --- a/Sources/SwiftMathRender/MathRender/MTMathListBuilder.swift +++ b/Sources/SwiftMathRender/MathRender/MTMathListBuilder.swift @@ -92,6 +92,7 @@ public class MTMathListBuilder { ] init(string: String) { + self.error = nil self.string = string self.currentCharIndex = string.startIndex self.currentFontStyle = .defaultStyle @@ -115,176 +116,188 @@ public class MTMathListBuilder { return builder.build() } - public static func build(fromString string: String, error:inout NSError?) -> MTMathList? { + public static func build(fromString string: String, error:inout NSError) -> MTMathList? { let builder = MTMathListBuilder(string: string) let output = builder.build() if builder.error != nil { - if error != nil { - error = builder.error - } + error = builder.error! return nil + } else { + error = NSError() } return output } public static func mathListToString(_ ml: MTMathList?) -> String { - var output = "" - + var str = "" + var currentfontStyle = MTFontStyle.defaultStyle if let atomList = ml { for atom in atomList.atoms { - switch atom.type { - case .fraction: - if let frac = atom as? MTFraction { - if frac.hasRule { - output += "\\frac{\(mathListToString(frac.numerator!))}{\(mathListToString(frac.denominator!))}" - } else { - var command: String? = nil - - if frac.leftDelimiter == nil && frac.rightDelimiter == nil { - command = "atop" - } else if frac.leftDelimiter == "(" && frac.rightDelimiter == ")" { - command = "choose" - } else if frac.leftDelimiter == "{" && frac.rightDelimiter == "}" { - command = "brace" - } else if frac.leftDelimiter == "[" && frac.rightDelimiter == "]" { - command = "brack" - } else { - command = "atopwithdelims\(frac.leftDelimiter!)\(frac.rightDelimiter!)" - } - - output += "{\(mathListToString(frac.numerator!)) \\\(command!) \(mathListToString(frac.denominator!))}" - } - } - break - case .radical: - output += "\\sqrt" - if let rad = atom as? MTRadical { - if rad.degree != nil { - output += "[\(mathListToString(rad.degree!))]" - } - output += "{\(mathListToString(rad.radicand!))}" - } - break - case .inner: - if let inner = atom as? MTInner { - if inner.leftBoundary != nil || inner.rightBoundary != nil { - if inner.leftBoundary != nil { - output += "\\left\(delimToString(delim: inner.leftBoundary!))" - } else { - output += "\\left. " - } - - output += mathListToString(inner.innerList!) - - if inner.rightBoundary != nil { - output += "\\right\(delimToString(delim: inner.rightBoundary!))" - } else { - output += "\\right. " - } - } else { - output += "{\(mathListToString(inner.innerList!))}" - } - } - break - case .table: - if let table = atom as? MTMathTable { - if table.environment != nil { - output += "\\begin{\(table.environment!)}" - } - - for i in 0..= 1 && cell.atoms[0].type == .style { - // remove first atom - cell.atoms.removeFirst() - } - } - if table.environment == "eqalign" || table.environment == "aligned" || table.environment == "split" { - if j == 1 && cell.atoms.count >= 1 && cell.atoms[0].type == .ordinary && cell.atoms[0].nucleus.count == 0 { - // remove empty nucleus added for spacing - cell.atoms.removeFirst() - } - } - - output += mathListToString(cell) - - if j < table.cells[i].count { - output += "&" - } - } - if i < table.numberOfRows() - 1 { - output += "\\\\ " - } - } - - if table.environment != nil { - output += "\\end{\(table.environment!)}" - } - } - break - case .overline: - if let overline = atom as? MTOverLine { - output += "\\overline" - output += "{\(mathListToString(overline.innerList!))}" - } - break - case .underline: - if let underline = atom as? MTUnderLine { - output += "\\underline" - output += "{\(mathListToString(underline.innerList!))}" - } - break - case .accent: - if let accent = atom as? MTAccent { - output += "\\\(MTMathAtomFactory.getName(of: accent)!){\(mathListToString(accent.innerList!))}" - } - break - case .space: - if let space = atom as? MTMathSpace { - if let command = MTMathListBuilder.spaceToCommands[space.space] { - output += "\\\(command)" - } else { - output += String.init(format: "\\mkern%.1fmu", space.space) - } - } - break - case .style: - if let style = atom as? MTMathStyle { - if let command = MTMathListBuilder.styleToCommands[style.style] { - output += "\\\(command)" - } - } - break - default: - if atom.nucleus.count == 0 { - output += "{}" - } else if atom.nucleus == "\u{2236}" { - output += ":" - } else if atom.nucleus == "\u{2212}" { - output += "-" + 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 { - if let command = MTMathAtomFactory.latexSymbolName(for: atom) { - output += "\\\(command)" + 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 { - output += "\(atom.nucleus)" + 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 += "\\\\ " } } - break + + 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 { - output += "^{\(mathListToString(atom.superScript!))}" + str += "^{\(mathListToString(atom.superScript!))}" } if atom.subScript != nil { - output += "_{\(mathListToString(atom.subScript!))}" + str += "_{\(mathListToString(atom.subScript!))}" } } } - return output + if currentfontStyle != .defaultStyle { + str += "}" + } + return str } public static func delimToString(delim: MTMathAtom) -> String { @@ -302,14 +315,11 @@ public class MTMathListBuilder { return "" } - func getNextCharacter() -> Character? { + func getNextCharacter() -> Character { assert(self.hasCharacters, "Retrieving character at index \(self.currentCharIndex) beyond length \(self.string.count)") - if self.hasCharacters { - let ch = string[currentCharIndex] - currentCharIndex = string.index(after: currentCharIndex) - return ch - } - return nil + let ch = string[currentCharIndex] + currentCharIndex = string.index(after: currentCharIndex) + return ch } func unlookCharacter() { @@ -322,10 +332,10 @@ public class MTMathListBuilder { } func buildInternal(_ oneCharOnly: Bool) -> MTMathList? { - return self.buildInternal(oneCharOnly, stop: nil) + return self.buildInternal(oneCharOnly, stopChar: nil) } - func buildInternal(_ oneCharOnly: Bool, stop: Character?) -> MTMathList? { + 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 @@ -333,7 +343,7 @@ public class MTMathListBuilder { if error != nil { return nil } var atom: MTMathAtom? = nil - let char = self.getNextCharacter()! + let char = self.getNextCharacter() if oneCharOnly { if char == "^" || char == "}" || char == "_" || char == "&" { @@ -342,7 +352,7 @@ public class MTMathListBuilder { } } - if stop != nil && char == stop { + if stop != nil && char == stop! { return list } @@ -355,7 +365,7 @@ public class MTMathListBuilder { list.add(prevAtom!) } - prevAtom!.setSuperScript(self.buildInternal(true)) + prevAtom!.superScript = self.buildInternal(true) continue } else if char == "_" { assert(!oneCharOnly, "This should have been handled before") @@ -365,46 +375,43 @@ public class MTMathListBuilder { prevAtom = MTMathAtom(type: .ordinary, value: "") list.add(prevAtom!) } - prevAtom!.setSubScript(self.buildInternal(true)) + prevAtom!.subScript = self.buildInternal(true) continue } else if char == "{" { // this puts us in a recursive routine, and sets oneCharOnly to false and no stop character - if let subList = self.buildInternal(false, stop: "}") { - prevAtom = subList.atoms.last - list.append(subList) - if oneCharOnly { - return list - } - continue - } else { - print("open brackets but inner...") - return nil + let subList = self.buildInternal(false, stopChar: "}") + prevAtom = subList!.atoms.last + list.append(subList!) + if oneCharOnly { + return list } + continue } else if char == "}" { assert(!oneCharOnly, "This should have been handled before") assert(stop == nil, "This should have been handled before") // We encountered a closing brace when there is no stop set, that means there was no // corresponding opening brace. - print("Mismatched braces") + let errorMessage = "Mismatched braces." + self.setError(.mismatchBraces, message:errorMessage) return nil } else if char == "\\" { let command = readCommand() - let done = stopCommand(command, list:list, stop:stop) + let done = stopCommand(command, list:list, stopChar:stop) if done != nil { return done } else if error != nil { return nil } if self.applyModifier(command, atom:prevAtom) { - continue; + continue } - let fontStyle = MTMathAtomFactory.fontStyleWithName(command) - if fontStyle != nil { + + if let fontStyle = MTMathAtomFactory.fontStyleWithName(command) { let oldSpacesAllowed = spacesAllowed // Text has special consideration where it allows spaces without escaping. spacesAllowed = command == "text" let oldFontStyle = currentFontStyle - currentFontStyle = fontStyle! + currentFontStyle = fontStyle let sublist = self.buildInternal(true)! // Restore the font style. currentFontStyle = oldFontStyle @@ -431,27 +438,20 @@ public class MTMathListBuilder { if self.currentEnv != nil { return list } else { - if let table = self.buildTable(env: nil, firstList: list, isRow: false) { - return MTMathList(atom: table) - } + let table = self.buildTable(env: nil, firstList: list, isRow: false) + return MTMathList(atom: table!) } } else if spacesAllowed && char == " " { atom = MTMathAtomFactory.atom(forLatexSymbol: " ") } else { - atom = MTMathAtomFactory.atom(for: String(char)) - + atom = MTMathAtomFactory.atom(forCharacter: char) if atom == nil { continue } } assert(atom != nil, "Atom shouldn't be nil") - - if atom == nil { - print("wtf atom why nil?") - return nil - } - + atom?.fontStyle = currentFontStyle list.add(atom!) prevAtom = atom @@ -462,12 +462,14 @@ public class MTMathListBuilder { if stop != nil { if stop == "}" { - print("Missing Closing Brace") + // We did not find a corresponding closing brace. + self.setError(.mismatchBraces, message:"Missing closing brace") } else { - print("Expected Character not found: \(stop!)") + // we never found our stop character + let errorMessage = "Expected character not found: \(stop!)" + self.setError(.characterNotFound, message:errorMessage) } } - return list } @@ -475,7 +477,7 @@ public class MTMathListBuilder { if let atom = MTMathAtomFactory.atom(forLatexSymbol: command) { return atom } - if let accent = MTMathAtomFactory.getAccent(withName: command) { + if let accent = MTMathAtomFactory.accent(withName: command) { // The command is an accent accent.innerList = self.buildInternal(true) return accent; @@ -499,7 +501,7 @@ public class MTMathListBuilder { let ch = self.getNextCharacter() if (ch == "[") { // special handling for sqrt[degree]{radicand} - rad.degree = self.buildInternal(false, stop:"]") + rad.degree = self.buildInternal(false, stopChar:"]") rad.radicand = self.buildInternal(true) } else { self.unlookCharacter() @@ -574,8 +576,8 @@ public class MTMathListBuilder { // a string of all upper and lower case characters. var mutable = "" while self.hasCharacters { - let ch = self.getNextCharacter()! - if (ch == "#" || (ch >= "A" && ch <= "F") || (ch >= "a" && ch <= "f") || (ch >= "0" && ch <= "9")) { + let ch = self.getNextCharacter() + if ch == "#" || (ch >= "A" && ch <= "F") || (ch >= "a" && ch <= "f") || (ch >= "0" && ch <= "9") { mutable.append(ch) // appendString:[NSString stringWithCharacters:&ch length:1]]; } else { // we went too far @@ -594,7 +596,7 @@ public class MTMathListBuilder { func skipSpaces() { while self.hasCharacters { - let ch = self.getNextCharacter()?.asciiValue ?? 0 + let ch = self.getNextCharacter().utf32Char if ch < 0x21 || ch > 0x7E { // skip non ascii characters and spaces continue; @@ -605,7 +607,7 @@ public class MTMathListBuilder { } } - func stopCommand(_ command: String, list:MTMathList, stop:Character?) -> MTMathList? { + func stopCommand(_ command: String, list:MTMathList, stopChar:Character?) -> MTMathList? { var fractionCommands: [String:[Character]] { [ "over": [], @@ -627,20 +629,19 @@ public class MTMathListBuilder { } // return the list read so far. return list - } else if let _ = fractionCommands[command] { + } else if let delims = fractionCommands[command] { var frac:MTFraction! = nil; if command == "over" { frac = MTFraction() } else { frac = MTFraction(hasRule: false) } - let delims = fractionCommands[command]! if delims.count == 2 { frac.leftDelimiter = String(delims[0]) frac.rightDelimiter = String(delims[1]) } frac.numerator = list; - frac.denominator = self.buildInternal(false, stop: stop) + frac.denominator = self.buildInternal(false, stopChar: stopChar) if error != nil { return nil; } @@ -658,7 +659,7 @@ public class MTMathListBuilder { return MTMathList(atom: table!) } } else if command == "end" { - if currentEnv != nil { + if currentEnv == nil { let errorMessage = "Missing \\begin"; self.setError(.missingBegin, message:errorMessage) return nil; @@ -691,7 +692,7 @@ public class MTMathListBuilder { } return true } else if modifier == "nolimits" { - if atom!.type != .largeOperator { + if atom?.type != .largeOperator { let errorMessage = "No limits can only be applied to an operator." self.setError(.invalidLimits, message:errorMessage) return true @@ -715,8 +716,7 @@ public class MTMathListBuilder { if let atom = MTMathAtomFactory.atom(forLatexSymbol: command) { return atom } - - if let accent = MTMathAtomFactory.getAccent(withName: command) { + if let accent = MTMathAtomFactory.accent(withName: command) { accent.innerList = self.buildInternal(true) return accent } else if command == "frac" { @@ -734,9 +734,8 @@ public class MTMathListBuilder { } else if command == "sqrt" { let rad = MTRadical() let char = self.getNextCharacter() - if char == "[" { - rad.degree = self.buildInternal(false, stop: "]") + rad.degree = self.buildInternal(false, stopChar: "]") rad.radicand = self.buildInternal(true) } else { self.unlookCharacter() @@ -752,7 +751,7 @@ public class MTMathListBuilder { } self.currentInnerAtom!.innerList = self.buildInternal(false) if self.currentInnerAtom?.rightBoundary == nil { - print("Missing \\right") + self.setError(.missingRight, message: "Missing \\right") return nil } let newInner = self.currentInnerAtom @@ -775,86 +774,24 @@ public class MTMathListBuilder { } else { return nil } + } else if command == "color" { + // A color command has 2 arguments + let mathColor = MTMathColor() + mathColor.colorString = self.readColor()! + mathColor.innerList = self.buildInternal(true) + return mathColor + } else if command == "colorbox" { + // A color command has 2 arguments + let mathColorbox = MTMathColorbox() + mathColorbox.colorString = self.readColor()! + mathColorbox.innerList = self.buildInternal(true) + return mathColorbox } else { - print("Invalid Command") + self.setError(.invalidCommand, message: "Invalid command \\\(command)") return nil } } -// func stop(command: String, list: MTMathList, stopChar: Character) -> MTMathList? { -// let fractionCommands = [ -// "over": [], -// "atop": [], -// "choose": ["(", ")"], -// "brack": ["[", "]"], -// "brace": ["{", "}"] -// ] -// -// if command == "right" { -// if self.currentInnerAtom == nil { -// print("missing left") -// return nil -// } -// -// if let rightBoundary = self.getBoundaryAtom("right") { -// self.currentInnerAtom!.rightBoundary = rightBoundary -// -// return list -// } else { -// return nil -// } -// } else if let delims = fractionCommands[command] { -// let frac: MTFraction -// if command == "over" { -// frac = MTFraction() -// } else { -// frac = MTFraction(hasRule: false) -// } -// -// if delims.count == 2 { -// frac.leftDelimiter = delims[0] -// frac.rightDelimiter = delims[1] -// } -// -// frac.numerator = list -// frac.denominator = self.buildInternal(false, stopChar: stopChar) -// -// let fracList = MTMathList() -// fracList.add(frac) -// return fracList -// } else if command == "\\" || command == "cr" { -// if currentEnv != nil { -// currentEnv!.numRows += 1 -// return list -// } else { -// if let table = self.buildTable(env: nil, firstList: list, isRow: true) { -// return MTMathList(atoms: [table]) -// } else { -// return nil -// } -// } -// } else if command == "end" { -// if self.currentEnv == nil { -// print("Missing \\begin") -// return nil -// } -// -// if let env = self.readEnvironment() { -// if env != self.currentEnv?.envName { -// print("Begin environment name \(currentEnv!.envName!) does not match end name: \(env)") -// return nil -// } -// -// currentEnv?.ended = true -// -// return list -// } else { -// return nil -// } -// } -// return nil -// } - func readEnvironment() -> String? { if !self.expectCharacter("{") { // We didn't find an opening brace, so no env found. @@ -862,7 +799,7 @@ public class MTMathListBuilder { return nil } - self.skipSpace() + self.skipSpaces() let env = self.readString() if !self.expectCharacter("}") { @@ -873,13 +810,18 @@ public class MTMathListBuilder { return env } - func expectCharacter(_ char: Character) -> Bool { - self.skipSpace() + func MTAssertNotSpace(_ ch: Character) { + assert(ch >= "\u{21}" && ch <= "\u{7E}", "Expected non-space character \(ch)") + } + + func expectCharacter(_ ch: Character) -> Bool { + MTAssertNotSpace(ch) + self.skipSpaces() if self.hasCharacters { - let nextChar = self.getNextCharacter()! - - if nextChar == char { + let nextChar = self.getNextCharacter() + MTAssertNotSpace(nextChar) + if nextChar == ch { return true } else { self.unlookCharacter() @@ -899,64 +841,68 @@ public class MTMathListBuilder { var currentCol = 0 var rows = [[MTMathList]]() - + rows.append([MTMathList]()) if firstList != nil { - rows[currentRow][currentCol] = firstList! - + rows[currentRow].append(firstList!) if isRow { - currentEnv?.numRows += 1 - currentRow += 1 + currentEnv!.numRows+=1 + currentRow+=1 + rows.append([MTMathList]()) } else { - currentCol += 1 + currentCol+=1 } } - while !currentEnv!.ended && self.hasCharacters { - if let list = self.buildInternal(false) { - rows[currentRow][currentCol] = list - currentCol += 1 - if self.currentEnv!.numRows > currentRow { - currentRow = self.currentEnv!.numRows - currentCol = 0 - } - } else { + let list = self.buildInternal(false) + if list == nil { + // If there is an error building the list, bail out early. return nil } + rows[currentRow].append(list!) + currentCol+=1 + if currentEnv!.numRows > currentRow { + currentRow = currentEnv!.numRows + rows.append([MTMathList]()) + currentCol = 0 + } } - if !currentEnv!.ended && currentEnv?.envName == nil { - print("Missing \\end") + if !currentEnv!.ended && currentEnv!.envName != nil { + self.setError(.missingEnd, message: "Missing \\end") return nil } - if let table = MTMathAtomFactory.table(withEnvironment: currentEnv?.envName, rows: rows) { - self.currentEnv = oldEnv - return table - } else { + var error:NSError? + let table = MTMathAtomFactory.table(withEnvironment: currentEnv?.envName, rows: rows, error: &error) + if table == nil && self.error == nil { + self.error = error return nil } + self.currentEnv = oldEnv + return table } func getBoundaryAtom(_ delimiterType: String) -> MTMathAtom? { let delim = self.readDelimiter() if delim == nil { - assertionFailure("Missing delimiter for \(delimiterType)") + let errorMessage = "Missing delimiter for \\\(delimiterType)" + self.setError(.missingDelimiter, message:errorMessage) return nil } let boundary = MTMathAtomFactory.boundary(forDelimiter: delim!) if boundary == nil { - assertionFailure("Invalid delimiter for \(delimiterType): \(delim!)") + let errorMessage = "Invalid delimiter for \(delimiterType): \(delim!)" + self.setError(.missingDelimiter, message:errorMessage) return nil } return boundary } func readDelimiter() -> String? { - self.skipSpace() - + self.skipSpaces() while self.hasCharacters { - let char = self.getNextCharacter()! - + let char = self.getNextCharacter() + MTAssertNotSpace(char) if char == "\\" { let command = self.readCommand() if command == "|" { @@ -970,31 +916,16 @@ public class MTMathListBuilder { return nil } - func skipSpace() { - while self.hasCharacters { - let char = self.getNextCharacter() - let asciiCharacterSet = CharacterSet(charactersIn: UnicodeScalar(0x21)...UnicodeScalar(0x7e)) - if String(char!).rangeOfCharacter(from: asciiCharacterSet) != nil { - self.unlookCharacter() - return - } else { - continue - } - } - } - func readCommand() -> String { - let singleChars = Array("{}$#%_| ,>;!\\") + let singleChars = "{}$#%_| ,>;!\\" if self.hasCharacters { - if let char = self.getNextCharacter() { - if let _ = singleChars.firstIndex(of: char) { - return String(char) - } else { - self.unlookCharacter() - } + let char = self.getNextCharacter() + if let _ = singleChars.firstIndex(of: char) { + return String(char) + } else { + self.unlookCharacter() } } - return self.readString() } @@ -1002,13 +933,12 @@ public class MTMathListBuilder { // a string of all upper and lower case characters. var output = "" while self.hasCharacters { - if let char = self.getNextCharacter() { - if char.isLowercase || char.isUppercase { - output.append(char) - } else { - self.unlookCharacter() - break - } + let char = self.getNextCharacter() + if char.isLowercase || char.isUppercase { + output.append(char) + } else { + self.unlookCharacter() + break } } return output diff --git a/Sources/SwiftMathRender/MathRender/MTMathUILabel.swift b/Sources/SwiftMathRender/MathRender/MTMathUILabel.swift index 3e5258e..6b6c072 100644 --- a/Sources/SwiftMathRender/MathRender/MTMathUILabel.swift +++ b/Sources/SwiftMathRender/MathRender/MTMathUILabel.swift @@ -71,12 +71,12 @@ class MTMathUILabel : MTView { var latex = "" { didSet { self.error = nil - var error: NSError? = nil + var error = NSError() self.mathList = MTMathListBuilder.build(fromString: latex, error: &error) - if error != nil { + if error != NSError() { self.mathList = nil self.error = error - self.errorLabel?.text = error?.localizedDescription + self.errorLabel?.text = error.localizedDescription self.errorLabel?.frame = self.bounds self.errorLabel?.isHidden = !self.displayErrorInline } else { diff --git a/Sources/SwiftMathRender/MathRender/MTTypesetter.swift b/Sources/SwiftMathRender/MathRender/MTTypesetter.swift index bff1d3b..defef58 100644 --- a/Sources/SwiftMathRender/MathRender/MTTypesetter.swift +++ b/Sources/SwiftMathRender/MathRender/MTTypesetter.swift @@ -1637,7 +1637,7 @@ class MTTypesetter { for i in 0.. [TestRecord] { + [ + TestRecord(build: "x", atomType: [.variable ], types: [], result: "x"), + TestRecord(build: "1", atomType: [.number ] , types: [], result: "1"), + TestRecord(build: "*", atomType: [.binaryOperator ] ,types: [], result:"*"), + TestRecord(build: "+", atomType: [.binaryOperator ], types: [], result:"+"), + TestRecord(build: ".", atomType: [.number ], types: [], result:"."), + TestRecord(build: "(", atomType: [.open ], types: [], result:"(" ), + TestRecord(build: ")", atomType: [.close ], types: [], result:")"), + TestRecord(build: ",", atomType: [.punctuation], types: [], result:","), + TestRecord(build: "!", atomType: [.close], types: [], result:"!"), + TestRecord(build: "=", atomType: [.relation], types: [], result:"="), + TestRecord(build: "x+2", atomType: [.variable, .binaryOperator, .number ], types: [], result:"x+2"), + // spaces are ignored + TestRecord(build: "(2.3 * 8)", atomType: [.open, .number, .number, .number, .binaryOperator, .number , .close ], types: [], result:"(2.3*8)"), + // braces are just for grouping + TestRecord(build: "5{3+4}", atomType: [.number, .number, .binaryOperator, .number], types: [], result:"53+4"), + // commands + TestRecord(build: "\\pi+\\theta\\geq 3",atomType: [.variable, .binaryOperator, .variable, .relation, .number], types: [], result:"\\pi +\\theta \\geq 3"), + // aliases + TestRecord(build: "\\pi\\ne 5 \\land 3", atomType: [.variable, .relation, .number, .binaryOperator, .number], types: [], result:"\\pi \\neq 5\\wedge 3"), + // control space + TestRecord(build: "x \\ y", atomType: [ .variable, .ordinary, .variable], types: [], result:"x\\ y"), + // spacing + TestRecord(build: "x \\quad y \\; z \\! q", atomType: [ .variable, .space, .variable,.space, .variable, .space, .variable], types: [], result:"x\\quad y\\; z\\! q") + ] + } + func getTestDataSuperScript() -> [TestRecord] { [ TestRecord(build: "x^2", atomType: [.variable], types: [.number], result: "x^{2}"), @@ -124,14 +152,69 @@ final class SwiftMathRenderTests: XCTestCase { TestRecord2(build: "\\left( 2 \\right.", type1: [ .inner], number: 0, type2: [ .number], left: "(", right: "", result: "\\left( 2\\right. ") ] } + + func getTestDataParseErrors() -> [(String, MTParseErrors)] { + return [ + ("}a", .mismatchBraces), + ("\\notacommand", .invalidCommand), + ("\\sqrt[5+3", .characterNotFound), + ("{5+3", .mismatchBraces), + ("5+3}", .mismatchBraces), + ("{1+\\frac{3+2", .mismatchBraces), + ("1+\\left", .missingDelimiter), + ("\\left(\\frac12\\right", .missingDelimiter), + ("\\left 5 + 3 \\right)", .invalidDelimiter), + ("\\left(\\frac12\\right + 3", .invalidDelimiter), + ("\\left\\lmoustache 5 + 3 \\right)", .invalidDelimiter), + ("\\left(\\frac12\\right\\rmoustache + 3", .invalidDelimiter), + ("5 + 3 \\right)", .missingLeft), + ("\\left(\\frac12", .missingRight), + ("\\left(5 + \\left| \\frac12 \\right)", .missingRight), + ("5+ \\left|\\frac12\\right| \\right)", .missingLeft), + ("\\begin matrix \\end matrix", .characterNotFound), // missing { + ("\\begin", .characterNotFound), // missing { + ("\\begin{", .characterNotFound), // missing } + ("\\begin{matrix parens}", .characterNotFound), // missing } (no spaces in env) + ("\\begin{matrix} x", .missingEnd), + ("\\begin{matrix} x \\end", .characterNotFound), // missing { + ("\\begin{matrix} x \\end + 3", .characterNotFound), // missing { + ("\\begin{matrix} x \\end{", .characterNotFound), // missing } + ("\\begin{matrix} x \\end{matrix + 3", .characterNotFound), // missing } + ("\\begin{matrix} x \\end{pmatrix}", .invalidEnv), + ("x \\end{matrix}", .missingBegin), + ("\\begin{notanenv} x \\end{notanenv}", .invalidEnv), + ("\\begin{matrix} \\notacommand \\end{matrix}", .invalidCommand), + ("\\begin{displaylines} x & y \\end{displaylines}", .invalidNumColumns), + ("\\begin{eqalign} x \\end{eqalign}", .invalidNumColumns), + ("\\nolimits", .invalidLimits), + ("\\frac\\limits{1}{2}", .invalidLimits) + ] + } + + func testBuilder() throws { + let data = getTestData() + for testCase in data { + let str = testCase.build + var error = NSError() + let list = MTMathListBuilder.build(fromString: str, error: &error) + XCTAssert(error.code == 0) + let desc = "Error for string:\(str)" + let atomTypes = testCase.atomType + self.checkAtomTypes(list, types:atomTypes, desc:desc) + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, testCase.result, desc) + } + } func testSuperScript() throws { let data = getTestDataSuperScript() for testCase in data { let str = testCase.build - var error:NSError? + var error = NSError() let list = MTMathListBuilder.build(fromString: str, error:&error) - XCTAssertNil(error) + XCTAssert(error.code == NSNotFound) let desc = "Error for string:\(str)" let atomTypes = testCase.atomType checkAtomTypes(list, types:atomTypes, desc:desc) @@ -163,9 +246,9 @@ final class SwiftMathRenderTests: XCTestCase { let data = getTestDataSubScript() for testCase in data { let str = testCase.build - var error:NSError? + var error = NSError() let list = MTMathListBuilder.build(fromString: str, error:&error) - XCTAssertNil(error) + XCTAssert(error.code == NSNotFound) let desc = "Error for string:\(str)" let atomTypes = testCase.atomType checkAtomTypes(list, types:atomTypes, desc:desc) @@ -197,9 +280,9 @@ final class SwiftMathRenderTests: XCTestCase { let data = getTestDataSuperSubScript() for testCase in data { let str = testCase.build - var error:NSError? + var error = NSError() let list = MTMathListBuilder.build(fromString: str, error:&error) - XCTAssertNil(error) + XCTAssert(error.code == NSNotFound) let desc = "Error for string:\(str)" let atomTypes = testCase.atomType checkAtomTypes(list, types:atomTypes, desc:desc) @@ -425,7 +508,7 @@ final class SwiftMathRenderTests: XCTestCase { for testCase in data { let str = testCase.build - var error:NSError? + var error = NSError() let list = MTMathListBuilder.build(fromString: str, error: &error)! XCTAssertNotNil(list, str); @@ -455,7 +538,825 @@ final class SwiftMathRenderTests: XCTestCase { XCTAssertEqual(latex, testCase.result, str); } } + + func testOver() throws { + let str = "1 \\over c"; + let list = MTMathListBuilder.build(fromString:str)! + let desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc); + XCTAssertEqual((list.atoms.count), 1, desc); + let frac = list.atoms[0] as! MTFraction + XCTAssertEqual(frac.type, .fraction, desc); + XCTAssertEqual(frac.nucleus, "", desc); + XCTAssertTrue(frac.hasRule); + XCTAssertNil(frac.rightDelimiter); + XCTAssertNil(frac.leftDelimiter); + + var subList = frac.numerator! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + var atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .number, desc); + XCTAssertEqual(atom.nucleus, "1", desc); + + atom = list.atoms[0]; + subList = frac.denominator! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .variable, desc); + XCTAssertEqual(atom.nucleus, "c", desc); + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "\\frac{1}{c}", desc); + } + func testOverInParens() throws { + let str = "5 + {1 \\over c} + 8"; + let list = MTMathListBuilder.build(fromString:str)! + let desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc); + XCTAssertEqual((list.atoms.count), 5, desc); + let types = [MTMathAtomType.number, .binaryOperator, .fraction, .binaryOperator, .number] + self.checkAtomTypes(list, types:types, desc:desc) + + let frac = list.atoms[2] as! MTFraction + XCTAssertEqual(frac.type, .fraction, desc); + XCTAssertEqual(frac.nucleus, "", desc); + XCTAssertTrue(frac.hasRule); + XCTAssertNil(frac.rightDelimiter); + XCTAssertNil(frac.leftDelimiter); + + var subList = frac.numerator! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + var atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .number, desc); + XCTAssertEqual(atom.nucleus, "1", desc); + + atom = list.atoms[0]; + subList = frac.denominator! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .variable, desc); + XCTAssertEqual(atom.nucleus, "c", desc); + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "5+\\frac{1}{c}+8", desc); + } + + func testAtop() throws { + let str = "1 \\atop c"; + let list = MTMathListBuilder.build(fromString:str)! + let desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc); + XCTAssertEqual((list.atoms.count), 1, desc); + let frac = list.atoms[0] as! MTFraction + XCTAssertEqual(frac.type, .fraction, desc); + XCTAssertEqual(frac.nucleus, "", desc); + XCTAssertFalse(frac.hasRule); + XCTAssertNil(frac.rightDelimiter); + XCTAssertNil(frac.leftDelimiter); + + var subList = frac.numerator! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + var atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .number, desc); + XCTAssertEqual(atom.nucleus, "1", desc); + + atom = list.atoms[0]; + subList = frac.denominator! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .variable, desc); + XCTAssertEqual(atom.nucleus, "c", desc); + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "{1 \\atop c}", desc); + } + + func testAtopInParens() throws { + let str = "5 + {1 \\atop c} + 8"; + let list = MTMathListBuilder.build(fromString:str)! + let desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc); + XCTAssertEqual((list.atoms.count), 5, desc); + let types = [MTMathAtomType.number, .binaryOperator, .fraction, .binaryOperator, .number] + self.checkAtomTypes(list, types:types, desc:desc) + + let frac = list.atoms[2] as! MTFraction + XCTAssertEqual(frac.type, .fraction, desc); + XCTAssertEqual(frac.nucleus, "", desc); + XCTAssertFalse(frac.hasRule); + XCTAssertNil(frac.rightDelimiter); + XCTAssertNil(frac.leftDelimiter); + + var subList = frac.numerator! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + var atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .number, desc); + XCTAssertEqual(atom.nucleus, "1", desc); + + atom = list.atoms[0]; + subList = frac.denominator! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .variable, desc); + XCTAssertEqual(atom.nucleus, "c", desc); + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "5+{1 \\atop c}+8", desc); + } + + func testChoose() throws { + let str = "n \\choose k"; + let list = MTMathListBuilder.build(fromString:str)! + let desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc); + XCTAssertEqual((list.atoms.count), 1, desc); + let frac = list.atoms[0] as! MTFraction + XCTAssertEqual(frac.type, .fraction, desc); + XCTAssertEqual(frac.nucleus, "", desc); + XCTAssertFalse(frac.hasRule); + XCTAssertEqual(frac.rightDelimiter, ")"); + XCTAssertEqual(frac.leftDelimiter, "("); + + var subList = frac.numerator! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + var atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .variable, desc); + XCTAssertEqual(atom.nucleus, "n", desc); + + atom = list.atoms[0]; + subList = frac.denominator! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .variable, desc); + XCTAssertEqual(atom.nucleus, "k", desc); + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "{n \\choose k}", desc); + } + + func testBrack() throws { + let str = "n \\brack k"; + let list = MTMathListBuilder.build(fromString:str)! + let desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc); + XCTAssertEqual((list.atoms.count), 1, desc); + let frac = list.atoms[0] as! MTFraction + XCTAssertEqual(frac.type, .fraction, desc); + XCTAssertEqual(frac.nucleus, "", desc); + XCTAssertFalse(frac.hasRule); + XCTAssertEqual(frac.rightDelimiter, "]"); + XCTAssertEqual(frac.leftDelimiter, "["); + + var subList = frac.numerator! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + var atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .variable, desc); + XCTAssertEqual(atom.nucleus, "n", desc); + + atom = list.atoms[0]; + subList = frac.denominator! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .variable, desc); + XCTAssertEqual(atom.nucleus, "k", desc); + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "{n \\brack k}", desc); + } + + func testBrace() throws { + let str = "n \\brace k"; + let list = MTMathListBuilder.build(fromString:str)! + let desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc); + XCTAssertEqual((list.atoms.count), 1, desc); + let frac = list.atoms[0] as! MTFraction + XCTAssertEqual(frac.type, .fraction, desc); + XCTAssertEqual(frac.nucleus, "", desc); + XCTAssertFalse(frac.hasRule); + XCTAssertEqual(frac.rightDelimiter, "}"); + XCTAssertEqual(frac.leftDelimiter, "{"); + + var subList = frac.numerator! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + var atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .variable, desc); + XCTAssertEqual(atom.nucleus, "n", desc); + + atom = list.atoms[0]; + subList = frac.denominator! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .variable, desc); + XCTAssertEqual(atom.nucleus, "k", desc); + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "{n \\brace k}", desc); + } + + func testBinom() throws { + let str = "\\binom{n}{k}"; + let list = MTMathListBuilder.build(fromString:str)! + let desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc); + XCTAssertEqual((list.atoms.count), 1, desc); + let frac = list.atoms[0] as! MTFraction + XCTAssertEqual(frac.type, .fraction, desc); + XCTAssertEqual(frac.nucleus, "", desc); + XCTAssertFalse(frac.hasRule); + XCTAssertEqual(frac.rightDelimiter, ")"); + XCTAssertEqual(frac.leftDelimiter, "("); + + var subList = frac.numerator! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + var atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .variable, desc); + XCTAssertEqual(atom.nucleus, "n", desc); + + atom = list.atoms[0]; + subList = frac.denominator! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .variable, desc); + XCTAssertEqual(atom.nucleus, "k", desc); + + // convert it back to latex (binom converts to choose) + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "{n \\choose k}", desc); + } + + func testOverLine() throws { + let str = "\\overline 2"; + let list = MTMathListBuilder.build(fromString:str)! + let desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc); + XCTAssertEqual((list.atoms.count), 1, desc); + let over = list.atoms[0] as! MTOverLine + XCTAssertEqual(over.type, .overline, desc); + XCTAssertEqual(over.nucleus, "", desc); + + let subList = over.innerList! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + let atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .number, desc); + XCTAssertEqual(atom.nucleus, "2", desc); + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "\\overline{2}", desc); + } + + func testUnderline() throws { + let str = "\\underline 2"; + let list = MTMathListBuilder.build(fromString:str)! + let desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc); + XCTAssertEqual((list.atoms.count), 1, desc); + let under = list.atoms[0] as! MTUnderLine + XCTAssertEqual(under.type, .underline, desc); + XCTAssertEqual(under.nucleus, "", desc); + + let subList = under.innerList! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + let atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .number, desc); + XCTAssertEqual(atom.nucleus, "2", desc); + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "\\underline{2}", desc); + } + + func testAccent() throws { + let str = "\\bar x"; + let list = MTMathListBuilder.build(fromString:str)! + let desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc); + XCTAssertEqual((list.atoms.count), 1, desc); + let accent = list.atoms[0] as! MTAccent + XCTAssertEqual(accent.type, .accent, desc); + XCTAssertEqual(accent.nucleus, "\u{0304}", desc); + + let subList = accent.innerList! + XCTAssertNotNil(subList, desc); + XCTAssertEqual((subList.atoms.count), 1, desc); + let atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .variable, desc); + XCTAssertEqual(atom.nucleus, "x", desc); + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "\\bar{x}", desc); + } + + func testMathSpace() throws { + let str = "\\!"; + let list = MTMathListBuilder.build(fromString:str)! + let desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc); + XCTAssertEqual((list.atoms.count), 1, desc); + let space = list.atoms[0] as! MTMathSpace + XCTAssertEqual(space.type, .space, desc); + XCTAssertEqual(space.nucleus, "", desc); + XCTAssertEqual(space.space, -3); + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "\\! ", desc); + } + + func testMathStyle() throws { + let str = "\\textstyle y \\scriptstyle x"; + let list = MTMathListBuilder.build(fromString:str)! + let desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc); + XCTAssertEqual((list.atoms.count), 4, desc); + let style = list.atoms[0] as! MTMathStyle + XCTAssertEqual(style.type, .style, desc); + XCTAssertEqual(style.nucleus, "", desc); + XCTAssertEqual(style.style, .text); + + let style2 = list.atoms[2] as! MTMathStyle + XCTAssertEqual(style2.type, .style, desc); + XCTAssertEqual(style2.nucleus, "", desc); + XCTAssertEqual(style2.style, .script); + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "\\textstyle y\\scriptstyle x", desc); + } + + func testMatrix() throws { + let str = "\\begin{matrix} x & y \\\\ z & w \\end{matrix}"; + let list = MTMathListBuilder.build(fromString:str)! + + XCTAssertNotNil(list); + XCTAssertEqual((list.atoms.count), 1); + let table = list.atoms[0] as! MTMathTable + XCTAssertEqual(table.type, .table); + XCTAssertEqual(table.nucleus, ""); + XCTAssertEqual(table.environment, "matrix"); + XCTAssertEqual(table.interRowAdditionalSpacing, 0); + XCTAssertEqual(table.interColumnSpacing, 18); + XCTAssertEqual(table.numRows, 2); + XCTAssertEqual(table.numColumns, 2); + + for i in 0..<2 { + let alignment = table.get(alignmentForColumn:i) + XCTAssertEqual(alignment, .center); + for j in 0..<2 { + let cell = table.cells[j][i]; + XCTAssertEqual(cell.atoms.count, 2); + let style = cell.atoms[0] as! MTMathStyle + XCTAssertEqual(style.type, .style); + XCTAssertEqual(style.style, .text); + + let atom = cell.atoms[1]; + XCTAssertEqual(atom.type, .variable); + } + } + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "\\begin{matrix}x&y\\\\ z&w\\end{matrix}"); + } + + func testPMatrix() throws { + let str = "\\begin{pmatrix} x & y \\\\ z & w \\end{pmatrix}"; + let list = MTMathListBuilder.build(fromString:str)! + + XCTAssertNotNil(list); + XCTAssertEqual((list.atoms.count), 1); + let inner = list.atoms[0] as! MTInner + XCTAssertEqual(inner.type, .inner, str); + XCTAssertEqual(inner.nucleus, "", str); + + let innerList = inner.innerList! + XCTAssertNotNil(innerList, str); + + XCTAssertNotNil(inner.leftBoundary, str); + XCTAssertEqual(inner.leftBoundary!.type, .boundary, str); + XCTAssertEqual(inner.leftBoundary!.nucleus, "(", str); + + XCTAssertNotNil(inner.rightBoundary, str); + XCTAssertEqual(inner.rightBoundary!.type, .boundary, str); + XCTAssertEqual(inner.rightBoundary!.nucleus, ")", str); + + XCTAssertEqual((innerList.atoms.count), 1); + let table = innerList.atoms[0] as! MTMathTable + XCTAssertEqual(table.type, .table); + XCTAssertEqual(table.nucleus, ""); + XCTAssertEqual(table.environment, "matrix"); + XCTAssertEqual(table.interRowAdditionalSpacing, 0); + XCTAssertEqual(table.interColumnSpacing, 18); + XCTAssertEqual(table.numRows, 2); + XCTAssertEqual(table.numColumns, 2); + + for i in 0..<2 { + let alignment = table.get(alignmentForColumn:i) + XCTAssertEqual(alignment, .center); + for j in 0..<2 { + let cell = table.cells[j][i]; + XCTAssertEqual(cell.atoms.count, 2); + let style = cell.atoms[0] as! MTMathStyle + XCTAssertEqual(style.type, .style); + XCTAssertEqual(style.style, .text); + + let atom = cell.atoms[1]; + XCTAssertEqual(atom.type, .variable); + } + } + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "\\left( \\begin{matrix}x&y\\\\ z&w\\end{matrix}\\right) "); + } + + func testDefaultTable() throws { + let str = "x \\\\ y"; + let list = MTMathListBuilder.build(fromString:str)! + + XCTAssertNotNil(list); + XCTAssertEqual(list.atoms.count, 1); + let table = list.atoms[0] as! MTMathTable + XCTAssertEqual(table.type, .table); + XCTAssertEqual(table.nucleus, ""); + XCTAssertNil(table.environment); + XCTAssertEqual(table.interRowAdditionalSpacing, 1); + XCTAssertEqual(table.interColumnSpacing, 0); + XCTAssertEqual(table.numRows, 2); + XCTAssertEqual(table.numColumns, 1); + + for i in 0..<1 { + let alignment = table.get(alignmentForColumn: i) + XCTAssertEqual(alignment, .left); + for j in 0..<2 { + let cell = table.cells[j][i]; + XCTAssertEqual(cell.atoms.count, 1); + let atom = cell.atoms[0]; + XCTAssertEqual(atom.type, .variable); + } + } + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "x\\\\ y"); + } + + func testDefaultTableWithCols() throws { + let str = "x & y \\\\ z & w"; + let list = MTMathListBuilder.build(fromString:str)! + + XCTAssertNotNil(list); + XCTAssertEqual((list.atoms.count), 1); + let table = list.atoms[0] as! MTMathTable + XCTAssertEqual(table.type, .table); + XCTAssertEqual(table.nucleus, ""); + XCTAssertNil(table.environment); + XCTAssertEqual(table.interRowAdditionalSpacing, 1); + XCTAssertEqual(table.interColumnSpacing, 0); + XCTAssertEqual(table.numRows, 2); + XCTAssertEqual(table.numColumns, 2); + + for i in 0..<2 { + let alignment = table.get(alignmentForColumn:i) + XCTAssertEqual(alignment, .left); + for j in 0..<2 { + let cell = table.cells[j][i]; + XCTAssertEqual(cell.atoms.count, 1); + let atom = cell.atoms[0]; + XCTAssertEqual(atom.type, .variable); + } + } + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "x&y\\\\ z&w"); + } + + func testEqalign() throws { + let str1 = "\\begin{eqalign}x&y\\\\ z&w\\end{eqalign}"; + let str2 = "\\begin{split}x&y\\\\ z&w\\end{split}"; + let str3 = "\\begin{aligned}x&y\\\\ z&w\\end{aligned}"; + for str in [str1, str2, str3] { + let list = MTMathListBuilder.build(fromString:str)! + + XCTAssertNotNil(list); + XCTAssertEqual((list.atoms.count), 1); + let table = list.atoms[0] as! MTMathTable + XCTAssertEqual(table.type, .table); + XCTAssertEqual(table.nucleus, ""); + XCTAssertEqual(table.interRowAdditionalSpacing, 1); + XCTAssertEqual(table.interColumnSpacing, 0); + XCTAssertEqual(table.numRows, 2); + XCTAssertEqual(table.numColumns, 2); + + for i in 0..<2 { + let alignment = table.get(alignmentForColumn:i) + XCTAssertEqual(alignment, (i == 0) ? .right: .left); + for j in 0..<2 { + let cell = table.cells[j][i]; + if (i == 0) { + XCTAssertEqual(cell.atoms.count, 1); + let atom = cell.atoms[0]; + XCTAssertEqual(atom.type, .variable); + } else { + XCTAssertEqual(cell.atoms.count, 2); + self.checkAtomTypes(cell, types:[.ordinary, .variable], desc:str) + } + } + } + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, str); + } + } + + func testDisplayLines() throws { + let str1 = "\\begin{displaylines}x\\\\ y\\end{displaylines}"; + let str2 = "\\begin{gather}x\\\\ y\\end{gather}"; + for str in [str1, str2] { + let list = MTMathListBuilder.build(fromString:str) + + XCTAssertNotNil(list) + XCTAssertEqual(list?.atoms.count, 1); + let table = list?.atoms[0] as! MTMathTable + XCTAssertEqual(table.type, .table); + XCTAssertEqual(table.nucleus, ""); + XCTAssertEqual(table.interRowAdditionalSpacing, 1); + XCTAssertEqual(table.interColumnSpacing, 0); + XCTAssertEqual(table.numRows, 2); + XCTAssertEqual(table.numColumns, 1); + + for i in 0..<1 { + let alignment = table.get(alignmentForColumn:i) + XCTAssertEqual(alignment, .center); + for j in 0..<2 { + let cell = table.cells[j][i]; + XCTAssertEqual(cell.atoms.count, 1); + let atom = cell.atoms[0]; + XCTAssertEqual(atom.type, .variable); + } + } + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, str); + } + } + + func testErrors() throws { + let data = getTestDataParseErrors() + for testCase in data { + let str = testCase.0 + var error = NSError() + let list = MTMathListBuilder.build(fromString: str, error:&error) + let desc = "Error for string:\(str)" + XCTAssertNil(list, desc) + XCTAssertNotNil(error, desc) + XCTAssertEqual(error.domain, MTParseError, desc) + let num = testCase.1 + let code = num.rawValue + XCTAssertEqual(error.code, code, desc) + } + } + + func testCustom() throws { + let str = "\\lcm(a,b)"; + var error = NSError() + var list = MTMathListBuilder.build(fromString: str, error:&error) + XCTAssertNil(list); + XCTAssert(error.code == NSNotFound) + + MTMathAtomFactory.add(latexSymbol: "lcm", value: MTMathAtomFactory.operatorWithName("lcm", limits:false)) + list = MTMathListBuilder.build(fromString: str, error:&error) + let atomTypes = [MTMathAtomType.largeOperator, .open, .variable, .punctuation, .variable, .close] + self.checkAtomTypes(list, types:atomTypes, desc:"Error for lcm") + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "\\lcm (a,b)"); + } + + func testFontSingle() throws { + let str = "\\mathbf x"; + let list = MTMathListBuilder.build(fromString: str)! + let desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc) + XCTAssertEqual(list.atoms.count, 1, desc) + let atom = list.atoms[0]; + XCTAssertEqual(atom.type, .variable, desc) + XCTAssertEqual(atom.nucleus, "x", desc) + XCTAssertEqual(atom.fontStyle, .bold) + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "\\mathbf{x}", desc) + } + + func testFontOneChar() throws { + let str = "\\cal xy"; + let list = MTMathListBuilder.build(fromString: str)! + let desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc) + XCTAssertEqual((list.atoms.count), 2, desc) + var atom = list.atoms[0]; + XCTAssertEqual(atom.type, .variable, desc) + XCTAssertEqual(atom.nucleus, "x", desc) + XCTAssertEqual(atom.fontStyle, .caligraphic); + + atom = list.atoms[1]; + XCTAssertEqual(atom.type, .variable, desc) + XCTAssertEqual(atom.nucleus, "y", desc) + XCTAssertEqual(atom.fontStyle, .defaultStyle); + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "\\mathcal{x}y", desc) + } + + func testFontMultipleChars() throws { + let str = "\\frak{xy}"; + let list = MTMathListBuilder.build(fromString: str)! + let desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc) + XCTAssertEqual((list.atoms.count), 2, desc) + var atom = list.atoms[0]; + XCTAssertEqual(atom.type, .variable, desc) + XCTAssertEqual(atom.nucleus, "x", desc) + XCTAssertEqual(atom.fontStyle, .fraktur); + + atom = list.atoms[1]; + XCTAssertEqual(atom.type, .variable, desc) + XCTAssertEqual(atom.nucleus, "y", desc) + XCTAssertEqual(atom.fontStyle, .fraktur); + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "\\mathfrak{xy}", desc) + } + + func testFontOneCharInside() throws { + let str = "\\sqrt \\mathrm x y"; + let list = MTMathListBuilder.build(fromString: str)! + let desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc) + XCTAssertEqual((list.atoms.count), 2, desc) + + let rad = list.atoms[0] as! MTRadical + XCTAssertEqual(rad.type, .radical, desc) + XCTAssertEqual(rad.nucleus, "", desc) + + let subList = rad.radicand! + var atom = subList.atoms[0]; + XCTAssertEqual(atom.type, .variable, desc) + XCTAssertEqual(atom.nucleus, "x", desc) + XCTAssertEqual(atom.fontStyle, .roman); + + atom = list.atoms[1]; + XCTAssertEqual(atom.type, .variable, desc) + XCTAssertEqual(atom.nucleus, "y", desc) + XCTAssertEqual(atom.fontStyle, .defaultStyle) + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "\\sqrt{\\mathrm{x}}y", desc) + } + + func testText() throws { + let str = "\\text{x y}"; + let list = MTMathListBuilder.build(fromString: str)! + let desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc) + XCTAssertEqual((list.atoms.count), 3, desc) + var atom = list.atoms[0]; + XCTAssertEqual(atom.type, .variable, desc) + XCTAssertEqual(atom.nucleus, "x", desc) + XCTAssertEqual(atom.fontStyle, .roman); + + atom = list.atoms[1]; + XCTAssertEqual(atom.type, .ordinary, desc) + XCTAssertEqual(atom.nucleus, " ", desc) + + atom = list.atoms[2]; + XCTAssertEqual(atom.type, .variable, desc) + XCTAssertEqual(atom.nucleus, "y", desc) + XCTAssertEqual(atom.fontStyle, .roman); + + + // convert it back to latex + let latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "\\mathrm{x\\ y}", desc) + } + + func testLimits() throws { + // Int with no limits (default) + var str = "\\int"; + var list = MTMathListBuilder.build(fromString: str)! + var desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc) + XCTAssertEqual((list.atoms.count), 1, desc) + var op = list.atoms[0] as! MTLargeOperator + XCTAssertEqual(op.type, .largeOperator, desc) + XCTAssertFalse(op.limits); + + // convert it back to latex + var latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "\\int ", desc) + + // Int with limits + str = "\\int\\limits" + list = MTMathListBuilder.build(fromString: str)! + desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc) + XCTAssertEqual((list.atoms.count), 1, desc) + op = list.atoms[0] as! MTLargeOperator + XCTAssertEqual(op.type, .largeOperator, desc) + XCTAssertTrue(op.limits); + + // convert it back to latex + latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "\\int \\limits ", desc) + } + + func testNoLimits() throws { + // Sum with limits (default) + var str = "\\sum"; + var list = MTMathListBuilder.build(fromString: str)! + var desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc) + XCTAssertEqual((list.atoms.count), 1, desc) + var op = list.atoms[0] as! MTLargeOperator + XCTAssertEqual(op.type, .largeOperator, desc) + XCTAssertTrue(op.limits); + + // convert it back to latex + var latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "\\sum ", desc) + + // Int with limits + str = "\\sum\\nolimits"; + list = MTMathListBuilder.build(fromString: str)! + desc = "Error for string:\(str)" + + XCTAssertNotNil(list, desc) + XCTAssertEqual(list.atoms.count, 1, desc) + op = list.atoms[0] as! MTLargeOperator + XCTAssertEqual(op.type, .largeOperator, desc) + XCTAssertFalse(op.limits); + + // convert it back to latex + latex = MTMathListBuilder.mathListToString(list) + XCTAssertEqual(latex, "\\sum \\nolimits ", desc) + } // func testPerformanceExample() throws { // // This is an example of a performance test case.