源码注释: - 为 ~60 个 Swift 文件补充缺失的 doc comment(file header、类型、属性、方法) - 修正 4 处错误注释:翻页模式数量、搜索行为描述、手势识别器描述、悬空文档块 文档维护: - 删除重复文档:WXRead/读书EPUB阅读器实现架构.md(与微信读书版完全一致) - 合并重叠文档:阅读器规划.md → 阅读器功能开发计划.md(单一真值) - 修正过时内容:所有文档中"四种翻页模式"→"三种",移除 horizontalCoverScroll - 更新架构图:补齐 EPUBUI/ReaderController、Paging/、Typesetter/ 等子目录 - 更新 index.md 索引:新增开发计划和架构对比文档引用
119 lines
5.0 KiB
Swift
119 lines
5.0 KiB
Swift
// RDEPUBTextPositionConverter.swift
|
||
// EPUB 全书文本位置转换器,在多种坐标体系之间进行双向映射。
|
||
|
||
import Foundation
|
||
|
||
/// 对标 WXRead `WREpubPositionConverter` 的全书位置转换器。
|
||
///
|
||
/// 负责在四套坐标之间做双向转换:
|
||
/// - `(fileIndex, row, column)` 文件级语义锚点
|
||
/// - 章节内字符偏移
|
||
/// - 全书字符偏移
|
||
/// - 页码 / `RDEPUBLocation`
|
||
public struct RDEPUBTextPositionConverter {
|
||
/// 当前转换器所关联的 EPUB 文本对象。
|
||
public let book: RDEPUBTextBook
|
||
|
||
/// 创建指定 EPUB 文本对象的位置转换器。
|
||
/// - Parameter book: 要关联的 EPUB 文本对象。
|
||
public init(book: RDEPUBTextBook) {
|
||
self.book = book
|
||
}
|
||
|
||
/// 全书字符总数(跨所有文件累计)。
|
||
public var totalCharacterCount: Int {
|
||
book.indexTable.totalCharacterCount
|
||
}
|
||
|
||
/// 将语义锚点转换为全书字符偏移。
|
||
/// - Parameter anchor: 由文件索引、行号、列号组成的语义锚点。
|
||
/// - Returns: 对应的全书字符偏移量。
|
||
public func globalIndex(for anchor: RDEPUBTextAnchor) -> Int {
|
||
book.indexTable.globalIndex(for: anchor)
|
||
}
|
||
|
||
/// 将语义范围锚点转换为全书字符范围。
|
||
/// - Parameter rangeAnchor: 由起止锚点组成的语义范围。
|
||
/// - Returns: 对应的全书字符范围(`NSRange`)。
|
||
public func globalRange(for rangeAnchor: RDEPUBTextRangeAnchor) -> NSRange {
|
||
book.indexTable.globalRange(for: rangeAnchor)
|
||
}
|
||
|
||
/// 根据全书字符偏移查找所属文件的索引。
|
||
/// - Parameter index: 全书字符偏移量。
|
||
/// - Returns: 对应的文件索引,超出范围时返回 `nil`。
|
||
public func fileIndex(forCharacterPosition index: Int) -> Int? {
|
||
book.indexTable.fileIndex(forCharacterPosition: index)
|
||
}
|
||
|
||
/// 将全书字符偏移转换为指定文件内的本地偏移。
|
||
/// - Parameters:
|
||
/// - fileIndex: 目标文件索引。
|
||
/// - index: 全书字符偏移量。
|
||
/// - Returns: 文件内的本地字符偏移,越界时返回 `nil`。
|
||
public func localOffsetInFile(at fileIndex: Int, forGlobalPosition index: Int) -> Int? {
|
||
book.indexTable.localOffsetInFile(at: fileIndex, forGlobalPosition: index)
|
||
}
|
||
|
||
/// 将全书字符偏移转换为语义锚点。
|
||
/// - Parameter index: 全书字符偏移量。
|
||
/// - Returns: 对应的语义锚点,无效位置时返回 `nil`。
|
||
public func anchor(forCharacterPosition index: Int) -> RDEPUBTextAnchor? {
|
||
book.indexTable.anchor(forGlobalIndex: index)
|
||
}
|
||
|
||
/// 将 `RDEPUBLocation` 转换为语义锚点。
|
||
/// - Parameter location: EPUB 位置对象。
|
||
/// - Returns: 对应的语义锚点,无法映射时返回 `nil`。
|
||
public func anchor(for location: RDEPUBLocation) -> RDEPUBTextAnchor? {
|
||
book.indexTable.anchor(for: location)
|
||
}
|
||
|
||
/// 根据语义锚点获取页码(从 1 开始)。
|
||
/// - Parameter anchor: 由文件索引、行号、列号组成的语义锚点。
|
||
/// - Returns: 对应的页码,无法定位时返回 `nil`。
|
||
public func pageNumber(for anchor: RDEPUBTextAnchor) -> Int? {
|
||
book.indexTable.pageNumber(for: anchor, in: book).map { $0 + 1 }
|
||
}
|
||
|
||
/// 根据全书字符偏移获取页码(从 1 开始)。
|
||
/// - Parameter index: 全书字符偏移量。
|
||
/// - Returns: 对应的页码,无效位置时返回 `nil`。
|
||
public func pageNumber(forCharacterPosition index: Int) -> Int? {
|
||
guard let anchor = anchor(forCharacterPosition: index) else {
|
||
return nil
|
||
}
|
||
return pageNumber(for: anchor)
|
||
}
|
||
|
||
/// 将语义锚点转换为可序列化的 `RDEPUBLocation`。
|
||
/// - Parameters:
|
||
/// - anchor: 由文件索引、行号、列号组成的语义锚点。
|
||
/// - bookIdentifier: 书籍标识符,会写入 location 的 `bookId` 字段。
|
||
/// - Returns: 对应的 `RDEPUBLocation`,无法映射时返回 `nil`。
|
||
public func location(
|
||
for anchor: RDEPUBTextAnchor,
|
||
bookIdentifier: String?
|
||
) -> RDEPUBLocation? {
|
||
guard let chapter = book.chapters.first(where: { $0.spineIndex == anchor.fileIndex }) else {
|
||
return nil
|
||
}
|
||
return book.indexTable.location(for: anchor, in: chapter, bookIdentifier: bookIdentifier)
|
||
}
|
||
|
||
/// 将语义范围锚点转换为可序列化的 `RDEPUBLocation`。
|
||
/// - Parameters:
|
||
/// - rangeAnchor: 由起止锚点组成的语义范围。
|
||
/// - bookIdentifier: 书籍标识符,会写入 location 的 `bookId` 字段。
|
||
/// - Returns: 对应的 `RDEPUBLocation`,无法映射时返回 `nil`。
|
||
public func location(
|
||
for rangeAnchor: RDEPUBTextRangeAnchor,
|
||
bookIdentifier: String?
|
||
) -> RDEPUBLocation? {
|
||
guard let chapter = book.chapters.first(where: { $0.spineIndex == rangeAnchor.start.fileIndex }) else {
|
||
return nil
|
||
}
|
||
return book.indexTable.location(for: rangeAnchor, in: chapter, bookIdentifier: bookIdentifier)
|
||
}
|
||
}
|