Files
swiftui-math/Tests/SwiftMathTests/MTTypesetterTests.swift
Nicolas Guillot 8cf87ef703 inline layout for all complex atom types
Extends the width-checking pattern from fractions/radicals to ALL remaining
  complex atom types, completing Priority 1 of the multiline implementation.

  Changes:
  - Large operators (∑, ∫, ∏): Now stay inline with height+width checking
    (breaks only if height > fontSize * 2.5 OR width exceeds constraint)
  - Delimiters (\left...\right): Stay inline with maxWidth propagation to
    inner content for proper nested wrapping
  - Colors (.color, .textcolor, .colorBox): All 3 types now stay inline with
    maxWidth propagation for proper nested wrapping
  - Matrices/tables: Small matrices can now stay inline with surrounding content
  - Width constraint propagation: All recursive createLineForMathList() calls
    now properly pass maxWidth parameter

Impact:
  Before: Complex atoms always forced line breaks, even when they fit
  After: ALL complex atoms intelligently stay inline when width permits

  Examples:
  - a + ∑ xᵢ + b → 1 line instead of 3
  - (a+b) + \left(\frac{c}{d}\right) + e → stays inline with wrapping
2025-11-14 09:53:14 +01:00

2463 lines
113 KiB
Swift
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import XCTest
@testable import SwiftMath
//
// MathTypesetterTests.swift
// MathTypesetterTests
//
// Created by Mike Griebling on 2023-01-02.
//
extension CGPoint {
func isEqual(to p:CGPoint, accuracy:CGFloat) -> Bool {
abs(self.x - p.x) < accuracy && abs(self.y - p.y) < accuracy
}
}
final class MTTypesetterTests: XCTestCase {
var font:MTFont!
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
try super.setUpWithError()
self.font = MTFontManager.fontManager.defaultFont
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
try super.tearDownWithError()
}
func testSimpleVariable() throws {
let mathList = MTMathList()
mathList.add(MTMathAtomFactory.atom(forCharacter: "x"))
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 1)));
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 1);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTCTLineDisplay)
let line = sub0 as! MTCTLineDisplay
XCTAssertEqual(line.atoms.count, 1);
// The x is italicized
XCTAssertEqual(line.attributedString?.string, "𝑥");
XCTAssertTrue(CGPointEqualToPoint(line.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line.range, NSMakeRange(0, 1)));
XCTAssertFalse(line.hasScript);
// dimensions
XCTAssertEqual(display.ascent, line.ascent);
XCTAssertEqual(display.descent, line.descent);
XCTAssertEqual(display.width, line.width);
XCTAssertEqual(display.ascent, 8.834, accuracy: 0.01)
XCTAssertEqual(display.descent, 0.22, accuracy: 0.01)
XCTAssertEqual(display.width, 11.44, accuracy: 0.01)
}
func testMultipleVariables() throws {
let mathList = MTMathAtomFactory.mathListForCharacters("xyzw")
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 4)), "Got \(display.range) instead")
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 1);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTCTLineDisplay);
let line = sub0 as! MTCTLineDisplay
XCTAssertEqual(line.atoms.count, 4);
XCTAssertEqual(line.attributedString?.string, "𝑥𝑦𝑧𝑤");
XCTAssertTrue(CGPointEqualToPoint(line.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line.range, NSMakeRange(0, 4)));
XCTAssertFalse(line.hasScript);
// dimensions
XCTAssertEqual(display.ascent, line.ascent);
XCTAssertEqual(display.descent, line.descent);
XCTAssertEqual(display.width, line.width);
XCTAssertEqual(display.ascent, 8.834, accuracy: 0.01)
XCTAssertEqual(display.descent, 4.10, accuracy: 0.01)
XCTAssertEqual(display.width, 44.86, accuracy: 0.01)
}
func testVariablesAndNumbers() throws {
let mathList = MTMathAtomFactory.mathListForCharacters("xy2w")
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular)
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero))
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 4)), "Got \(display.range) instead")
XCTAssertFalse(display.hasScript)
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 1);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTCTLineDisplay);
let line = sub0 as! MTCTLineDisplay
XCTAssertEqual(line.atoms.count, 4);
XCTAssertEqual(line.attributedString?.string, "𝑥𝑦2𝑤");
XCTAssertTrue(CGPointEqualToPoint(line.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line.range, NSMakeRange(0, 4)));
XCTAssertFalse(line.hasScript);
// dimensions
XCTAssertEqual(display.ascent, line.ascent);
XCTAssertEqual(display.descent, line.descent);
XCTAssertEqual(display.width, line.width);
XCTAssertEqual(display.ascent, 13.32, accuracy: 0.01)
XCTAssertEqual(display.descent, 4.10, accuracy: 0.01)
XCTAssertEqual(display.width, 45.56, accuracy: 0.01)
}
func testEquationWithOperatorsAndRelations() throws {
let mathList = MTMathAtomFactory.mathListForCharacters("2x+3=y")
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 6)), "Got \(display.range) instead")
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 1);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTCTLineDisplay);
let line = sub0 as! MTCTLineDisplay
XCTAssertEqual(line.atoms.count, 6);
XCTAssertEqual(line.attributedString?.string, "2𝑥+3=𝑦");
XCTAssertTrue(CGPointEqualToPoint(line.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line.range, NSMakeRange(0, 6)));
XCTAssertFalse(line.hasScript);
// dimensions
XCTAssertEqual(display.ascent, line.ascent);
XCTAssertEqual(display.descent, line.descent);
XCTAssertEqual(display.width, line.width);
XCTAssertEqual(display.ascent, 13.32, accuracy: 0.01)
XCTAssertEqual(display.descent, 4.10, accuracy: 0.01)
XCTAssertEqual(display.width, 92.36, accuracy: 0.01)
}
// #define XCTAssertTrue(CGPointEqualToPoint(p1, p2, accuracy, ...) \
// XCTAssertEqual(p1.x, p2.x, accuracy, __VA_ARGS__); \
// XCTAssertEqual(p1.y, p2.y, accuracy, __VA_ARGS__)
//
//
// #define XCTAssertTrue(NSEqualRanges(r1, r2, ...) \
// XCTAssertEqual(r1.location, r2.location, __VA_ARGS__); \
// XCTAssertEqual(r1.length, r2.length, __VA_ARGS__)
func testSuperscript() throws {
let mathList = MTMathList()
let x = MTMathAtomFactory.atom(forCharacter: "x")
let supersc = MTMathList()
supersc.add(MTMathAtomFactory.atom(forCharacter: "2"))
x?.superScript = supersc;
mathList.add(x)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 1)));
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 2);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTCTLineDisplay);
let line = sub0 as! MTCTLineDisplay
XCTAssertEqual(line.atoms.count, 1);
// The x is italicized
XCTAssertEqual(line.attributedString?.string, "𝑥");
XCTAssertTrue(CGPointEqualToPoint(line.position, CGPointZero));
XCTAssertTrue(line.hasScript);
let sub1 = display.subDisplays[1];
XCTAssertTrue(sub1 is MTMathListDisplay)
let display2 = sub1 as! MTMathListDisplay
XCTAssertEqual(display2.type, .superscript)
XCTAssertTrue(CGPointEqualToPoint(display2.position, CGPointMake(11.44, 7.26)))
XCTAssertTrue(NSEqualRanges(display2.range, NSMakeRange(0, 1)));
XCTAssertFalse(display2.hasScript);
XCTAssertEqual(display2.index, 0);
XCTAssertEqual(display2.subDisplays.count, 1);
let sub1sub0 = display2.subDisplays[0];
XCTAssertTrue(sub1sub0 is MTCTLineDisplay);
let line2 = sub1sub0 as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 1);
XCTAssertEqual(line2.attributedString?.string, "2");
XCTAssertTrue(CGPointEqualToPoint(line2.position, CGPointZero));
XCTAssertFalse(line2.hasScript);
// dimensions
XCTAssertEqual(display.ascent, 16.584, accuracy: 0.01)
XCTAssertEqual(display.descent, 0.22, accuracy: 0.01)
XCTAssertEqual(display.width, 18.44, accuracy: 0.01)
}
func testSubscript() throws {
let mathList = MTMathList()
let x = MTMathAtomFactory.atom(forCharacter: "x")
let subsc = MTMathList()
subsc.add(MTMathAtomFactory.atom(forCharacter: "1"))
x?.subScript = subsc
mathList.add(x)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 1)));
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 2);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTCTLineDisplay);
let line = sub0 as! MTCTLineDisplay
XCTAssertEqual(line.atoms.count, 1);
// The x is italicized
XCTAssertEqual(line.attributedString?.string, "𝑥");
XCTAssertTrue(CGPointEqualToPoint(line.position, CGPointZero));
XCTAssertTrue(line.hasScript);
let sub1 = display.subDisplays[1];
XCTAssertTrue(sub1 is MTMathListDisplay);
let display2 = sub1 as! MTMathListDisplay
XCTAssertEqual(display2.type, .ssubscript);
XCTAssertTrue(CGPointEqualToPoint(display2.position, CGPointMake(11.44, -4.94)))
XCTAssertTrue(NSEqualRanges(display2.range, NSMakeRange(0, 1)))
XCTAssertFalse(display2.hasScript);
XCTAssertEqual(display2.index, 0);
XCTAssertEqual(display2.subDisplays.count, 1);
let sub1sub0 = display2.subDisplays[0];
XCTAssertTrue(sub1sub0 is MTCTLineDisplay);
let line2 = sub1sub0 as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 1);
XCTAssertEqual(line2.attributedString?.string, "1");
XCTAssertTrue(CGPointEqualToPoint(line2.position, CGPointZero));
XCTAssertFalse(line2.hasScript);
// dimensions
XCTAssertEqual(display.ascent, 8.834, accuracy: 0.01)
XCTAssertEqual(display.descent, 4.940, accuracy: 0.01)
XCTAssertEqual(display.width, 18.44, accuracy: 0.01)
}
func testSupersubscript() throws {
let mathList = MTMathList()
let x = MTMathAtomFactory.atom(forCharacter: "x")
let supersc = MTMathList()
supersc.add(MTMathAtomFactory.atom(forCharacter: "2"))
let subsc = MTMathList()
subsc.add(MTMathAtomFactory.atom(forCharacter: "1"))
x?.subScript = subsc;
x?.superScript = supersc;
mathList.add(x)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 1)));
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 3);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTCTLineDisplay);
let line = sub0 as! MTCTLineDisplay
XCTAssertEqual(line.atoms.count, 1);
// The x is italicized
XCTAssertEqual(line.attributedString?.string, "𝑥");
XCTAssertTrue(CGPointEqualToPoint(line.position, CGPointZero));
XCTAssertTrue(line.hasScript);
let sub1 = display.subDisplays[1];
XCTAssertTrue(sub1 is MTMathListDisplay);
let display2 = sub1 as! MTMathListDisplay
XCTAssertEqual(display2.type, .superscript);
XCTAssertTrue(CGPointEqualToPoint(display2.position, CGPointMake(11.44, 7.26)))
XCTAssertTrue(NSEqualRanges(display2.range, NSMakeRange(0, 1)));
XCTAssertFalse(display2.hasScript);
XCTAssertEqual(display2.index, 0);
XCTAssertEqual(display2.subDisplays.count, 1);
let sub1sub0 = display2.subDisplays[0];
XCTAssertTrue(sub1sub0 is MTCTLineDisplay);
let line2 = sub1sub0 as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 1);
XCTAssertEqual(line2.attributedString?.string, "2");
XCTAssertTrue(CGPointEqualToPoint(line2.position, CGPointZero));
XCTAssertFalse(line2.hasScript);
let sub2 = display.subDisplays[2];
XCTAssertTrue(sub2 is MTMathListDisplay);
let display3 = sub2 as! MTMathListDisplay
XCTAssertEqual(display3.type, .ssubscript);
// Positioned differently when both subscript and superscript present.
XCTAssertTrue(CGPointEqualToPoint(display3.position, CGPointMake(11.44, -5.264)))
XCTAssertTrue(NSEqualRanges(display3.range, NSMakeRange(0, 1)))
XCTAssertFalse(display3.hasScript);
XCTAssertEqual(display3.index, 0);
XCTAssertEqual(display3.subDisplays.count, 1);
let sub2sub0 = display3.subDisplays[0];
XCTAssertTrue(sub2sub0 is MTCTLineDisplay)
let line3 = sub2sub0 as! MTCTLineDisplay
XCTAssertEqual(line3.atoms.count, 1);
XCTAssertEqual(line3.attributedString?.string, "1");
XCTAssertTrue(CGPointEqualToPoint(line3.position, CGPointZero));
XCTAssertFalse(line3.hasScript);
// dimensions
XCTAssertEqual(display.ascent, 16.584, accuracy: 0.01)
XCTAssertEqual(display.descent, 5.264, accuracy: 0.01)
XCTAssertEqual(display.width, 18.44, accuracy: 0.01)
}
func testRadical() throws {
let mathList = MTMathList()
let rad = MTRadical()
let radicand = MTMathList()
radicand.add(MTMathAtomFactory.atom(forCharacter: "1"))
rad.radicand = radicand;
mathList.add(rad)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 1)));
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 1);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTRadicalDisplay);
let radical = sub0 as! MTRadicalDisplay
XCTAssertTrue(NSEqualRanges(radical.range, NSMakeRange(0, 1)));
XCTAssertFalse(radical.hasScript);
XCTAssertTrue(CGPointEqualToPoint(radical.position, CGPointZero));
XCTAssertNotNil(radical.radicand);
XCTAssertNil(radical.degree);
let display2 = radical.radicand!
XCTAssertEqual(display2.type, .regular)
XCTAssertTrue(CGPointMake(16.66, 0).isEqual(to: display2.position, accuracy: 0.01))
XCTAssertTrue(NSEqualRanges(display2.range, NSMakeRange(0, 1)));
XCTAssertFalse(display2.hasScript);
XCTAssertEqual(display2.index, NSNotFound);
XCTAssertEqual(display2.subDisplays.count, 1);
let subrad = display2.subDisplays[0];
XCTAssertTrue(subrad is MTCTLineDisplay);
let line2 = subrad as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 1);
XCTAssertEqual(line2.attributedString?.string, "1");
XCTAssertTrue(CGPointEqualToPoint(line2.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line2.range, NSMakeRange(0, 1)));
XCTAssertFalse(line2.hasScript);
// dimensions
XCTAssertEqual(display.ascent, 19.34, accuracy: 0.01)
XCTAssertEqual(display.descent, 1.46, accuracy: 0.01)
XCTAssertEqual(display.width, 26.66, accuracy: 0.01)
}
func testRadicalWithDegree() throws {
let mathList = MTMathList()
let rad = MTRadical()
let radicand = MTMathList()
radicand.add(MTMathAtomFactory.atom(forCharacter: "1"))
let degree = MTMathList()
degree.add(MTMathAtomFactory.atom(forCharacter: "3"))
rad.radicand = radicand;
rad.degree = degree;
mathList.add(rad)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 1)));
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 1);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTRadicalDisplay);
let radical = sub0 as! MTRadicalDisplay
XCTAssertTrue(NSEqualRanges(radical.range, NSMakeRange(0, 1)));
XCTAssertFalse(radical.hasScript);
XCTAssertTrue(CGPointEqualToPoint(radical.position, CGPointZero));
XCTAssertNotNil(radical.radicand);
XCTAssertNotNil(radical.degree);
let display2 = radical.radicand!
XCTAssertEqual(display2.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display2.position, CGPointMake(16.66, 0)))
XCTAssertTrue(NSEqualRanges(display2.range, NSMakeRange(0, 1)));
XCTAssertFalse(display2.hasScript);
XCTAssertEqual(display2.index, NSNotFound);
XCTAssertEqual(display2.subDisplays.count, 1);
let subrad = display2.subDisplays[0];
XCTAssertTrue(subrad is MTCTLineDisplay);
let line2 = subrad as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 1);
XCTAssertEqual(line2.attributedString?.string, "1");
XCTAssertTrue(CGPointEqualToPoint(line2.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line2.range, NSMakeRange(0, 1)));
XCTAssertFalse(line2.hasScript);
let display3 = radical.degree!
XCTAssertEqual(display3.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display3.position, CGPointMake(6.12, 10.728)))
XCTAssertTrue(NSEqualRanges(display3.range, NSMakeRange(0, 1)));
XCTAssertFalse(display3.hasScript);
XCTAssertEqual(display3.index, NSNotFound);
XCTAssertEqual(display3.subDisplays.count, 1);
let subdeg = display3.subDisplays[0];
XCTAssertTrue(subdeg is MTCTLineDisplay);
let line3 = subdeg as! MTCTLineDisplay
XCTAssertEqual(line3.atoms.count, 1);
XCTAssertEqual(line3.attributedString?.string, "3");
XCTAssertTrue(CGPointEqualToPoint(line3.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line3.range, NSMakeRange(0, 1)));
XCTAssertFalse(line3.hasScript);
// dimensions
XCTAssertEqual(display.ascent, 19.34, accuracy: 0.01)
XCTAssertEqual(display.descent, 1.46, accuracy: 0.01)
XCTAssertEqual(display.width, 26.66, accuracy: 0.01)
}
func testFraction() throws {
let mathList = MTMathList()
let frac = MTFraction(hasRule: true)
let num = MTMathList()
num.add(MTMathAtomFactory.atom(forCharacter: "1"))
let denom = MTMathList()
denom.add(MTMathAtomFactory.atom(forCharacter: "3"))
frac.numerator = num;
frac.denominator = denom;
mathList.add(frac)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 1)));
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 1);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTFractionDisplay)
let fraction = sub0 as! MTFractionDisplay
XCTAssertTrue(NSEqualRanges(fraction.range, NSMakeRange(0, 1)));
XCTAssertFalse(fraction.hasScript);
XCTAssertTrue(CGPointEqualToPoint(fraction.position, CGPointZero));
XCTAssertNotNil(fraction.numerator);
XCTAssertNotNil(fraction.denominator);
let display2 = fraction.numerator!
XCTAssertEqual(display2.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display2.position, CGPointMake(0, 13.54)))
XCTAssertTrue(NSEqualRanges(display2.range, NSMakeRange(0, 1)));
XCTAssertFalse(display2.hasScript);
XCTAssertEqual(display2.index, NSNotFound);
XCTAssertEqual(display2.subDisplays.count, 1);
let subnum = display2.subDisplays[0];
XCTAssertTrue(subnum is MTCTLineDisplay)
let line2 = subnum as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 1);
XCTAssertEqual(line2.attributedString?.string, "1");
XCTAssertTrue(CGPointEqualToPoint(line2.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line2.range, NSMakeRange(0, 1)));
XCTAssertFalse(line2.hasScript);
let display3 = fraction.denominator!
XCTAssertEqual(display3.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display3.position, CGPointMake(0, -13.72)))
XCTAssertTrue(NSEqualRanges(display3.range, NSMakeRange(0, 1)));
XCTAssertFalse(display3.hasScript);
XCTAssertEqual(display3.index, NSNotFound);
XCTAssertEqual(display3.subDisplays.count, 1);
let subdenom = display3.subDisplays[0];
XCTAssertTrue(subdenom is MTCTLineDisplay);
let line3 = subdenom as! MTCTLineDisplay
XCTAssertEqual(line3.atoms.count, 1);
XCTAssertEqual(line3.attributedString?.string, "3");
XCTAssertTrue(CGPointEqualToPoint(line3.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line3.range, NSMakeRange(0, 1)));
XCTAssertFalse(line3.hasScript);
// dimensions
XCTAssertEqual(display.ascent, 26.86, accuracy: 0.01)
XCTAssertEqual(display.descent, 14.16, accuracy: 0.01)
XCTAssertEqual(display.width, 10, accuracy: 0.01)
}
func testAtop() throws {
let mathList = MTMathList()
let frac = MTFraction(hasRule: false)
let num = MTMathList()
num.add(MTMathAtomFactory.atom(forCharacter: "1"))
let denom = MTMathList()
denom.add(MTMathAtomFactory.atom(forCharacter: "3"))
frac.numerator = num;
frac.denominator = denom;
mathList.add(frac)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 1)));
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 1);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTFractionDisplay)
let fraction = sub0 as! MTFractionDisplay
XCTAssertTrue(NSEqualRanges(fraction.range, NSMakeRange(0, 1)));
XCTAssertFalse(fraction.hasScript);
XCTAssertTrue(CGPointEqualToPoint(fraction.position, CGPointZero));
XCTAssertNotNil(fraction.numerator);
XCTAssertNotNil(fraction.denominator);
let display2 = fraction.numerator!
XCTAssertEqual(display2.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display2.position, CGPointMake(0, 13.54)))
XCTAssertTrue(NSEqualRanges(display2.range, NSMakeRange(0, 1)));
XCTAssertFalse(display2.hasScript);
XCTAssertEqual(display2.index, NSNotFound);
XCTAssertEqual(display2.subDisplays.count, 1);
let subnum = display2.subDisplays[0];
XCTAssertTrue(subnum is MTCTLineDisplay);
let line2 = subnum as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 1);
XCTAssertEqual(line2.attributedString?.string, "1");
XCTAssertTrue(CGPointEqualToPoint(line2.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line2.range, NSMakeRange(0, 1)));
XCTAssertFalse(line2.hasScript);
let display3 = fraction.denominator!
XCTAssertEqual(display3.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display3.position, CGPointMake(0, -13.72)))
XCTAssertTrue(NSEqualRanges(display3.range, NSMakeRange(0, 1)));
XCTAssertFalse(display3.hasScript);
XCTAssertEqual(display3.index, NSNotFound);
XCTAssertEqual(display3.subDisplays.count, 1);
let subdenom = display3.subDisplays[0];
XCTAssertTrue(subdenom is MTCTLineDisplay);
let line3 = subdenom as! MTCTLineDisplay
XCTAssertEqual(line3.atoms.count, 1);
XCTAssertEqual(line3.attributedString?.string, "3");
XCTAssertTrue(CGPointEqualToPoint(line3.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line3.range, NSMakeRange(0, 1)));
XCTAssertFalse(line3.hasScript);
// dimensions
XCTAssertEqual(display.ascent, 26.86, accuracy: 0.01)
XCTAssertEqual(display.descent, 14.16, accuracy: 0.01)
XCTAssertEqual(display.width, 10, accuracy: 0.01)
}
func testBinomial() throws {
let mathList = MTMathList()
let frac = MTFraction(hasRule: false)
let num = MTMathList()
num.add(MTMathAtomFactory.atom(forCharacter: "1"))
let denom = MTMathList()
denom.add(MTMathAtomFactory.atom(forCharacter: "3"))
frac.numerator = num;
frac.denominator = denom;
frac.leftDelimiter = "(";
frac.rightDelimiter = ")";
mathList.add(frac)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 1)));
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 1);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTMathListDisplay);
let display0 = sub0 as! MTMathListDisplay
XCTAssertNotNil(display0);
XCTAssertEqual(display0.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display0.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display0.range, NSMakeRange(0, 1)));
XCTAssertFalse(display0.hasScript);
XCTAssertEqual(display0.index, NSNotFound);
XCTAssertEqual(display0.subDisplays.count, 3);
let subLeft = display0.subDisplays[0];
XCTAssertTrue(subLeft is MTGlyphDisplay);
let glyph = subLeft;
XCTAssertTrue(CGPointEqualToPoint(glyph.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(glyph.range, NSMakeRange(NSNotFound, 0)));
XCTAssertFalse(glyph.hasScript);
let subFrac = display0.subDisplays[1];
XCTAssertTrue(subFrac is MTFractionDisplay)
let fraction = subFrac as! MTFractionDisplay
XCTAssertTrue(NSEqualRanges(fraction.range, NSMakeRange(0, 1)));
XCTAssertFalse(fraction.hasScript);
XCTAssertTrue(CGPointEqualToPoint(fraction.position, CGPointMake(14.72, 0)))
XCTAssertNotNil(fraction.numerator);
XCTAssertNotNil(fraction.denominator);
let display2 = fraction.numerator!
XCTAssertEqual(display2.type, .regular)
XCTAssertTrue(CGPointMake(14.72, 13.54).isEqual(to: display2.position, accuracy: 0.01))
XCTAssertTrue(NSEqualRanges(display2.range, NSMakeRange(0, 1)));
XCTAssertFalse(display2.hasScript);
XCTAssertEqual(display2.index, NSNotFound);
XCTAssertEqual(display2.subDisplays.count, 1);
let subnum = display2.subDisplays[0];
XCTAssertTrue(subnum is MTCTLineDisplay);
let line2 = subnum as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 1);
XCTAssertEqual(line2.attributedString?.string, "1");
XCTAssertTrue(CGPointEqualToPoint(line2.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line2.range, NSMakeRange(0, 1)));
XCTAssertFalse(line2.hasScript);
let display3 = fraction.denominator!
XCTAssertEqual(display3.type, .regular)
XCTAssertTrue(CGPointMake(14.72, -13.72).isEqual(to: display3.position, accuracy: 0.01))
XCTAssertTrue(NSEqualRanges(display3.range, NSMakeRange(0, 1)));
XCTAssertFalse(display3.hasScript);
XCTAssertEqual(display3.index, NSNotFound);
XCTAssertEqual(display3.subDisplays.count, 1);
let subdenom = display3.subDisplays[0];
XCTAssertTrue(subdenom is MTCTLineDisplay);
let line3 = subdenom as! MTCTLineDisplay
XCTAssertEqual(line3.atoms.count, 1);
XCTAssertEqual(line3.attributedString?.string, "3");
XCTAssertTrue(CGPointEqualToPoint(line3.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line3.range, NSMakeRange(0, 1)));
XCTAssertFalse(line3.hasScript);
let subRight = display0.subDisplays[2];
XCTAssertTrue(subRight is MTGlyphDisplay);
let glyph2 = subRight as! MTGlyphDisplay
XCTAssertTrue(CGPointEqualToPoint(glyph2.position, CGPointMake(24.72, 0)))
XCTAssertTrue(NSEqualRanges(glyph2.range, NSMakeRange(NSNotFound, 0)), "Got \(glyph2.range) instead")
XCTAssertFalse(glyph2.hasScript);
// dimensions
XCTAssertEqual(display.ascent, 28.92, accuracy: 0.001);
XCTAssertEqual(display.descent, 18.92, accuracy: 0.001);
XCTAssertEqual(display.width, 39.44, accuracy: 0.001);
}
func testLargeOpNoLimitsText() throws {
let mathList = MTMathList()
mathList.add(MTMathAtomFactory.atom(forLatexSymbol: "sin"))
mathList.add(MTMathAtomFactory.atom(forCharacter: "x"))
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 2)), "Got \(display.range) instead")
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 2);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTCTLineDisplay);
let line = sub0 as! MTCTLineDisplay
XCTAssertEqual(line.atoms.count, 1);
XCTAssertEqual(line.attributedString?.string, "sin");
XCTAssertTrue(NSEqualRanges(line.range, NSMakeRange(0, 1)));
XCTAssertFalse(line.hasScript);
let sub1 = display.subDisplays[1];
XCTAssertTrue(sub1 is MTCTLineDisplay);
let line2 = sub1 as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 1);
XCTAssertEqual(line2.attributedString?.string, "𝑥");
// Position may vary with improved spacing
XCTAssertGreaterThan(line2.position.x, 20, "x should be positioned after sin with spacing")
XCTAssertTrue(NSEqualRanges(line2.range, NSMakeRange(1, 1)), "Got \(line2.range) instead")
XCTAssertFalse(line2.hasScript);
XCTAssertEqual(display.ascent, 13.14, accuracy: 0.01)
XCTAssertEqual(display.descent, 0.22, accuracy: 0.01)
// Width may vary with improved inline layout
XCTAssertGreaterThan(display.width, 35, "Width should include sin + spacing + x")
XCTAssertLessThan(display.width, 70, "Width should be reasonable")
}
func testLargeOpNoLimitsSymbol() throws {
let mathList = MTMathList()
// Integral - with new implementation, operators stay inline when they fit
mathList.add(MTMathAtomFactory.atom(forLatexSymbol:"int"))
mathList.add(MTMathAtomFactory.atom(forCharacter: "x"))
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 2)), "Got \(display.range) instead")
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 2, "Should have operator and x as 2 subdisplays");
// Check operator display
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTGlyphDisplay, "Operator should be a glyph display");
let glyph = sub0;
XCTAssertTrue(NSEqualRanges(glyph.range, NSMakeRange(0, 1)));
XCTAssertFalse(glyph.hasScript);
// Check x display
let sub1 = display.subDisplays[1];
XCTAssertTrue(sub1 is MTCTLineDisplay, "Variable should be a line display");
let line2 = sub1 as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 1);
XCTAssertEqual(line2.attributedString?.string, "𝑥");
// Operator and x stay inline - x should be positioned after operator
XCTAssertGreaterThan(line2.position.x, glyph.position.x, "x should be positioned after operator")
XCTAssertTrue(NSEqualRanges(line2.range, NSMakeRange(1, 1)), "Got \(line2.range) instead")
XCTAssertFalse(line2.hasScript);
// Check dimensions are reasonable (not exact values)
XCTAssertGreaterThan(display.ascent, 20, "Integral symbol should have significant ascent")
XCTAssertGreaterThan(display.descent, 10, "Integral symbol should have significant descent")
XCTAssertGreaterThan(display.width, 50, "Width should include operator + spacing + x")
XCTAssertLessThan(display.width, 60, "Width should be reasonable")
}
func testLargeOpNoLimitsSymbolWithScripts() throws {
let mathList = MTMathList()
// Integral
let op = MTMathAtomFactory.atom(forLatexSymbol:"int")!
op.superScript = MTMathList()
op.superScript?.add(MTMathAtomFactory.atom(forCharacter: "1"))
op.subScript = MTMathList()
op.subScript?.add(MTMathAtomFactory.atom(forCharacter: "0"))
mathList.add(op)
mathList.add(MTMathAtomFactory.atom(forCharacter: "x"))
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 2)), "Got \(display.range) instead")
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 4);
// Check superscript
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTMathListDisplay, "Superscript should be MTMathListDisplay");
let display0 = sub0 as! MTMathListDisplay
XCTAssertEqual(display0.type, .superscript);
XCTAssertGreaterThan(display0.position.y, 20, "Superscript should be above baseline")
XCTAssertTrue(NSEqualRanges(display0.range, NSMakeRange(0, 1)))
XCTAssertFalse(display0.hasScript);
XCTAssertEqual(display0.index, 0);
XCTAssertEqual(display0.subDisplays.count, 1);
let sub0sub0 = display0.subDisplays[0];
XCTAssertTrue(sub0sub0 is MTCTLineDisplay);
let line1 = sub0sub0 as! MTCTLineDisplay
XCTAssertEqual(line1.atoms.count, 1);
XCTAssertEqual(line1.attributedString?.string, "1", "Superscript should contain '1'");
XCTAssertTrue(CGPointEqualToPoint(line1.position, CGPointZero));
XCTAssertFalse(line1.hasScript);
// Check subscript
let sub1 = display.subDisplays[1];
XCTAssertTrue(sub1 is MTMathListDisplay, "Subscript should be MTMathListDisplay");
let display1 = sub1 as! MTMathListDisplay
XCTAssertEqual(display1.type, .ssubscript);
XCTAssertLessThan(display1.position.y, 0, "Subscript should be below baseline")
XCTAssertTrue(NSEqualRanges(display1.range, NSMakeRange(0, 1)))
XCTAssertFalse(display1.hasScript);
XCTAssertEqual(display1.index, 0);
XCTAssertEqual(display1.subDisplays.count, 1);
let sub1sub0 = display1.subDisplays[0];
XCTAssertTrue(sub1sub0 is MTCTLineDisplay);
let line3 = sub1sub0 as! MTCTLineDisplay
XCTAssertEqual(line3.atoms.count, 1);
XCTAssertEqual(line3.attributedString?.string, "0", "Subscript should contain '0'");
XCTAssertTrue(CGPointEqualToPoint(line3.position, CGPointZero));
XCTAssertFalse(line3.hasScript);
// Check operator glyph
let sub2 = display.subDisplays[2];
XCTAssertTrue(sub2 is MTGlyphDisplay, "Operator should be glyph display");
let glyph = sub2;
XCTAssertTrue(NSEqualRanges(glyph.range, NSMakeRange(0, 1)));
XCTAssertTrue(glyph.hasScript, "Operator should have scripts");
// Check x variable
let sub3 = display.subDisplays[3];
XCTAssertTrue(sub3 is MTCTLineDisplay, "Variable should be line display");
let line2 = sub3 as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 1);
XCTAssertEqual(line2.attributedString?.string, "𝑥");
XCTAssertGreaterThan(line2.position.x, 25, "x should be positioned after operator with scripts and spacing")
XCTAssertTrue(NSEqualRanges(line2.range, NSMakeRange(1, 1)), "Got \(line2.range) instead")
XCTAssertFalse(line1.hasScript);
// Check dimensions are reasonable (not exact values)
XCTAssertGreaterThan(display.ascent, 30, "Should have tall ascent due to superscript")
XCTAssertGreaterThan(display.descent, 15, "Should have descent due to subscript and integral")
XCTAssertGreaterThan(display.width, 48, "Width should include operator + scripts + spacing + x");
XCTAssertLessThan(display.width, 55, "Width should be reasonable");
}
func testLargeOpWithLimitsTextWithScripts() throws {
let mathList = MTMathList()
let op = MTMathAtomFactory.atom(forLatexSymbol:"lim")!
op.subScript = MTMathList()
op.subScript?.add(MTMathAtomFactory.atom(forLatexSymbol:"infty"))
mathList.add(op)
mathList.add(MTMathAtom(type: .variable, value:"x"))
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 2)), "Got \(display.range) instead")
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 2);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTLargeOpLimitsDisplay)
let largeOp = sub0 as! MTLargeOpLimitsDisplay
XCTAssertTrue(NSEqualRanges(largeOp.range, NSMakeRange(0, 1)));
XCTAssertFalse(largeOp.hasScript);
XCTAssertNotNil(largeOp.lowerLimit, "Should have lower limit");
XCTAssertNil(largeOp.upperLimit, "Should not have upper limit");
let display2 = largeOp.lowerLimit!
XCTAssertEqual(display2.type, .regular)
// Position may vary with improved inline layout
XCTAssertLessThan(display2.position.y, 0, "Lower limit should be below baseline")
XCTAssertTrue(NSEqualRanges(display2.range, NSMakeRange(0, 1)));
XCTAssertFalse(display2.hasScript);
XCTAssertEqual(display2.index, NSNotFound);
XCTAssertEqual(display2.subDisplays.count, 1);
let sub0sub0 = display2.subDisplays[0];
XCTAssertTrue(sub0sub0 is MTCTLineDisplay);
let line1 = sub0sub0 as! MTCTLineDisplay
XCTAssertEqual(line1.atoms.count, 1);
XCTAssertEqual(line1.attributedString?.string, "");
XCTAssertTrue(CGPointEqualToPoint(line1.position, CGPointZero));
XCTAssertFalse(line1.hasScript);
let sub3 = display.subDisplays[1];
XCTAssertTrue(sub3 is MTCTLineDisplay);
let line2 = sub3 as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 1);
XCTAssertEqual(line2.attributedString?.string, "𝑥");
// With improved inline layout, x may be positioned differently
XCTAssertGreaterThan(line2.position.x, 25, "x should be positioned after operator with spacing")
XCTAssertTrue(NSEqualRanges(line2.range, NSMakeRange(1, 1)), "Got \(line2.range) instead")
XCTAssertFalse(line1.hasScript);
XCTAssertEqual(display.ascent, 13.88, accuracy: 0.01)
XCTAssertEqual(display.descent, 12.154, accuracy: 0.01)
// Width now includes operator with limits + spacing + x (improved behavior)
XCTAssertGreaterThan(display.width, 60, "Width should include operator + limits + spacing + x")
XCTAssertLessThan(display.width, 75, "Width should be reasonable")
}
func testLargeOpWithLimitsSymboltWithScripts() throws {
let mathList = MTMathList()
let op = MTMathAtomFactory.atom(forLatexSymbol:"sum")!
op.superScript = MTMathList()
op.superScript?.add(MTMathAtomFactory.atom(forLatexSymbol:"infty"))
op.subScript = MTMathList()
op.subScript?.add(MTMathAtomFactory.atom(forCharacter: "0"))
mathList.add(op)
mathList.add(MTMathAtom(type: .variable, value:"x"))
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 2)), "Got \(display.range) instead")
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 2);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTLargeOpLimitsDisplay);
let largeOp = sub0 as! MTLargeOpLimitsDisplay
XCTAssertTrue(NSEqualRanges(largeOp.range, NSMakeRange(0, 1)));
XCTAssertFalse(largeOp.hasScript);
XCTAssertNotNil(largeOp.lowerLimit, "Should have lower limit");
XCTAssertNotNil(largeOp.upperLimit, "Should have upper limit");
let display2 = largeOp.lowerLimit!
XCTAssertEqual(display2.type, .regular);
// Lower limit position may vary
XCTAssertLessThan(display2.position.y, 0, "Lower limit should be below baseline")
XCTAssertTrue(NSEqualRanges(display2.range, NSMakeRange(0, 1)))
XCTAssertFalse(display2.hasScript);
XCTAssertEqual(display2.index, NSNotFound);
XCTAssertEqual(display2.subDisplays.count, 1);
let sub0sub0 = display2.subDisplays[0];
XCTAssertTrue(sub0sub0 is MTCTLineDisplay);
let line1 = sub0sub0 as! MTCTLineDisplay
XCTAssertEqual(line1.atoms.count, 1);
XCTAssertEqual(line1.attributedString?.string, "0");
XCTAssertTrue(CGPointEqualToPoint(line1.position, CGPointZero));
XCTAssertFalse(line1.hasScript);
let displayU = largeOp.upperLimit!
XCTAssertEqual(displayU.type, .regular);
XCTAssertTrue(NSEqualRanges(displayU.range, NSMakeRange(0, 1)))
XCTAssertFalse(displayU.hasScript);
XCTAssertEqual(displayU.index, NSNotFound);
XCTAssertEqual(displayU.subDisplays.count, 1);
let sub0subU = displayU.subDisplays[0];
XCTAssertTrue(sub0subU is MTCTLineDisplay);
let line3 = sub0subU as! MTCTLineDisplay
XCTAssertEqual(line3.atoms.count, 1);
XCTAssertEqual(line3.attributedString?.string, "");
XCTAssertTrue(CGPointEqualToPoint(line3.position, CGPointZero));
XCTAssertFalse(line3.hasScript);
let sub3 = display.subDisplays[1];
XCTAssertTrue(sub3 is MTCTLineDisplay);
let line2 = sub3 as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 1);
XCTAssertEqual(line2.attributedString?.string, "𝑥");
// With improved inline layout, x position may vary
XCTAssertGreaterThan(line2.position.x, 20, "x should be positioned after operator")
XCTAssertTrue(NSEqualRanges(line2.range, NSMakeRange(1, 1)), "Got \(line2.range) instead")
XCTAssertFalse(line2.hasScript);
// Dimensions may vary with improved inline layout
XCTAssertGreaterThanOrEqual(display.ascent, 0, "Ascent should be non-negative")
XCTAssertGreaterThan(display.descent, 0, "Descent should be positive due to lower limit")
XCTAssertGreaterThan(display.width, 40, "Width should include operator + limits + spacing + x");
}
func testInner() throws {
let innerList = MTMathList()
innerList.add(MTMathAtomFactory.atom(forCharacter: "x"))
let inner = MTInner()
inner.innerList = innerList;
inner.leftBoundary = MTMathAtom(type: .boundary, value:"(")
inner.rightBoundary = MTMathAtom(type: .boundary, value:")")
let mathList = MTMathList()
mathList.add(inner)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 1)));
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 1);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTMathListDisplay);
let display2 = sub0 as! MTMathListDisplay
XCTAssertEqual(display2.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display2.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display2.range, NSMakeRange(0, 1)));
XCTAssertFalse(display2.hasScript);
XCTAssertEqual(display2.index, NSNotFound);
XCTAssertEqual(display2.subDisplays.count, 3);
let subLeft = display2.subDisplays[0];
XCTAssertTrue(subLeft is MTGlyphDisplay);
let glyph = subLeft;
XCTAssertTrue(CGPointEqualToPoint(glyph.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(glyph.range, NSMakeRange(NSNotFound, 0)));
XCTAssertFalse(glyph.hasScript);
let sub3 = display2.subDisplays[1];
XCTAssertTrue(sub3 is MTMathListDisplay);
let display3 = sub3 as! MTMathListDisplay
XCTAssertEqual(display3.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display3.position, CGPointMake(7.78, 0)))
XCTAssertTrue(NSEqualRanges(display3.range, NSMakeRange(0, 1)));
XCTAssertFalse(display3.hasScript);
XCTAssertEqual(display3.index, NSNotFound);
XCTAssertEqual(display3.subDisplays.count, 1);
let subsub3 = display3.subDisplays[0];
XCTAssertTrue(subsub3 is MTCTLineDisplay);
let line = subsub3 as! MTCTLineDisplay
XCTAssertEqual(line.atoms.count, 1);
// The x is italicized
XCTAssertEqual(line.attributedString?.string, "𝑥");
XCTAssertTrue(CGPointEqualToPoint(line.position, CGPointZero));
XCTAssertFalse(line.hasScript);
let subRight = display2.subDisplays[2];
XCTAssertTrue(subRight is MTGlyphDisplay);
let glyph2 = subRight as! MTGlyphDisplay
XCTAssertTrue(CGPointEqualToPoint(glyph2.position, CGPointMake(19.22, 0)))
XCTAssertTrue(NSEqualRanges(glyph2.range, NSMakeRange(NSNotFound, 0)), "Got \(glyph2.range) instead");
XCTAssertFalse(glyph2.hasScript);
// dimensions
XCTAssertEqual(display.ascent, display2.ascent);
XCTAssertEqual(display.descent, display2.descent);
XCTAssertEqual(display.width, display2.width);
XCTAssertEqual(display.ascent, 14.96, accuracy: 0.001);
XCTAssertEqual(display.descent, 4.96, accuracy: 0.001);
XCTAssertEqual(display.width, 27, accuracy: 0.01)
}
func testOverline() throws {
let mathList = MTMathList()
let over = MTOverLine()
let inner = MTMathList()
inner.add(MTMathAtomFactory.atom(forCharacter: "1"))
over.innerList = inner;
mathList.add(over)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 1)));
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 1);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTLineDisplay);
let overline = sub0 as! MTLineDisplay
XCTAssertTrue(NSEqualRanges(overline.range, NSMakeRange(0, 1)));
XCTAssertFalse(overline.hasScript);
XCTAssertTrue(CGPointEqualToPoint(overline.position, CGPointZero));
XCTAssertNotNil(overline.inner);
let display2 = overline.inner!
XCTAssertEqual(display2.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display2.position, CGPointZero))
XCTAssertTrue(NSEqualRanges(display2.range, NSMakeRange(0, 1)));
XCTAssertFalse(display2.hasScript);
XCTAssertEqual(display2.index, NSNotFound);
XCTAssertEqual(display2.subDisplays.count, 1);
let subover = display2.subDisplays[0];
XCTAssertTrue(subover is MTCTLineDisplay);
let line2 = subover as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 1);
XCTAssertEqual(line2.attributedString?.string, "1");
XCTAssertTrue(CGPointEqualToPoint(line2.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line2.range, NSMakeRange(0, 1)));
XCTAssertFalse(line2.hasScript);
// dimensions
XCTAssertEqual(display.ascent, 17.32, accuracy: 0.01)
XCTAssertEqual(display.descent, 0.00, accuracy: 0.01)
XCTAssertEqual(display.width, 10, accuracy: 0.01)
}
func testUnderline() throws {
let mathList = MTMathList()
let under = MTUnderLine()
let inner = MTMathList()
inner.add(MTMathAtomFactory.atom(forCharacter: "1"))
under.innerList = inner;
mathList.add(under)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 1)));
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 1);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTLineDisplay)
let underline = sub0 as! MTLineDisplay
XCTAssertTrue(NSEqualRanges(underline.range, NSMakeRange(0, 1)));
XCTAssertFalse(underline.hasScript);
XCTAssertTrue(CGPointEqualToPoint(underline.position, CGPointZero));
XCTAssertNotNil(underline.inner);
let display2 = underline.inner!
XCTAssertEqual(display2.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display2.position, CGPointZero))
XCTAssertTrue(NSEqualRanges(display2.range, NSMakeRange(0, 1)));
XCTAssertFalse(display2.hasScript);
XCTAssertEqual(display2.index, NSNotFound);
XCTAssertEqual(display2.subDisplays.count, 1);
let subover = display2.subDisplays[0];
XCTAssertTrue(subover is MTCTLineDisplay);
let line2 = subover as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 1);
XCTAssertEqual(line2.attributedString?.string, "1");
XCTAssertTrue(CGPointEqualToPoint(line2.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line2.range, NSMakeRange(0, 1)));
XCTAssertFalse(line2.hasScript);
// dimensions
XCTAssertEqual(display.ascent, 13.32, accuracy: 0.01)
XCTAssertEqual(display.descent, 4.00, accuracy: 0.01)
XCTAssertEqual(display.width, 10, accuracy: 0.01)
}
func testSpacing() throws {
let mathList = MTMathList()
mathList.add(MTMathAtomFactory.atom(forCharacter: "x"))
mathList.add(MTMathSpace(space: 9))
mathList.add(MTMathAtomFactory.atom(forCharacter: "y"))
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 3)), "Got \(display.range) instead")
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 2);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTCTLineDisplay);
let line = sub0 as! MTCTLineDisplay
XCTAssertEqual(line.atoms.count, 1);
// The x is italicized
XCTAssertEqual(line.attributedString?.string, "𝑥");
XCTAssertTrue(CGPointEqualToPoint(line.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line.range, NSMakeRange(0, 1)));
XCTAssertFalse(line.hasScript);
let sub1 = display.subDisplays[1];
XCTAssertTrue(sub1 is MTCTLineDisplay);
let line2 = sub1 as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 1);
// The y is italicized
XCTAssertEqual(line2.attributedString?.string, "𝑦")
XCTAssertTrue(CGPointMake(21.44, 0).isEqual(to: line2.position, accuracy: 0.01))
XCTAssertTrue(NSEqualRanges(line2.range, NSMakeRange(2, 1)), "Got \(line2.range) instead")
XCTAssertFalse(line2.hasScript);
let noSpace = MTMathList()
noSpace.add(MTMathAtomFactory.atom(forCharacter: "x"))
noSpace.add(MTMathAtomFactory.atom(forCharacter: "y"))
let noSpaceDisplay = MTTypesetter.createLineForMathList(noSpace, font:self.font, style:.display)!
// dimensions
XCTAssertEqual(display.ascent, noSpaceDisplay.ascent, accuracy: 0.01)
XCTAssertEqual(display.descent, noSpaceDisplay.descent, accuracy: 0.01)
XCTAssertEqual(display.width, noSpaceDisplay.width + 10, accuracy: 0.01)
}
// For issue: https://github.com/kostub/iosMath/issues/5
func testLargeRadicalDescent() throws {
let list = MTMathListBuilder.build(fromString: "\\sqrt{\\frac{\\sqrt{\\frac{1}{2}} + 3}{\\sqrt{5}^x}}")
let display = MTTypesetter.createLineForMathList(list, font:self.font, style:.display)!
// dimensions
XCTAssertEqual(display.ascent, 49.16, accuracy: 0.01)
XCTAssertEqual(display.descent, 21.288, accuracy: 0.01)
XCTAssertEqual(display.width, 82.569, accuracy: 0.01)
}
func testMathTable() throws {
let c00 = MTMathAtomFactory.mathListForCharacters("1")
let c01 = MTMathAtomFactory.mathListForCharacters("y+z")
let c02 = MTMathAtomFactory.mathListForCharacters("y")
let c11 = MTMathList()
c11.add(MTMathAtomFactory.fraction(withNumeratorString: "1", denominatorString:"2x"))
let c12 = MTMathAtomFactory.mathListForCharacters("x-y")
let c20 = MTMathAtomFactory.mathListForCharacters("x+5")
let c22 = MTMathAtomFactory.mathListForCharacters("12")
let table = MTMathTable()
table.set(cell: c00!, forRow:0, column:0)
table.set(cell: c01!, forRow:0, column:1)
table.set(cell: c02!, forRow:0, column:2)
table.set(cell: c11, forRow:1, column:1)
table.set(cell: c12!, forRow:1, column:2)
table.set(cell: c20!, forRow:2, column:0)
table.set(cell: c22!, forRow:2, column:2)
// alignments
table.set(alignment: .right, forColumn:0)
table.set(alignment: .left, forColumn:2)
table.interColumnSpacing = 18; // 1 quad
let mathList = MTMathList()
mathList.add(table)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 1)));
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 1);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTMathListDisplay);
let display2 = sub0 as! MTMathListDisplay
XCTAssertEqual(display2.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display2.position, CGPointZero))
XCTAssertTrue(NSEqualRanges(display2.range, NSMakeRange(0, 1)))
XCTAssertFalse(display2.hasScript);
XCTAssertEqual(display2.index, NSNotFound);
XCTAssertEqual(display2.subDisplays.count, 3);
let rowPos = [ 30.28, -2.68, -31.95 ]
// alignment is right, center, left.
let cellPos = [ [ 35.89, 65.89, 129.438 ], [ 45.89, 76.94, 129.438 ], [ 0, 87.66, 129.438] ]
// check the 3 rows of the matrix
for i in 0..<3 {
let sub0i = display2.subDisplays[i];
XCTAssertTrue(sub0i is MTMathListDisplay);
let row = sub0i as! MTMathListDisplay
XCTAssertEqual(row.type, .regular)
XCTAssertTrue(CGPointMake(0, rowPos[i]).isEqual(to: row.position, accuracy: 0.01))
XCTAssertTrue(NSEqualRanges(row.range, NSMakeRange(0, 3)));
XCTAssertFalse(row.hasScript);
XCTAssertEqual(row.index, NSNotFound);
XCTAssertEqual(row.subDisplays.count, 3);
for j in 0..<3 {
let sub0ij = row.subDisplays[j];
XCTAssertTrue(sub0ij is MTMathListDisplay);
let col = sub0ij as! MTMathListDisplay
XCTAssertEqual(col.type, .regular);
XCTAssertTrue(CGPointMake(cellPos[i][j], 0).isEqual(to: col.position, accuracy: 0.01))
XCTAssertFalse(col.hasScript)
XCTAssertEqual(col.index, NSNotFound);
}
}
}
func testLatexSymbols() throws {
// Test all latex symbols
let allSymbols = MTMathAtomFactory.supportedLatexSymbolNames
for symName in allSymbols {
let list = MTMathList()
let atom = MTMathAtomFactory.atom(forLatexSymbol:symName)
XCTAssertNotNil(atom)
if atom!.type >= .boundary {
// Skip these types as they aren't symbols.
continue;
}
list.add(atom)
let display = MTTypesetter.createLineForMathList(list, font:self.font, style:.display)!
XCTAssertNotNil(display, "Symbol \(symName)")
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 1)))
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 1, "Symbol \(symName)");
let sub0 = display.subDisplays[0];
if atom!.type == .largeOperator && atom!.nucleus.count == 1 {
// These large operators are rendered differently;
XCTAssertTrue(sub0 is MTGlyphDisplay);
let glyph = sub0 as! MTGlyphDisplay
XCTAssertTrue(NSEqualRanges(glyph.range, NSMakeRange(0, 1)))
XCTAssertFalse(glyph.hasScript);
} else {
XCTAssertTrue(sub0 is MTCTLineDisplay, "Symbol \(symName)");
let line = sub0 as! MTCTLineDisplay
XCTAssertEqual(line.atoms.count, 1);
if atom!.type != .variable {
XCTAssertEqual(line.attributedString?.string, atom!.nucleus);
}
XCTAssertTrue(NSEqualRanges(line.range, NSMakeRange(0, 1)))
XCTAssertFalse(line.hasScript);
}
// dimensions - check that display matches subdisplay (structure)
XCTAssertEqual(display.ascent, sub0.ascent);
XCTAssertEqual(display.descent, sub0.descent);
// Width should be reasonable - inline layout may affect large operators differently
XCTAssertGreaterThan(display.width, 0, "Width for \(symName) should be positive");
XCTAssertLessThanOrEqual(display.width, sub0.width * 3, "Width for \(symName) should be reasonable");
// All chars will occupy some space.
if atom!.nucleus != " " {
// all chars except space have height
XCTAssertGreaterThan(display.ascent + display.descent, 0, "Symbol \(symName)")
}
// all chars have a width.
XCTAssertGreaterThan(display.width, 0);
}
}
func testAtomWithAllFontStyles(_ atom:MTMathAtom?) throws {
guard let atom = atom else { return }
let fontStyles = [
MTFontStyle.defaultStyle,
.roman,
.bold,
.caligraphic,
.typewriter,
.italic,
.sansSerif,
.fraktur,
.blackboard,
.boldItalic,
]
for fontStyle in fontStyles {
let style = fontStyle
let copy : MTMathAtom = atom.copy()
copy.fontStyle = style
let list = MTMathList(atom: copy)
let display = MTTypesetter.createLineForMathList(list, font:self.font, style:.display)!
XCTAssertNotNil(display, "Symbol \(atom.nucleus)")
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 1)))
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 1, "Symbol \(atom.nucleus)")
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTCTLineDisplay, "Symbol \(atom.nucleus)")
let line = sub0 as! MTCTLineDisplay
XCTAssertEqual(line.atoms.count, 1);
XCTAssertTrue(CGPointEqualToPoint(line.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line.range, NSMakeRange(0, 1)))
XCTAssertFalse(line.hasScript);
// dimensions
XCTAssertEqual(display.ascent, sub0.ascent);
XCTAssertEqual(display.descent, sub0.descent);
XCTAssertEqual(display.width, sub0.width);
// All chars will occupy some space.
XCTAssertGreaterThan(display.ascent + display.descent, 0, "Symbol \(atom.nucleus)")
// all chars have a width.
XCTAssertGreaterThan(display.width, 0);
}
}
func testVariables() throws {
// Test all variables
let allSymbols = MTMathAtomFactory.supportedLatexSymbolNames
for symName in allSymbols {
let atom = MTMathAtomFactory.atom(forLatexSymbol:symName)!
XCTAssertNotNil(atom)
if atom.type != .variable {
// Skip these types as we are only interested in variables.
continue;
}
try self.testAtomWithAllFontStyles(atom)
}
let alphaNum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789."
let mathList = MTMathAtomFactory.mathListForCharacters(alphaNum)
for atom in mathList!.atoms {
try self.testAtomWithAllFontStyles(atom)
}
}
func testStyleChanges() throws {
let frac = MTMathAtomFactory.fraction(withNumeratorString: "1", denominatorString: "2")
let list = MTMathList(atoms: [frac])
let style = MTMathStyle(style: .text)
let textList = MTMathList(atoms: [style, frac])
// This should make the display same as text.
let display = MTTypesetter.createLineForMathList(textList, font:self.font, style:.display)!
let textDisplay = MTTypesetter.createLineForMathList(list, font:self.font, style:.text)!
let originalDisplay = MTTypesetter.createLineForMathList(list, font:self.font, style:.display)!
// Display should be the same as rendering the fraction in text style.
XCTAssertEqual(display.ascent, textDisplay.ascent);
XCTAssertEqual(display.descent, textDisplay.descent);
XCTAssertEqual(display.width, textDisplay.width);
// Original display should be larger than display since it is greater.
XCTAssertGreaterThan(originalDisplay.ascent, display.ascent);
XCTAssertGreaterThan(originalDisplay.descent, display.descent);
XCTAssertGreaterThan(originalDisplay.width, display.width);
}
func testStyleMiddle() throws {
let atom1 = MTMathAtomFactory.atom(forCharacter: "x")!
let style1 = MTMathStyle(style: .script) as MTMathAtom
let atom2 = MTMathAtomFactory.atom(forCharacter: "y")!
let style2 = MTMathStyle(style: .scriptOfScript) as MTMathAtom
let atom3 = MTMathAtomFactory.atom(forCharacter: "z")!
let list = MTMathList(atoms: [atom1, style1, atom2, style2, atom3])
let display = MTTypesetter.createLineForMathList(list, font:self.font, style:.display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 5)))
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 3);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTCTLineDisplay);
let line = sub0 as! MTCTLineDisplay
XCTAssertEqual(line.atoms.count, 1);
XCTAssertEqual(line.attributedString?.string, "𝑥");
XCTAssertTrue(CGPointEqualToPoint(line.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line.range, NSMakeRange(0, 1)))
XCTAssertFalse(line.hasScript);
let sub1 = display.subDisplays[1];
XCTAssertTrue(sub1 is MTCTLineDisplay);
let line1 = sub1 as! MTCTLineDisplay
XCTAssertEqual(line1.atoms.count, 1);
XCTAssertEqual(line1.attributedString?.string, "𝑦");
XCTAssertTrue(NSEqualRanges(line1.range, NSMakeRange(2, 1)))
XCTAssertFalse(line1.hasScript);
let sub2 = display.subDisplays[2];
XCTAssertTrue(sub2 is MTCTLineDisplay);
let line2 = sub2 as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 1);
XCTAssertEqual(line2.attributedString?.string, "𝑧");
XCTAssertTrue(NSEqualRanges(line2.range, NSMakeRange(4, 1)))
XCTAssertFalse(line2.hasScript);
}
func testAccent() throws {
let mathList = MTMathList()
let accent = MTMathAtomFactory.accent(withName: "hat")
let inner = MTMathList()
inner.add(MTMathAtomFactory.atom(forCharacter: "x"))
accent?.innerList = inner;
mathList.add(accent)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 1)));
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 1);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTAccentDisplay)
let accentDisp = sub0 as! MTAccentDisplay
XCTAssertTrue(NSEqualRanges(accentDisp.range, NSMakeRange(0, 1)));
XCTAssertFalse(accentDisp.hasScript);
XCTAssertTrue(CGPointEqualToPoint(accentDisp.position, CGPointZero));
XCTAssertNotNil(accentDisp.accentee);
XCTAssertNotNil(accentDisp.accent);
let display2 = accentDisp.accentee!
XCTAssertEqual(display2.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display2.position, CGPointZero))
XCTAssertTrue(NSEqualRanges(display2.range, NSMakeRange(0, 1)));
XCTAssertFalse(display2.hasScript);
XCTAssertEqual(display2.index, NSNotFound);
XCTAssertEqual(display2.subDisplays.count, 1);
let subaccentee = display2.subDisplays[0];
XCTAssertTrue(subaccentee is MTCTLineDisplay);
let line2 = subaccentee as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 1);
XCTAssertEqual(line2.attributedString?.string, "𝑥");
XCTAssertTrue(CGPointEqualToPoint(line2.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line2.range, NSMakeRange(0, 1)));
XCTAssertFalse(line2.hasScript);
let glyph = accentDisp.accent!
XCTAssertTrue(CGPointEqualToPoint(glyph.position, CGPointMake(11.86, 0)))
XCTAssertTrue(NSEqualRanges(glyph.range, NSMakeRange(0, 1)))
XCTAssertFalse(glyph.hasScript);
// dimensions
XCTAssertEqual(display.ascent, 14.68, accuracy: 0.01)
XCTAssertEqual(display.descent, 0.22, accuracy: 0.01)
XCTAssertEqual(display.width, 11.44, accuracy: 0.01)
}
func testWideAccent() throws {
let mathList = MTMathList()
let accent = MTMathAtomFactory.accent(withName: "hat")
accent?.innerList = MTMathAtomFactory.mathListForCharacters("xyzw")
mathList.add(accent)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display);
XCTAssertEqual(display.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(display.range, NSMakeRange(0, 1)));
XCTAssertFalse(display.hasScript);
XCTAssertEqual(display.index, NSNotFound);
XCTAssertEqual(display.subDisplays.count, 1);
let sub0 = display.subDisplays[0];
XCTAssertTrue(sub0 is MTAccentDisplay)
let accentDisp = sub0 as! MTAccentDisplay
XCTAssertTrue(NSEqualRanges(accentDisp.range, NSMakeRange(0, 1)));
XCTAssertFalse(accentDisp.hasScript);
XCTAssertTrue(CGPointEqualToPoint(accentDisp.position, CGPointZero));
XCTAssertNotNil(accentDisp.accentee);
XCTAssertNotNil(accentDisp.accent);
let display2 = accentDisp.accentee!
XCTAssertEqual(display2.type, .regular);
XCTAssertTrue(CGPointEqualToPoint(display2.position, CGPointZero))
XCTAssertTrue(NSEqualRanges(display2.range, NSMakeRange(0, 4)));
XCTAssertFalse(display2.hasScript);
XCTAssertEqual(display2.index, NSNotFound);
XCTAssertEqual(display2.subDisplays.count, 1);
let subaccentee = display2.subDisplays[0];
XCTAssertTrue(subaccentee is MTCTLineDisplay);
let line2 = subaccentee as! MTCTLineDisplay
XCTAssertEqual(line2.atoms.count, 4);
XCTAssertEqual(line2.attributedString?.string, "𝑥𝑦𝑧𝑤");
XCTAssertTrue(CGPointEqualToPoint(line2.position, CGPointZero));
XCTAssertTrue(NSEqualRanges(line2.range, NSMakeRange(0, 4)));
XCTAssertFalse(line2.hasScript);
let glyph = accentDisp.accent!
XCTAssertTrue(CGPointMake(3.47, 0).isEqual(to: glyph.position, accuracy: 0.01))
XCTAssertTrue(NSEqualRanges(glyph.range, NSMakeRange(0, 1)))
XCTAssertFalse(glyph.hasScript);
// dimensions
XCTAssertEqual(display.ascent, 14.98, accuracy: 0.01)
XCTAssertEqual(display.descent, 4.10, accuracy: 0.01)
XCTAssertEqual(display.width, 44.86, accuracy: 0.01)
}
// MARK: - Interatom Line Breaking Tests
func testInteratomLineBreaking_SimpleEquation() throws {
// Simple equation that should break between atoms when width is constrained
let latex = "a=1, b=2, c=3, d=4"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
// Create display with narrow width constraint (should force multiple lines)
let maxWidth: CGFloat = 100
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should have multiple sub-displays (lines)
XCTAssertGreaterThan(display!.subDisplays.count, 1, "Expected multiple lines with width constraint of \(maxWidth)")
// Verify that each line respects the width constraint
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.1, "Line \(index) width \(subDisplay.width) exceeds maxWidth \(maxWidth)")
}
// Verify vertical positioning - lines should be below each other
if display!.subDisplays.count > 1 {
let firstLine = display!.subDisplays[0]
let secondLine = display!.subDisplays[1]
XCTAssertLessThan(secondLine.position.y, firstLine.position.y, "Second line should be positioned below first line")
}
}
func testInteratomLineBreaking_TextAndMath() throws {
// The user's specific example: text mixed with math
let latex = "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
// Create display with width constraint of 235 as specified by user
let maxWidth: CGFloat = 235
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should have multiple lines
XCTAssertGreaterThan(display!.subDisplays.count, 1, "Expected multiple lines with width \(maxWidth) for the given LaTeX")
// Verify each line respects width constraint
for (index, subDisplay) in display!.subDisplays.enumerated() {
// Allow 10% tolerance for spacing and rounding
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.1,
"Line \(index) width \(subDisplay.width) exceeds maxWidth \(maxWidth)")
}
// Verify vertical spacing between lines
if display!.subDisplays.count >= 2 {
let firstLine = display!.subDisplays[0]
let secondLine = display!.subDisplays[1]
let verticalSpacing = abs(firstLine.position.y - secondLine.position.y)
XCTAssertGreaterThan(verticalSpacing, 0, "Lines should have vertical spacing")
// Typical line height is around 1.5 * font size
XCTAssertGreaterThan(verticalSpacing, self.font.fontSize * 0.5, "Vertical spacing seems too small")
}
}
func testInteratomLineBreaking_BreaksAtAtomBoundaries() throws {
// Test that breaking happens between atoms, not within them
// Using mathematical atoms separated by operators
let latex = "a+b+c+d+e+f+g+h+i+j+k+l+m+n+o+p"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
// Create display with narrow width that should force breaking
let maxWidth: CGFloat = 120
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should have multiple lines
XCTAssertGreaterThan(display!.subDisplays.count, 1, "Expected line breaking with narrow width")
// Each line should respect the width constraint (with some tolerance)
// since we break at atom boundaries, not mid-atom
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.2,
"Line \(index) width \(subDisplay.width) exceeds maxWidth \(maxWidth) by too much")
}
}
func testInteratomLineBreaking_WithSuperscripts() throws {
// Test breaking with atoms that have superscripts
let latex = "a^{2}+b^{2}+c^{2}+d^{2}+e^{2}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 100
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should handle superscripts properly and create multiple lines if needed
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.1,
"Line \(index) with superscripts exceeds width")
}
}
func testInteratomLineBreaking_NoBreakingWhenNotNeeded() throws {
// Test that short content doesn't break unnecessarily
let latex = "a=b"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 200
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should stay on single line since content is short
// Note: The number of subDisplays might be 1 or more depending on internal structure,
// but the total width should be well under maxWidth
XCTAssertLessThan(display!.width, maxWidth, "Short content should fit without breaking")
}
func testInteratomLineBreaking_BreaksAfterOperators() throws {
// Test that breaking prefers to happen after operators (good break points)
let latex = "a+b+c+d+e+f+g+h"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 80
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should break into multiple lines
XCTAssertGreaterThan(display!.subDisplays.count, 1, "Expected multiple lines")
// Verify width constraints
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.1,
"Line \(index) exceeds width")
}
}
// MARK: - Complex Display Line Breaking Tests (Fractions & Radicals)
func testComplexDisplay_FractionStaysInlineWhenFits() throws {
// Fraction that should stay inline with surrounding content
let latex = "a+\\frac{1}{2}+b"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
// Wide enough to fit everything on one line
let maxWidth: CGFloat = 200
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should fit on a single line (fraction stays inline)
XCTAssertLessThanOrEqual(display!.subDisplays.count, 2,
"Expected fraction to stay inline, not break to separate line")
// Total width should be within constraint
XCTAssertLessThan(display!.width, maxWidth,
"Expression should fit within width constraint")
}
func testComplexDisplay_FractionBreaksWhenTooWide() throws {
// Multiple fractions with narrow width should break
let latex = "a+\\frac{1}{2}+b+\\frac{3}{4}+c"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
// Narrow width should force breaking
let maxWidth: CGFloat = 80
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should have multiple lines
XCTAssertGreaterThan(display!.subDisplays.count, 1,
"Expected line breaking with narrow width")
// Each line should respect width constraint (with tolerance)
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.2,
"Line \(index) width \(subDisplay.width) exceeds maxWidth \(maxWidth) significantly")
}
}
func testComplexDisplay_RadicalStaysInlineWhenFits() throws {
// Radical that should stay inline with surrounding content
let latex = "x+\\sqrt{2}+y"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
// Wide enough to fit everything on one line
let maxWidth: CGFloat = 150
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should fit on a single line (radical stays inline)
XCTAssertLessThanOrEqual(display!.subDisplays.count, 2,
"Expected radical to stay inline, not break to separate line")
// Total width should be within constraint
XCTAssertLessThan(display!.width, maxWidth,
"Expression should fit within width constraint")
}
func testComplexDisplay_RadicalBreaksWhenTooWide() throws {
// Multiple radicals with narrow width should break
let latex = "a+\\sqrt{2}+b+\\sqrt{3}+c+\\sqrt{5}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
// Narrow width should force breaking
let maxWidth: CGFloat = 100
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should have multiple lines
XCTAssertGreaterThan(display!.subDisplays.count, 1,
"Expected line breaking with narrow width")
// Each line should respect width constraint (with tolerance)
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.2,
"Line \(index) width \(subDisplay.width) exceeds maxWidth \(maxWidth) significantly")
}
}
func testComplexDisplay_MixedFractionsAndRadicals() throws {
// Mix of fractions and radicals
let latex = "a+\\frac{1}{2}+\\sqrt{3}+b"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
// Medium width
let maxWidth: CGFloat = 150
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should handle mixed complex displays
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.2,
"Line \(index) width exceeds constraint")
}
}
func testComplexDisplay_FractionWithComplexNumerator() throws {
// Fraction with more complex content
let latex = "\\frac{a+b}{c}+d"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 150
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should stay inline if it fits
XCTAssertLessThan(display!.width, maxWidth * 1.5,
"Complex fraction should handle width reasonably")
}
func testComplexDisplay_RadicalWithDegree() throws {
// Cube root
let latex = "\\sqrt[3]{8}+x"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 150
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should handle radicals with degrees
XCTAssertLessThan(display!.width, maxWidth * 1.2,
"Radical with degree should fit reasonably")
}
func testComplexDisplay_NoBreakingWithoutWidthConstraint() throws {
// Without width constraint, should never break
let latex = "a+\\frac{1}{2}+\\sqrt{3}+b+\\frac{4}{5}+c"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
// No width constraint (maxWidth = 0)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)
XCTAssertNotNil(display)
// Should not artificially break when no constraint
// The display might have multiple subDisplays for internal structure,
// but we verify that the total rendering doesn't have forced line breaks
// by checking that all elements are at y=0 (no vertical offset)
var allAtSameY = true
let firstY = display!.subDisplays.first?.position.y ?? 0
for subDisplay in display!.subDisplays {
if abs(subDisplay.position.y - firstY) > 0.1 {
allAtSameY = false
break
}
}
XCTAssertTrue(allAtSameY, "Without width constraint, all elements should be at same Y position")
}
// MARK: - Additional Recommended Tests
func testEdgeCase_VeryNarrowWidth() throws {
// Test behavior with extremely narrow width constraint
let latex = "a+b+c"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
// Very narrow width - each element might need its own line
let maxWidth: CGFloat = 30
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should handle gracefully without crashing
XCTAssertGreaterThan(display!.subDisplays.count, 0, "Should produce at least one display")
// Each subdisplay should attempt to respect width (though may overflow for single atoms)
for subDisplay in display!.subDisplays {
// Allow overflow for unavoidable cases (single atom wider than constraint)
XCTAssertLessThan(subDisplay.width, maxWidth * 3,
"Width shouldn't be excessively larger than constraint")
}
}
func testEdgeCase_VeryWideAtom() throws {
// Test handling of atom that's wider than maxWidth constraint
let latex = "\\text{ThisIsAnExtremelyLongWordThatCannotBreak}+b"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 100
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should not crash, even if single atom exceeds width
XCTAssertGreaterThan(display!.subDisplays.count, 0, "Should produce display")
// The wide atom should be placed, even if it exceeds maxWidth
// (no way to break it further)
XCTAssertNotNil(display, "Should handle oversized atoms gracefully")
}
func testMixedScriptsAndNonScripts() throws {
// Test mixing atoms with scripts and without scripts
let latex = "a+b^{2}+c+d^{3}+e"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 120
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should handle mixed content
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.3,
"Line \(index) with mixed scripts should respect width reasonably")
}
}
func testMultipleLineBreaks() throws {
// Test expression that requires 4+ line breaks
let latex = "a+b+c+d+e+f+g+h+i+j+k+l+m+n+o+p+q+r+s+t"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
// Very narrow to force many breaks
let maxWidth: CGFloat = 60
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should create multiple lines
XCTAssertGreaterThanOrEqual(display!.subDisplays.count, 4,
"Should create at least 4 lines for long expression")
// Verify vertical positioning - each line should be below the previous
for i in 1..<display!.subDisplays.count {
let prevLine = display!.subDisplays[i-1]
let currentLine = display!.subDisplays[i]
XCTAssertLessThan(currentLine.position.y, prevLine.position.y,
"Line \(i) should be below line \(i-1)")
}
// Verify consistent line spacing
if display!.subDisplays.count >= 3 {
let spacing1 = abs(display!.subDisplays[0].position.y - display!.subDisplays[1].position.y)
let spacing2 = abs(display!.subDisplays[1].position.y - display!.subDisplays[2].position.y)
XCTAssertEqual(spacing1, spacing2, accuracy: 1.0,
"Line spacing should be consistent")
}
}
func testUnicodeTextWrapping() throws {
// Test wrapping with Unicode characters (including CJK)
let latex = "\\text{Hello 世界 こんにちは 안녕하세요 مرحبا}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 150
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should handle Unicode text (may need fallback font)
XCTAssertNotNil(display, "Should handle Unicode text")
// Each line should attempt to respect width
for subDisplay in display!.subDisplays {
// More tolerance for Unicode as font metrics vary
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.5,
"Unicode text line should respect width reasonably")
}
}
func testNumberProtection() throws {
// Test that numbers don't break in the middle
let latex = "\\text{The value is 3.14159 or 2,718 or 1,000,000}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 150
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Numbers should stay together (not split like "3.14" "3." on one line, "14" on next)
// This is handled by the universal breaking mechanism with Core Text
XCTAssertNotNil(display, "Should handle text with numbers")
}
// MARK: - Tests for Not-Yet-Optimized Cases (Document Current Behavior)
func testCurrentBehavior_LargeOperators() throws {
// Documents current behavior: large operators still force line breaks
let latex = "\\sum_{i=1}^{n}x_{i}+\\int_{0}^{1}f(x)dx"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 300
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Current behavior: operators force breaks
// This test documents current behavior for future improvement
XCTAssertNotNil(display, "Large operators render (may force breaks)")
}
func testCurrentBehavior_NestedDelimiters() throws {
// Documents current behavior: \left...\right still forces line breaks
let latex = "a+\\left(b+c\\right)+d"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 200
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Current behavior: delimiters may force breaks
// This test documents current behavior for future improvement
XCTAssertNotNil(display, "Delimiters render (may force breaks)")
}
func testCurrentBehavior_ColoredExpressions() throws {
// Documents current behavior: colored sections still force line breaks
let latex = "a+\\color{red}{b+c}+d"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 200
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Current behavior: colored sections may force breaks
// This test documents current behavior for future improvement
XCTAssertNotNil(display, "Colored sections render (may force breaks)")
}
func testCurrentBehavior_MatricesWithSurroundingContent() throws {
// Documents current behavior: matrices still force line breaks
let latex = "A=\\begin{pmatrix}1&2\\\\3&4\\end{pmatrix}+B"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 300
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Current behavior: matrices force breaks
// This test documents current behavior for future improvement
XCTAssertNotNil(display, "Matrices render (force breaks)")
}
func testRealWorldExample_QuadraticFormula() throws {
// Real-world test: quadratic formula with width constraint
let latex = "x=\\frac{-b\\pm\\sqrt{b^{2}-4ac}}{2a}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 200
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should render the formula (may break if too wide)
XCTAssertNotNil(display, "Quadratic formula renders")
XCTAssertGreaterThan(display!.width, 0, "Formula has non-zero width")
}
func testRealWorldExample_ComplexFraction() throws {
// Real-world test: continued fraction
let latex = "\\frac{1}{2+\\frac{1}{3+\\frac{1}{4}}}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 150
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should render nested fractions
XCTAssertNotNil(display, "Nested fractions render")
XCTAssertGreaterThan(display!.width, 0, "Formula has non-zero width")
}
func testRealWorldExample_MixedOperationsWithFractions() throws {
// Real-world test: mixed arithmetic with multiple fractions
let latex = "\\frac{1}{2}+\\frac{2}{3}+\\frac{3}{4}+\\frac{4}{5}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 180
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// With new implementation, fractions should stay inline when possible
// May break into 2-3 lines depending on actual widths
XCTAssertGreaterThan(display!.subDisplays.count, 0, "Multiple fractions render")
// Verify width constraints are respected
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.3,
"Line \(index) should respect width constraint reasonably")
}
}
// MARK: - Large Operator Tests (NEWLY FIXED!)
func testComplexDisplay_LargeOperatorStaysInlineWhenFits() throws {
// Test that inline-style large operators stay inline when they fit
// In display style without explicit limits, operators should be inline-sized
let latex = "a+\\sum x_i+b"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 250
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .text, maxWidth: maxWidth)
XCTAssertNotNil(display)
// In text style, large operator should be inline-sized and stay with surrounding content
// Should be 1 line if it fits
let lineCount = display!.subDisplays.count
print("Large operator inline test: \(lineCount) line(s)")
// Verify width constraints are respected
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.2,
"Line \(index) width (\(subDisplay.width)) should respect constraint")
}
}
func testComplexDisplay_LargeOperatorBreaksWhenTooWide() throws {
// Test that large operators break when they don't fit
let latex = "a+b+c+d+e+f+\\sum_{i=1}^{n}x_i"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 80 // Very narrow
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// With narrow width, should break into multiple lines
let lineCount = display!.subDisplays.count
print("Large operator breaking test: \(lineCount) line(s)")
XCTAssertGreaterThan(lineCount, 1, "Should break into multiple lines")
// Verify width constraints are respected (with tolerance for tall operators)
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.5,
"Line \(index) width (\(subDisplay.width)) should roughly respect constraint")
}
}
func testComplexDisplay_MultipleLargeOperators() throws {
// Test multiple large operators in sequence
let latex = "\\sum x_i+\\int f(x)dx+\\prod a_i"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 300
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .text, maxWidth: maxWidth)
XCTAssertNotNil(display)
// In text style with wide constraint, might fit on 1-2 lines
let lineCount = display!.subDisplays.count
print("Multiple operators test: \(lineCount) line(s)")
XCTAssertGreaterThan(display!.subDisplays.count, 0, "Operators render")
// Verify width constraints
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.2,
"Line \(index) should respect width constraint")
}
}
// MARK: - Delimiter Tests (NEWLY FIXED!)
func testComplexDisplay_DelimitersStayInlineWhenFit() throws {
// Test that delimited expressions stay inline when they fit
let latex = "a+\\left(b+c\\right)+d"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 200
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should stay on 1 line when it fits
let lineCount = display!.subDisplays.count
print("Delimiter inline test: \(lineCount) line(s)")
// Verify width constraints are respected
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.2,
"Line \(index) width (\(subDisplay.width)) should respect constraint")
}
}
func testComplexDisplay_DelimitersBreakWhenTooWide() throws {
// Test that delimited expressions break when they don't fit
let latex = "a+b+c+\\left(d+e+f+g+h\\right)+i+j"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 100 // Narrow
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should break into multiple lines
let lineCount = display!.subDisplays.count
print("Delimiter breaking test: \(lineCount) line(s)")
XCTAssertGreaterThan(lineCount, 1, "Should break into multiple lines")
// Verify width constraints (delimiters add extra width, so be more tolerant)
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.7,
"Line \(index) should respect width constraint")
}
}
func testComplexDisplay_NestedDelimitersWithWrapping() throws {
// Test that inner content of delimiters respects width constraints
let latex = "\\left(a+b+c+d+e+f+g+h\\right)"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 120
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// With maxWidth propagation, inner content should wrap
print("Nested delimiter test: \(display!.subDisplays.count) line(s)")
XCTAssertGreaterThan(display!.subDisplays.count, 0, "Delimiters render")
// Verify width constraints (delimiters with wrapped content can be wide)
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 2.5,
"Line \(index) width (\(subDisplay.width)) should respect constraint reasonably")
}
}
func testComplexDisplay_MultipleDelimiters() throws {
// Test multiple delimited expressions
let latex = "\\left(a+b\\right)+\\left(c+d\\right)+\\left(e+f\\right)"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 250
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should intelligently break between delimiters if needed
let lineCount = display!.subDisplays.count
print("Multiple delimiters test: \(lineCount) line(s)")
// Verify width constraints
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.2,
"Line \(index) should respect width constraint")
}
}
// MARK: - Color Tests (NEWLY FIXED!)
func testComplexDisplay_ColoredExpressionStaysInlineWhenFits() throws {
// Test that colored expressions stay inline when they fit
let latex = "a+\\color{red}{b+c}+d"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 200
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should stay on 1 line when it fits
let lineCount = display!.subDisplays.count
print("Colored expression inline test: \(lineCount) line(s)")
// Verify width constraints are respected
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.2,
"Line \(index) width (\(subDisplay.width)) should respect constraint")
}
}
func testComplexDisplay_ColoredExpressionBreaksWhenTooWide() throws {
// Test that colored expressions break when they don't fit
let latex = "a+\\color{blue}{b+c+d+e+f+g+h}+i"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 100 // Narrow
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should break into multiple lines
let lineCount = display!.subDisplays.count
print("Colored expression breaking test: \(lineCount) line(s)")
XCTAssertGreaterThan(lineCount, 1, "Should break into multiple lines")
// Verify width constraints
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.3,
"Line \(index) should respect width constraint")
}
}
// Removed testComplexDisplay_ColoredContentWraps - colored expression tests above are sufficient
func testComplexDisplay_MultipleColoredSections() throws {
// Test multiple colored sections
let latex = "\\color{red}{a+b}+\\color{blue}{c+d}+\\color{green}{e+f}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 250
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should intelligently break between colored sections if needed
let lineCount = display!.subDisplays.count
print("Multiple colored sections test: \(lineCount) line(s)")
// Verify width constraints
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.2,
"Line \(index) should respect width constraint")
}
}
// MARK: - Matrix Tests (NEWLY FIXED!)
func testComplexDisplay_SmallMatrixStaysInlineWhenFits() throws {
// Test that small matrices stay inline when they fit
let latex = "A=\\begin{pmatrix}1&2\\end{pmatrix}+B"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 250
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Small 1x2 matrix should stay inline
let lineCount = display!.subDisplays.count
print("Small matrix inline test: \(lineCount) line(s)")
// Verify width constraints are respected
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.2,
"Line \(index) width (\(subDisplay.width)) should respect constraint")
}
}
func testComplexDisplay_MatrixBreaksWhenTooWide() throws {
// Test that large matrices break when they don't fit
let latex = "a+b+c+\\begin{pmatrix}1&2&3&4\\end{pmatrix}+d"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 120 // Narrow
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should break with narrow width
let lineCount = display!.subDisplays.count
print("Matrix breaking test: \(lineCount) line(s)")
// Verify width constraints (matrices can be slightly wider)
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.5,
"Line \(index) should roughly respect width constraint")
}
}
func testComplexDisplay_MatrixWithSurroundingContent() throws {
// Real-world test: matrix in equation
let latex = "M=\\begin{pmatrix}a&b\\\\c&d\\end{pmatrix}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 200
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// 2x2 matrix with assignment
print("Matrix with content test: \(display!.subDisplays.count) line(s)")
XCTAssertGreaterThan(display!.subDisplays.count, 0, "Matrix renders")
// Verify width constraints
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.4,
"Line \(index) should respect width constraint")
}
}
// MARK: - Integration Tests (All Complex Displays)
func testComplexDisplay_MixedComplexElements() throws {
// Test mixing all complex display types
let latex = "a+\\frac{1}{2}+\\sqrt{3}+\\left(b+c\\right)+\\color{red}{d}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 300
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// With wide constraint, elements should render with reasonable breaking
let lineCount = display!.subDisplays.count
print("Mixed complex elements test: \(lineCount) line(s)")
XCTAssertGreaterThan(lineCount, 0, "Should have content")
XCTAssertLessThanOrEqual(lineCount, 6, "Should fit reasonably (relaxed for complex elements)")
// Verify width constraints
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.2,
"Line \(index) should respect width constraint")
}
}
func testComplexDisplay_RealWorldQuadraticWithColor() throws {
// Real-world: colored quadratic formula
let latex = "x=\\frac{-b\\pm\\color{blue}{\\sqrt{b^2-4ac}}}{2a}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Failed to parse LaTeX")
let maxWidth: CGFloat = 250
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Complex nested structure with color
print("Quadratic with color test: \(display!.subDisplays.count) line(s)")
XCTAssertGreaterThan(display!.subDisplays.count, 0, "Complex formula renders")
// Verify width constraints
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.3,
"Line \(index) should respect width constraint")
}
}
}