feat(epub): complete wxread parity scope

This commit is contained in:
shen
2026-05-26 09:19:37 +08:00
parent 23182e8b5a
commit 0e7d952fe5
31 changed files with 2619 additions and 1533 deletions
@@ -30,8 +30,19 @@ public final class RDEPUBChapterData {
public var attributedContent: NSAttributedString { chapter.attributedContent }
///
public var pages: [RDEPUBTextPage] { chapter.pages }
///
public var pageCount: Int { chapter.pages.count }
/// fragment ID
public var fragmentOffsets: [String: Int] { chapter.fragmentOffsets }
/// /
public var chapterInfo: EPUBChapterInfo {
EPUBChapterInfo(spineIndex: spineIndex, title: title, pageCount: pageCount)
}
///
public var absolutePageRange: ClosedRange<Int>? {
guard let firstPage = pages.first, let lastPage = pages.last else { return nil }
return firstPage.absolutePageIndex...lastPage.absolutePageIndex
}
// MARK: -
@@ -52,20 +63,36 @@ public final class RDEPUBChapterData {
chapter.pages.first { $0.absolutePageIndex == absolutePageIndex }
}
/// 1
public func page(atPageNumber pageNumber: Int) -> RDEPUBTextPage? {
guard pageNumber > 0, pages.indices.contains(pageNumber - 1) else { return nil }
return pages[pageNumber - 1]
}
// MARK: -
/// fileIndex/row/column
/// fileIndex/row/column
public func anchor(forAbsoluteIndex index: Int) -> RDEPUBTextAnchor {
indexTable.anchor(forAbsoluteIndex: index, in: chapter)
}
///
///
public func anchor(forGlobalIndex index: Int) -> RDEPUBTextAnchor? {
indexTable.anchor(forGlobalIndex: index)
}
///
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)
}
///
public func globalRange(for rangeAnchor: RDEPUBTextRangeAnchor) -> NSRange {
indexTable.globalRange(for: rangeAnchor)
}
/// /
/// - Parameters:
/// - absoluteRange:
@@ -102,9 +129,21 @@ public final class RDEPUBChapterData {
location(for: page.contentRange, bookIdentifier: bookIdentifier)
}
///
public func page(for location: RDEPUBLocation) -> RDEPUBTextPage? {
guard let range = absoluteRange(for: location) else { return nil }
return page(containing: range.location)
}
///
public func page(for searchMatch: RDEPUBSearchMatch) -> RDEPUBTextPage? {
guard let range = absoluteRange(for: searchMatch) else { return nil }
return page(containing: range.location)
}
// MARK: - Location
///
///
///
///
/// 1. rangeAnchor
@@ -112,7 +151,7 @@ public final class RDEPUBChapterData {
/// 3. navigationProgression退
public func absoluteRange(for location: RDEPUBLocation) -> NSRange? {
if let rangeAnchor = location.rangeAnchor {
return indexTable.absoluteRange(for: rangeAnchor)
return indexTable.chapterRange(for: rangeAnchor)
}
if let fragment = location.fragment,
@@ -128,7 +167,7 @@ public final class RDEPUBChapterData {
///
public func absoluteRange(for highlight: RDEPUBHighlight) -> NSRange? {
if let rangeAnchor = highlight.location.rangeAnchor {
return indexTable.absoluteRange(for: rangeAnchor)
return indexTable.chapterRange(for: rangeAnchor)
}
return RDEPUBTextOffsetRangeInfo.decode(from: highlight.rangeInfo)?.nsRange
}
@@ -136,7 +175,7 @@ public final class RDEPUBChapterData {
///
public func absoluteRange(for searchMatch: RDEPUBSearchMatch) -> NSRange? {
if let rangeAnchor = searchMatch.rangeAnchor {
return indexTable.absoluteRange(for: rangeAnchor)
return indexTable.chapterRange(for: rangeAnchor)
}
if let location = searchMatch.rangeLocation {
return NSRange(location: location, length: searchMatch.rangeLength)
@@ -144,6 +183,42 @@ public final class RDEPUBChapterData {
return nil
}
///
public func globalRange(for location: RDEPUBLocation) -> NSRange? {
if let rangeAnchor = location.rangeAnchor {
return indexTable.globalRange(for: rangeAnchor)
}
guard let chapterRange = absoluteRange(for: location),
let chapterStart = indexTable.chapterStartOffset(forFileIndex: spineIndex) else {
return nil
}
return NSRange(location: chapterStart + chapterRange.location, length: chapterRange.length)
}
///
public func globalRange(for highlight: RDEPUBHighlight) -> NSRange? {
if let rangeAnchor = highlight.location.rangeAnchor {
return indexTable.globalRange(for: rangeAnchor)
}
guard let chapterRange = absoluteRange(for: highlight),
let chapterStart = indexTable.chapterStartOffset(forFileIndex: spineIndex) else {
return nil
}
return NSRange(location: chapterStart + chapterRange.location, length: chapterRange.length)
}
///
public func globalRange(for searchMatch: RDEPUBSearchMatch) -> NSRange? {
if let rangeAnchor = searchMatch.rangeAnchor {
return indexTable.globalRange(for: rangeAnchor)
}
guard let chapterRange = absoluteRange(for: searchMatch),
let chapterStart = indexTable.chapterStartOffset(forFileIndex: spineIndex) else {
return nil
}
return NSRange(location: chapterStart + chapterRange.location, length: chapterRange.length)
}
// MARK: - /
///
@@ -163,7 +238,6 @@ public final class RDEPUBChapterData {
///
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) {
@@ -173,12 +247,54 @@ public final class RDEPUBChapterData {
}
}
/// `searchMatches(on:from:)` /WXRead
public func searchResults(on page: RDEPUBTextPage, from matches: [RDEPUBSearchMatch]) -> [RDEPUBSearchMatch] {
searchMatches(on: page, from: matches)
}
/// 1
public func pageNumber(for location: RDEPUBLocation) -> Int? {
if let range = absoluteRange(for: location) {
return pageNumber(containing: range.location).map { $0 + 1 }
guard let page = page(for: location) else { return nil }
return page.absolutePageIndex + 1
}
/// 1
public func pageNumber(for searchMatch: RDEPUBSearchMatch) -> Int? {
guard let page = page(for: searchMatch) else { return nil }
return page.absolutePageIndex + 1
}
// MARK: -
/// TOC
public func contains(
tableOfContentsItem item: EPUBTableOfContentsItem,
normalizer: (String) -> String?
) -> Bool {
guard let chapterHref = normalizer(href),
let itemHref = normalizer(item.href.components(separatedBy: "#").first ?? item.href) else {
return false
}
return nil
return chapterHref == itemHref
}
///
public func tableOfContentsItems(
from items: [EPUBTableOfContentsItem],
normalizer: (String) -> String?
) -> [EPUBTableOfContentsItem] {
items.flatMap { item -> [EPUBTableOfContentsItem] in
let descendants = tableOfContentsItems(from: item.children, normalizer: normalizer)
return contains(tableOfContentsItem: item, normalizer: normalizer) ? [item] + descendants : descendants
}
}
///
public func primaryTableOfContentsItem(
from items: [EPUBTableOfContentsItem],
normalizer: (String) -> String?
) -> EPUBTableOfContentsItem? {
tableOfContentsItems(from: items, normalizer: normalizer).first
}
// MARK: -
@@ -190,12 +306,8 @@ public final class RDEPUBChapterData {
return lowerBound..<max(upperBound, lowerBound)
}
/// 退 chapterOffset
/// 退 chapterOffset
private func absoluteOffset(for anchor: RDEPUBTextAnchor) -> Int {
indexTable.absoluteIndex(
fileIndex: anchor.fileIndex,
row: anchor.row,
column: anchor.column
) ?? anchor.chapterOffset
indexTable.chapterOffset(for: anchor)
}
}