Clean slate
This commit is contained in:
@@ -1,47 +0,0 @@
|
||||
//
|
||||
// ConcurrencyThreadsafeTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Peter Tang on 26/9/2023.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SwiftUIMath
|
||||
|
||||
final class ConcurrencyThreadsafeTests: XCTestCase {
|
||||
|
||||
private let executionQueue = DispatchQueue(label: "com.swiftmath.concurrencytests", attributes: .concurrent)
|
||||
private let executionGroup = DispatchGroup()
|
||||
|
||||
let totalCases = 20
|
||||
var testCount = 0
|
||||
|
||||
func testSwiftMathConcurrentScript() throws {
|
||||
for caseNumber in 0 ..< totalCases {
|
||||
helperConcurrency(caseNumber, in: executionGroup, on: executionQueue) {
|
||||
let result1 = getInterElementSpaces()
|
||||
let result2 = MTMathAtomFactory.delimValueToName
|
||||
let result3 = MTMathAtomFactory.accentValueToName
|
||||
let result4 = MTMathAtomFactory.textToLatexSymbolName
|
||||
XCTAssertNotNil(result1)
|
||||
XCTAssertNotNil(result2)
|
||||
XCTAssertNotNil(result3)
|
||||
XCTAssertNotNil(result4)
|
||||
}
|
||||
}
|
||||
// executionGroup.notify(queue: .main) { [weak self] in
|
||||
// // print("All test cases completed: \(self?.testCount ?? 0)")
|
||||
// }
|
||||
executionGroup.wait()
|
||||
}
|
||||
func helperConcurrency(_ count: Int, in group: DispatchGroup, on queue: DispatchQueue, _ testClosure: @escaping () -> (Void)) {
|
||||
let workitem = DispatchWorkItem {
|
||||
testClosure()
|
||||
}
|
||||
workitem.notify(queue: .main) { [weak self] in
|
||||
self?.testCount += 1
|
||||
}
|
||||
queue.async(group: group, execute: workitem)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
//
|
||||
// MTFontMathTableV2Tests.swift
|
||||
//
|
||||
//
|
||||
// Created by Peter Tang on 15/9/2023.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SwiftUIMath
|
||||
|
||||
final class MTFontMathTableV2Tests: XCTestCase {
|
||||
func testMTFontV2Script() throws {
|
||||
let size = CGFloat(Int.random(in: 20 ... 40))
|
||||
MathFont.allCases.forEach {
|
||||
let mTable = $0.mtfont(size: size).mathTable
|
||||
XCTAssertNotNil(mTable)
|
||||
}
|
||||
}
|
||||
private let executionQueue = DispatchQueue(label: "com.swiftmath.mathbundle", attributes: .concurrent)
|
||||
private let executionGroup = DispatchGroup()
|
||||
let totalCases = 1000
|
||||
var testCount = 0
|
||||
func testConcurrentThreadsafeScript() throws {
|
||||
testCount = 0
|
||||
var mathFont: MathFont { .allCases.randomElement()! }
|
||||
var size: CGFloat { CGFloat.random(in: 20 ... 40) }
|
||||
let mtfonts = Array( 0 ..< 10 ).map { _ in mathFont.mtfont(size: size) }
|
||||
for caseNumber in 0 ..< totalCases {
|
||||
helperConcurrentMTFontMathTableV2(caseNumber, mtfont: mtfonts.randomElement()!, in: executionGroup, on: executionQueue)
|
||||
}
|
||||
executionGroup.notify(queue: .main) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
XCTAssertEqual(self.testCount, totalCases)
|
||||
}
|
||||
executionGroup.wait()
|
||||
}
|
||||
func helperConcurrentMTFontMathTableV2(_ count: Int, mtfont: MTFontV2, in group: DispatchGroup, on queue: DispatchQueue) {
|
||||
let workitem = DispatchWorkItem {
|
||||
let mTable = mtfont.mathTable
|
||||
XCTAssertNotNil(mTable)
|
||||
}
|
||||
workitem.notify(queue: .main) { [weak self] in
|
||||
self?.testCount += 1
|
||||
}
|
||||
queue.async(group: group, execute: workitem)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
//
|
||||
// MTFontV2Tests.swift
|
||||
//
|
||||
//
|
||||
// Created by Peter Tang on 15/9/2023.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SwiftUIMath
|
||||
|
||||
final class MTFontV2Tests: XCTestCase {
|
||||
func testMTFontV2Script() throws {
|
||||
let size = CGFloat(Int.random(in: 20 ... 40))
|
||||
MathFont.allCases.forEach {
|
||||
let mtfont = $0.mtfont(size: size)
|
||||
let mTable = mtfont.mathTable?._mathTable
|
||||
XCTAssertNotNil(mtfont)
|
||||
XCTAssertNotNil(mTable)
|
||||
}
|
||||
}
|
||||
private let executionQueue = DispatchQueue(label: "com.swiftmath.mathbundle", attributes: .concurrent)
|
||||
private let executionGroup = DispatchGroup()
|
||||
let totalCases = 1000
|
||||
var testCount = 0
|
||||
func testConcurrentThreadsafeScript() throws {
|
||||
testCount = 0
|
||||
var mathFont: MathFont { .allCases.randomElement()! }
|
||||
for caseNumber in 0 ..< totalCases {
|
||||
helperConcurrentMTFontV2(caseNumber, mathFont: mathFont, in: executionGroup, on: executionQueue)
|
||||
}
|
||||
executionGroup.notify(queue: .main) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
XCTAssertEqual(self.testCount, totalCases)
|
||||
}
|
||||
executionGroup.wait()
|
||||
}
|
||||
func helperConcurrentMTFontV2(_ count: Int, mathFont: MathFont, in group: DispatchGroup, on queue: DispatchQueue) {
|
||||
let size = CGFloat.random(in: 20 ... 40)
|
||||
let workitem = DispatchWorkItem {
|
||||
let fontV2 = mathFont.mtfont(size: size)
|
||||
XCTAssertNotNil(fontV2)
|
||||
let (cgfont, ctfont) = (fontV2.defaultCGFont, fontV2.ctFont)
|
||||
XCTAssertNotNil(cgfont)
|
||||
XCTAssertNotNil(ctfont)
|
||||
}
|
||||
workitem.notify(queue: .main) { [weak self] in
|
||||
// print("\(Thread.isMainThread ? "main" : "global") completed .....")
|
||||
let fontV2 = mathFont.mtfont(size: size)
|
||||
XCTAssertNotNil(fontV2)
|
||||
let (cgfont, ctfont) = (fontV2.defaultCGFont, fontV2.ctFont)
|
||||
XCTAssertNotNil(cgfont)
|
||||
XCTAssertNotNil(ctfont)
|
||||
let mTable = mathFont.rawMathTable()
|
||||
XCTAssertNotNil(mTable)
|
||||
self?.testCount += 1
|
||||
}
|
||||
queue.async(group: group, execute: workitem)
|
||||
}
|
||||
func testConcurrentThreadsafeMathTableLockScript() throws {
|
||||
testCount = 0
|
||||
var mathFont: MathFont { .allCases.randomElement()! }
|
||||
var size: CGFloat { CGFloat.random(in: 20 ... 40) }
|
||||
let mtfonts = Array( 0 ..< 5 ).map { _ in mathFont.mtfont(size: size) }
|
||||
for caseNumber in 0 ..< totalCases {
|
||||
helperConcurrentMTFontV2MathTableLock(caseNumber, mtfont: mtfonts.randomElement()!, in: executionGroup, on: executionQueue)
|
||||
}
|
||||
executionGroup.notify(queue: .main) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
XCTAssertEqual(self.testCount, totalCases)
|
||||
}
|
||||
executionGroup.wait()
|
||||
}
|
||||
func helperConcurrentMTFontV2MathTableLock(_ count: Int, mtfont: MTFontV2, in group: DispatchGroup, on queue: DispatchQueue) {
|
||||
let workitem = DispatchWorkItem {
|
||||
let mathTable = mtfont.mathTable as? MTFontMathTableV2
|
||||
// each mathTable is initialized once per mtfont with a NSLock.
|
||||
// this is even when mathTable is accessed via different threads.
|
||||
XCTAssertNotNil(mathTable)
|
||||
}
|
||||
workitem.notify(queue: .main) { [weak self] in
|
||||
// print("\(Thread.isMainThread ? "main" : "global") completed .....")
|
||||
self?.testCount += 1
|
||||
}
|
||||
queue.async(group: group, execute: workitem)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,624 +0,0 @@
|
||||
import XCTest
|
||||
@testable import SwiftUIMath
|
||||
|
||||
//
|
||||
// MathRenderSwiftTests.swift
|
||||
// MathRenderSwiftTests
|
||||
//
|
||||
// Created by Mike Griebling on 2023-01-02.
|
||||
//
|
||||
|
||||
final class MTMathListTests: XCTestCase {
|
||||
|
||||
func testSubScript() throws {
|
||||
let str = "-52x^{13+y}_{15-} + (-12.3 *)\\frac{-12}{15.2}"
|
||||
let list = MTMathListBuilder.build(fromString: str)!
|
||||
let finalized = list.finalized
|
||||
try self.checkListContents(finalized)
|
||||
// refinalizing a finalized list should not cause any more changes
|
||||
try self.checkListContents(finalized.finalized)
|
||||
}
|
||||
|
||||
func checkListContents(_ finalized:MTMathList) throws {
|
||||
// check
|
||||
XCTAssertEqual((finalized.atoms.count), 10, "Num atoms");
|
||||
var atom = finalized.atoms[0];
|
||||
XCTAssertEqual(atom.type, .unaryOperator, "Atom 0");
|
||||
XCTAssertEqual(atom.nucleus, "−", "Atom 0 value");
|
||||
XCTAssertTrue(NSEqualRanges(atom.indexRange, NSMakeRange(0, 1)), "Range");
|
||||
atom = finalized.atoms[1];
|
||||
XCTAssertEqual(atom.type, .number, "Atom 1");
|
||||
XCTAssertEqual(atom.nucleus, "52", "Atom 1 value");
|
||||
XCTAssertTrue(NSEqualRanges(atom.indexRange, NSMakeRange(1, 2)), "Range");
|
||||
atom = finalized.atoms[2];
|
||||
XCTAssertEqual(atom.type, .variable, "Atom 2");
|
||||
XCTAssertEqual(atom.nucleus, "x", "Atom 2 value");
|
||||
XCTAssertTrue(NSEqualRanges(atom.indexRange, NSMakeRange(3, 1)), "Range");
|
||||
|
||||
let superScr = atom.superScript!
|
||||
XCTAssertEqual((superScr.atoms.count), 3, "Super script");
|
||||
atom = superScr.atoms[0];
|
||||
XCTAssertEqual(atom.type, .number, "Super Atom 0");
|
||||
XCTAssertEqual(atom.nucleus, "13", "Super Atom 0 value");
|
||||
XCTAssertTrue(NSEqualRanges(atom.indexRange, NSMakeRange(0, 2)), "Range");
|
||||
atom = superScr.atoms[1];
|
||||
XCTAssertEqual(atom.type, .binaryOperator, "Super Atom 1");
|
||||
XCTAssertEqual(atom.nucleus, "+", "Super Atom 1 value");
|
||||
XCTAssertTrue(NSEqualRanges(atom.indexRange, NSMakeRange(2, 1)), "Range");
|
||||
atom = superScr.atoms[2];
|
||||
XCTAssertEqual(atom.type, .variable, "Super Atom 2");
|
||||
XCTAssertEqual(atom.nucleus, "y", "Super Atom 2 value");
|
||||
XCTAssertTrue(NSEqualRanges(atom.indexRange, NSMakeRange(3, 1)), "Range");
|
||||
|
||||
atom = finalized.atoms[2];
|
||||
let subScr = atom.subScript!
|
||||
XCTAssertEqual((subScr.atoms.count), 2, "Sub script");
|
||||
atom = subScr.atoms[0];
|
||||
XCTAssertEqual(atom.type, .number, "Sub Atom 0");
|
||||
XCTAssertEqual(atom.nucleus, "15", "Sub Atom 0 value");
|
||||
XCTAssertTrue(NSEqualRanges(atom.indexRange, NSMakeRange(0, 2)), "Range");
|
||||
atom = subScr.atoms[1];
|
||||
XCTAssertEqual(atom.type, .unaryOperator, "Sub Atom 1");
|
||||
XCTAssertEqual(atom.nucleus, "−", "Sub Atom 1 value");
|
||||
XCTAssertTrue(NSEqualRanges(atom.indexRange, NSMakeRange(2, 1)), "Range");
|
||||
|
||||
atom = finalized.atoms[3];
|
||||
XCTAssertEqual(atom.type, .binaryOperator, "Atom 3");
|
||||
XCTAssertEqual(atom.nucleus, "+", "Atom 3 value");
|
||||
XCTAssertTrue(NSEqualRanges(atom.indexRange, NSMakeRange(4, 1)), "Range");
|
||||
atom = finalized.atoms[4];
|
||||
XCTAssertEqual(atom.type, .open, "Atom 4");
|
||||
XCTAssertEqual(atom.nucleus, "(", "Atom 4 value");
|
||||
XCTAssertTrue(NSEqualRanges(atom.indexRange, NSMakeRange(5, 1)), "Range");
|
||||
atom = finalized.atoms[5];
|
||||
XCTAssertEqual(atom.type, .unaryOperator, "Atom 5");
|
||||
XCTAssertEqual(atom.nucleus, "−", "Atom 5 value");
|
||||
XCTAssertTrue(NSEqualRanges(atom.indexRange, NSMakeRange(6, 1)), "Range");
|
||||
atom = finalized.atoms[6];
|
||||
XCTAssertEqual(atom.type, .number, "Atom 6");
|
||||
XCTAssertEqual(atom.nucleus, "12.3", "Atom 6 value");
|
||||
XCTAssertTrue(NSEqualRanges(atom.indexRange, NSMakeRange(7, 4)), "Range");
|
||||
atom = finalized.atoms[7];
|
||||
XCTAssertEqual(atom.type, .unaryOperator, "Atom 7");
|
||||
XCTAssertEqual(atom.nucleus, "*", "Atom 7 value");
|
||||
XCTAssertTrue(NSEqualRanges(atom.indexRange, NSMakeRange(11, 1)), "Range");
|
||||
atom = finalized.atoms[8];
|
||||
XCTAssertEqual(atom.type, .close, "Atom 8");
|
||||
XCTAssertEqual(atom.nucleus, ")", "Atom 8 value");
|
||||
XCTAssertTrue(NSEqualRanges(atom.indexRange, NSMakeRange(12, 1)), "Range");
|
||||
|
||||
let frac = finalized.atoms[9] as! MTFraction
|
||||
XCTAssertEqual(frac.type, .fraction, "Atom 9");
|
||||
XCTAssertEqual(frac.nucleus, "", "Atom 9 value");
|
||||
XCTAssertTrue(NSEqualRanges(frac.indexRange, NSMakeRange(13, 1)), "Range");
|
||||
|
||||
let numer = frac.numerator!
|
||||
XCTAssertNotNil(numer, "Numerator");
|
||||
XCTAssertEqual((numer.atoms.count), 2, "Numer script");
|
||||
atom = numer.atoms[0];
|
||||
XCTAssertEqual(atom.type, .unaryOperator, "Numer Atom 0");
|
||||
XCTAssertEqual(atom.nucleus, "−", "Numer Atom 0 value");
|
||||
XCTAssertTrue(NSEqualRanges(atom.indexRange, NSMakeRange(0, 1)), "Range");
|
||||
atom = numer.atoms[1];
|
||||
XCTAssertEqual(atom.type, .number, "Numer Atom 1");
|
||||
XCTAssertEqual(atom.nucleus, "12", "Numer Atom 1 value");
|
||||
XCTAssertTrue(NSEqualRanges(atom.indexRange, NSMakeRange(1, 2)), "Range");
|
||||
|
||||
|
||||
let denom = frac.denominator!
|
||||
XCTAssertNotNil(denom, "Denominator");
|
||||
XCTAssertEqual((denom.atoms.count), 1, "Denom script");
|
||||
atom = denom.atoms[0];
|
||||
XCTAssertEqual(atom.type, .number, "Denom Atom 0");
|
||||
XCTAssertEqual(atom.nucleus, "15.2", "Denom Atom 0 value");
|
||||
XCTAssertTrue(NSEqualRanges(atom.indexRange, NSMakeRange(0, 4)), "Range");
|
||||
|
||||
}
|
||||
|
||||
func testAdd() throws {
|
||||
let list = MTMathList()
|
||||
XCTAssertEqual(list.atoms.count, 0);
|
||||
let atom = MTMathAtomFactory.placeholder()
|
||||
list.add(atom)
|
||||
XCTAssertEqual(list.atoms.count, 1);
|
||||
XCTAssertEqual(list.atoms[0], atom);
|
||||
let atom2 = MTMathAtomFactory.placeholder()
|
||||
list.add(atom2);
|
||||
XCTAssertEqual(list.atoms.count, 2);
|
||||
XCTAssertEqual(list.atoms[0], atom);
|
||||
XCTAssertEqual(list.atoms[1], atom2);
|
||||
}
|
||||
|
||||
private var options : XCTExpectedFailure.Options {
|
||||
let op = XCTExpectedFailure.Options()
|
||||
op.isStrict = true
|
||||
return op
|
||||
}
|
||||
|
||||
func testAddErrors() throws {
|
||||
let list = MTMathList()
|
||||
var atom : MTMathAtom? = nil
|
||||
list.add(atom)
|
||||
atom = MTMathAtom(type: .boundary, value: "")
|
||||
XCTExpectFailure("Test adding an illegal atom", options:options) {
|
||||
XCTAssertThrowsError(list.add(atom))
|
||||
}
|
||||
}
|
||||
|
||||
func testInsert() throws {
|
||||
let list = MTMathList()
|
||||
XCTAssertEqual(list.atoms.count, 0);
|
||||
let atom = MTMathAtomFactory.placeholder()
|
||||
list.insert(atom, at: 0)
|
||||
XCTAssertEqual(list.atoms.count, 1);
|
||||
XCTAssertEqual(list.atoms[0], atom);
|
||||
let atom2 = MTMathAtomFactory.placeholder()
|
||||
list.insert(atom2, at: 0)
|
||||
XCTAssertEqual(list.atoms.count, 2);
|
||||
XCTAssertEqual(list.atoms[0], atom2);
|
||||
XCTAssertEqual(list.atoms[1], atom);
|
||||
let atom3 = MTMathAtomFactory.placeholder()
|
||||
list.insert(atom3, at: 2)
|
||||
XCTAssertEqual(list.atoms.count, 3);
|
||||
XCTAssertEqual(list.atoms[0], atom2);
|
||||
XCTAssertEqual(list.atoms[1], atom);
|
||||
XCTAssertEqual(list.atoms[2], atom3);
|
||||
}
|
||||
|
||||
func testInsertErrors() throws {
|
||||
let list = MTMathList()
|
||||
var atom : MTMathAtom? = nil
|
||||
list.insert(atom, at: 0)
|
||||
atom = MTMathAtom(type: .boundary, value:"")
|
||||
XCTExpectFailure("Test adding an illegal atom", options:options) {
|
||||
XCTAssertThrowsError(list.insert(atom, at:0))
|
||||
}
|
||||
atom = MTMathAtomFactory.placeholder()
|
||||
list.insert(atom, at:1)
|
||||
}
|
||||
|
||||
func testAppend() throws {
|
||||
let list1 = MTMathList()
|
||||
let atom = MTMathAtomFactory.placeholder()
|
||||
let atom2 = MTMathAtomFactory.placeholder()
|
||||
let atom3 = MTMathAtomFactory.placeholder()
|
||||
list1.add(atom)
|
||||
list1.add(atom2)
|
||||
list1.add(atom3)
|
||||
|
||||
let list2 = MTMathList()
|
||||
let atom5 = MTMathAtomFactory.times()
|
||||
let atom6 = MTMathAtomFactory.divide()
|
||||
list2.add(atom5)
|
||||
list2.add(atom6)
|
||||
|
||||
XCTAssertEqual(list1.atoms.count, 3);
|
||||
XCTAssertEqual(list2.atoms.count, 2);
|
||||
|
||||
list1.append(list2)
|
||||
XCTAssertEqual(list1.atoms.count, 5);
|
||||
XCTAssertEqual(list1.atoms[3], atom5);
|
||||
XCTAssertEqual(list1.atoms[4], atom6);
|
||||
}
|
||||
|
||||
func testRemoveLast() throws {
|
||||
let list = MTMathList()
|
||||
let atom = MTMathAtomFactory.placeholder()
|
||||
list.add(atom)
|
||||
XCTAssertEqual(list.atoms.count, 1);
|
||||
list.removeLastAtom()
|
||||
XCTAssertEqual(list.atoms.count, 0);
|
||||
// Removing from empty list.
|
||||
list.removeLastAtom()
|
||||
XCTAssertEqual(list.atoms.count, 0);
|
||||
let atom2 = MTMathAtomFactory.placeholder()
|
||||
list.add(atom)
|
||||
list.add(atom2);
|
||||
XCTAssertEqual(list.atoms.count, 2);
|
||||
list.removeLastAtom()
|
||||
XCTAssertEqual(list.atoms.count, 1);
|
||||
XCTAssertEqual(list.atoms[0], atom);
|
||||
}
|
||||
|
||||
func testRemoveAtomAtIndex() throws {
|
||||
let list = MTMathList()
|
||||
let atom = MTMathAtomFactory.placeholder()
|
||||
let atom2 = MTMathAtomFactory.placeholder()
|
||||
list.add(atom)
|
||||
list.add(atom2);
|
||||
XCTAssertEqual(list.atoms.count, 2);
|
||||
list.removeAtom(at:0)
|
||||
XCTAssertEqual(list.atoms.count, 1);
|
||||
XCTAssertEqual(list.atoms[0], atom2);
|
||||
|
||||
// Index out of range
|
||||
XCTExpectFailure("Test removing an out-of-index cell", options: options) {
|
||||
XCTAssertThrowsError(list.removeAtom(at:2))
|
||||
}
|
||||
}
|
||||
|
||||
func testRemoveAtomsInRange() throws {
|
||||
let list = MTMathList()
|
||||
let atom = MTMathAtomFactory.placeholder()
|
||||
let atom2 = MTMathAtomFactory.placeholder()
|
||||
let atom3 = MTMathAtomFactory.placeholder()
|
||||
list.add(atom)
|
||||
list.add(atom2);
|
||||
list.add(atom3)
|
||||
XCTAssertEqual(list.atoms.count, 3)
|
||||
list.removeAtoms(in: 1...2)
|
||||
XCTAssertEqual(list.atoms.count, 1);
|
||||
XCTAssertEqual(list.atoms[0], atom);
|
||||
|
||||
// Index out of range
|
||||
XCTExpectFailure("Test removing an out-of-bounds range", options: options) {
|
||||
XCTAssertThrowsError(list.removeAtoms(in: 1...3))
|
||||
}
|
||||
}
|
||||
|
||||
// func MTAssertEqual(test, expression1, expression2, ...) \
|
||||
// _XCTPrimitiveAssertEqual(test, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)
|
||||
//
|
||||
// func MTAssertNotEqual(test, expression1, expression2, ...) \
|
||||
// _XCTPrimitiveAssertNotEqual(test, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)
|
||||
|
||||
func checkAtomCopy(_ copy:MTMathAtom?, original:MTMathAtom?, forTest test:String) throws {
|
||||
guard let copy = copy, let original = original else { return }
|
||||
XCTAssertEqual(copy.type, original.type, test)
|
||||
XCTAssertEqual(copy.nucleus, original.nucleus, test)
|
||||
// Should be different objects with the same content
|
||||
XCTAssertNotEqual(copy, original, test)
|
||||
}
|
||||
|
||||
func checkListCopy(_ copy:MTMathList?, original:MTMathList?, forTest test:String) throws {
|
||||
guard let copy = copy, let original = original else { return }
|
||||
XCTAssertEqual(copy.atoms.count, original.atoms.count, test)
|
||||
for (i, copyAtom) in copy.atoms.enumerated() {
|
||||
let origAtom = original.atoms[i];
|
||||
try self.checkAtomCopy(copyAtom, original:origAtom, forTest:test)
|
||||
}
|
||||
}
|
||||
|
||||
func testCopy() throws {
|
||||
let list = MTMathList()
|
||||
let atom = MTMathAtomFactory.placeholder()
|
||||
let atom2 = MTMathAtomFactory.times()
|
||||
let atom3 = MTMathAtomFactory.divide()
|
||||
list.add(atom)
|
||||
list.add(atom2);
|
||||
list.add(atom3)
|
||||
|
||||
let list2 = MTMathList(list)
|
||||
try checkListCopy(list2, original:list, forTest:self.description)
|
||||
}
|
||||
|
||||
func testAtomInit() throws {
|
||||
var atom = MTMathAtom(type: .open, value: "(")
|
||||
XCTAssertEqual(atom.nucleus, "(")
|
||||
XCTAssertEqual(atom.type, .open)
|
||||
|
||||
atom = MTMathAtom(type: .radical, value:"(")
|
||||
XCTAssertEqual(atom.nucleus, "");
|
||||
XCTAssertEqual(atom.type, .radical);
|
||||
}
|
||||
|
||||
func testAtomScripts() throws {
|
||||
var atom = MTMathAtom(type: .open, value:"(")
|
||||
XCTAssertTrue(atom.isScriptAllowed())
|
||||
atom.subScript = MTMathList()
|
||||
XCTAssertNotNil(atom.subScript);
|
||||
atom.superScript = MTMathList()
|
||||
XCTAssertNotNil(atom.superScript);
|
||||
|
||||
atom = MTMathAtom(type: .boundary, value:"(")
|
||||
XCTAssertFalse(atom.isScriptAllowed());
|
||||
// Can set to nil
|
||||
atom.subScript = nil;
|
||||
XCTAssertNil(atom.subScript);
|
||||
atom.superScript = nil;
|
||||
XCTAssertNil(atom.superScript);
|
||||
// Can't set to value
|
||||
let list = MTMathList()
|
||||
|
||||
XCTExpectFailure("No sub/super-script on boundary atoms", options: options) {
|
||||
XCTAssertThrowsError(atom.subScript = list)
|
||||
XCTAssertThrowsError(atom.superScript = list)
|
||||
}
|
||||
}
|
||||
|
||||
func testAtomCopy() throws {
|
||||
let list = MTMathList()
|
||||
let atom1 = MTMathAtomFactory.placeholder()
|
||||
let atom2 = MTMathAtomFactory.times()
|
||||
let atom3 = MTMathAtomFactory.divide()
|
||||
list.add(atom1)
|
||||
list.add(atom2);
|
||||
list.add(atom3)
|
||||
|
||||
let list2 = MTMathList()
|
||||
list2.add(atom3)
|
||||
list2.add(atom2)
|
||||
|
||||
let atom = MTMathAtom(type: .open, value:"(")
|
||||
atom.subScript = list;
|
||||
atom.superScript = list2;
|
||||
let copy : MTMathAtom = atom.copy()
|
||||
|
||||
try checkAtomCopy(copy, original:atom, forTest:self.description)
|
||||
try checkListCopy(copy.superScript, original:atom.superScript, forTest:self.description)
|
||||
try checkListCopy(copy.subScript, original:atom.subScript, forTest:self.description)
|
||||
}
|
||||
|
||||
func testCopyFraction() throws {
|
||||
let list = MTMathList()
|
||||
let atom = MTMathAtomFactory.placeholder()
|
||||
let atom2 = MTMathAtomFactory.times()
|
||||
let atom3 = MTMathAtomFactory.divide()
|
||||
list.add(atom)
|
||||
list.add(atom2);
|
||||
list.add(atom3)
|
||||
|
||||
let list2 = MTMathList()
|
||||
list2.add(atom3)
|
||||
list2.add(atom2)
|
||||
|
||||
let frac = MTFraction(hasRule: false)
|
||||
XCTAssertEqual(frac.type, .fraction);
|
||||
frac.numerator = list;
|
||||
frac.denominator = list2;
|
||||
frac.leftDelimiter = "a";
|
||||
frac.rightDelimiter = "b";
|
||||
|
||||
let copy = MTFraction(frac)
|
||||
try checkAtomCopy(copy, original:frac, forTest:self.description)
|
||||
try checkListCopy(copy.numerator, original:frac.numerator, forTest:self.description)
|
||||
try checkListCopy(copy.denominator, original:frac.denominator, forTest:self.description)
|
||||
XCTAssertFalse(copy.hasRule)
|
||||
XCTAssertEqual(copy.leftDelimiter, "a");
|
||||
XCTAssertEqual(copy.rightDelimiter, "b");
|
||||
}
|
||||
|
||||
func testCopyRadical() throws {
|
||||
let list = MTMathList()
|
||||
let atom = MTMathAtomFactory.placeholder()
|
||||
let atom2 = MTMathAtomFactory.times()
|
||||
let atom3 = MTMathAtomFactory.divide()
|
||||
list.add(atom)
|
||||
list.add(atom2);
|
||||
list.add(atom3)
|
||||
|
||||
let list2 = MTMathList()
|
||||
list2.add(atom3)
|
||||
list2.add(atom2)
|
||||
|
||||
let rad = MTRadical()
|
||||
XCTAssertEqual(rad.type, .radical)
|
||||
rad.radicand = list;
|
||||
rad.degree = list2;
|
||||
|
||||
let copy = MTRadical(rad)
|
||||
try checkAtomCopy(copy, original:rad, forTest:self.description)
|
||||
try checkListCopy(copy.radicand, original:rad.radicand ,forTest:self.description)
|
||||
try checkListCopy(copy.degree, original:rad.degree, forTest:self.description)
|
||||
}
|
||||
|
||||
func testCopyLargeOperator() throws {
|
||||
let lg = MTLargeOperator(value: "lim", limits:true)
|
||||
XCTAssertEqual(lg.type, .largeOperator);
|
||||
XCTAssertTrue(lg.limits);
|
||||
|
||||
let copy = MTLargeOperator(lg)
|
||||
try checkAtomCopy(copy, original:lg, forTest:self.description)
|
||||
XCTAssertEqual(copy.limits, lg.limits);
|
||||
}
|
||||
|
||||
func testCopyInner() throws {
|
||||
let list = MTMathList()
|
||||
let atom = MTMathAtomFactory.placeholder()
|
||||
let atom2 = MTMathAtomFactory.times()
|
||||
let atom3 = MTMathAtomFactory.divide()
|
||||
list.add(atom)
|
||||
list.add(atom2);
|
||||
list.add(atom3)
|
||||
|
||||
let inner = MTInner()
|
||||
inner.innerList = list;
|
||||
inner.leftBoundary = MTMathAtom(type: .boundary, value: "(")
|
||||
inner.rightBoundary = MTMathAtom(type: .boundary, value:")")
|
||||
XCTAssertEqual(inner.type, .inner);
|
||||
|
||||
let copy = MTInner(inner)
|
||||
try checkAtomCopy(copy, original:inner, forTest:self.description)
|
||||
try checkListCopy(copy.innerList, original:inner.innerList, forTest:self.description)
|
||||
try checkAtomCopy(copy.leftBoundary!, original:inner.leftBoundary, forTest:self.description)
|
||||
try checkAtomCopy(copy.rightBoundary, original:inner.rightBoundary, forTest:self.description)
|
||||
}
|
||||
|
||||
func testSetInnerBoundary() throws {
|
||||
let inner = MTInner()
|
||||
|
||||
// Can set non-nil
|
||||
inner.leftBoundary = MTMathAtom(type: .boundary, value:"(")
|
||||
inner.rightBoundary = MTMathAtom(type: .boundary, value:")")
|
||||
XCTAssertNotNil(inner.leftBoundary);
|
||||
XCTAssertNotNil(inner.rightBoundary);
|
||||
// Can set nil
|
||||
inner.leftBoundary = nil;
|
||||
inner.rightBoundary = nil;
|
||||
XCTAssertNil(inner.leftBoundary);
|
||||
XCTAssertNil(inner.rightBoundary);
|
||||
// Can't set non boundary
|
||||
let atom = MTMathAtomFactory.placeholder()
|
||||
XCTExpectFailure("Setting illegal boundary atoms", options: options) {
|
||||
XCTAssertThrowsError(inner.leftBoundary = atom);
|
||||
XCTAssertThrowsError(inner.rightBoundary = atom);
|
||||
}
|
||||
}
|
||||
|
||||
func testCopyOverline() throws {
|
||||
let list = MTMathList()
|
||||
let atom = MTMathAtomFactory.placeholder()
|
||||
let atom2 = MTMathAtomFactory.times()
|
||||
let atom3 = MTMathAtomFactory.divide()
|
||||
list.add(atom)
|
||||
list.add(atom2);
|
||||
list.add(atom3)
|
||||
|
||||
let over = MTOverLine()
|
||||
XCTAssertEqual(over.type, .overline);
|
||||
over.innerList = list;
|
||||
|
||||
let copy = MTOverLine(over)
|
||||
try checkAtomCopy(copy, original:over, forTest:self.description)
|
||||
try checkListCopy(copy.innerList, original:over.innerList, forTest:self.description)
|
||||
}
|
||||
|
||||
func testCopyUnderline() throws {
|
||||
let list = MTMathList()
|
||||
let atom = MTMathAtomFactory.placeholder()
|
||||
let atom2 = MTMathAtomFactory.times()
|
||||
let atom3 = MTMathAtomFactory.divide()
|
||||
list.add(atom)
|
||||
list.add(atom2);
|
||||
list.add(atom3)
|
||||
|
||||
let under = MTUnderLine()
|
||||
XCTAssertEqual(under.type, .underline);
|
||||
under.innerList = list;
|
||||
|
||||
let copy = MTUnderLine(under)
|
||||
try checkAtomCopy(copy, original:under, forTest:self.description)
|
||||
try checkListCopy(copy.innerList, original:under.innerList, forTest:self.description)
|
||||
}
|
||||
|
||||
func testCopyAcccent() throws {
|
||||
let list = MTMathList()
|
||||
let atom = MTMathAtomFactory.placeholder()
|
||||
let atom2 = MTMathAtomFactory.times()
|
||||
let atom3 = MTMathAtomFactory.divide()
|
||||
list.add(atom)
|
||||
list.add(atom2);
|
||||
list.add(atom3)
|
||||
|
||||
let accent = MTAccent(value: "^")
|
||||
XCTAssertEqual(accent.type, .accent);
|
||||
accent.innerList = list;
|
||||
|
||||
let copy = MTAccent(accent)
|
||||
try checkAtomCopy(copy, original:accent, forTest:self.description)
|
||||
try checkListCopy(copy.innerList ,original:accent.innerList, forTest:self.description)
|
||||
}
|
||||
|
||||
func testCopySpace() throws {
|
||||
let space = MTMathSpace(space: 3)
|
||||
XCTAssertEqual(space.type, .space);
|
||||
|
||||
let copy = MTMathSpace(space)
|
||||
try checkAtomCopy(copy, original:space, forTest:self.description)
|
||||
XCTAssertEqual(space.space, copy.space);
|
||||
}
|
||||
|
||||
func testCopyStyle() throws {
|
||||
let style = MTMathStyle(style: .script)
|
||||
XCTAssertEqual(style.type, .style);
|
||||
|
||||
let copy = MTMathStyle(style)
|
||||
try checkAtomCopy(copy, original:style, forTest:self.description)
|
||||
XCTAssertEqual(style.style, copy.style);
|
||||
}
|
||||
|
||||
func testCreateMathTable() throws {
|
||||
let table = MTMathTable()
|
||||
XCTAssertEqual(table.type, .table);
|
||||
|
||||
let list = MTMathList()
|
||||
let atom = MTMathAtomFactory.placeholder()
|
||||
let atom2 = MTMathAtomFactory.times()
|
||||
let atom3 = MTMathAtomFactory.divide()
|
||||
list.add(atom)
|
||||
list.add(atom2);
|
||||
list.add(atom3)
|
||||
|
||||
let list2 = MTMathList()
|
||||
list2.add(atom3)
|
||||
list2.add(atom2)
|
||||
|
||||
table.set(cell: list, forRow:3, column:2)
|
||||
table.set(cell: list2, forRow:1, column:0)
|
||||
|
||||
table.set(alignment: .left, forColumn: 2)
|
||||
table.set(alignment: .right, forColumn:1)
|
||||
|
||||
// Verify that everything is created correctly
|
||||
XCTAssertEqual(table.cells.count, 4); // 4 rows
|
||||
XCTAssertNotNil(table.cells[0]);
|
||||
XCTAssertEqual(table.cells[0].count, 0); // 0 elements in row 0
|
||||
XCTAssertEqual(table.cells[1].count, 1); // 1 element in row 1
|
||||
XCTAssertNotNil(table.cells[2]);
|
||||
XCTAssertEqual(table.cells[2].count, 0);
|
||||
XCTAssertEqual(table.cells[3].count, 3);
|
||||
|
||||
// Verify the elements in the rows
|
||||
XCTAssertEqual(table.cells[1][0].atoms.count, 2);
|
||||
XCTAssertEqual(table.cells[1][0], list2);
|
||||
XCTAssertNotNil(table.cells[3][0]);
|
||||
XCTAssertEqual(table.cells[3][0].atoms.count, 0);
|
||||
|
||||
XCTAssertNotNil(table.cells[3][0]);
|
||||
XCTAssertEqual(table.cells[3][0].atoms.count, 0);
|
||||
|
||||
XCTAssertNotNil(table.cells[3][1]);
|
||||
XCTAssertEqual(table.cells[3][1].atoms.count, 0);
|
||||
|
||||
XCTAssertEqual(table.cells[3][2], list);
|
||||
|
||||
XCTAssertEqual(table.numRows, 4);
|
||||
XCTAssertEqual(table.numColumns, 3);
|
||||
|
||||
// Verify the alignments
|
||||
XCTAssertEqual(table.alignments.count, 3);
|
||||
XCTAssertEqual(table.alignments[0], .center);
|
||||
XCTAssertEqual(table.alignments[1], .right);
|
||||
XCTAssertEqual(table.alignments[2], .left);
|
||||
}
|
||||
|
||||
func testCopyMathTable() throws {
|
||||
let table = MTMathTable()
|
||||
XCTAssertEqual(table.type, .table);
|
||||
|
||||
let list = MTMathList()
|
||||
let atom = MTMathAtomFactory.placeholder()
|
||||
let atom2 = MTMathAtomFactory.times()
|
||||
let atom3 = MTMathAtomFactory.divide()
|
||||
list.add(atom)
|
||||
list.add(atom2);
|
||||
list.add(atom3)
|
||||
|
||||
let list2 = MTMathList()
|
||||
list2.add(atom3)
|
||||
list2.add(atom2)
|
||||
|
||||
table.set(cell:list, forRow:0, column:1)
|
||||
table.set(cell:list2, forRow:0, column:2)
|
||||
|
||||
table.set(alignment: .left, forColumn:2)
|
||||
table.set(alignment: .right, forColumn:1)
|
||||
table.interRowAdditionalSpacing = 3;
|
||||
table.interColumnSpacing = 10;
|
||||
|
||||
let copy = MTMathTable(table)
|
||||
try checkAtomCopy(copy, original:table, forTest:self.description)
|
||||
XCTAssertEqual(copy.interColumnSpacing, table.interColumnSpacing);
|
||||
XCTAssertEqual(copy.interRowAdditionalSpacing, table.interRowAdditionalSpacing);
|
||||
XCTAssertEqual(copy.alignments, table.alignments)
|
||||
|
||||
XCTAssertNotEqual(copy.cells, table.cells);
|
||||
XCTAssertNotEqual(copy.cells[0], table.cells[0] );
|
||||
XCTAssertEqual(copy.cells[0].count, table.cells[0].count);
|
||||
XCTAssertEqual(copy.cells[0][0].atoms.count, 0);
|
||||
XCTAssertNotEqual(copy.cells[0][0], table.cells[0][0]);
|
||||
try checkListCopy(copy.cells[0][1], original:list, forTest:self.description)
|
||||
try checkListCopy(copy.cells[0][2], original:list2, forTest:self.description)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,737 +0,0 @@
|
||||
//
|
||||
// MTMathUILabelLineWrappingTests.swift
|
||||
// SwiftMathTests
|
||||
//
|
||||
// Tests for line wrapping functionality in MTMathUILabel
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SwiftUIMath
|
||||
|
||||
class MTMathUILabelLineWrappingTests: XCTestCase {
|
||||
|
||||
func testBasicIntrinsicContentSize() {
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "\\(x + y\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
|
||||
// Debug: check if parsing worked
|
||||
XCTAssertNotNil(label.mathList, "Math list should not be nil")
|
||||
XCTAssertNil(label.error, "Should have no parsing error, got: \(String(describing: label.error))")
|
||||
XCTAssertNotNil(label.font, "Font should not be nil")
|
||||
|
||||
let size = label.intrinsicContentSize
|
||||
|
||||
XCTAssertGreaterThan(size.width, 0, "Width should be greater than 0, got \(size.width)")
|
||||
XCTAssertGreaterThan(size.height, 0, "Height should be greater than 0, got \(size.height)")
|
||||
}
|
||||
|
||||
func testTextModeIntrinsicContentSize() {
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "\\(\\text{Hello World}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
let size = label.intrinsicContentSize
|
||||
|
||||
XCTAssertGreaterThan(size.width, 0, "Width should be greater than 0, got \(size.width)")
|
||||
XCTAssertGreaterThan(size.height, 0, "Height should be greater than 0, got \(size.height)")
|
||||
}
|
||||
|
||||
func testLongTextIntrinsicContentSize() {
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "\\(\\text{Rappelons la conversion : 1 km équivaut à 1000 m.}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
let size = label.intrinsicContentSize
|
||||
|
||||
XCTAssertGreaterThan(size.width, 0, "Width should be greater than 0, got \(size.width)")
|
||||
XCTAssertGreaterThan(size.height, 0, "Height should be greater than 0, got \(size.height)")
|
||||
}
|
||||
|
||||
func testSizeThatFitsWithoutConstraint() {
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "\\(\\text{Hello World}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
|
||||
let size = label.sizeThatFits(CGSize.zero)
|
||||
|
||||
XCTAssertGreaterThan(size.width, 0, "Width should be greater than 0, got \(size.width)")
|
||||
XCTAssertGreaterThan(size.height, 0, "Height should be greater than 0, got \(size.height)")
|
||||
}
|
||||
|
||||
func testSizeThatFitsWithWidthConstraint() {
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "\\(\\text{Rappelons la conversion : 1 km équivaut à 1000 m.}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
// Get unconstrained size first
|
||||
let unconstrainedSize = label.sizeThatFits(CGSize.zero)
|
||||
XCTAssertGreaterThan(unconstrainedSize.width, 0, "Unconstrained width should be > 0")
|
||||
|
||||
// Test with width constraint (use 300 since longest word might be ~237pt)
|
||||
let constrainedSize = label.sizeThatFits(CGSize(width: 300, height: CGFloat.greatestFiniteMagnitude))
|
||||
|
||||
XCTAssertGreaterThan(constrainedSize.width, 0, "Constrained width should be greater than 0, got \(constrainedSize.width)")
|
||||
XCTAssertLessThan(constrainedSize.width, unconstrainedSize.width, "Constrained width (\(constrainedSize.width)) should be less than unconstrained (\(unconstrainedSize.width))")
|
||||
XCTAssertGreaterThan(constrainedSize.height, 0, "Constrained height should be greater than 0, got \(constrainedSize.height)")
|
||||
|
||||
// When constrained, height should increase when text wraps
|
||||
XCTAssertGreaterThan(constrainedSize.height, unconstrainedSize.height,
|
||||
"Constrained height (\(constrainedSize.height)) should be > unconstrained (\(unconstrainedSize.height)) when text wraps")
|
||||
}
|
||||
|
||||
func testPreferredMaxLayoutWidth() {
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "\\(\\text{Rappelons la conversion : 1 km équivaut à 1000 m.}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
// Get unconstrained size
|
||||
let unconstrainedSize = label.intrinsicContentSize
|
||||
|
||||
// Now set preferred max width (use 300 since longest word might be ~237pt)
|
||||
label.preferredMaxLayoutWidth = 300
|
||||
let constrainedSize = label.intrinsicContentSize
|
||||
|
||||
XCTAssertGreaterThan(constrainedSize.width, 0, "Width should be greater than 0, got \(constrainedSize.width)")
|
||||
XCTAssertLessThan(constrainedSize.width, unconstrainedSize.width, "Constrained width (\(constrainedSize.width)) should be < unconstrained (\(unconstrainedSize.width))")
|
||||
XCTAssertGreaterThan(constrainedSize.height, unconstrainedSize.height, "Constrained height (\(constrainedSize.height)) should be > unconstrained (\(unconstrainedSize.height)) due to wrapping")
|
||||
}
|
||||
|
||||
func testWordBoundaryBreaking() {
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "\\(\\text{Word1 Word2 Word3 Word4 Word5}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
|
||||
let size = label.intrinsicContentSize
|
||||
|
||||
XCTAssertGreaterThan(size.width, 0, "Width should be greater than 0, got \(size.width)")
|
||||
XCTAssertGreaterThan(size.height, 0, "Height should be greater than 0, got \(size.height)")
|
||||
|
||||
// Verify it actually uses the layout
|
||||
label.frame = CGRect(origin: .zero, size: size)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
}
|
||||
|
||||
func testEmptyLatex() {
|
||||
let label = MTMathUILabel()
|
||||
label.latex = ""
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
|
||||
let size = label.intrinsicContentSize
|
||||
|
||||
// Empty latex should still return a valid size (might be zero or minimal)
|
||||
XCTAssertGreaterThanOrEqual(size.width, 0, "Width should be >= 0 for empty latex, got \(size.width)")
|
||||
XCTAssertGreaterThanOrEqual(size.height, 0, "Height should be >= 0 for empty latex, got \(size.height)")
|
||||
}
|
||||
|
||||
func testMathAndTextMixed() {
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "\\(\\text{Result: } x^2 + y^2 = z^2\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
let size = label.intrinsicContentSize
|
||||
|
||||
XCTAssertGreaterThan(size.width, 0, "Width should be greater than 0, got \(size.width)")
|
||||
XCTAssertGreaterThan(size.height, 0, "Height should be greater than 0, got \(size.height)")
|
||||
}
|
||||
|
||||
func testDebugSizeThatFitsWithConstraint() {
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "\\(\\text{Word1 Word2 Word3 Word4 Word5}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
let unconstr = label.sizeThatFits(CGSize.zero)
|
||||
let constr = label.sizeThatFits(CGSize(width: 150, height: 999))
|
||||
|
||||
XCTAssertLessThan(constr.width, unconstr.width, "Constrained (\(constr.width)) should be < unconstrained (\(unconstr.width))")
|
||||
XCTAssertGreaterThan(constr.height, unconstr.height, "Constrained height (\(constr.height)) should be > unconstrained (\(unconstr.height))")
|
||||
}
|
||||
|
||||
func testAccentedCharactersWithLineWrapping() {
|
||||
let label = MTMathUILabel()
|
||||
// French text with accented characters: è, é, à
|
||||
label.latex = "\\(\\text{Rappelons la relation entre kilomètres et mètres.}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
// Get unconstrained size
|
||||
let unconstrainedSize = label.intrinsicContentSize
|
||||
|
||||
// Set a width constraint that should cause wrapping
|
||||
label.preferredMaxLayoutWidth = 250
|
||||
let constrainedSize = label.intrinsicContentSize
|
||||
|
||||
// Verify wrapping occurred
|
||||
XCTAssertGreaterThan(constrainedSize.width, 0, "Width should be > 0")
|
||||
XCTAssertLessThan(constrainedSize.width, unconstrainedSize.width, "Constrained width should be < unconstrained")
|
||||
XCTAssertGreaterThan(constrainedSize.height, unconstrainedSize.height, "Height should increase when wrapped")
|
||||
|
||||
// Verify the label can render without errors
|
||||
label.frame = CGRect(origin: .zero, size: constrainedSize)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
XCTAssertNil(label.error, "Should have no rendering error")
|
||||
}
|
||||
|
||||
func testUnicodeWordBreaking_EquivautCase() {
|
||||
// Specific test for the reported issue: "équivaut" should not break at "é"
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "\\(\\text{Rappelons la conversion : 1 km équivaut à 1000 m.}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
// Set the exact width constraint from the bug report
|
||||
label.preferredMaxLayoutWidth = 235
|
||||
let constrainedSize = label.intrinsicContentSize
|
||||
|
||||
// Verify the label can render without errors
|
||||
label.frame = CGRect(origin: .zero, size: constrainedSize)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
XCTAssertNil(label.error, "Should have no rendering error")
|
||||
|
||||
// Verify that the text wrapped (multiple lines)
|
||||
XCTAssertGreaterThan(constrainedSize.height, 20, "Should have wrapped to multiple lines")
|
||||
|
||||
// The critical check: ensure "équivaut" is not broken in the middle
|
||||
// We can't easily check the exact line breaks, but we can verify:
|
||||
// 1. The rendering succeeded without crashes
|
||||
// 2. The display has reasonable dimensions
|
||||
XCTAssertGreaterThan(constrainedSize.width, 100, "Width should be reasonable")
|
||||
XCTAssertLessThan(constrainedSize.width, 250, "Width should respect constraint")
|
||||
}
|
||||
|
||||
func testMixedTextMathNoTruncation() {
|
||||
// Test for truncation bug: content should wrap, not be lost
|
||||
// Input: \(\text{Calculer le discriminant }\Delta=b^{2}-4ac\text{ avec }a=1\text{, }b=-1\text{, }c=-5\)
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "\\(\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
// Set width constraint that should cause wrapping
|
||||
label.preferredMaxLayoutWidth = 235
|
||||
let constrainedSize = label.intrinsicContentSize
|
||||
|
||||
// Verify the label can render without errors
|
||||
label.frame = CGRect(origin: .zero, size: constrainedSize)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
XCTAssertNil(label.error, "Should have no rendering error")
|
||||
|
||||
// Verify content is not truncated - should wrap to multiple lines
|
||||
XCTAssertGreaterThan(constrainedSize.height, 30, "Should wrap to multiple lines (not truncate)")
|
||||
|
||||
// Check that we have multiple display elements (wrapped content)
|
||||
if let displayList = label.displayList {
|
||||
XCTAssertGreaterThan(displayList.subDisplays.count, 1, "Should have multiple display elements from wrapping")
|
||||
}
|
||||
}
|
||||
|
||||
func testNumberProtection_FrenchDecimal() {
|
||||
let label = MTMathUILabel()
|
||||
// French decimal number should NOT be broken
|
||||
label.latex = "\\(\\text{La valeur de pi est approximativement 3,14 dans ce calcul simple.}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
// Constrain to force wrapping, but 3,14 should stay together
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
let size = label.intrinsicContentSize
|
||||
|
||||
// Verify it renders without error
|
||||
label.frame = CGRect(origin: .zero, size: size)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
XCTAssertNil(label.error, "Should have no rendering error")
|
||||
}
|
||||
|
||||
func testNumberProtection_ThousandsSeparator() {
|
||||
let label = MTMathUILabel()
|
||||
// Number with comma separator should stay together
|
||||
label.latex = "\\(\\text{The population is approximately 1,000,000 people in this city.}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
let size = label.intrinsicContentSize
|
||||
|
||||
label.frame = CGRect(origin: .zero, size: size)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
XCTAssertNil(label.error, "Should have no rendering error")
|
||||
}
|
||||
|
||||
func testNumberProtection_MixedWithText() {
|
||||
let label = MTMathUILabel()
|
||||
// Mixed numbers and text - numbers should be protected
|
||||
label.latex = "\\(\\text{Results: 3.14, 2.71, and 1.41 are important constants.}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
label.preferredMaxLayoutWidth = 180
|
||||
let size = label.intrinsicContentSize
|
||||
|
||||
label.frame = CGRect(origin: .zero, size: size)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
XCTAssertNil(label.error, "Should have no rendering error")
|
||||
}
|
||||
|
||||
// MARK: - International Text Tests
|
||||
|
||||
func testChineseTextWrapping() {
|
||||
let label = MTMathUILabel()
|
||||
// Chinese text: "Mathematical equations are an important tool for describing natural phenomena"
|
||||
label.latex = "\\(\\text{数学方程式は自然現象を記述するための重要なツールです。}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
// Get unconstrained size
|
||||
let unconstrainedSize = label.intrinsicContentSize
|
||||
|
||||
// Set constraint to force wrapping
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
let constrainedSize = label.intrinsicContentSize
|
||||
|
||||
// Chinese should wrap (can break between characters)
|
||||
XCTAssertGreaterThan(constrainedSize.width, 0, "Width should be > 0")
|
||||
XCTAssertLessThanOrEqual(constrainedSize.width, 200, "Width should not exceed constraint")
|
||||
XCTAssertGreaterThan(constrainedSize.height, unconstrainedSize.height, "Height should increase when wrapped")
|
||||
|
||||
label.frame = CGRect(origin: .zero, size: constrainedSize)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
XCTAssertNil(label.error, "Should have no rendering error")
|
||||
}
|
||||
|
||||
func testJapaneseTextWrapping() {
|
||||
let label = MTMathUILabel()
|
||||
// Japanese text (Hiragana + Kanji): "This is a mathematics explanation"
|
||||
label.latex = "\\(\\text{これは数学の説明です。計算式を使います。}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
let unconstrainedSize = label.intrinsicContentSize
|
||||
|
||||
label.preferredMaxLayoutWidth = 180
|
||||
let constrainedSize = label.intrinsicContentSize
|
||||
|
||||
XCTAssertGreaterThan(constrainedSize.width, 0, "Width should be > 0")
|
||||
XCTAssertLessThanOrEqual(constrainedSize.width, 180, "Width should not exceed constraint")
|
||||
XCTAssertGreaterThan(constrainedSize.height, unconstrainedSize.height, "Height should increase when wrapped")
|
||||
|
||||
label.frame = CGRect(origin: .zero, size: constrainedSize)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
XCTAssertNil(label.error, "Should have no rendering error")
|
||||
}
|
||||
|
||||
func testKoreanTextWrapping() {
|
||||
let label = MTMathUILabel()
|
||||
// Korean text: "Mathematics is a very important subject"
|
||||
label.latex = "\\(\\text{수학은 매우 중요한 과목입니다. 방정식을 배웁니다.}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
let constrainedSize = label.intrinsicContentSize
|
||||
|
||||
// Korean uses spaces, should wrap at word boundaries
|
||||
XCTAssertGreaterThan(constrainedSize.width, 0, "Width should be > 0")
|
||||
XCTAssertLessThanOrEqual(constrainedSize.width, 200, "Width should not exceed constraint")
|
||||
|
||||
label.frame = CGRect(origin: .zero, size: constrainedSize)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
XCTAssertNil(label.error, "Should have no rendering error")
|
||||
}
|
||||
|
||||
func testMixedLatinCJKWrapping() {
|
||||
let label = MTMathUILabel()
|
||||
// Mixed English and Chinese
|
||||
label.latex = "\\(\\text{The equation is 方程式: } x^2 + y^2 = r^2 \\text{ です。}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
label.preferredMaxLayoutWidth = 250
|
||||
let constrainedSize = label.intrinsicContentSize
|
||||
|
||||
XCTAssertGreaterThan(constrainedSize.width, 0, "Width should be > 0")
|
||||
XCTAssertLessThanOrEqual(constrainedSize.width, 250, "Width should not exceed constraint")
|
||||
|
||||
label.frame = CGRect(origin: .zero, size: constrainedSize)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
XCTAssertNil(label.error, "Should have no rendering error")
|
||||
}
|
||||
|
||||
func testEmojiGraphemeClusters() {
|
||||
let label = MTMathUILabel()
|
||||
// Emoji and complex grapheme clusters should not be broken
|
||||
label.latex = "\\(\\text{Math is fun! 🎉📐📊 The formula is } E = mc^2 \\text{ 🚀✨}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
let size = label.intrinsicContentSize
|
||||
|
||||
// Should wrap but not break emoji
|
||||
XCTAssertGreaterThan(size.width, 0, "Width should be > 0")
|
||||
XCTAssertLessThanOrEqual(size.width, 200, "Width should not exceed constraint")
|
||||
|
||||
label.frame = CGRect(origin: .zero, size: size)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
XCTAssertNil(label.error, "Should have no rendering error")
|
||||
}
|
||||
|
||||
func testLongEnglishMultiSentence() {
|
||||
let label = MTMathUILabel()
|
||||
// Standard English multi-sentence paragraph
|
||||
label.latex = "\\(\\text{Mathematics is the study of numbers, shapes, and patterns. It is used in science, engineering, and everyday life. Equations help us solve problems.}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
let unconstrainedSize = label.intrinsicContentSize
|
||||
|
||||
label.preferredMaxLayoutWidth = 300
|
||||
let constrainedSize = label.intrinsicContentSize
|
||||
|
||||
// Should wrap at word boundaries (spaces)
|
||||
XCTAssertGreaterThan(constrainedSize.width, 0, "Width should be > 0")
|
||||
XCTAssertLessThanOrEqual(constrainedSize.width, 300, "Width should not exceed constraint")
|
||||
XCTAssertGreaterThan(constrainedSize.height, unconstrainedSize.height, "Height should increase when wrapped")
|
||||
|
||||
label.frame = CGRect(origin: .zero, size: constrainedSize)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
XCTAssertNil(label.error, "Should have no rendering error")
|
||||
}
|
||||
|
||||
func testSpanishAccentedText() {
|
||||
let label = MTMathUILabel()
|
||||
// Spanish with various accents
|
||||
label.latex = "\\(\\text{La ecuación es muy útil para cálculos científicos y matemáticos.}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
let unconstrainedSize = label.intrinsicContentSize
|
||||
|
||||
label.preferredMaxLayoutWidth = 220
|
||||
let constrainedSize = label.intrinsicContentSize
|
||||
|
||||
XCTAssertGreaterThan(constrainedSize.width, 0, "Width should be > 0")
|
||||
XCTAssertLessThanOrEqual(constrainedSize.width, 220, "Width should not exceed constraint")
|
||||
XCTAssertGreaterThan(constrainedSize.height, unconstrainedSize.height, "Height should increase when wrapped")
|
||||
|
||||
label.frame = CGRect(origin: .zero, size: constrainedSize)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
XCTAssertNil(label.error, "Should have no rendering error")
|
||||
}
|
||||
|
||||
func testGermanUmlautsWrapping() {
|
||||
let label = MTMathUILabel()
|
||||
// German with umlauts
|
||||
label.latex = "\\(\\text{Mathematische Gleichungen können für Berechnungen verwendet werden.}\\)"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
label.labelMode = .text
|
||||
|
||||
let unconstrainedSize = label.intrinsicContentSize
|
||||
|
||||
label.preferredMaxLayoutWidth = 250
|
||||
let constrainedSize = label.intrinsicContentSize
|
||||
|
||||
XCTAssertGreaterThan(constrainedSize.width, 0, "Width should be > 0")
|
||||
XCTAssertLessThanOrEqual(constrainedSize.width, 250, "Width should not exceed constraint")
|
||||
XCTAssertGreaterThan(constrainedSize.height, unconstrainedSize.height, "Height should increase when wrapped")
|
||||
|
||||
label.frame = CGRect(origin: .zero, size: constrainedSize)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
XCTAssertNil(label.error, "Should have no rendering error")
|
||||
}
|
||||
|
||||
// MARK: - Tests for Complex Math Expressions with Line Breaking
|
||||
|
||||
func testComplexExpressionWithRadicalWrapping() {
|
||||
// This is the reported issue: y=x^{2}+3x+4x+9x+8x+8+\sqrt{\dfrac{3x^{2}+5x}{\cos x}}
|
||||
// The sqrt part is displayed on the second line and overlaps the first line
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "y=x^{2}+3x+4x+9x+8x+8+\\sqrt{\\dfrac{3x^{2}+5x}{\\cos x}}"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
|
||||
// Get unconstrained size first
|
||||
let unconstrainedSize = label.intrinsicContentSize
|
||||
XCTAssertGreaterThan(unconstrainedSize.width, 0, "Unconstrained width should be > 0")
|
||||
XCTAssertGreaterThan(unconstrainedSize.height, 0, "Unconstrained height should be > 0")
|
||||
|
||||
// Now constrain the width to force wrapping
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
let constrainedSize = label.intrinsicContentSize
|
||||
|
||||
XCTAssertGreaterThan(constrainedSize.width, 0, "Width should be > 0")
|
||||
XCTAssertLessThanOrEqual(constrainedSize.width, 200, "Width should not exceed constraint")
|
||||
XCTAssertGreaterThan(constrainedSize.height, unconstrainedSize.height, "Height should increase when wrapped")
|
||||
|
||||
// Layout and check for overlapping
|
||||
label.frame = CGRect(origin: .zero, size: constrainedSize)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
XCTAssertNil(label.error, "Should have no rendering error")
|
||||
|
||||
// Check that displays don't overlap by examining positions
|
||||
// Group displays by line (similar y positions) and check for overlap between lines
|
||||
if let displayList = label.displayList {
|
||||
// Group displays by line based on their y position
|
||||
var lineGroups: [[MTDisplay]] = []
|
||||
var currentLineDisplays: [MTDisplay] = []
|
||||
var currentLineY: CGFloat? = nil
|
||||
let yTolerance: CGFloat = 15.0 // Displays within 15 units are considered on same line (accounts for superscripts/subscripts)
|
||||
|
||||
for display in displayList.subDisplays {
|
||||
if let lineY = currentLineY {
|
||||
if abs(display.position.y - lineY) < yTolerance {
|
||||
// Same line
|
||||
currentLineDisplays.append(display)
|
||||
} else {
|
||||
// New line
|
||||
lineGroups.append(currentLineDisplays)
|
||||
currentLineDisplays = [display]
|
||||
currentLineY = display.position.y
|
||||
}
|
||||
} else {
|
||||
// First display
|
||||
currentLineDisplays = [display]
|
||||
currentLineY = display.position.y
|
||||
}
|
||||
}
|
||||
if !currentLineDisplays.isEmpty {
|
||||
lineGroups.append(currentLineDisplays)
|
||||
}
|
||||
|
||||
// Check for overlap between consecutive lines
|
||||
for i in 1..<lineGroups.count {
|
||||
let previousLine = lineGroups[i-1]
|
||||
let currentLine = lineGroups[i]
|
||||
|
||||
// Find the minimum bottom edge of previous line (Y-up: bottom = pos - desc, smaller Y)
|
||||
let previousLineMinBottom = previousLine.map { $0.position.y - $0.descent }.min() ?? 0
|
||||
|
||||
// Find the maximum top edge of current line (Y-up: top = pos + asc, larger Y)
|
||||
let currentLineMaxTop = currentLine.map { $0.position.y + $0.ascent }.max() ?? 0
|
||||
|
||||
// Check for overlap: if current line's top > previous line's bottom, they overlap
|
||||
// (In Y-up coordinate system: positive Y is upward, negative Y is downward)
|
||||
// Allow 0.5 points tolerance for floating-point precision and small adjustments
|
||||
XCTAssertLessThanOrEqual(currentLineMaxTop, previousLineMinBottom + 0.5,
|
||||
"Line \(i) (top at \(currentLineMaxTop)) overlaps with line \(i-1) (bottom at \(previousLineMinBottom))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRadicalWithFractionInsideWrapping() {
|
||||
// Simplified version: just a radical with a fraction inside
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "x+y+z+\\sqrt{\\dfrac{a}{b}}"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
|
||||
let unconstrainedSize = label.intrinsicContentSize
|
||||
|
||||
label.preferredMaxLayoutWidth = 100
|
||||
let constrainedSize = label.intrinsicContentSize
|
||||
|
||||
XCTAssertGreaterThan(constrainedSize.width, 0, "Width should be > 0")
|
||||
XCTAssertGreaterThan(constrainedSize.height, unconstrainedSize.height, "Height should increase when wrapped")
|
||||
|
||||
label.frame = CGRect(origin: .zero, size: constrainedSize)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
XCTAssertNil(label.error, "Should have no rendering error")
|
||||
}
|
||||
|
||||
func testTallElementsOnSecondLine() {
|
||||
// Test case with tall fractions and radicals breaking to second line
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "a+b+c+\\dfrac{x^2+y^2}{z^2}+\\sqrt{\\dfrac{p}{q}}"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
|
||||
let unconstrainedSize = label.intrinsicContentSize
|
||||
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
let constrainedSize = label.intrinsicContentSize
|
||||
|
||||
XCTAssertGreaterThan(constrainedSize.width, 0, "Width should be > 0")
|
||||
XCTAssertGreaterThan(constrainedSize.height, unconstrainedSize.height, "Height should increase when wrapped")
|
||||
|
||||
label.frame = CGRect(origin: .zero, size: constrainedSize)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
XCTAssertNil(label.error, "Should have no rendering error")
|
||||
|
||||
// Verify no overlapping displays between lines
|
||||
if let displayList = label.displayList {
|
||||
// Group displays by line
|
||||
var lineGroups: [[MTDisplay]] = []
|
||||
var currentLineDisplays: [MTDisplay] = []
|
||||
var currentLineY: CGFloat? = nil
|
||||
let yTolerance: CGFloat = 15.0
|
||||
|
||||
for display in displayList.subDisplays {
|
||||
if let lineY = currentLineY {
|
||||
if abs(display.position.y - lineY) < yTolerance {
|
||||
currentLineDisplays.append(display)
|
||||
} else {
|
||||
lineGroups.append(currentLineDisplays)
|
||||
currentLineDisplays = [display]
|
||||
currentLineY = display.position.y
|
||||
}
|
||||
} else {
|
||||
currentLineDisplays = [display]
|
||||
currentLineY = display.position.y
|
||||
}
|
||||
}
|
||||
if !currentLineDisplays.isEmpty {
|
||||
lineGroups.append(currentLineDisplays)
|
||||
}
|
||||
|
||||
// Check for overlap between consecutive lines
|
||||
for i in 1..<lineGroups.count {
|
||||
let previousLine = lineGroups[i-1]
|
||||
let currentLine = lineGroups[i]
|
||||
|
||||
let previousLineMinBottom = previousLine.map { $0.position.y - $0.descent }.min() ?? 0
|
||||
let currentLineMaxTop = currentLine.map { $0.position.y + $0.ascent }.max() ?? 0
|
||||
|
||||
// Allow 0.5 points tolerance for floating-point precision
|
||||
XCTAssertLessThanOrEqual(currentLineMaxTop, previousLineMinBottom + 0.5,
|
||||
"Line \(i) overlaps with line \(i-1)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testMultipleLinesWithVaryingHeights() {
|
||||
// Test expression that should wrap to multiple lines with different heights
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "x+y+z+a+b+c+\\sqrt{d}+e+f+g+h+\\dfrac{i}{j}+k"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
|
||||
let unconstrainedSize = label.intrinsicContentSize
|
||||
|
||||
label.preferredMaxLayoutWidth = 120
|
||||
let constrainedSize = label.intrinsicContentSize
|
||||
|
||||
XCTAssertGreaterThan(constrainedSize.width, 0, "Width should be > 0")
|
||||
XCTAssertGreaterThan(constrainedSize.height, unconstrainedSize.height, "Height should increase when wrapped")
|
||||
|
||||
label.frame = CGRect(origin: .zero, size: constrainedSize)
|
||||
#if os(macOS)
|
||||
label.layout()
|
||||
#else
|
||||
label.layoutSubviews()
|
||||
#endif
|
||||
|
||||
XCTAssertNotNil(label.displayList, "Display list should be created")
|
||||
XCTAssertNil(label.error, "Should have no rendering error")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,187 +0,0 @@
|
||||
import XCTest
|
||||
@testable import SwiftUIMath
|
||||
|
||||
//
|
||||
// MathFontTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Peter Tang on 12/9/2023.
|
||||
//
|
||||
|
||||
final class MathFontTests: XCTestCase {
|
||||
func testMathFontScript() throws {
|
||||
let size = Int.random(in: 20 ... 40)
|
||||
MathFont.allCases.forEach {
|
||||
// print("\(#function) cgfont \($0.cgFont())")
|
||||
// print("\(#function) ctfont \($0.ctFont(withSize: CGFloat(size)))")
|
||||
XCTAssertNotNil($0.cgFont())
|
||||
XCTAssertNotNil($0.ctFont(withSize: CGFloat(size)))
|
||||
XCTAssertEqual($0.ctFont(withSize: CGFloat(size)).fontSize, CGFloat(size), "ctFont fontSize != size.")
|
||||
XCTAssertEqual($0.cgFont().postScriptName as? String, $0.postScriptName, "cgFont.postScriptName != postScriptName")
|
||||
// XCTAssertEqual($0.uiFont(withSize: CGFloat(size))?.familyName, $0.fontFamilyName, "uifont familyName != familyName.")
|
||||
XCTAssertEqual(CTFontCopyFamilyName($0.ctFont(withSize: CGFloat(size))) as String, $0.fontFamilyName, "ctfont.family != familyName")
|
||||
}
|
||||
#if os(iOS) || os(visionOS)
|
||||
// for family in UIFont.familyNames.sorted() {
|
||||
// let names = UIFont.fontNames(forFamilyName: family)
|
||||
// print("Family: \(family) Font names: \(names)")
|
||||
// }
|
||||
fontNames.forEach { name in
|
||||
XCTAssertNotNil(UIFont(name: name, size: CGFloat(size)))
|
||||
}
|
||||
fontFamilyNames.forEach { name in
|
||||
XCTAssertNotNil(UIFont.fontNames(forFamilyName: name))
|
||||
}
|
||||
#endif
|
||||
#if os(macOS)
|
||||
fontNames.forEach { name in
|
||||
let font = NSFont(name: name, size: CGFloat(size))
|
||||
XCTAssertNotNil(font)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
func testOnDemandMathFontScript() throws {
|
||||
let size = Int.random(in: 20 ... 40)
|
||||
let mathFont = MathFont.allCases.randomElement()!
|
||||
XCTAssertNotNil(mathFont.cgFont())
|
||||
XCTAssertNotNil(mathFont.ctFont(withSize: CGFloat(size)))
|
||||
XCTAssertEqual(mathFont.ctFont(withSize: CGFloat(size)).fontSize, CGFloat(size), "ctFont fontSize test")
|
||||
}
|
||||
var fontNames: [String] {
|
||||
MathFont.allCases.map { $0.postScriptName }
|
||||
}
|
||||
var fontFamilyNames: [String] {
|
||||
MathFont.allCases.map { $0.fontFamilyName }
|
||||
}
|
||||
|
||||
private let executionQueue = DispatchQueue(label: "com.swiftmath.mathbundle", attributes: .concurrent)
|
||||
private let executionGroup = DispatchGroup()
|
||||
|
||||
let totalCases = 5000
|
||||
var testCount = 0
|
||||
func testConcurrentThreadsafeScript() throws {
|
||||
var mathFont: MathFont { .allCases.randomElement()! }
|
||||
for caseNumber in 0 ..< totalCases {
|
||||
switch caseNumber % 3 {
|
||||
case 0:
|
||||
helperConcurrentCGFont(caseNumber, mathFont: mathFont, in: executionGroup, on: executionQueue)
|
||||
case 1:
|
||||
helperConcurrentCTFont(caseNumber, mathFont: mathFont, in: executionGroup, on: executionQueue)
|
||||
case 2:
|
||||
helperConcurrentMathTable(caseNumber, mathFont: mathFont, in: executionGroup, on: executionQueue)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
executionGroup.notify(queue: .main) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
XCTAssertEqual(self.testCount, totalCases)
|
||||
}
|
||||
executionGroup.wait()
|
||||
}
|
||||
// func helperConcurrentOnDemandRegistration(_ count: Int, mathFont: MathFont, in group: DispatchGroup, on queue: DispatchQueue) {
|
||||
// let workitem = DispatchWorkItem {
|
||||
// BundleManager.manager.onDemandRegistration(mathFont: mathFont)
|
||||
// }
|
||||
// workitem.notify(queue: .main) { [weak self] in
|
||||
// self?.testCount += 1
|
||||
// }
|
||||
// queue.async(group: group, execute: workitem)
|
||||
// }
|
||||
// func helperConcurrentBundleRegistration(mathFont: MathFont, in group: DispatchGroup, on queue: DispatchQueue) {
|
||||
// let workitem = DispatchWorkItem {
|
||||
// // BundleManager.manager.onDemandRegistration(mathFont: mathFont)
|
||||
// try? BundleManager.manager.registerCGFont(mathFont: mathFont)
|
||||
// try? BundleManager.manager.registerMathTable(mathFont: mathFont)
|
||||
// let font = BundleManager.manager.cgFonts[mathFont]
|
||||
// XCTAssertNotNil(font, "font != nil")
|
||||
// }
|
||||
// workitem.notify(queue: .main) { [weak self] in
|
||||
// // print("\(Thread.isMainThread ? "main" : "global") completed .....")
|
||||
// let font = mathFont.cgFont()
|
||||
// XCTAssertNotNil(font, "font != nil")
|
||||
// self?.testCount += 1
|
||||
// }
|
||||
// queue.async(group: group, execute: workitem)
|
||||
// }
|
||||
func helperConcurrentCGFont(_ count: Int, mathFont: MathFont, in group: DispatchGroup, on queue: DispatchQueue) {
|
||||
let workitem = DispatchWorkItem {
|
||||
let font = mathFont.cgFont()
|
||||
XCTAssertNotNil(font, "font != nil")
|
||||
}
|
||||
workitem.notify(queue: .main) { [weak self] in
|
||||
// print("\(Thread.isMainThread ? "main" : "global") completed .....")
|
||||
let font = mathFont.cgFont()
|
||||
XCTAssertNotNil(font, "font != nil")
|
||||
self?.testCount += 1
|
||||
}
|
||||
queue.async(group: group, execute: workitem)
|
||||
}
|
||||
func helperConcurrentCTFont(_ count: Int, mathFont: MathFont, in group: DispatchGroup, on queue: DispatchQueue) {
|
||||
let size = CGFloat.random(in: 20 ... 40)
|
||||
let workitem = DispatchWorkItem {
|
||||
let font = mathFont.ctFont(withSize: size)
|
||||
XCTAssertNotNil(font, "font != nil")
|
||||
}
|
||||
workitem.notify(queue: .main) { [weak self] in
|
||||
// print("\(Thread.isMainThread ? "main" : "global") completed .....")
|
||||
let font = mathFont.ctFont(withSize: size)
|
||||
XCTAssertNotNil(font, "font != nil")
|
||||
self?.testCount += 1
|
||||
}
|
||||
queue.async(group: group, execute: workitem)
|
||||
}
|
||||
func helperConcurrentMathTable(_ count: Int, mathFont: MathFont, in group: DispatchGroup, on queue: DispatchQueue) {
|
||||
let workitem = DispatchWorkItem {
|
||||
let mathtable = mathFont.rawMathTable()
|
||||
XCTAssertNotNil(mathtable, "mathTable != nil")
|
||||
}
|
||||
workitem.notify(queue: .main) { [weak self] in
|
||||
// print("\(Thread.isMainThread ? "main" : "global") completed .....")
|
||||
let mathtable = mathFont.rawMathTable()
|
||||
XCTAssertNotNil(mathtable, "mathTable != nil")
|
||||
self?.testCount += 1
|
||||
}
|
||||
queue.async(group: group, execute: workitem)
|
||||
}
|
||||
|
||||
func testFallbackFont() throws {
|
||||
#if os(iOS) || os(visionOS)
|
||||
let systemFont = UIFont.systemFont(ofSize: 20)
|
||||
let systemCTFont = CTFontCreateWithName(systemFont.fontName as CFString, 20, nil)
|
||||
#elseif os(macOS)
|
||||
let systemFont = NSFont.systemFont(ofSize: 20)
|
||||
let systemCTFont = CTFontCreateWithName(systemFont.fontName as CFString, 20, nil)
|
||||
#endif
|
||||
|
||||
// Create a math font with fallback
|
||||
guard let mathFont = MTFontManager().font(withName: MathFont.latinModernFont.rawValue, size: 20) else {
|
||||
XCTFail("Failed to create math font")
|
||||
return
|
||||
}
|
||||
mathFont.fallbackFont = systemCTFont
|
||||
|
||||
// Build a math list with Chinese text
|
||||
var error: NSError?
|
||||
let mathList = MTMathListBuilder.build(fromString: "\\text{中文测试}", error: &error)
|
||||
|
||||
XCTAssertNil(error, "Should parse Chinese text without error")
|
||||
XCTAssertNotNil(mathList, "Math list should be created")
|
||||
|
||||
// \text{...} creates atoms for each character (4 Chinese characters = 4 atoms)
|
||||
XCTAssertEqual(mathList?.atoms.count, 4, "Should have 4 atoms for 4 Chinese characters")
|
||||
|
||||
// Verify atoms have the correct font style (roman for text)
|
||||
for atom in mathList?.atoms ?? [] {
|
||||
XCTAssertEqual(atom.fontStyle, .roman, "Text atoms should have roman font style")
|
||||
}
|
||||
|
||||
// Create a display to verify glyph rendering works with fallback
|
||||
let display = MTTypesetter.createLineForMathList(mathList!, font: mathFont, style: .text)
|
||||
|
||||
XCTAssertNotNil(display, "Display should be created with fallback font")
|
||||
|
||||
// Verify the display was actually created (would be nil if all glyphs failed)
|
||||
XCTAssertGreaterThan(display?.width ?? 0, 0, "Display should have non-zero width with fallback font")
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
//
|
||||
// MathImageTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Peter Tang on 18/9/2023.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SwiftUIMath
|
||||
|
||||
final class MathImageTests: XCTestCase {
|
||||
func safeImage(fileName: String, pngData: Data) {
|
||||
let imageFileURL = URL(fileURLWithPath: NSTemporaryDirectory().appending("image-\(fileName).png"))
|
||||
try? pngData.write(to: imageFileURL, options: [.atomicWrite])
|
||||
//print("\(#function) \(imageFileURL.path)")
|
||||
}
|
||||
func testMathImageScript() throws {
|
||||
let latex = Latex.samples.randomElement()!
|
||||
let mathfont = MathFont.allCases.randomElement()!
|
||||
let fontsize = CGFloat.random(in: 24 ... 36)
|
||||
let result = SwiftMathImageResult.useMathImage(latex: latex, font: mathfont, fontSize: fontsize)
|
||||
XCTAssertNil(result.error)
|
||||
XCTAssertNotNil(result.image)
|
||||
XCTAssertNotNil(result.layoutInfo)
|
||||
if result.error == nil, let image = result.image, let imageData = image.pngData() {
|
||||
safeImage(fileName: "test", pngData: imageData)
|
||||
}
|
||||
}
|
||||
func testSequentialMultipleImageScript() throws {
|
||||
var latex: String { Latex.samples.randomElement()! }
|
||||
var mathfont: MathFont { MathFont.allCases.randomElement()! }
|
||||
var fontsize: CGFloat { CGFloat.random(in: 20 ... 40) }
|
||||
for caseNumber in 0 ..< 20 {
|
||||
let result: SwiftMathImageResult
|
||||
switch caseNumber % 2 {
|
||||
case 0:
|
||||
result = SwiftMathImageResult.useMathImage(latex: latex, font: mathfont, fontSize: fontsize)
|
||||
XCTAssertNil(result.error)
|
||||
XCTAssertNotNil(result.image)
|
||||
XCTAssertNotNil(result.layoutInfo)
|
||||
if result.error == nil, let image = result.image, let imageData = image.pngData() {
|
||||
safeImage(fileName: "\(caseNumber)", pngData: imageData)
|
||||
}
|
||||
default:
|
||||
result = SwiftMathImageResult.useMTMathImage(latex: latex, font: mathfont, fontSize: fontsize)
|
||||
XCTAssertNil(result.error)
|
||||
XCTAssertNotNil(result.image)
|
||||
if result.error == nil, let image = result.image, let imageData = image.pngData() {
|
||||
safeImage(fileName: "\(caseNumber)", pngData: imageData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let executionQueue = DispatchQueue(label: "com.swiftmath.mathbundle", attributes: .concurrent)
|
||||
private let executionGroup = DispatchGroup()
|
||||
|
||||
let totalCases = 20
|
||||
var testCount = 0
|
||||
|
||||
func testConcurrentMathImageScript() throws {
|
||||
var latex: String { Latex.samples.randomElement()! }
|
||||
var mathfont: MathFont { MathFont.allCases.randomElement()! }
|
||||
var size: CGFloat { CGFloat.random(in: 20 ... 40) }
|
||||
for caseNumber in 0 ..< totalCases {
|
||||
switch caseNumber % 2 {
|
||||
case 0:
|
||||
helperConcurrentMathImage(caseNumber, latex: latex, mathfont: mathfont, fontsize: size, in: executionGroup, on: executionQueue)
|
||||
default:
|
||||
helperConcurrentMTMathImage(caseNumber, latex: latex, mathfont: mathfont, fontsize: size, in: executionGroup, on: executionQueue)
|
||||
}
|
||||
}
|
||||
executionGroup.notify(queue: .main) { [weak self] in
|
||||
XCTAssertEqual(self?.testCount,self?.totalCases)
|
||||
}
|
||||
executionGroup.wait()
|
||||
}
|
||||
func helperConcurrentMathImage(_ count: Int, latex: String, mathfont: MathFont, fontsize: CGFloat, in group: DispatchGroup, on queue: DispatchQueue) {
|
||||
let workitem = DispatchWorkItem { [weak self] in
|
||||
let result = SwiftMathImageResult.useMathImage(latex: latex, font: mathfont, fontSize: fontsize)
|
||||
XCTAssertNil(result.error)
|
||||
XCTAssertNotNil(result.image)
|
||||
XCTAssertNotNil(result.layoutInfo)
|
||||
if result.error == nil, let image = result.image, let imageData = image.pngData() {
|
||||
self?.safeImage(fileName: "\(count)", pngData: imageData)
|
||||
}
|
||||
}
|
||||
workitem.notify(queue: .main) { [weak self] in
|
||||
self?.testCount += 1
|
||||
}
|
||||
queue.async(group: group, execute: workitem)
|
||||
}
|
||||
func helperConcurrentMTMathImage(_ count: Int, latex: String, mathfont: MathFont, fontsize: CGFloat, in group: DispatchGroup, on queue: DispatchQueue) {
|
||||
let workitem = DispatchWorkItem { [weak self] in
|
||||
let result = SwiftMathImageResult.useMTMathImage(latex: latex, font: mathfont, fontSize: fontsize)
|
||||
XCTAssertNil(result.error)
|
||||
XCTAssertNotNil(result.image)
|
||||
if result.error == nil, let image = result.image, let imageData = image.pngData() {
|
||||
self?.safeImage(fileName: "\(count)", pngData: imageData)
|
||||
}
|
||||
}
|
||||
workitem.notify(queue: .main) { [weak self] in
|
||||
self?.testCount += 1
|
||||
}
|
||||
queue.async(group: group, execute: workitem)
|
||||
}
|
||||
}
|
||||
public struct SwiftMathImageResult {
|
||||
let error: NSError?
|
||||
let image: MTImage?
|
||||
let layoutInfo: MathImage.LayoutInfo?
|
||||
}
|
||||
extension SwiftMathImageResult {
|
||||
public static func useMTMathImage(latex: String, font: MathFont, fontSize: CGFloat, textColor: MTColor = MTColor.black) -> SwiftMathImageResult {
|
||||
let alignment = MTTextAlignment.left
|
||||
let formatter = MTMathImage(latex: latex, fontSize: fontSize - 1.0,
|
||||
textColor: textColor,
|
||||
labelMode: .text, textAlignment: alignment)
|
||||
formatter.font = font.mtfont(size: fontSize)
|
||||
let (error, image) = formatter.asImage()
|
||||
return SwiftMathImageResult(error: error, image: image, layoutInfo: nil)
|
||||
}
|
||||
public static func useMathImage(latex: String, font: MathFont, fontSize: CGFloat, textColor: MTColor = MTColor.black) -> SwiftMathImageResult {
|
||||
let alignment = MTTextAlignment.left
|
||||
var formatter = MathImage(latex: latex, fontSize: fontSize - 1.0,
|
||||
textColor: textColor,
|
||||
labelMode: .text, textAlignment: alignment)
|
||||
formatter.font = font
|
||||
let (error, image, layoutInfo) = formatter.asImage()
|
||||
return SwiftMathImageResult(error: error, image: image, layoutInfo: layoutInfo)
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
extension NSBitmapImageRep {
|
||||
var png: Data? { representation(using: .png, properties: [:]) }
|
||||
}
|
||||
extension Data {
|
||||
var bitmap: NSBitmapImageRep? { NSBitmapImageRep(data: self) }
|
||||
}
|
||||
extension NSImage {
|
||||
func pngData() -> Data? {
|
||||
tiffRepresentation?.bitmap?.png
|
||||
}
|
||||
}
|
||||
#endif
|
||||
enum Latex {
|
||||
static let samples: [String] = [
|
||||
#"(a_1 + a_2)^2 = a_1^2 + 2a_1a_2 + a_2^2"#,
|
||||
#"x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}"#,
|
||||
#"\sigma = \sqrt{\frac{1}{N}\sum_{i=1}^N (x_i - \mu)^2}"#,
|
||||
#"\neg(P\land Q) \iff (\neg P)\lor(\neg Q)"#,
|
||||
#"\cos(\theta + \varphi) = \cos(\theta)\cos(\varphi) - \sin(\theta)\sin(\varphi)"#,
|
||||
#"\lim_{x\to\infty}\left(1 + \frac{k}{x}\right)^x = e^k"#,
|
||||
#"f(x) = \int\limits_{-\infty}^\infty\hat f(\xi)\,e^{2 \pi i \xi x}\,\mathrm{d}\xi"#,
|
||||
#"{n \brace k} = \frac{1}{k!}\sum_{j=0}^k (-1)^{k-j}\binom{k}{j}(k-j)^n"#,
|
||||
#"\int_{-\infty}^{\infty} \! e^{-x^2} dx = \sqrt{\pi}"#,
|
||||
#"\frac{1}{n}\sum_{i=1}^{n}x_i \geq \sqrt[n]{\prod_{i=1}^{n}x_i}"#,
|
||||
#"\left(\sum_{k=1}^n a_k b_k \right)^2 \le \left(\sum_{k=1}^n a_k^2\right)\left(\sum_{k=1}^n b_k^2\right)"#,
|
||||
#"\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)"#,
|
||||
#"i\hbar\frac{\partial}{\partial t}\mathbf\Psi(\mathbf{x},t) = -\frac{\hbar}{2m}\nabla^2\mathbf\Psi(\mathbf{x},t) + V(\mathbf{x})\mathbf\Psi(\mathbf{x},t)"#,
|
||||
#"""
|
||||
\begin{gather}
|
||||
\dot{x} = \sigma(y-x) \\
|
||||
\dot{y} = \rho x - y - xz \\
|
||||
\dot{z} = -\beta z + xy"
|
||||
\end{gather}
|
||||
"""#,
|
||||
#"""
|
||||
\vec \bf V_1 \times \vec \bf V_2 = \begin{vmatrix}
|
||||
\hat \imath &\hat \jmath &\hat k \\
|
||||
\frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \\
|
||||
\frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0
|
||||
\end{vmatrix}
|
||||
"""#,
|
||||
#"""
|
||||
\begin{eqalign}
|
||||
\nabla \cdot \vec{\bf E} & = \frac {\rho} {\varepsilon_0} \\
|
||||
\nabla \cdot \vec{\bf B} & = 0 \\
|
||||
\nabla \times \vec{\bf E} &= - \frac{\partial\vec{\bf B}}{\partial t} \\
|
||||
\nabla \times \vec{\bf B} & = \mu_0\vec{\bf J} + \mu_0\varepsilon_0 \frac{\partial\vec{\bf E}}{\partial t}
|
||||
\end{eqalign}
|
||||
"""#,
|
||||
#"\log_b(x) = \frac{\log_a(x)}{\log_a(b)}"#,
|
||||
#"""
|
||||
\begin{pmatrix}
|
||||
a & b\\ c & d
|
||||
\end{pmatrix}
|
||||
\begin{pmatrix}
|
||||
\alpha & \beta \\ \gamma & \delta
|
||||
\end{pmatrix} =
|
||||
\begin{pmatrix}
|
||||
a\alpha + b\gamma & a\beta + b \delta \\
|
||||
c\alpha + d\gamma & c\beta + d \delta
|
||||
\end{pmatrix}
|
||||
"""#,
|
||||
#"""
|
||||
\frak Q(\lambda,\hat{\lambda}) =
|
||||
-\frac{1}{2} \mathbb P(O \mid \lambda ) \sum_s \sum_m \sum_t \gamma_m^{(s)} (t) +\\
|
||||
\quad \left( \log(2 \pi ) + \log \left| \cal C_m^{(s)} \right| +
|
||||
\left( o_t - \hat{\mu}_m^{(s)} \right) ^T \cal C_m^{(s)-1} \right)
|
||||
"""#
|
||||
]
|
||||
}
|
||||
6
Tests/SwiftUIMathTests/Untitled.swift
Normal file
6
Tests/SwiftUIMathTests/Untitled.swift
Normal file
@@ -0,0 +1,6 @@
|
||||
//
|
||||
// Untitled.swift
|
||||
// swiftui-math
|
||||
//
|
||||
// Created by Guille Gonzalez on 31/12/25.
|
||||
//
|
||||
Reference in New Issue
Block a user