feat(epub): complete wxread parity scope
This commit is contained in:
@@ -44,6 +44,15 @@ public struct RDEPUBTextIndexTable {
|
||||
/// spine 文件索引 → 行列索引数组的映射
|
||||
public let fileRowColumnMap: [Int: [RDEPUBRowColumnIndex]]
|
||||
|
||||
/// 全书总字符数(章节长度累加)
|
||||
public var totalCharacterCount: Int {
|
||||
guard let lastIndex = chapterStartOffsets.indices.last,
|
||||
chapterLengths.indices.contains(lastIndex) else {
|
||||
return 0
|
||||
}
|
||||
return chapterStartOffsets[lastIndex] + chapterLengths[lastIndex]
|
||||
}
|
||||
|
||||
/// 从文本章节列表构建索引表
|
||||
public init(chapters: [RDEPUBTextChapter]) {
|
||||
var offsets: [Int] = []
|
||||
@@ -118,11 +127,29 @@ public struct RDEPUBTextIndexTable {
|
||||
)
|
||||
}
|
||||
|
||||
/// 根据全书字符偏移量构建文本锚点。
|
||||
public func anchor(forGlobalIndex index: Int) -> RDEPUBTextAnchor? {
|
||||
guard let fileIndex = fileIndex(forCharacterPosition: index),
|
||||
let href = href(for: fileIndex) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let chapterOffset = localOffsetInFile(at: fileIndex, forGlobalPosition: index) ?? 0
|
||||
let fragmentID = nearestFragmentID(beforeOrAt: chapterOffset, inHref: href)
|
||||
return RDEPUBTextAnchor(
|
||||
fileIndex: fileIndex,
|
||||
row: row(forAbsoluteIndex: chapterOffset, inFileIndex: fileIndex),
|
||||
column: column(forAbsoluteIndex: chapterOffset, inFileIndex: fileIndex),
|
||||
chapterOffset: chapterOffset,
|
||||
fragmentID: fragmentID
|
||||
)
|
||||
}
|
||||
|
||||
/// 根据锚点查找对应的绝对页码
|
||||
public func pageNumber(for anchor: RDEPUBTextAnchor, in book: RDEPUBTextBook) -> Int? {
|
||||
guard let chapter = book.chapters.first(where: { $0.spineIndex == anchor.fileIndex }) else { return nil }
|
||||
let basePageIndex = chapter.pages.first?.absolutePageIndex ?? 0
|
||||
let resolvedOffset = absoluteIndex(for: anchor)
|
||||
let resolvedOffset = chapterOffset(for: anchor)
|
||||
return chapter.pages.firstIndex { page in
|
||||
NSLocationInRange(resolvedOffset, page.contentRange)
|
||||
}.map { $0 + basePageIndex }
|
||||
@@ -138,31 +165,77 @@ public struct RDEPUBTextIndexTable {
|
||||
hrefToChapterIndex[href]
|
||||
}
|
||||
|
||||
/// 将锚点转换为全书绝对字符索引
|
||||
public func absoluteIndex(for anchor: RDEPUBTextAnchor) -> Int {
|
||||
absoluteIndex(
|
||||
/// 通过文件索引获取章节起始的全书字符偏移量。
|
||||
public func chapterStartOffset(forFileIndex fileIndex: Int) -> Int? {
|
||||
guard let href = href(for: fileIndex),
|
||||
let chapterIndex = hrefToChapterIndex[href],
|
||||
chapterStartOffsets.indices.contains(chapterIndex) else {
|
||||
return nil
|
||||
}
|
||||
return chapterStartOffsets[chapterIndex]
|
||||
}
|
||||
|
||||
/// 通过文件索引获取章节长度。
|
||||
public func chapterLength(forFileIndex fileIndex: Int) -> Int? {
|
||||
guard let href = href(for: fileIndex),
|
||||
let chapterIndex = hrefToChapterIndex[href],
|
||||
chapterLengths.indices.contains(chapterIndex) else {
|
||||
return nil
|
||||
}
|
||||
return chapterLengths[chapterIndex]
|
||||
}
|
||||
|
||||
/// 将锚点转换为章节内字符偏移量。
|
||||
public func chapterOffset(for anchor: RDEPUBTextAnchor) -> Int {
|
||||
chapterOffset(
|
||||
fileIndex: anchor.fileIndex,
|
||||
row: anchor.row,
|
||||
column: anchor.column
|
||||
) ?? anchor.chapterOffset
|
||||
}
|
||||
|
||||
/// 将范围锚点转换为全书绝对 NSRange
|
||||
public func absoluteRange(for rangeAnchor: RDEPUBTextRangeAnchor) -> NSRange {
|
||||
let start = absoluteIndex(for: rangeAnchor.start)
|
||||
let end = max(start, absoluteIndex(for: rangeAnchor.end))
|
||||
/// 将锚点转换为全书绝对字符索引。
|
||||
public func globalIndex(for anchor: RDEPUBTextAnchor) -> Int {
|
||||
let chapterOffset = chapterOffset(for: anchor)
|
||||
let baseOffset = chapterStartOffset(forFileIndex: anchor.fileIndex) ?? 0
|
||||
let chapterLength = max(chapterLength(forFileIndex: anchor.fileIndex) ?? 0, 0)
|
||||
let lastOffset = max(chapterLength - 1, 0)
|
||||
return baseOffset + min(max(chapterOffset, 0), lastOffset)
|
||||
}
|
||||
|
||||
/// 兼容旧调用:返回全书绝对字符索引。
|
||||
public func absoluteIndex(for anchor: RDEPUBTextAnchor) -> Int {
|
||||
globalIndex(for: anchor)
|
||||
}
|
||||
|
||||
/// 将范围锚点转换为章节内 NSRange。
|
||||
public func chapterRange(for rangeAnchor: RDEPUBTextRangeAnchor) -> NSRange {
|
||||
let start = chapterOffset(for: rangeAnchor.start)
|
||||
let end = max(start, chapterOffset(for: rangeAnchor.end))
|
||||
return NSRange(location: start, length: max(end - start, 0))
|
||||
}
|
||||
|
||||
/// 将范围锚点转换为全书绝对 NSRange。
|
||||
public func globalRange(for rangeAnchor: RDEPUBTextRangeAnchor) -> NSRange {
|
||||
let start = globalIndex(for: rangeAnchor.start)
|
||||
let end = max(start, globalIndex(for: rangeAnchor.end))
|
||||
return NSRange(location: start, length: max(end - start, 0))
|
||||
}
|
||||
|
||||
/// 兼容旧调用:返回全书绝对范围。
|
||||
public func absoluteRange(for rangeAnchor: RDEPUBTextRangeAnchor) -> NSRange {
|
||||
globalRange(for: rangeAnchor)
|
||||
}
|
||||
|
||||
/// 将单个锚点转换为阅读位置(计算 progression)
|
||||
public func location(
|
||||
for anchor: RDEPUBTextAnchor,
|
||||
in chapter: RDEPUBTextChapter,
|
||||
bookIdentifier: String?
|
||||
) -> RDEPUBLocation {
|
||||
let absoluteOffset = absoluteIndex(for: anchor)
|
||||
let chapterOffset = self.chapterOffset(for: anchor)
|
||||
let totalLength = max(chapter.attributedContent.length - 1, 1)
|
||||
let progression = Double(min(max(absoluteOffset, 0), totalLength)) / Double(totalLength)
|
||||
let progression = Double(min(max(chapterOffset, 0), totalLength)) / Double(totalLength)
|
||||
return RDEPUBLocation(
|
||||
bookIdentifier: bookIdentifier,
|
||||
href: chapter.href,
|
||||
@@ -179,8 +252,8 @@ public struct RDEPUBTextIndexTable {
|
||||
in chapter: RDEPUBTextChapter,
|
||||
bookIdentifier: String?
|
||||
) -> RDEPUBLocation {
|
||||
let start = absoluteIndex(for: rangeAnchor.start)
|
||||
let end = max(start, absoluteIndex(for: rangeAnchor.end))
|
||||
let start = chapterOffset(for: rangeAnchor.start)
|
||||
let end = max(start, chapterOffset(for: rangeAnchor.end))
|
||||
let totalLength = max(chapter.attributedContent.length - 1, 1)
|
||||
let clampedStart = min(max(start, 0), totalLength)
|
||||
let clampedEnd = min(max(end, clampedStart), totalLength)
|
||||
@@ -213,8 +286,8 @@ public struct RDEPUBTextIndexTable {
|
||||
return max(index - lastRow.startOffset, 0)
|
||||
}
|
||||
|
||||
/// 根据文件索引、行号和列号计算全书绝对索引
|
||||
public func absoluteIndex(fileIndex: Int, row: Int, column: Int) -> Int? {
|
||||
/// 根据文件索引、行号和列号计算章节内偏移量。
|
||||
public func chapterOffset(fileIndex: Int, row: Int, column: Int) -> Int? {
|
||||
guard let rows = fileRowColumnMap[fileIndex], !rows.isEmpty else { return nil }
|
||||
let normalizedRow = min(max(row, 0), rows.count - 1)
|
||||
let rowEntry = rows[normalizedRow]
|
||||
@@ -222,6 +295,46 @@ public struct RDEPUBTextIndexTable {
|
||||
return rowEntry.startOffset + min(max(column, 0), maxColumn)
|
||||
}
|
||||
|
||||
/// 根据文件索引、行号和列号计算全书绝对索引。
|
||||
public func absoluteIndex(fileIndex: Int, row: Int, column: Int) -> Int? {
|
||||
guard let chapterOffset = chapterOffset(fileIndex: fileIndex, row: row, column: column),
|
||||
let baseOffset = chapterStartOffset(forFileIndex: fileIndex) else {
|
||||
return nil
|
||||
}
|
||||
return baseOffset + chapterOffset
|
||||
}
|
||||
|
||||
/// 全书字符偏移量 → 文件索引。
|
||||
public func fileIndex(forCharacterPosition index: Int) -> Int? {
|
||||
guard totalCharacterCount > 0 else { return nil }
|
||||
let normalized = min(max(index, 0), max(totalCharacterCount - 1, 0))
|
||||
for (href, chapterIndex) in hrefToChapterIndex {
|
||||
guard chapterStartOffsets.indices.contains(chapterIndex),
|
||||
chapterLengths.indices.contains(chapterIndex),
|
||||
let fileIndex = hrefToFileIndex[href] else {
|
||||
continue
|
||||
}
|
||||
|
||||
let start = chapterStartOffsets[chapterIndex]
|
||||
let length = chapterLengths[chapterIndex]
|
||||
let endExclusive = start + max(length, 1)
|
||||
if normalized >= start && normalized < endExclusive {
|
||||
return fileIndex
|
||||
}
|
||||
}
|
||||
return fileIndexToHref.keys.sorted().last
|
||||
}
|
||||
|
||||
/// 全书字符偏移量 → 指定文件内的章节偏移量。
|
||||
public func localOffsetInFile(at fileIndex: Int, forGlobalPosition index: Int) -> Int? {
|
||||
guard let start = chapterStartOffset(forFileIndex: fileIndex),
|
||||
let chapterLength = chapterLength(forFileIndex: fileIndex) else {
|
||||
return nil
|
||||
}
|
||||
let normalized = min(max(index, start), start + max(chapterLength - 1, 0))
|
||||
return normalized - start
|
||||
}
|
||||
|
||||
/// 将偏移量限制在章节范围内
|
||||
private func clampedOffset(_ index: Int, in chapter: RDEPUBTextChapter) -> Int {
|
||||
let lastOffset = max(chapter.attributedContent.length - 1, 0)
|
||||
@@ -243,6 +356,21 @@ public struct RDEPUBTextIndexTable {
|
||||
return bestID
|
||||
}
|
||||
|
||||
/// 查找给定 href 中距离偏移量最近(且在其之前)的 fragment ID。
|
||||
private func nearestFragmentID(beforeOrAt offset: Int, inHref href: String) -> String? {
|
||||
guard let fragmentOffsets = fragmentOffsetsByHref[href] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var bestID: String?
|
||||
var bestOffset = -1
|
||||
for (id, fragmentOffset) in fragmentOffsets where fragmentOffset <= offset && fragmentOffset > bestOffset {
|
||||
bestOffset = fragmentOffset
|
||||
bestID = id
|
||||
}
|
||||
return bestID
|
||||
}
|
||||
|
||||
/// 从文本字符串构建行列索引数组(按行分割,记录每行的起止偏移)
|
||||
private static func makeRowColumnIndices(for text: String) -> [RDEPUBRowColumnIndex] {
|
||||
let nsText = text as NSString
|
||||
|
||||
Reference in New Issue
Block a user