- 给全部 78 个 Swift 源文件添加详细的中文注释(文件级、类级、方法级) - 删除 LegacyRDReaderController/ 死代码目录(16 文件 4592 行) - 根目录翻页容器文件移入 ReaderView/ 目录 - Resources/ 移入 EPUBCore/Resources/(与使用者归属一致) - RDEPUBTextIndexTable.swift 移入 EPUBTextRendering/(消除反向依赖) - RDURLReaderController.swift 移入 EPUBUI/(入口控制器归入 UI 层) - 更新 podspec 资源路径
202 lines
8.5 KiB
Swift
202 lines
8.5 KiB
Swift
import UIKit
|
||
|
||
/// 章节数据访问层:为已分页章节提供便捷的查询接口。
|
||
///
|
||
/// 本类是 `RDEPUBTextChapter` 的轻量级封装,持有 `indexTable`(全局索引表),
|
||
/// 将分页数据、fragment 锚点、高亮、搜索结果等统一到同一套查询 API 中。
|
||
/// 由 `RDEPUBTextBook.chapterData(for:)` 或 `chapterData(atChapterIndex:)` 创建。
|
||
public final class RDEPUBChapterData {
|
||
/// 底层章节模型(只读)
|
||
public let chapter: RDEPUBTextChapter
|
||
/// 全局索引表,用于 fileIndex/row/column 到绝对偏移量的映射
|
||
public let indexTable: RDEPUBTextIndexTable
|
||
|
||
public init(chapter: RDEPUBTextChapter, indexTable: RDEPUBTextIndexTable) {
|
||
self.chapter = chapter
|
||
self.indexTable = indexTable
|
||
}
|
||
|
||
// MARK: - 便捷属性(转发自 chapter)
|
||
|
||
/// 章节在全书中的序号
|
||
public var chapterIndex: Int { chapter.chapterIndex }
|
||
/// spine 顺序索引
|
||
public var spineIndex: Int { chapter.spineIndex }
|
||
/// 章节文件相对路径(如 "OEBPS/chapter1.xhtml")
|
||
public var href: String { chapter.href }
|
||
/// 章节标题
|
||
public var title: String { chapter.title }
|
||
/// 章节完整的富文本内容
|
||
public var attributedContent: NSAttributedString { chapter.attributedContent }
|
||
/// 分页后的页面列表
|
||
public var pages: [RDEPUBTextPage] { chapter.pages }
|
||
/// fragment ID → 字符偏移量的映射表(用于锚点定位)
|
||
public var fragmentOffsets: [String: Int] { chapter.fragmentOffsets }
|
||
|
||
// MARK: - 页面查询
|
||
|
||
/// 查找包含指定绝对字符偏移量的页面
|
||
/// - Parameter absoluteOffset: 全书绝对字符偏移量
|
||
/// - Returns: 包含该偏移量的页面,未找到返回 nil
|
||
public func page(containing absoluteOffset: Int) -> RDEPUBTextPage? {
|
||
chapter.pages.first { NSLocationInRange(absoluteOffset, $0.contentRange) }
|
||
}
|
||
|
||
/// 获取指定绝对偏移量所在页的绝对页码(从 0 开始)
|
||
public func pageNumber(containing absoluteOffset: Int) -> Int? {
|
||
page(containing: absoluteOffset)?.absolutePageIndex
|
||
}
|
||
|
||
/// 按绝对页码查找页面
|
||
public func page(atAbsolutePageIndex absolutePageIndex: Int) -> RDEPUBTextPage? {
|
||
chapter.pages.first { $0.absolutePageIndex == absolutePageIndex }
|
||
}
|
||
|
||
// MARK: - 锚点与位置映射
|
||
|
||
/// 将绝对字符索引转换为语义锚点(fileIndex/row/column 三元组)
|
||
public func anchor(forAbsoluteIndex index: Int) -> RDEPUBTextAnchor {
|
||
indexTable.anchor(forAbsoluteIndex: index, in: chapter)
|
||
}
|
||
|
||
/// 将绝对字符范围转换为起止锚点对
|
||
public func rangeAnchor(for absoluteRange: NSRange) -> RDEPUBTextRangeAnchor {
|
||
let start = anchor(forAbsoluteIndex: absoluteRange.location)
|
||
let end = anchor(forAbsoluteIndex: absoluteRange.location + absoluteRange.length)
|
||
return RDEPUBTextRangeAnchor(start: start, end: end)
|
||
}
|
||
|
||
/// 从绝对字符范围构建选区对象(用于复制/高亮分享)
|
||
/// - Parameters:
|
||
/// - absoluteRange: 选区在全书中的字符范围
|
||
/// - bookIdentifier: 书籍标识符
|
||
/// - Returns: 选区对象,若起始偏移量不在任何页面内则返回 nil
|
||
public func selection(from absoluteRange: NSRange, bookIdentifier: String?) -> RDEPUBSelection? {
|
||
guard page(containing: absoluteRange.location) != nil else { return nil }
|
||
let location = self.location(for: absoluteRange, bookIdentifier: bookIdentifier)
|
||
let text = chapter.attributedContent.attributedSubstring(from: absoluteRange).string
|
||
let rangeInfo = RDEPUBTextOffsetRangeInfo(
|
||
href: chapter.href,
|
||
start: absoluteRange.location,
|
||
end: absoluteRange.location + absoluteRange.length
|
||
).jsonString()
|
||
return RDEPUBSelection(
|
||
bookIdentifier: bookIdentifier,
|
||
location: location,
|
||
text: text,
|
||
rangeInfo: rangeInfo
|
||
)
|
||
}
|
||
|
||
/// 将绝对字符范围转换为持久化位置对象(RDEPUBLocation)
|
||
public func location(for absoluteRange: NSRange, bookIdentifier: String?) -> RDEPUBLocation {
|
||
indexTable.location(
|
||
for: rangeAnchor(for: absoluteRange),
|
||
in: chapter,
|
||
bookIdentifier: bookIdentifier
|
||
)
|
||
}
|
||
|
||
/// 将页面转换为对应的 RDEPUBLocation
|
||
public func location(forPage page: RDEPUBTextPage, bookIdentifier: String?) -> RDEPUBLocation {
|
||
location(for: page.contentRange, bookIdentifier: bookIdentifier)
|
||
}
|
||
|
||
// MARK: - 位置反向解析(Location → 绝对偏移量)
|
||
|
||
/// 将持久化位置还原为章节内的绝对字符范围。
|
||
///
|
||
/// 解析优先级:
|
||
/// 1. rangeAnchor(锚点定位,最精确)
|
||
/// 2. fragment(片段 ID 定位)
|
||
/// 3. navigationProgression(进度百分比回退)
|
||
public func absoluteRange(for location: RDEPUBLocation) -> NSRange? {
|
||
if let rangeAnchor = location.rangeAnchor {
|
||
return indexTable.absoluteRange(for: rangeAnchor)
|
||
}
|
||
|
||
if let fragment = location.fragment,
|
||
let offset = fragmentOffsets[fragment] {
|
||
return NSRange(location: offset, length: 1)
|
||
}
|
||
|
||
let lastOffset = max(attributedContent.length - 1, 0)
|
||
let offset = min(lastOffset, max(0, Int(round(Double(lastOffset) * location.navigationProgression))))
|
||
return NSRange(location: offset, length: 1)
|
||
}
|
||
|
||
/// 从高亮的持久化位置还原绝对字符范围
|
||
public func absoluteRange(for highlight: RDEPUBHighlight) -> NSRange? {
|
||
if let rangeAnchor = highlight.location.rangeAnchor {
|
||
return indexTable.absoluteRange(for: rangeAnchor)
|
||
}
|
||
return RDEPUBTextOffsetRangeInfo.decode(from: highlight.rangeInfo)?.nsRange
|
||
}
|
||
|
||
/// 从搜索结果还原绝对字符范围
|
||
public func absoluteRange(for searchMatch: RDEPUBSearchMatch) -> NSRange? {
|
||
if let rangeAnchor = searchMatch.rangeAnchor {
|
||
return indexTable.absoluteRange(for: rangeAnchor)
|
||
}
|
||
if let location = searchMatch.rangeLocation {
|
||
return NSRange(location: location, length: searchMatch.rangeLength)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// MARK: - 高亮/搜索结果与页面的交叉查询
|
||
|
||
/// 获取指定页面上出现的所有高亮
|
||
public func highlights(on page: RDEPUBTextPage, from allHighlights: [RDEPUBHighlight]) -> [RDEPUBHighlight] {
|
||
let pageRange = absoluteOffsetRange(for: page)
|
||
return allHighlights.filter { highlight in
|
||
guard highlight.location.href == chapter.href else { return false }
|
||
if let anchor = highlight.location.rangeAnchor?.start {
|
||
return pageRange.contains(absoluteOffset(for: anchor))
|
||
}
|
||
guard let range = RDEPUBTextOffsetRangeInfo.decode(from: highlight.rangeInfo)?.nsRange else {
|
||
return false
|
||
}
|
||
return NSIntersectionRange(range, page.contentRange).length > 0
|
||
}
|
||
}
|
||
|
||
/// 获取指定页面上出现的所有搜索匹配结果
|
||
public func searchMatches(on page: RDEPUBTextPage, from matches: [RDEPUBSearchMatch]) -> [RDEPUBSearchMatch] {
|
||
let pageRange = absoluteOffsetRange(for: page)
|
||
return matches.filter { match in
|
||
guard match.href == chapter.href else { return false }
|
||
if let range = absoluteRange(for: match) {
|
||
return NSIntersectionRange(range, page.contentRange).length > 0
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
|
||
/// 根据持久化位置获取对应页码(从 1 开始),用于阅读进度跳转
|
||
public func pageNumber(for location: RDEPUBLocation) -> Int? {
|
||
if let range = absoluteRange(for: location) {
|
||
return pageNumber(containing: range.location).map { $0 + 1 }
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// MARK: - 私有工具方法
|
||
|
||
/// 将页面的起止偏移量转换为半开区间 [start, end+1)
|
||
private func absoluteOffsetRange(for page: RDEPUBTextPage) -> Range<Int> {
|
||
let lowerBound = page.pageStartOffset
|
||
let upperBound = page.pageEndOffset + 1
|
||
return lowerBound..<max(upperBound, lowerBound)
|
||
}
|
||
|
||
/// 将语义锚点转换为绝对字符偏移量,若索引表无法解析则回退到 chapterOffset
|
||
private func absoluteOffset(for anchor: RDEPUBTextAnchor) -> Int {
|
||
indexTable.absoluteIndex(
|
||
fileIndex: anchor.fileIndex,
|
||
row: anchor.row,
|
||
column: anchor.column
|
||
) ?? anchor.chapterOffset
|
||
}
|
||
}
|