Add Typesetter tests

This commit is contained in:
Guille Gonzalez
2026-01-03 09:27:32 +01:00
parent e26d7d01b5
commit a80b1ea3db
2 changed files with 3443 additions and 42 deletions

View File

@@ -510,8 +510,8 @@ extension Math {
// MARK: - Interatom Line Breaking // MARK: - Interatom Line Breaking
/// Calculate the width that would result from adding this atom to the current line // Calculate the width that would result from adding this atom to the current line
/// Returns the approximate width including inter-element spacing // Returns the approximate width including inter-element spacing
func calculateAtomWidth(_ atom: Atom, prevNode: Atom?) -> CGFloat { func calculateAtomWidth(_ atom: Atom, prevNode: Atom?) -> CGFloat {
// Skip atoms that don't participate in normal width calculation // Skip atoms that don't participate in normal width calculation
// These are handled specially in the rendering code // These are handled specially in the rendering code
@@ -539,7 +539,7 @@ extension Math {
return interElementSpace + atomWidth return interElementSpace + atomWidth
} }
/// Calculate the current line width // Calculate the current line width
func getCurrentLineWidth() -> CGFloat { func getCurrentLineWidth() -> CGFloat {
if currentLine.length == 0 { if currentLine.length == 0 {
return 0 return 0
@@ -552,9 +552,9 @@ extension Math {
return CGFloat(CTLineGetTypographicBounds(ctLine, nil, nil, nil)) return CGFloat(CTLineGetTypographicBounds(ctLine, nil, nil, nil))
} }
/// Check if we should break to a new line before adding this atom // Check if we should break to a new line before adding this atom
/// Uses look-ahead to find better break points aesthetically // Uses look-ahead to find better break points aesthetically
/// Returns true if a line break was performed // Returns true if a line break was performed
@discardableResult @discardableResult
func checkAndPerformInteratomLineBreak(_ atom: Atom, prevNode: Atom?, nextAtoms: [Atom] = []) func checkAndPerformInteratomLineBreak(_ atom: Atom, prevNode: Atom?, nextAtoms: [Atom] = [])
-> Bool -> Bool
@@ -682,8 +682,8 @@ extension Math {
return true return true
} }
/// Estimate the approximate width of remaining atoms // Estimate the approximate width of remaining atoms
/// Returns a conservative (upper bound) estimate // Returns a conservative (upper bound) estimate
private func estimateRemainingAtomsWidth(_ atoms: [Atom]) -> CGFloat { private func estimateRemainingAtomsWidth(_ atoms: [Atom]) -> CGFloat {
// Use a simple heuristic: average character width * character count // Use a simple heuristic: average character width * character count
let avgCharWidth = styleFont.metrics.mathUnit let avgCharWidth = styleFont.metrics.mathUnit
@@ -706,7 +706,7 @@ extension Math {
return CGFloat(totalChars) * avgCharWidth * 1.5 return CGFloat(totalChars) * avgCharWidth * 1.5
} }
/// Perform the actual line break operation // Perform the actual line break operation
private func performInteratomLineBreak() { private func performInteratomLineBreak() {
// Reset optimization flag - after breaking, we need to check again // Reset optimization flag - after breaking, we need to check again
remainingContentFits = false remainingContentFits = false
@@ -730,8 +730,8 @@ extension Math {
currentLineIndexRange = NSMakeRange(NSNotFound, NSNotFound) currentLineIndexRange = NSMakeRange(NSNotFound, NSNotFound)
} }
/// Check if we should break before adding a complex display (fraction, radical, etc.) // Check if we should break before adding a complex display (fraction, radical, etc.)
/// Returns true if breaking is needed // Returns true if breaking is needed
func shouldBreakBeforeDisplay( func shouldBreakBeforeDisplay(
_ display: DisplayNode, prevNode: Atom?, displayType: AtomType = .ordinary _ display: DisplayNode, prevNode: Atom?, displayType: AtomType = .ordinary
) -> Bool { ) -> Bool {
@@ -755,20 +755,20 @@ extension Math {
return projectedWidth > maxWidth return projectedWidth > maxWidth
} }
/// Adjust the current position to avoid overlap between the new display and previous line's displays // Adjust the current position to avoid overlap between the new display and previous line's displays
/// This is called when adding displays to a line below the first line // This is called when adding displays to a line below the first line
/// //
/// Coordinate formulas (from test expectations): // Coordinate formulas (from test expectations):
/// - Bottom of display = position.y + descent // - Bottom of display = position.y + descent
/// - Top of display = position.y - ascent // - Top of display = position.y - ascent
/// - No overlap when: prevBottom <= currTop + spacing // - No overlap when: prevBottom <= currTop + spacing
/// - Which means: prevBottom <= (currPosition - currAscent) + spacing // - Which means: prevBottom <= (currPosition - currAscent) + spacing
/// - Rearranging: currPosition >= prevBottom + currAscent - spacing // - Rearranging: currPosition >= prevBottom + currAscent - spacing
/// //
/// Recursively adjust positions of a display and all its nested sub-displays // Recursively adjust positions of a display and all its nested sub-displays
/// Note: For DisplayRadical and DisplayFraction, their position setters automatically // Note: For DisplayRadical and DisplayFraction, their position setters automatically
/// update child positions (radicand/degree, numerator/denominator), so we don't need // update child positions (radicand/degree, numerator/denominator), so we don't need
/// to manually adjust those. We only need to adjust subdisplays within DisplayList. // to manually adjust those. We only need to adjust subdisplays within DisplayList.
private func adjustDisplayPosition(_ display: DisplayNode, by delta: CGFloat) { private func adjustDisplayPosition(_ display: DisplayNode, by delta: CGFloat) {
display.position.y += delta display.position.y += delta
@@ -783,12 +783,12 @@ extension Math {
// Their position setters handle updating child positions automatically // Their position setters handle updating child positions automatically
} }
/// Adjust position to avoid overlap with previous line // Adjust position to avoid overlap with previous line
/// In CoreText's Y-up coordinate system: // In CoreText's Y-up coordinate system:
/// - Positive Y = upward, Negative Y = downward // - Positive Y = upward, Negative Y = downward
/// - Top of display = position + ascent (higher Y) // - Top of display = position + ascent (higher Y)
/// - Bottom of display = position - descent (lower Y) // - Bottom of display = position - descent (lower Y)
/// - No overlap when: prevBottom >= currTop (with spacing) // - No overlap when: prevBottom >= currTop (with spacing)
private func adjustPositionToAvoidOverlap(_ display: DisplayNode) { private func adjustPositionToAvoidOverlap(_ display: DisplayNode) {
// Find all displays on previous lines and calculate their minimum bottom edge // Find all displays on previous lines and calculate their minimum bottom edge
// In Y-up: Bottom = position - descent (lower Y value) // In Y-up: Bottom = position - descent (lower Y value)
@@ -825,7 +825,7 @@ extension Math {
} }
} }
/// Perform line break for complex displays // Perform line break for complex displays
func performLineBreak() { func performLineBreak() {
if currentLine.length > 0 { if currentLine.length > 0 {
self.addDisplayLine() self.addDisplayLine()
@@ -842,8 +842,8 @@ extension Math {
currentLineStartIndex = displayAtoms.count currentLineStartIndex = displayAtoms.count
} }
/// Calculate the height of the current line based on actual display heights // Calculate the height of the current line based on actual display heights
/// Returns the total height (max ascent + max descent) plus minimum spacing // Returns the total height (max ascent + max descent) plus minimum spacing
func calculateCurrentLineHeight() -> CGFloat { func calculateCurrentLineHeight() -> CGFloat {
// If no displays added for current line, use default spacing // If no displays added for current line, use default spacing
guard currentLineStartIndex < displayAtoms.count else { guard currentLineStartIndex < displayAtoms.count else {
@@ -867,8 +867,8 @@ extension Math {
return max(lineHeight, styleFont.font.size * 1.2) return max(lineHeight, styleFont.font.size * 1.2)
} }
/// Estimate the width of an atom including its scripts (without actually creating the displays) // Estimate the width of an atom including its scripts (without actually creating the displays)
/// This is used for width-checking decisions for atoms with super/subscripts // This is used for width-checking decisions for atoms with super/subscripts
func estimateAtomWidthWithScripts(_ atom: Atom) -> CGFloat { func estimateAtomWidthWithScripts(_ atom: Atom) -> CGFloat {
// Estimate base atom width // Estimate base atom width
var atomWidth = CGFloat(atom.nucleus.count) * styleFont.font.size * 0.5 // rough estimate var atomWidth = CGFloat(atom.nucleus.count) * styleFont.font.size * 0.5 // rough estimate
@@ -897,8 +897,8 @@ extension Math {
return atomWidth return atomWidth
} }
/// Calculate break penalty score for breaking after a given atom type // Calculate break penalty score for breaking after a given atom type
/// Lower scores indicate better break points (0 = best, higher = worse) // Lower scores indicate better break points (0 = best, higher = worse)
func calculateBreakPenalty(afterAtom: Atom?, beforeAtom: Atom?) -> Int { func calculateBreakPenalty(afterAtom: Atom?, beforeAtom: Atom?) -> Int {
// No atom context - neutral penalty // No atom context - neutral penalty
guard let after = afterAtom else { return 50 } guard let after = afterAtom else { return 50 }
@@ -1576,7 +1576,7 @@ extension Math {
// MARK: - Unicode-aware Line Breaking // MARK: - Unicode-aware Line Breaking
/// Find the best break point using Core Text, with conservative number protection // Find the best break point using Core Text, with conservative number protection
func findBestBreakPoint(in text: String, font: CTFont, maxWidth: CGFloat) -> String.Index? { func findBestBreakPoint(in text: String, font: CTFont, maxWidth: CGFloat) -> String.Index? {
let attributes: [NSAttributedString.Key: Any] = [ let attributes: [NSAttributedString.Key: Any] = [
kCTFontAttributeName as NSAttributedString.Key: font kCTFontAttributeName as NSAttributedString.Key: font
@@ -1612,7 +1612,7 @@ extension Math {
return findPreviousSafeBreak(in: text, before: breakIndex) return findPreviousSafeBreak(in: text, before: breakIndex)
} }
/// Check if breaking at this index would split a number // Check if breaking at this index would split a number
func isBreakingSafeForNumbers(text: String, breakIndex: String.Index) -> Bool { func isBreakingSafeForNumbers(text: String, breakIndex: String.Index) -> Bool {
guard breakIndex > text.startIndex && breakIndex < text.endIndex else { guard breakIndex > text.startIndex && breakIndex < text.endIndex else {
return true return true
@@ -1668,7 +1668,7 @@ extension Math {
return true // Safe to break return true // Safe to break
} }
/// Find previous safe break point before the given index // Find previous safe break point before the given index
func findPreviousSafeBreak(in text: String, before breakIndex: String.Index) -> String.Index? { func findPreviousSafeBreak(in text: String, before breakIndex: String.Index) -> String.Index? {
var currentIndex = breakIndex var currentIndex = breakIndex
@@ -1690,7 +1690,7 @@ extension Math {
return nil return nil
} }
/// Check if the current line exceeds maxWidth and break if needed // Check if the current line exceeds maxWidth and break if needed
func checkAndBreakLine() { func checkAndBreakLine() {
guard maxWidth > 0 && currentLine.length > 0 else { return } guard maxWidth > 0 && currentLine.length > 0 else { return }

File diff suppressed because it is too large Load Diff