ReadViewSDK/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBChapterTailNormalizer.swift
shenlei 15b15d0e11 Remove all RDEPUBBackgroundTrace.log and bare print diagnostic calls
Keep only the evaluateFullPageMapTakeover: fullReplace diagnostic log
in RDEPUBPageMapReconciliationCoordinator.swift for future debugging.

Simplified RDEPUBBackgroundTrace to just the log method (removed measure
and debug gating). Removed all [EPUB][...], [ReadViewDemo], and
[EPUB][Pagination] print statements across the project.

Also includes earlier bug fixes:
- Fix stale currentPageNumber in extendPartial commit
- Fix pageCurl rebindVisiblePage during transition
- Fix keepCurrentWindow not removing pending update from queue
- Fix right-aligned text (巫鸿 bug) via avoidPageBreakInside,
  tail merger, and continuation paragraph normalization
- Add text-indent reset for aligned blocks in CSS compatibility layer

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-25 18:25:22 +08:00

198 lines
7.3 KiB
Swift

import Foundation
struct RDEPUBChapterTailNormalizer {
func normalize(
_ frames: [RDEPUBTextLayoutFrame],
content: NSAttributedString,
href: String
) -> [RDEPUBTextLayoutFrame] {
guard frames.count > 1 else { return frames }
var normalized = frames
var compacted: [RDEPUBTextLayoutFrame] = []
compacted.reserveCapacity(normalized.count)
for frame in normalized {
if shouldDropWhitespaceOnlyFrame(frame, in: content) {
let note = "normalized: dropped whitespace-only intermediate page \(NSStringFromRange(frame.contentRange))"
if var previous = compacted.popLast() {
previous.diagnostics.append(note)
compacted.append(previous)
} else {
}
continue
}
compacted.append(frame)
}
normalized = compacted
while let lastFrame = normalized.last,
shouldDropWhitespaceOnlyFrame(lastFrame, in: content) {
normalized.removeLast()
let note = "normalized: dropped whitespace-only trailing page \(NSStringFromRange(lastFrame.contentRange))"
if var previousFrame = normalized.popLast() {
previousFrame.diagnostics.append(note)
normalized.append(previousFrame)
} else {
}
}
guard normalized.count > 1,
let lastFrame = normalized.last,
let previousFrame = normalized.dropLast().last,
shouldMergeShortTrailingFrame(lastFrame, previousFrame: previousFrame, in: content) else {
return normalized
}
let mergedFrame = mergeTrailingFrame(previousFrame, with: lastFrame)
normalized.removeLast(2)
normalized.append(mergedFrame)
return normalized
}
private func shouldDropWhitespaceOnlyFrame(
_ frame: RDEPUBTextLayoutFrame,
in content: NSAttributedString
) -> Bool {
guard frame.contentRange.length > 0,
attachmentCount(in: content, range: frame.contentRange) == 0 else {
return false
}
return visibleCharacterCount(in: content, range: frame.contentRange) == 0
}
private func shouldMergeShortTrailingFrame(
_ trailingFrame: RDEPUBTextLayoutFrame,
previousFrame: RDEPUBTextLayoutFrame,
in content: NSAttributedString
) -> Bool {
guard trailingFrame.contentRange.length > 0,
NSMaxRange(previousFrame.contentRange) == trailingFrame.contentRange.location else {
return false
}
let visibleCount = visibleCharacterCount(in: content, range: trailingFrame.contentRange)
let trailingAttachmentCount = attachmentCount(in: content, range: trailingFrame.contentRange)
guard visibleCount <= 2,
trailingFrame.contentRange.length <= 2,
visibleCount > 0 || trailingAttachmentCount > 0 else {
return false
}
guard !frameHasAvoidPageBreakInside(trailingFrame, in: content) else {
return false
}
let previousVisibleCount = visibleCharacterCount(in: content, range: previousFrame.contentRange)
return previousVisibleCount >= max(visibleCount * 8, 12)
}
private func frameHasAvoidPageBreakInside(
_ frame: RDEPUBTextLayoutFrame,
in content: NSAttributedString
) -> Bool {
if frame.semanticHints.contains(.avoidPageBreakInside) {
return true
}
guard content.length > 0,
let safeRange = clampedRange(frame.contentRange, in: content),
safeRange.length > 0 else {
return false
}
var found = false
content.enumerateAttribute(.rdPageSemanticHints, in: safeRange) { value, _, stop in
guard let rawValue = value as? String else { return }
let hints = rawValue
.split(separator: ",")
.compactMap { RDEPUBTextSemanticHint(rawValue: String($0)) }
if hints.contains(.avoidPageBreakInside) {
found = true
stop.pointee = true
}
}
return found
}
private func mergeTrailingFrame(
_ previousFrame: RDEPUBTextLayoutFrame,
with trailingFrame: RDEPUBTextLayoutFrame
) -> RDEPUBTextLayoutFrame {
let mergedRange = NSRange(
location: previousFrame.contentRange.location,
length: NSMaxRange(trailingFrame.contentRange) - previousFrame.contentRange.location
)
return RDEPUBTextLayoutFrame(
contentRange: mergedRange,
breakReason: trailingFrame.breakReason,
blockRange: trailingFrame.blockRange ?? previousFrame.blockRange,
attachmentRanges: uniqueRanges(from: previousFrame.attachmentRanges + trailingFrame.attachmentRanges),
attachmentKinds: uniqueValues(from: previousFrame.attachmentKinds + trailingFrame.attachmentKinds),
blockKinds: uniqueValues(from: previousFrame.blockKinds + trailingFrame.blockKinds),
semanticHints: uniqueValues(from: previousFrame.semanticHints + trailingFrame.semanticHints),
attachmentPlacements: uniqueValues(from: previousFrame.attachmentPlacements + trailingFrame.attachmentPlacements),
trailingFragmentID: trailingFrame.trailingFragmentID ?? previousFrame.trailingFragmentID,
diagnostics: previousFrame.diagnostics
+ trailingFrame.diagnostics
+ ["normalized: merged short trailing page \(NSStringFromRange(trailingFrame.contentRange)) into previous page"]
)
}
private func visibleCharacterCount(
in content: NSAttributedString,
range: NSRange
) -> Int {
guard range.length > 0 else { return 0 }
let string = content.attributedSubstring(from: range).string
let filteredScalars = string.unicodeScalars.filter { scalar in
!CharacterSet.whitespacesAndNewlines.contains(scalar)
&& !CharacterSet.controlCharacters.contains(scalar)
}
return filteredScalars.count
}
private func attachmentCount(in content: NSAttributedString, range: NSRange) -> Int {
guard content.length > 0, range.length > 0 else { return 0 }
var count = 0
content.enumerateAttribute(.attachment, in: range) { value, _, _ in
if value != nil {
count += 1
}
}
return count
}
private func clampedRange(_ range: NSRange, in content: NSAttributedString) -> NSRange? {
guard range.location >= 0, range.length >= 0, range.location < content.length else {
return nil
}
return NSRange(location: range.location, length: min(range.length, content.length - range.location))
}
private func uniqueValues<T: Equatable>(from values: [T]) -> [T] {
values.reduce(into: [T]()) { result, value in
if !result.contains(value) {
result.append(value)
}
}
}
private func uniqueRanges(from ranges: [NSRange]) -> [NSRange] {
ranges.reduce(into: [NSRange]()) { result, value in
if !result.contains(value) {
result.append(value)
}
}
}
}