Files
swiftui-math/Tests/SwiftMathTests/MTTypesetterTests.swift
Nicolas Guillot cc1a7b8023 Fix inline mode rendering for large operators and fractions
This commit addresses three issues with math rendering:

  1. Large operator limits positioning (continued from previous commit)
  Modified makeLargeOp() and addLimitsToDisplay() to show limits above/below
  in both display and text (inline) modes:
    - Changed: op.limits && style == .display
    - To: op.limits && (style == .display || style == .text)

  This enables operators like \lim, \sum, and \prod to show subscripts/
  superscripts above and below even in inline mode \(...\), not just in
  display mode \[...\].

  2. Fraction font size issue
  Fixed fractions appearing too small in inline mode. Previously, fractions
  used one style level smaller than their parent (standard LaTeX behavior):
    - Display mode → fractions use text style (acceptable)
    - Text mode →

  Root cause:
  Inline delimiters \(...\) insert \textstyle, forcing text mode. In text
  mode, fractionStyle() returned style.inc(), making numerator/denominator
  use script style (two levels smaller than display). This made fraction
  numbers tiny compared to surrounding text in expressions like:
    \(\frac{a}{b} = c\) - a, b were script-sized while c was text-sized

  Solution:
  Modified fractionStyle() to return the SAME style instead of incrementing:
    func fractionStyle() -> MTLineStyle {
        return style  // Was: return style.inc()
    }

  This keeps fraction numerators/denominators at the same font size as
  regular text, preventing them from becoming too small. Spacing and
  positioning (numeratorShiftUp, etc.) still vary by parent style.

  3. Non-regression fixes
  Updated test expectations to match new fraction sizing behavior
2025-11-17 10:12:45 +01:00

3382 lines
155 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, 30, "Width should include operator + spacing + x")
XCTAssertLessThan(display.width, 40, "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, 38, "Width should include operator + scripts + spacing + x");
XCTAssertLessThan(display.width, 48, "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, 38, "Width should include operator + limits + spacing + x")
XCTAssertLessThan(display.width, 48, "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 testLargeOpWithLimitsInlineMode_Limit() throws {
// Test that \lim in inline/text mode shows limits above/below (not to the side)
// This tests the fix for: \(\lim_{n \to \infty} \frac{1}{n} = 0\)
let latex = "\\lim_{n\\to\\infty}\\frac{1}{n}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
// Use .text style to simulate inline mode \(...\)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .text)!
XCTAssertNotNil(display)
XCTAssertEqual(display.type, .regular)
// Should have at least 2 subdisplays: lim with limits, and fraction
XCTAssertGreaterThanOrEqual(display.subDisplays.count, 2)
// First subdisplay should be the limit operator with limits display
let limDisplay = display.subDisplays[0]
XCTAssertTrue(limDisplay is MTLargeOpLimitsDisplay, "Limit should use MTLargeOpLimitsDisplay in inline mode")
if let limitsDisplay = limDisplay as? MTLargeOpLimitsDisplay {
XCTAssertNotNil(limitsDisplay.lowerLimit, "Should have lower limit (n→∞)")
XCTAssertNil(limitsDisplay.upperLimit, "Should not have upper limit")
XCTAssertLessThan(limitsDisplay.lowerLimit!.position.y, 0, "Lower limit should be below baseline")
}
}
func testLargeOpWithLimitsInlineMode_Sum() throws {
// Test that \sum in inline/text mode shows limits above/below (not to the side)
// This tests the fix for: \(\sum_{i=1}^{n} i\)
let latex = "\\sum_{i=1}^{n}i"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
// Use .text style to simulate inline mode \(...\)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .text)!
XCTAssertNotNil(display)
XCTAssertEqual(display.type, .regular)
// Should have at least 2 subdisplays: sum with limits, and variable i
XCTAssertGreaterThanOrEqual(display.subDisplays.count, 2)
// First subdisplay should be the sum operator with limits display
let sumDisplay = display.subDisplays[0]
XCTAssertTrue(sumDisplay is MTLargeOpLimitsDisplay, "Sum should use MTLargeOpLimitsDisplay in inline mode")
if let limitsDisplay = sumDisplay as? MTLargeOpLimitsDisplay {
XCTAssertNotNil(limitsDisplay.upperLimit, "Should have upper limit (n)")
XCTAssertNotNil(limitsDisplay.lowerLimit, "Should have lower limit (i=1)")
XCTAssertGreaterThan(limitsDisplay.upperLimit!.position.y, 0, "Upper limit should be above baseline")
XCTAssertLessThan(limitsDisplay.lowerLimit!.position.y, 0, "Lower limit should be below baseline")
}
}
func testLargeOpWithLimitsInlineMode_Product() throws {
// Test that \prod in inline/text mode shows limits above/below (not to the side)
// This tests the fix for: \(\prod_{k=1}^{\infty} (1 + x^k)\)
let latex = "\\prod_{k=1}^{\\infty}x"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
// Use .text style to simulate inline mode \(...\)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .text)!
XCTAssertNotNil(display)
XCTAssertEqual(display.type, .regular)
// Should have at least 2 subdisplays: prod with limits, and variable x
XCTAssertGreaterThanOrEqual(display.subDisplays.count, 2)
// First subdisplay should be the product operator with limits display
let prodDisplay = display.subDisplays[0]
XCTAssertTrue(prodDisplay is MTLargeOpLimitsDisplay, "Product should use MTLargeOpLimitsDisplay in inline mode")
if let limitsDisplay = prodDisplay as? MTLargeOpLimitsDisplay {
XCTAssertNotNil(limitsDisplay.upperLimit, "Should have upper limit (∞)")
XCTAssertNotNil(limitsDisplay.lowerLimit, "Should have lower limit (k=1)")
XCTAssertGreaterThan(limitsDisplay.upperLimit!.position.y, 0, "Upper limit should be above baseline")
XCTAssertLessThan(limitsDisplay.lowerLimit!.position.y, 0, "Lower limit should be below baseline")
}
}
func testFractionInlineMode_NormalFontSize() throws {
// Test that \(...\) delimiter doesn't make fractions too small
// This tests the fix for: \(\frac{a}{b} = c\)
let latex = "\\frac{a}{b}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
// Create display without any style forcing
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display)
XCTAssertEqual(display.type, .regular)
// Should have 1 subdisplay: the fraction
XCTAssertEqual(display.subDisplays.count, 1)
// First subdisplay should be the fraction
let fracDisplay = display.subDisplays[0]
XCTAssertTrue(fracDisplay is MTFractionDisplay, "Should be a fraction display")
if let fractionDisplay = fracDisplay as? MTFractionDisplay {
XCTAssertNotNil(fractionDisplay.numerator, "Should have numerator")
XCTAssertNotNil(fractionDisplay.denominator, "Should have denominator")
// The numerator and denominator should use text style (not script style)
// In display mode, fractions use text style for numerator/denominator
// Check that the font size is reasonable (not script-sized)
let numDisplay = fractionDisplay.numerator!
XCTAssertGreaterThan(numDisplay.width, 5, "Numerator should have reasonable size, not script-sized")
XCTAssertGreaterThan(numDisplay.ascent, 5, "Numerator should have reasonable ascent, not script-sized")
}
}
func testFractionInlineDelimiters_NormalSize() throws {
// Test that \(\frac{a}{b}\) has full-sized numerator/denominator
// Inline delimiters insert \textstyle, but fractions maintain same font size
let latex1 = "\\(\\frac{a}{b}\\)"
let mathList1 = MTMathListBuilder.build(fromString: latex1)
XCTAssertNotNil(mathList1, "Should parse LaTeX with delimiters")
let display1 = MTTypesetter.createLineForMathList(mathList1, font: self.font, style: .display)!
// Should have subdisplays (style atom + fraction)
XCTAssertGreaterThanOrEqual(display1.subDisplays.count, 1)
// Find the fraction display (it might be after a style atom)
let fracDisplay = display1.subDisplays.first(where: { $0 is MTFractionDisplay }) as? MTFractionDisplay
XCTAssertNotNil(fracDisplay, "Should have fraction display")
// The numerator should have reasonable size (not script-sized)
XCTAssertGreaterThan(fracDisplay!.numerator!.width, 8, "Numerator should have reasonable width")
XCTAssertGreaterThan(fracDisplay!.numerator!.ascent, 6, "Numerator should have reasonable ascent")
}
func testComplexFractionInlineMode() throws {
// Test that complex fractions in inline mode render at normal size
// This tests: \(\frac{x^2 + 1}{y - 3}\)
let latex = "\\frac{x^2+1}{y-3}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)!
XCTAssertNotNil(display)
// Should have a fraction display
XCTAssertEqual(display.subDisplays.count, 1)
let fracDisplay = display.subDisplays[0]
XCTAssertTrue(fracDisplay is MTFractionDisplay)
if let fractionDisplay = fracDisplay as? MTFractionDisplay {
// Numerator should contain multiple atoms (x^2 + 1)
let numDisplay = fractionDisplay.numerator!
XCTAssertGreaterThanOrEqual(numDisplay.subDisplays.count, 1, "Numerator should have content")
// Check that the numerator has reasonable size (not script-sized)
XCTAssertGreaterThan(numDisplay.width, 20, "Complex numerator should have reasonable width")
XCTAssertGreaterThan(numDisplay.ascent, 5, "Numerator with superscript should have reasonable height")
}
}
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 (updated for new fraction sizing where fractions maintain same size as parent style)
XCTAssertEqual(display.ascent, 61.16, accuracy: 0.01)
XCTAssertEqual(display.descent, 21.288, accuracy: 0.01)
XCTAssertEqual(display.width, 85.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);
// With updated fractionStyle(), fractions use the same font size in display and text modes,
// but spacing/positioning is still different (numeratorShiftUp, etc. check parent style).
// So originalDisplay (display mode) will be larger than display (text mode).
XCTAssertGreaterThan(originalDisplay.ascent, display.ascent, "Display mode fractions have more vertical spacing");
XCTAssertGreaterThan(originalDisplay.descent, display.descent, "Display mode fractions have more vertical spacing");
}
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 (all elements have same y position)
// Note: subdisplays may be > 1 due to flushing currentLine before complex atoms
// What matters is that they're all at the same y position (no line breaks)
let firstY = display!.subDisplays.first?.position.y ?? 0
for subDisplay in display!.subDisplays {
XCTAssertEqual(subDisplay.position.y, firstY, accuracy: 0.1,
"All elements should be on the same line (same y position)")
}
// 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 (all elements have same y position)
// Note: subdisplays may be > 1 due to flushing currentLine before complex atoms
// What matters is that they're all at the same y position (no line breaks)
let firstY = display!.subDisplays.first?.position.y ?? 0
for subDisplay in display!.subDisplays {
XCTAssertEqual(subDisplay.position.y, firstY, accuracy: 0.1,
"All elements should be on the same line (same y position)")
}
// 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
// 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
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
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
// 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
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
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
// 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
// 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
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
// 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
// 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
// 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
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
XCTAssertGreaterThan(lineCount, 0, "Should have content")
// Note: lineCount may be higher due to flushing currentLine before each complex atom
// What matters is that they fit within the width constraint
XCTAssertLessThanOrEqual(lineCount, 12, "Should fit reasonably (increased for flushed segments)")
// 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
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")
}
}
// MARK: - Regression Test for Sum Equation Layout Bug
func testSumEquationWithFraction_CorrectOrdering() throws {
// Test case for: \(\sum_{i=1}^{n} i = \frac{n(n+1)}{2}\)
// Bug: The = sign was appearing at the end instead of between i and the fraction
let latex = "\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
// Create display without width constraint first to check ordering
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)
XCTAssertNotNil(display, "Should create display")
// Get the subdisplays to check ordering
let subDisplays = display!.subDisplays
// Print positions and types for debugging
for (index, subDisplay) in subDisplays.enumerated() {
if let lineDisplay = subDisplay as? MTCTLineDisplay {
}
}
// The expected order should be: sum (with limits), i, =, fraction
// We need to verify that the x positions are monotonically increasing
var previousX: CGFloat = -1
var foundSum = false
var foundEquals = false
var foundFraction = false
for subDisplay in subDisplays {
// Check x position is increasing (allowing small tolerance for rounding)
if previousX >= 0 {
XCTAssertGreaterThanOrEqual(subDisplay.position.x, previousX - 0.1,
"Displays should be ordered left to right, but got x=\(subDisplay.position.x) after x=\(previousX)")
}
previousX = subDisplay.position.x + subDisplay.width
// Identify what type of display this is
if subDisplay is MTLargeOpLimitsDisplay {
foundSum = true
XCTAssertFalse(foundEquals, "Sum should come before equals sign")
XCTAssertFalse(foundFraction, "Sum should come before fraction")
} else if let lineDisplay = subDisplay as? MTCTLineDisplay,
let text = lineDisplay.attributedString?.string {
if text.contains("=") {
foundEquals = true
XCTAssertTrue(foundSum, "Equals should come after sum")
XCTAssertFalse(foundFraction, "Equals should come before fraction")
}
} else if subDisplay is MTFractionDisplay {
foundFraction = true
XCTAssertTrue(foundSum, "Fraction should come after sum")
XCTAssertTrue(foundEquals, "Fraction should come after equals sign")
}
}
XCTAssertTrue(foundSum, "Should contain sum operator")
XCTAssertTrue(foundEquals, "Should contain equals sign")
XCTAssertTrue(foundFraction, "Should contain fraction")
}
func testSumEquationWithFraction_WithWidthConstraint() throws {
// Test case for: \(\sum_{i=1}^{n} i = \frac{n(n+1)}{2}\) with width constraint
// This reproduces the issue where = appears at the end instead of in the middle
let latex = "\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
// Create display with width constraint matching MathView preview (235)
// Use .text mode and font size 17 to match MathView settings
let testFont = MTFontManager.fontManager.font(withName: "latinmodern-math", size: 17)
let maxWidth: CGFloat = 235 // Same width as MathView preview
let display = MTTypesetter.createLineForMathList(mathList, font: testFont, style: .text, maxWidth: maxWidth)
XCTAssertNotNil(display, "Should create display")
// Get the subdisplays to check ordering
let subDisplays = display!.subDisplays
// Print positions and types for debugging
for (index, subDisplay) in subDisplays.enumerated() {
if let lineDisplay = subDisplay as? MTCTLineDisplay {
}
}
// Track what we find and their y positions
var sumX: CGFloat?
var sumY: CGFloat?
var iX: CGFloat?
var iY: CGFloat?
var equalsX: CGFloat?
var equalsY: CGFloat?
var fractionX: CGFloat?
var fractionY: CGFloat?
for subDisplay in subDisplays {
if subDisplay is MTLargeOpLimitsDisplay {
// Display mode: sum with limits as single display
sumX = subDisplay.position.x
sumY = subDisplay.position.y
} else if subDisplay is MTGlyphDisplay {
// Text mode: sum symbol as glyph display (check if it's the sum symbol)
if sumX == nil {
sumX = subDisplay.position.x
sumY = subDisplay.position.y
}
} else if let lineDisplay = subDisplay as? MTCTLineDisplay,
let text = lineDisplay.attributedString?.string {
if text.contains("=") && !text.contains("i") {
// Just the equals sign (not combined with i)
equalsX = subDisplay.position.x
equalsY = subDisplay.position.y
} else if text.contains("i") && text.contains("=") {
// i and = together (ideal case)
iX = subDisplay.position.x
iY = subDisplay.position.y
equalsX = subDisplay.position.x // They're together
equalsY = subDisplay.position.y
} else if text.contains("i") {
// Just i
iX = subDisplay.position.x
iY = subDisplay.position.y
}
} else if subDisplay is MTFractionDisplay {
fractionX = subDisplay.position.x
fractionY = subDisplay.position.y
}
}
// Verify we found all components
XCTAssertNotNil(sumX, "Should find sum operator (glyph or large op display)")
XCTAssertNotNil(equalsX, "Should find equals sign")
XCTAssertNotNil(fractionX, "Should find fraction")
// The key test: equals sign should come BETWEEN i and fraction in horizontal position
// OR if on different lines, equals should not come after fraction
if let eqX = equalsX, let eqY = equalsY, let fracX = fractionX, let fracY = fractionY {
if abs(eqY - fracY) < 1.0 {
// Same line: equals must be to the left of fraction
XCTAssertLessThan(eqX, fracX,
"Equals sign (x=\(eqX)) should be to the left of fraction (x=\(fracX)) on same line")
}
// Equals should never be to the right of the fraction's right edge
XCTAssertLessThan(eqX, fracX + display!.width,
"Equals sign should not appear after the fraction")
}
}
// MARK: - Improved Script Handling Tests
func testScriptedAtoms_StayInlineWhenFit() throws {
// Test that atoms with superscripts stay inline when they fit
let latex = "a^{2}+b^{2}+c^{2}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should 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)
// Check for line breaks (large y position gaps indicate line breaks)
// Note: Superscripts/subscripts have different y positions but are on same "line"
// Line breaks use fontSize * 1.5 spacing, so look for gaps > fontSize
var yPositions = display!.subDisplays.map { $0.position.y }.sorted()
var lineBreakCount = 0
for i in 1..<yPositions.count {
let gap = abs(yPositions[i] - yPositions[i-1])
if gap > self.font.fontSize {
lineBreakCount += 1
}
}
XCTAssertEqual(lineBreakCount, 0,
"Should have no line breaks when content fits within width")
// Total width should be within constraint
XCTAssertLessThan(display!.width, maxWidth,
"Expression should fit within width constraint")
}
func testScriptedAtoms_BreakWhenTooWide() throws {
// Test that atoms with superscripts break when width is exceeded
let latex = "a^{2}+b^{2}+c^{2}+d^{2}+e^{2}+f^{2}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should 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 (different y positions)
var uniqueYPositions = Set<CGFloat>()
for subDisplay in display!.subDisplays {
uniqueYPositions.insert(round(subDisplay.position.y * 10) / 10) // Round to avoid floating point issues
}
XCTAssertGreaterThan(uniqueYPositions.count, 1,
"Should have multiple lines due to width constraint")
// Each subdisplay should respect width constraint
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.2,
"Line \(index) width (\(subDisplay.width)) should respect constraint")
}
}
func testMixedScriptedAndNonScripted() throws {
// Test mixing scripted and non-scripted atoms
let latex = "a+b^{2}+c+d^{2}+e"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
let maxWidth: CGFloat = 180
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should fit on one or few lines
// Note: subdisplay count may be higher due to flushing before scripted atoms
XCTAssertLessThanOrEqual(display!.subDisplays.count, 8,
"Mixed expression should have reasonable line count")
// Verify width constraints
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.2,
"Line \(index) should respect width constraint")
}
}
func testSubscriptsAndSuperscripts() throws {
// Test atoms with both subscripts and superscripts
let latex = "x_{1}^{2}+x_{2}^{2}+x_{3}^{2}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
let maxWidth: CGFloat = 200
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should fit on reasonable number of lines
XCTAssertGreaterThan(display!.subDisplays.count, 0,
"Should have content")
// Verify width constraints
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.2,
"Line \(index) should respect width constraint")
}
}
func testRealWorld_QuadraticExpansion() throws {
// Real-world test: quadratic expansion with exponents
let latex = "(a+b)^{2}=a^{2}+2ab+b^{2}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
let maxWidth: CGFloat = 250
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should fit on reasonable number of lines
XCTAssertGreaterThan(display!.subDisplays.count, 0,
"Quadratic expansion should render")
// Verify width constraints
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.2,
"Line \(index) should respect width constraint")
}
}
func testRealWorld_Polynomial() throws {
// Real-world test: polynomial with multiple terms
let latex = "x^{4}+x^{3}+x^{2}+x+1"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
let maxWidth: CGFloat = 180
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should have reasonable structure
XCTAssertGreaterThan(display!.subDisplays.count, 0,
"Polynomial should render")
// Verify width constraints
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.2,
"Line \(index) should respect width constraint")
}
}
func testScriptedAtoms_NoBreakingWithoutConstraint() throws {
// Test that scripted atoms don't break unnecessarily without width constraint
let latex = "a^{2}+b^{2}+c^{2}+d^{2}+e^{2}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
// No width constraint (maxWidth = 0)
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: 0)
XCTAssertNotNil(display)
// Check for line breaks - should have none without width constraint
var yPositions = display!.subDisplays.map { $0.position.y }.sorted()
var lineBreakCount = 0
for i in 1..<yPositions.count {
let gap = abs(yPositions[i] - yPositions[i-1])
if gap > self.font.fontSize {
lineBreakCount += 1
}
}
XCTAssertEqual(lineBreakCount, 0,
"Without width constraint, should have no line breaks")
}
func testComplexScriptedExpression() throws {
// Test complex expression mixing fractions and scripts
let latex = "\\frac{x^{2}}{y^{2}}+a^{2}+\\sqrt{b^{2}}"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
let maxWidth: CGFloat = 220
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should render successfully
XCTAssertGreaterThan(display!.subDisplays.count, 0,
"Complex expression should render")
// Verify width constraints
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.3,
"Line \(index) should respect width constraint (with tolerance for complex atoms)")
}
}
// MARK: - Break Quality Scoring Tests
func testBreakQuality_PreferAfterBinaryOperator() throws {
// Test that breaks prefer to occur after binary operators (+, -, ×, ÷)
// Expression: "aaaa+bbbbcccc" where break should occur after + (not in middle of bbbbcccc)
let latex = "aaaa+bbbbcccc"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
// Set width to force a break somewhere between + and end
let maxWidth: CGFloat = 100
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Extract text content from each line to verify break location
var lineContents: [String] = []
for subDisplay in display!.subDisplays {
if let lineDisplay = subDisplay as? MTCTLineDisplay,
let text = lineDisplay.attributedString?.string {
lineContents.append(text)
}
}
// With break quality scoring, should break after the + operator
// First line should contain "aaaa+"
let hasGoodBreak = lineContents.contains { $0.contains("+") }
XCTAssertTrue(hasGoodBreak,
"Break should occur after binary operator +, found lines: \(lineContents)")
}
func testBreakQuality_PreferAfterRelation() throws {
// Test that breaks prefer to occur after relation operators (=, <, >)
let latex = "aaaa=bbbb+cccc"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
let maxWidth: CGFloat = 90
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Extract line contents
var lineContents: [String] = []
for subDisplay in display!.subDisplays {
if let lineDisplay = subDisplay as? MTCTLineDisplay,
let text = lineDisplay.attributedString?.string {
lineContents.append(text)
}
}
// Should break after the = operator
let hasGoodBreak = lineContents.contains { $0.contains("=") }
XCTAssertTrue(hasGoodBreak,
"Break should occur after relation operator =, found lines: \(lineContents)")
}
func testBreakQuality_AvoidAfterOpenBracket() throws {
// Test that breaks avoid occurring immediately after open brackets
// Expression: "aaaa+(bbb+ccc)" should NOT break as "aaaa+(\n bbb+ccc)"
let latex = "aaaa+(bbb+ccc)"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
let maxWidth: CGFloat = 100
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Extract line contents
var lineContents: [String] = []
for subDisplay in display!.subDisplays {
if let lineDisplay = subDisplay as? MTCTLineDisplay,
let text = lineDisplay.attributedString?.string {
lineContents.append(text)
}
}
// Should NOT have a line ending with "+(" - bad break point
let hasBadBreak = lineContents.contains { $0.hasSuffix("+(") }
XCTAssertFalse(hasBadBreak,
"Should avoid breaking after open bracket, found lines: \(lineContents)")
}
func testBreakQuality_LookAheadFindsBetterBreak() throws {
// Test that look-ahead finds better break points
// Expression: "aaabbb+ccc" with tight width
// Should defer break to after + rather than between aaa and bbb
let latex = "aaabbb+ccc"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
// Width set so that "aaabbb" slightly exceeds, but look-ahead should find + as better break
let maxWidth: CGFloat = 60
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Extract line contents
var lineContents: [String] = []
for subDisplay in display!.subDisplays {
if let lineDisplay = subDisplay as? MTCTLineDisplay,
let text = lineDisplay.attributedString?.string {
lineContents.append(text)
}
}
// Should break after + (penalty 0) rather than in the middle (penalty 10 or 50)
let hasGoodBreak = lineContents.contains { $0.contains("+") }
XCTAssertTrue(hasGoodBreak,
"Look-ahead should find better break after +, found lines: \(lineContents)")
}
func testBreakQuality_MultipleOperators() throws {
// Test with multiple operators - should break at best available points
let latex = "a+b+c+d+e+f"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
let maxWidth: CGFloat = 60
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Count line breaks
var yPositions = display!.subDisplays.map { $0.position.y }.sorted()
var lineBreakCount = 0
for i in 1..<yPositions.count {
let gap = abs(yPositions[i] - yPositions[i-1])
if gap > self.font.fontSize {
lineBreakCount += 1
}
}
// Should have some breaks
XCTAssertGreaterThan(lineBreakCount, 0, "Expression should break into multiple lines")
// Each line should respect width constraint
for subDisplay in display!.subDisplays {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.2,
"Each line should respect width constraint")
}
}
func testBreakQuality_ComplexExpression() throws {
// Test complex expression with various atom types
let latex = "x=a+b\\times c+\\frac{d}{e}+f"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
let maxWidth: CGFloat = 120
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should render successfully
XCTAssertGreaterThan(display!.subDisplays.count, 0, "Should have content")
// Verify all subdisplays respect width constraints
for (index, subDisplay) in display!.subDisplays.enumerated() {
XCTAssertLessThanOrEqual(subDisplay.width, maxWidth * 1.3,
"Line \(index) should respect width (with tolerance for complex atoms)")
}
}
func testBreakQuality_NoBreakWhenNotNeeded() throws {
// Test that break quality scoring doesn't add unnecessary breaks
let latex = "a+b+c"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
let maxWidth: CGFloat = 200 // Wide enough to fit everything
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should have no breaks when content fits
var yPositions = display!.subDisplays.map { $0.position.y }.sorted()
var lineBreakCount = 0
for i in 1..<yPositions.count {
let gap = abs(yPositions[i] - yPositions[i-1])
if gap > self.font.fontSize {
lineBreakCount += 1
}
}
XCTAssertEqual(lineBreakCount, 0,
"Should not add breaks when content fits within width")
}
func testBreakQuality_PenaltyOrdering() throws {
// Test that penalty system correctly orders break preferences
// Given: "aaaa+b(ccc" - when break is needed, should prefer breaking after + (penalty 0)
// rather than after ( (penalty 100)
let latex = "aaaa+b(ccc"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
let maxWidth: CGFloat = 70
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Extract line contents
var lineContents: [String] = []
for subDisplay in display!.subDisplays {
if let lineDisplay = subDisplay as? MTCTLineDisplay,
let text = lineDisplay.attributedString?.string {
lineContents.append(text)
}
}
// Should prefer breaking after "+" (penalty 0) rather than after "(" (penalty 100)
let breaksAfterPlus = lineContents.contains { $0.contains("+") && !$0.contains("(") }
XCTAssertTrue(breaksAfterPlus || lineContents.count == 1,
"Should prefer breaking after + operator or fit on one line, found lines: \(lineContents)")
}
// MARK: - Dynamic Line Height Tests
func testDynamicLineHeight_TallContentHasMoreSpacing() throws {
// Test that lines with tall content (fractions) have appropriate spacing
let latex = "a+b+c+\\frac{x^{2}}{y^{2}}+d+e+f"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
// Force multiple lines
let maxWidth: CGFloat = 80
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Collect unique y positions (representing different lines)
let yPositions = Set(display!.subDisplays.map { $0.position.y }).sorted(by: >)
// Should have multiple lines
XCTAssertGreaterThan(yPositions.count, 1, "Should have multiple lines")
// Calculate spacing between lines
var spacings: [CGFloat] = []
for i in 1..<yPositions.count {
let spacing = yPositions[i-1] - yPositions[i]
spacings.append(spacing)
}
// With dynamic line height, spacing should vary based on content height
// Line with fraction should have larger spacing than lines with just variables
// All spacings should be at least 20% of fontSize (minimum spacing)
let minExpectedSpacing = self.font.fontSize * 0.2
for spacing in spacings {
XCTAssertGreaterThanOrEqual(spacing, minExpectedSpacing,
"Line spacing should be at least minimum spacing")
}
}
func testDynamicLineHeight_RegularContentHasReasonableSpacing() throws {
// Test that lines with regular content don't have excessive spacing
let latex = "a+b+c+d+e+f+g+h+i+j"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
// Force multiple lines
let maxWidth: CGFloat = 60
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Collect unique y positions
let yPositions = Set(display!.subDisplays.map { $0.position.y }).sorted(by: >)
// Should have multiple lines
XCTAssertGreaterThan(yPositions.count, 1, "Should have multiple lines")
// Calculate spacing between lines
var spacings: [CGFloat] = []
for i in 1..<yPositions.count {
let spacing = yPositions[i-1] - yPositions[i]
spacings.append(spacing)
}
// For regular content, spacing should be reasonable (roughly 1.2-1.8x fontSize)
for spacing in spacings {
XCTAssertGreaterThanOrEqual(spacing, self.font.fontSize * 1.0,
"Spacing should be at least fontSize")
XCTAssertLessThanOrEqual(spacing, self.font.fontSize * 2.0,
"Spacing should not be excessive for regular content")
}
}
func testDynamicLineHeight_MixedContentVariesSpacing() throws {
// Test that spacing adapts to each line's content
// Line 1: regular (a+b)
// Line 2: with fraction (more height needed)
// Line 3: regular again (c+d)
let latex = "a+b+\\frac{x}{y}+c+d"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
// Force breaks to create multiple lines
let maxWidth: CGFloat = 50
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should render successfully with varying line heights
XCTAssertGreaterThan(display!.subDisplays.count, 0, "Should have content")
// Verify overall height is reasonable
let totalHeight = display!.ascent + display!.descent
XCTAssertGreaterThan(totalHeight, 0, "Total height should be positive")
}
func testDynamicLineHeight_LargeOperatorsGetAdequateSpace() throws {
// Test that large operators with limits get adequate vertical spacing
let latex = "\\sum_{i=1}^{n}i+\\prod_{j=1}^{m}j"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
// Force line break between operators
let maxWidth: CGFloat = 80
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Collect y positions
let yPositions = Set(display!.subDisplays.map { $0.position.y }).sorted(by: >)
if yPositions.count > 1 {
// Calculate spacing
var spacings: [CGFloat] = []
for i in 1..<yPositions.count {
let spacing = yPositions[i-1] - yPositions[i]
spacings.append(spacing)
}
// Large operators need substantial spacing
for spacing in spacings {
XCTAssertGreaterThanOrEqual(spacing, self.font.fontSize,
"Large operators should have at least fontSize spacing")
}
}
}
func testDynamicLineHeight_ConsistentWithinSimilarContent() throws {
// Test that similar lines get similar spacing
let latex = "a+b+c+d+e+f"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
// Force multiple lines with similar content
let maxWidth: CGFloat = 40
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Collect unique y positions
let yPositions = Set(display!.subDisplays.map { $0.position.y }).sorted(by: >)
if yPositions.count >= 3 {
// Calculate all spacings
var spacings: [CGFloat] = []
for i in 1..<yPositions.count {
let spacing = yPositions[i-1] - yPositions[i]
spacings.append(spacing)
}
// Similar content should have similar spacing (within 20% variance)
let avgSpacing = spacings.reduce(0, +) / CGFloat(spacings.count)
for spacing in spacings {
let variance = abs(spacing - avgSpacing) / avgSpacing
XCTAssertLessThanOrEqual(variance, 0.3,
"Spacing variance should be reasonable for similar content")
}
}
}
func testDynamicLineHeight_NoRegressionOnSingleLine() throws {
// Test that single-line expressions still work correctly
let latex = "a+b+c"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
// No width constraint
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display)
XCTAssertNotNil(display)
// Should be on single line
let yPositions = Set(display!.subDisplays.map { $0.position.y })
XCTAssertEqual(yPositions.count, 1, "Should be on single line")
}
func testDynamicLineHeight_DeepFractionsGetExtraSpace() throws {
// Test that nested/continued fractions get adequate spacing
let latex = "a+\\frac{1}{\\frac{2}{3}}+b+c"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
// Force line breaks
let maxWidth: CGFloat = 70
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Deep fractions are taller - verify reasonable total height
let totalHeight = display!.ascent + display!.descent
XCTAssertGreaterThan(totalHeight, 0, "Should have positive height")
// Should render without issues
XCTAssertGreaterThan(display!.subDisplays.count, 0, "Should have content")
}
func testDynamicLineHeight_RadicalsWithIndicesGetSpace() throws {
// Test that radicals (especially with degrees like cube roots) get adequate spacing
let latex = "a+\\sqrt[3]{x}+b+\\sqrt{y}+c"
let mathList = MTMathListBuilder.build(fromString: latex)
XCTAssertNotNil(mathList, "Should parse LaTeX")
// Force line breaks
let maxWidth: CGFloat = 70
let display = MTTypesetter.createLineForMathList(mathList, font: self.font, style: .display, maxWidth: maxWidth)
XCTAssertNotNil(display)
// Should render successfully
XCTAssertGreaterThan(display!.subDisplays.count, 0, "Should have content")
// Verify reasonable spacing
let yPositions = Set(display!.subDisplays.map { $0.position.y }).sorted(by: >)
if yPositions.count > 1 {
for i in 1..<yPositions.count {
let spacing = yPositions[i-1] - yPositions[i]
XCTAssertGreaterThanOrEqual(spacing, self.font.fontSize * 0.2,
"Should have minimum spacing")
}
}
}
}