- 实现EPUB阅读器搜索功能及选中注释功能 - 优化CFI模块,修复代码审查发现的11个问题 - 实现大书远距目录跳转与后台补全优化方案 - 优化设置面板与章节运行时联动 - 重构及大量改进优化
166 lines
6.3 KiB
Swift
166 lines
6.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 {
|
|
|
|
#if DEBUG
|
|
print("[EPUB][Pagination] href=\(href) dropped leading/intermediate whitespace frame \(NSStringFromRange(frame.contentRange))")
|
|
#endif
|
|
}
|
|
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 {
|
|
#if DEBUG
|
|
print("[EPUB][Pagination] href=\(href) dropped trailing frame \(NSStringFromRange(lastFrame.contentRange))")
|
|
#endif
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
let previousVisibleCount = visibleCharacterCount(in: content, range: previousFrame.contentRange)
|
|
return previousVisibleCount >= max(visibleCount * 8, 12)
|
|
}
|
|
|
|
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 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)
|
|
}
|
|
}
|
|
}
|
|
}
|