feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化

- 实现EPUB阅读器搜索功能及选中注释功能
- 优化CFI模块,修复代码审查发现的11个问题
- 实现大书远距目录跳转与后台补全优化方案
- 优化设置面板与章节运行时联动
- 重构及大量改进优化
This commit is contained in:
shenlei
2026-06-22 20:26:34 +08:00
parent f50495ad91
commit c65c190b71
178 changed files with 11380 additions and 6728 deletions
@@ -5,24 +5,20 @@ import UIKit
import DTCoreText
#endif
///
///
///
/// 1. avoidPageBreakInside WXRead 退
/// 2. keepWithNext
/// 3. pageBreakBefore/After
/// 4. pageRelate
/// 5.
/// 6. CoreText
struct RDEPUBChapterPageCounter {
private let factory: RDEPUBCoreTextPageFrameFactory
private let attributedString: NSAttributedString
private let pageSize: CGSize
private let config: RDEPUBTextLayoutConfig
private let pageBreakPolicy: RDEPUBPageBreakPolicy
private let framesetter: CTFramesetter
/// DTCoreText
private let dtLayoutRect: CGRect
init(factory: RDEPUBCoreTextPageFrameFactory) {
@@ -35,7 +31,6 @@ struct RDEPUBChapterPageCounter {
self.dtLayoutRect = factory.config.contentRect(fallback: factory.pageSize)
}
///
func layoutFrames(fragmentOffsets: [String: Int] = [:]) -> [RDEPUBTextLayoutFrame] {
guard attributedString.length > 0, pageSize.width > 0, pageSize.height > 0 else {
return []
@@ -48,8 +43,6 @@ struct RDEPUBChapterPageCounter {
#endif
}
// MARK: - CoreText 退
private func layoutFramesUsingCoreText(fragmentOffsets: [String: Int]) -> [RDEPUBTextLayoutFrame] {
guard attributedString.length > 0, pageSize.width > 0, pageSize.height > 0 else {
return []
@@ -129,9 +122,8 @@ struct RDEPUBChapterPageCounter {
return frames
}
// MARK: - DTCoreText
#if canImport(DTCoreText)
private func layoutFramesUsingDTCoreText(fragmentOffsets: [String: Int]) -> [RDEPUBTextLayoutFrame] {
guard config.numberOfColumns == 1 else {
return layoutFramesUsingCoreText(fragmentOffsets: fragmentOffsets)
@@ -252,9 +244,6 @@ struct RDEPUBChapterPageCounter {
}
#endif
// MARK: - WXRead
/// CoreText frame path
private func proposedVisibleRange(
from frame: CTFrame,
start location: Int,
@@ -5,13 +5,14 @@ import UIKit
import DTCoreText
#endif
/// CoreText
///
/// RDEPUBChapterPageCounter RDEPUBPageBreakPolicy
struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
let attributedString: NSAttributedString
let pageSize: CGSize
let config: RDEPUBTextLayoutConfig
private let pageBreakPolicy: RDEPUBPageBreakPolicy
init(attributedString: NSAttributedString, pageSize: CGSize, config: RDEPUBTextLayoutConfig = .default) {
@@ -21,13 +22,10 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
self.pageBreakPolicy = RDEPUBPageBreakPolicy(attributedString: attributedString)
}
/// 便使
init() {
self.init(attributedString: NSAttributedString(), pageSize: .zero, config: .default)
}
// MARK: - RDEPUBPageFrameBuilding
func makeFrames(
attributedString: NSAttributedString,
pageSize: CGSize,
@@ -39,9 +37,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
return counter.layoutFrames(fragmentOffsets: fragmentOffsets)
}
// MARK: -
/// CGPath CTFramesetterCreateFrame
static func makeLayoutPath(pageSize: CGSize, config: RDEPUBTextLayoutConfig) -> CGPath {
let columnRects = config.columnRects(fallback: pageSize)
guard columnRects.count > 1 else {
@@ -55,9 +50,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
return path
}
// MARK: -
/// CTFrame avoidPageBreakInside
func trimmedRangeForAvoidPageBreakInside(
from frame: CTFrame,
proposed: NSRange
@@ -74,7 +66,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
return trimmedRangeForAvoidPageBreakInside(proposed: proposed, lineRanges: lineRanges)
}
/// CoreText keepWithNext
func trimmedRangeForKeepWithNext(
from frame: CTFrame,
proposed: NSRange
@@ -88,7 +79,7 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
}
#if canImport(DTCoreText)
/// DTCoreText avoidPageBreakInside
func trimmedRangeForAvoidPageBreakInside(
from layoutFrame: DTCoreTextLayoutFrame,
proposed: NSRange
@@ -102,7 +93,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
return trimmedRangeForAvoidPageBreakInside(proposed: proposed, lineRanges: lineRanges)
}
/// DTCoreText keepWithNext
func trimmedRangeForKeepWithNext(
from layoutFrame: DTCoreTextLayoutFrame,
proposed: NSRange
@@ -115,7 +105,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
}
#endif
/// avoidPageBreakInside
func trimmedRangeForAvoidPageBreakInside(
proposed: NSRange,
lineRanges: [NSRange]
@@ -155,7 +144,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
return NSRange(location: proposed.location, length: adjustedLength)
}
/// keepWithNext
func trimmedRangeForKeepWithNext(
proposed: NSRange,
lineRanges: [NSRange]
@@ -195,7 +183,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
return NSRange(location: proposed.location, length: adjustedLength)
}
/// avoidWidows / avoidOrphans
func trimmedRangeForWidowAndOrphanControl(
proposed: NSRange,
lineRanges: [NSRange]
@@ -227,9 +214,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
return adjusted
}
// MARK: -
/// CTFrame
static func lineRanges(from frame: CTFrame) -> [NSRange] {
let lines = CTFrameGetLines(frame) as! [CTLine]
return lines.map {
@@ -239,7 +223,7 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
}
#if canImport(DTCoreText)
/// DTCoreTextLayoutFrame
static func lineRanges(from layoutFrame: DTCoreTextLayoutFrame) -> [NSRange] {
guard let lines = layoutFrame.lines as? [DTCoreTextLayoutLine] else {
return []
@@ -248,9 +232,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
}
#endif
// MARK: -
///
func blockRange(at location: Int) -> NSRange? {
guard location >= 0, location < attributedString.length else { return nil }
let attributes = attributedString.attributes(at: location, effectiveRange: nil)
@@ -260,7 +241,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
return nil
}
///
func blockKind(at location: Int) -> RDEPUBTextBlockKind? {
guard location >= 0, location < attributedString.length else { return nil }
let attributes = attributedString.attributes(at: location, effectiveRange: nil)
@@ -268,7 +248,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
return RDEPUBTextBlockKind(rawValue: rawValue)
}
///
func attachmentPlacement(at location: Int) -> RDEPUBTextAttachmentPlacement? {
guard location >= 0, location < attributedString.length else { return nil }
let attributes = attributedString.attributes(at: location, effectiveRange: nil)
@@ -276,7 +255,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
return RDEPUBTextAttachmentPlacement(rawValue: rawValue)
}
///
func paragraphRange(containing location: Int) -> NSRange {
let source = attributedString.string as NSString
guard source.length > 0 else { return NSRange(location: 0, length: 0) }
@@ -284,7 +262,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
return source.paragraphRange(for: NSRange(location: safeLocation, length: 0))
}
///
func attachmentRanges(in range: NSRange) -> [NSRange] {
guard let safeRange = clampedRange(range), safeRange.length > 0 else {
return []
@@ -297,7 +274,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
return results
}
///
func semanticHints(at location: Int) -> [RDEPUBTextSemanticHint] {
guard location >= 0, location < attributedString.length else { return [] }
let attributes = attributedString.attributes(at: location, effectiveRange: nil)
@@ -307,8 +283,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
.compactMap { RDEPUBTextSemanticHint(rawValue: String($0)) }
}
// MARK: - Widow / Orphan
private func trimmedRangeAvoidingWidow(
proposed: NSRange,
lineRanges: [NSRange]
@@ -408,7 +382,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
return lineCount
}
///
func attachmentKinds(in range: NSRange) -> [RDEPUBTextAttachmentKind] {
guard let safeRange = clampedRange(range), safeRange.length > 0 else {
return []
@@ -425,7 +398,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
return kinds
}
///
func blockKinds(in range: NSRange) -> [RDEPUBTextBlockKind] {
guard let safeRange = clampedRange(range), safeRange.length > 0 else {
return []
@@ -442,7 +414,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
return kinds
}
///
func semanticHints(in range: NSRange) -> [RDEPUBTextSemanticHint] {
guard let safeRange = clampedRange(range), safeRange.length > 0 else {
return []
@@ -457,7 +428,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
return hints
}
///
func attachmentPlacements(in range: NSRange) -> [RDEPUBTextAttachmentPlacement] {
guard let safeRange = clampedRange(range), safeRange.length > 0 else {
return []
@@ -474,7 +444,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
return placements
}
/// attributedString
func clampedRange(_ range: NSRange) -> NSRange? {
guard range.location >= 0, range.length >= 0 else { return nil }
guard attributedString.length > 0 else {
@@ -486,7 +455,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
return NSRange(location: range.location, length: min(range.length, maxLength))
}
/// fragment ID
func nearestTrailingFragmentID(
endingAt location: Int,
fragmentOffsets: [String: Int]
@@ -497,9 +465,6 @@ struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding {
.key
}
// MARK: -
///
func diagnostics(
reason: RDEPUBTextPageBreakReason,
range: NSRange,
@@ -1,19 +1,14 @@
import Foundation
import UIKit
///
///
/// CoreText frame attributed string
struct RDEPUBPageBreakPolicy {
private let attributedString: NSAttributedString
init(attributedString: NSAttributedString) {
self.attributedString = attributedString
}
// MARK: -
/// avoidPageBreakInside
func lineIsInAvoidPageBreakInsideBlock(_ lineRange: NSRange) -> Bool {
guard let probeRange = clampedProbeRange(for: lineRange) else {
return false
@@ -35,7 +30,6 @@ struct RDEPUBPageBreakPolicy {
return found
}
/// keepWithNext
func lineIsInKeepWithNextBlock(_ lineRange: NSRange) -> Bool {
guard let probeRange = clampedProbeRange(for: lineRange) else {
return false
@@ -54,14 +48,6 @@ struct RDEPUBPageBreakPolicy {
return found
}
// MARK: -
/// CoreText/DTCoreText
///
///
/// 1. chapterEnd
/// 2. pageRelate
/// 3. 使
func adjustedRange(
from proposedRange: NSRange,
totalLength: Int,
@@ -111,8 +97,6 @@ struct RDEPUBPageBreakPolicy {
let currentSemanticHints = proposedSemanticHints
let currentAttachmentPlacements = proposedAttachmentPlacements
// WXRead CTFrame pageRelate
//
if let pageRelateBoundary = preferredPageRelateBoundary(
after: proposedRange,
minimumEnd: proposedRange.location + 1,
@@ -142,7 +126,6 @@ struct RDEPUBPageBreakPolicy {
)
}
//
return (
range: proposedRange,
breakReason: .frameLimit,
@@ -164,9 +147,6 @@ struct RDEPUBPageBreakPolicy {
)
}
// MARK: -
/// pageRelate
func preferredPageRelateBoundary(
after range: NSRange,
minimumEnd: Int,
@@ -192,8 +172,6 @@ struct RDEPUBPageBreakPolicy {
return lastLineStart
}
// MARK: -
private func shouldTreatAvoidHintAsBlockProtection(_ attributes: [NSAttributedString.Key: Any]) -> Bool {
guard let rawValue = attributes[.rdPageSemanticHints] as? String else {
return false
@@ -1,33 +1,27 @@
import Foundation
/// CoreText
///
/// `RDEPUBTextLayouter`
/// `RDEPUBTextBookBuilder` `RDEPUBTextLayoutFrame`
/// `RDEPUBTextPage` `RDEPUBTextBook`
struct RDEPUBTextLayoutFrame: Equatable {
///
var contentRange: NSRange
///
var breakReason: RDEPUBTextPageBreakReason
/// HTML
var blockRange: NSRange?
///
var attachmentRanges: [NSRange]
///
var attachmentKinds: [RDEPUBTextAttachmentKind]
/// HTML
var blockKinds: [RDEPUBTextBlockKind]
///
var semanticHints: [RDEPUBTextSemanticHint]
/// 线
var attachmentPlacements: [RDEPUBTextAttachmentPlacement]
/// fragment ID
var trailingFragmentID: String?
///
var diagnostics: [String]
///
var metadata: RDEPUBTextPageMetadata {
RDEPUBTextPageMetadata(
breakReason: breakReason,
@@ -5,12 +5,6 @@ import UIKit
import DTCoreText
#endif
/// CoreText Facade
///
///
/// - RDEPUBCoreTextPageFrameFactory
/// - RDEPUBPageBreakPolicy keepWithNext
/// - RDEPUBChapterPageCounter
struct RDEPUBTextLayouter {
private let counter: RDEPUBChapterPageCounter
@@ -19,7 +13,6 @@ struct RDEPUBTextLayouter {
self.counter = RDEPUBChapterPageCounter(factory: factory)
}
///
func layoutFrames(fragmentOffsets: [String: Int] = [:]) -> [RDEPUBTextLayoutFrame] {
counter.layoutFrames(fragmentOffsets: fragmentOffsets)
}
@@ -1,30 +1,18 @@
// RDEPUBTextPaginationInterfaces.swift
// EPUB
import Foundation
import UIKit
// MARK: -
///
struct RDEPUBPageBreakDecision {
///
var range: NSRange
///
var reason: RDEPUBTextPageBreakReason
///
var diagnostics: [String]
}
///
protocol RDEPUBChapterPageCounting {
///
/// - Parameters:
/// - attributedString:
/// - pageSize:
/// - config:
/// - fragmentOffsets: fragment
/// - Returns:
func pageRanges(
for attributedString: NSAttributedString,
pageSize: CGSize,
@@ -33,15 +21,8 @@ protocol RDEPUBChapterPageCounting {
) -> [RDEPUBPageBreakDecision]
}
///
protocol RDEPUBPageFrameBuilding {
///
/// - Parameters:
/// - attributedString:
/// - pageSize:
/// - config:
/// - fragmentOffsets: fragment
/// - Returns:
func makeFrames(
attributedString: NSAttributedString,
pageSize: CGSize,
@@ -1,18 +1,8 @@
import CoreText
import UIKit
// MARK: - NSAttributedString
/// NSAttributedString 便 CoreText
extension NSAttributedString {
///
///
/// `RDEPUBTextBookBuilder` `RDPlainTextBookBuilder`
/// - Parameters:
/// - size:
/// - fragmentOffsets: fragment ID
/// - config: avoidPageBreakInside
/// - Returns:
func rd_paginatedFrames(
size: CGSize,
fragmentOffsets: [String: Int] = [:],
@@ -23,17 +13,13 @@ extension NSAttributedString {
return counter.layoutFrames(fragmentOffsets: fragmentOffsets)
}
/// NSRange
func ss_pageRanges(size: CGSize) -> [NSRange] {
rd_paginatedFrames(size: size).map(\.contentRange)
}
}
// MARK: - UIColor CSS
/// UIColor CSS CSS
extension UIColor {
/// UIColor CSS rgba()
var ss_cssString: String {
var red: CGFloat = 0
var green: CGFloat = 0