refactor: 添加中文注释 + 优化模块结构

- 给全部 78 个 Swift 源文件添加详细的中文注释(文件级、类级、方法级)
- 删除 LegacyRDReaderController/ 死代码目录(16 文件 4592 行)
- 根目录翻页容器文件移入 ReaderView/ 目录
- Resources/ 移入 EPUBCore/Resources/(与使用者归属一致)
- RDEPUBTextIndexTable.swift 移入 EPUBTextRendering/(消除反向依赖)
- RDURLReaderController.swift 移入 EPUBUI/(入口控制器归入 UI 层)
- 更新 podspec 资源路径
This commit is contained in:
shen
2026-05-25 10:19:14 +08:00
parent 5af90c1148
commit 54798ba578
88 changed files with 2492 additions and 4469 deletions
@@ -5,11 +5,29 @@ import UIKit
import DTCoreText
#endif
/// CoreText
///
///
/// 1. avoidPageBreakInside WXRead 退
/// 2. keepWithNext
/// 3. pageBreakBefore/After
/// 4. pageRelate
/// 5.
/// 6. CoreText
///
///
/// - DTCoreText DTCoreTextLayouter DTCoreTextLayoutFrame
/// - CoreText 退CTFramesetterCreateFrame
struct RDEPUBTextLayouter {
///
private let attributedString: NSAttributedString
///
private let pageSize: CGSize
/// CoreText
private let framesetter: CTFramesetter
/// CTFrame
private let path: CGPath
/// avoidPageBreakInside
private let config: RDEPUBTextLayoutConfig
init(attributedString: NSAttributedString, pageSize: CGSize, config: RDEPUBTextLayoutConfig = .default) {
@@ -20,6 +38,8 @@ struct RDEPUBTextLayouter {
self.path = CGPath(rect: CGRect(origin: .zero, size: pageSize), transform: nil)
}
///
/// DTCoreText CoreText
func layoutFrames(fragmentOffsets: [String: Int] = [:]) -> [RDEPUBTextLayoutFrame] {
guard attributedString.length > 0, pageSize.width > 0, pageSize.height > 0 else {
return []
@@ -32,6 +52,11 @@ struct RDEPUBTextLayouter {
#endif
}
// MARK: - CoreText 退
/// 使 CoreText API
///
/// CTFramesetterCreateFrame
private func layoutFramesUsingCoreText(fragmentOffsets: [String: Int]) -> [RDEPUBTextLayoutFrame] {
guard attributedString.length > 0, pageSize.width > 0, pageSize.height > 0 else {
return []
@@ -49,7 +74,7 @@ struct RDEPUBTextLayouter {
let proposedRange = NSRange(location: location, length: visibleRange.length)
// Line-level avoidPageBreakInside (WXRead approach: scan CTFrame lines backward)
// avoidPageBreakInside WXRead
let avoidAdjusted = trimmedRangeForAvoidPageBreakInside(from: frame, proposed: proposedRange)
let lineAdjusted = trimmedRangeForKeepWithNext(from: frame, proposed: avoidAdjusted)
let lineRanges = lineRanges(from: frame)
@@ -89,7 +114,10 @@ struct RDEPUBTextLayouter {
return frames
}
// MARK: - DTCoreText
#if canImport(DTCoreText)
/// 使 DTCoreTextLayouter
private func layoutFramesUsingDTCoreText(fragmentOffsets: [String: Int]) -> [RDEPUBTextLayoutFrame] {
guard let layouter = DTCoreTextLayouter(attributedString: attributedString) else {
return layoutFramesUsingCoreText(fragmentOffsets: fragmentOffsets)
@@ -152,6 +180,18 @@ struct RDEPUBTextLayouter {
}
#endif
// MARK: -
/// CoreText
///
///
/// 1. chapterEnd
/// 2. pageBreakBefore/After
/// 3. pageRelate
/// 4.
/// 5. 使
///
/// 55%
private func adjustedRange(
from proposedRange: NSRange,
totalLength: Int,
@@ -193,6 +233,7 @@ struct RDEPUBTextLayouter {
)
}
// 55%
let minLength = max(Int(Double(proposedRange.length) * 0.55), 1)
let minimumEnd = proposedRange.location + minLength
@@ -203,6 +244,7 @@ struct RDEPUBTextLayouter {
let currentSemanticHints = proposedSemanticHints
let currentAttachmentPlacements = proposedAttachmentPlacements
// 1. pageBreakBefore/After
if let semanticBoundary = preferredSemanticBoundary(
in: proposedRange,
minimumEnd: minimumEnd
@@ -230,6 +272,7 @@ struct RDEPUBTextLayouter {
)
}
// 2. pageRelate
if let pageRelateBoundary = preferredPageRelateBoundary(
after: proposedRange,
minimumEnd: minimumEnd,
@@ -258,6 +301,7 @@ struct RDEPUBTextLayouter {
)
}
// 3.
if let attachmentBoundary = preferredAttachmentBoundary(
in: proposedRange,
minimumEnd: minimumEnd
@@ -284,6 +328,7 @@ struct RDEPUBTextLayouter {
)
}
// 4.
return (
range: proposedRange,
breakReason: .frameLimit,
@@ -305,6 +350,11 @@ struct RDEPUBTextLayouter {
)
}
// MARK: -
/// pageBreakBefore / pageBreakAfter
///
/// minimumEnd
private func preferredSemanticBoundary(
in range: NSRange,
minimumEnd: Int
@@ -337,6 +387,8 @@ struct RDEPUBTextLayouter {
return boundary
}
/// .attachment .centered
///
private func preferredAttachmentBoundary(in range: NSRange, minimumEnd: Int) -> Int? {
var boundary: Int?
attributedString.enumerateAttribute(.rdPageAttachmentKind, in: range) { value, attributeRange, stop in
@@ -346,9 +398,8 @@ struct RDEPUBTextLayouter {
let placement = attachmentPlacement(at: location)
let blockKind = blockKind(at: location)
// Mirror WXRead more closely: only block-level attachments should
// push the entire block to the next page. Inline footnote icons and
// other inline attachments must not cause a whole paragraph to move.
// WXRead
//
let isBlockLevelAttachment = blockKind == .attachment || placement == .centered
guard isBlockLevelAttachment else { return }
@@ -361,6 +412,8 @@ struct RDEPUBTextLayouter {
return boundary
}
/// pageRelate pageRelate
///
private func preferredPageRelateBoundary(
after range: NSRange,
minimumEnd: Int,
@@ -384,6 +437,9 @@ struct RDEPUBTextLayouter {
return lastLineStart
}
// MARK: -
///
private func blockRange(at location: Int) -> NSRange? {
guard location >= 0, location < attributedString.length else { return nil }
let attributes = attributedString.attributes(at: location, effectiveRange: nil)
@@ -393,6 +449,7 @@ struct RDEPUBTextLayouter {
return nil
}
///
private func blockKind(at location: Int) -> RDEPUBTextBlockKind? {
guard location >= 0, location < attributedString.length else { return nil }
let attributes = attributedString.attributes(at: location, effectiveRange: nil)
@@ -400,6 +457,7 @@ struct RDEPUBTextLayouter {
return RDEPUBTextBlockKind(rawValue: rawValue)
}
///
private func attachmentPlacement(at location: Int) -> RDEPUBTextAttachmentPlacement? {
guard location >= 0, location < attributedString.length else { return nil }
let attributes = attributedString.attributes(at: location, effectiveRange: nil)
@@ -407,6 +465,7 @@ struct RDEPUBTextLayouter {
return RDEPUBTextAttachmentPlacement(rawValue: rawValue)
}
///
private func paragraphRange(containing location: Int) -> NSRange {
let source = attributedString.string as NSString
guard source.length > 0 else { return NSRange(location: 0, length: 0) }
@@ -414,6 +473,7 @@ struct RDEPUBTextLayouter {
return source.paragraphRange(for: NSRange(location: safeLocation, length: 0))
}
///
private func attachmentRanges(in range: NSRange) -> [NSRange] {
var results: [NSRange] = []
attributedString.enumerateAttribute(.rdPageAttachmentKind, in: range) { value, attributeRange, _ in
@@ -423,6 +483,7 @@ struct RDEPUBTextLayouter {
return results
}
///
private func semanticHints(at location: Int) -> [RDEPUBTextSemanticHint] {
guard location >= 0, location < attributedString.length else { return [] }
let attributes = attributedString.attributes(at: location, effectiveRange: nil)
@@ -432,6 +493,7 @@ struct RDEPUBTextLayouter {
.compactMap { RDEPUBTextSemanticHint(rawValue: String($0)) }
}
///
private func attachmentKinds(in range: NSRange) -> [RDEPUBTextAttachmentKind] {
var kinds: [RDEPUBTextAttachmentKind] = []
attributedString.enumerateAttribute(.rdPageAttachmentKind, in: range) { value, _, _ in
@@ -445,6 +507,7 @@ struct RDEPUBTextLayouter {
return kinds
}
///
private func blockKinds(in range: NSRange) -> [RDEPUBTextBlockKind] {
var kinds: [RDEPUBTextBlockKind] = []
attributedString.enumerateAttribute(.rdPageBlockKind, in: range) { value, _, _ in
@@ -458,6 +521,7 @@ struct RDEPUBTextLayouter {
return kinds
}
///
private func semanticHints(in range: NSRange) -> [RDEPUBTextSemanticHint] {
var hints: [RDEPUBTextSemanticHint] = []
attributedString.enumerateAttribute(.rdPageSemanticHints, in: range) { value, _, _ in
@@ -469,6 +533,7 @@ struct RDEPUBTextLayouter {
return hints
}
///
private func attachmentPlacements(in range: NSRange) -> [RDEPUBTextAttachmentPlacement] {
var placements: [RDEPUBTextAttachmentPlacement] = []
attributedString.enumerateAttribute(.rdPageAttachmentPlacement, in: range) { value, _, _ in
@@ -482,6 +547,7 @@ struct RDEPUBTextLayouter {
return placements
}
/// fragment ID
private func nearestTrailingFragmentID(
endingAt location: Int,
fragmentOffsets: [String: Int]
@@ -492,11 +558,11 @@ struct RDEPUBTextLayouter {
.key
}
// MARK: - Line-level avoidPageBreakInside (WXRead approach)
// MARK: - avoidPageBreakInsideWXRead
/// Scans CTFrame lines backward from the last line, removing trailing lines
/// that fall inside an avoidPageBreakInside block. Mirrors WXRead's
/// WRCoreTextLayoutFrame.avoidPageBreakInsideByRemovingLastLinesIfNeeded.
/// CTFrame avoidPageBreakInside
/// WXRead WRCoreTextLayoutFrame.avoidPageBreakInsideByRemovingLastLinesIfNeeded
/// 3 kMaxLinesToRemove
private func trimmedRangeForAvoidPageBreakInside(
from frame: CTFrame,
proposed: NSRange
@@ -509,8 +575,8 @@ struct RDEPUBTextLayouter {
var origins = [CGPoint](repeating: .zero, count: lines.count)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins)
// Scan backward: count consecutive trailing lines inside protected blocks
// kMaxLinesToRemove = 3 (same as WXRead)
//
// kMaxLinesToRemove = 3 WXRead
let kMaxLinesToRemove = 3
var linesToRemove = 0
@@ -521,8 +587,7 @@ struct RDEPUBTextLayouter {
if lineIsInAvoidPageBreakInsideBlock(lineNSRange) {
linesToRemove += 1
if linesToRemove >= kMaxLinesToRemove {
// Don't remove more than kMaxLinesToRemove; stop here
// (line at index i stays, so linesToRemove stays at kMaxLinesToRemove)
// kMaxLinesToRemove
linesToRemove = kMaxLinesToRemove
break
}
@@ -535,7 +600,7 @@ struct RDEPUBTextLayouter {
let validLineCount = lines.count - linesToRemove
guard validLineCount > 0 else {
// All lines are in protected blocks; fall back to proposed
// 退
return proposed
}
@@ -549,6 +614,7 @@ struct RDEPUBTextLayouter {
return NSRange(location: proposed.location, length: adjustedLength)
}
/// CoreText keepWithNext
private func trimmedRangeForKeepWithNext(
from frame: CTFrame,
proposed: NSRange
@@ -561,7 +627,10 @@ struct RDEPUBTextLayouter {
return trimmedRangeForKeepWithNext(proposed: proposed, lineRanges: lineRanges)
}
// MARK: - DTCoreText
#if canImport(DTCoreText)
/// DTCoreText avoidPageBreakInside
private func trimmedRangeForAvoidPageBreakInside(
from layoutFrame: DTCoreTextLayoutFrame,
proposed: NSRange
@@ -603,6 +672,7 @@ struct RDEPUBTextLayouter {
return NSRange(location: proposed.location, length: adjustedLength)
}
/// DTCoreText keepWithNext
private func trimmedRangeForKeepWithNext(
from layoutFrame: DTCoreTextLayoutFrame,
proposed: NSRange
@@ -615,6 +685,8 @@ struct RDEPUBTextLayouter {
}
#endif
/// keepWithNext
/// 3
private func trimmedRangeForKeepWithNext(
proposed: NSRange,
lineRanges: [NSRange]
@@ -648,7 +720,7 @@ struct RDEPUBTextLayouter {
return NSRange(location: proposed.location, length: adjustedLength)
}
/// Checks if a line's string range intersects with an avoidPageBreakInside block.
/// avoidPageBreakInside
private func lineIsInAvoidPageBreakInsideBlock(_ lineRange: NSRange) -> Bool {
var found = false
let probeRange = NSRange(location: lineRange.location, length: max(lineRange.length, 1))
@@ -665,6 +737,7 @@ struct RDEPUBTextLayouter {
return found
}
/// keepWithNext
private func lineIsInKeepWithNextBlock(_ lineRange: NSRange) -> Bool {
var found = false
let probeRange = NSRange(location: lineRange.location, length: max(lineRange.length, 1))
@@ -681,6 +754,7 @@ struct RDEPUBTextLayouter {
return found
}
/// CTFrame
private func lineRanges(from frame: CTFrame) -> [NSRange] {
let lines = CTFrameGetLines(frame) as! [CTLine]
return lines.map {
@@ -690,6 +764,7 @@ struct RDEPUBTextLayouter {
}
#if canImport(DTCoreText)
/// DTCoreTextLayoutFrame
private func lineRanges(from layoutFrame: DTCoreTextLayoutFrame) -> [NSRange] {
guard let lines = layoutFrame.lines as? [DTCoreTextLayoutLine] else {
return []
@@ -698,6 +773,9 @@ struct RDEPUBTextLayouter {
}
#endif
// MARK: -
///
private func diagnostics(
reason: RDEPUBTextPageBreakReason,
range: NSRange,