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

File diff suppressed because it is too large Load Diff