refactor: rename RDReaderView -> RDEpubReaderView, update pod config and docs
- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
extension RDEPUBReaderController {
|
||||
|
||||
func pageNumber(for location: RDEPUBLocation, rangeInfo: String? = nil) -> Int? {
|
||||
if let publication,
|
||||
let bookPageMap = readerContext.bookPageMap,
|
||||
let spineIndex = readerContext.normalizedSpineIndex(for: location),
|
||||
let entry = bookPageMap.entry(forSpineIndex: spineIndex) {
|
||||
let normalizedLocation = publication.resourceResolver.normalizedLocation(
|
||||
location,
|
||||
relativeToSpineIndex: nil,
|
||||
bookIdentifier: currentBookIdentifier
|
||||
) ?? location
|
||||
let localPageIndex = resolvedLocalPageIndex(
|
||||
for: normalizedLocation,
|
||||
spineIndex: spineIndex,
|
||||
fallbackEntry: entry,
|
||||
rangeInfo: rangeInfo
|
||||
) ?? fallbackLocalPageIndex(for: normalizedLocation, pageCount: entry.pageCount)
|
||||
return bookPageMap.absolutePageIndex(
|
||||
spineIndex: spineIndex,
|
||||
localPageIndex: min(max(localPageIndex, 0), max(entry.pageCount - 1, 0))
|
||||
).map { $0 + 1 }
|
||||
}
|
||||
|
||||
if let textBook {
|
||||
let normalizedLocation = publication?.resourceResolver.normalizedLocation(
|
||||
location,
|
||||
relativeToSpineIndex: nil,
|
||||
bookIdentifier: currentBookIdentifier
|
||||
) ?? location
|
||||
|
||||
if let pageNumber = pageNumberFromRangeInfo(rangeInfo, in: textBook, matching: normalizedLocation) {
|
||||
return pageNumber
|
||||
}
|
||||
|
||||
if let anchor = normalizedLocation.rangeAnchor?.start {
|
||||
if let page = textBook.indexTable.pageNumber(for: anchor, in: textBook) {
|
||||
return page + 1
|
||||
}
|
||||
}
|
||||
|
||||
if let publication {
|
||||
return textBook.pageNumber(
|
||||
for: normalizedLocation,
|
||||
resolver: publication.resourceResolver,
|
||||
bookIdentifier: currentBookIdentifier
|
||||
)
|
||||
}
|
||||
// External text books have no resolver; hrefs match verbatim.
|
||||
return textBook.chapterData(for: normalizedLocation.href)?
|
||||
.pageNumber(for: normalizedLocation)
|
||||
}
|
||||
|
||||
return readingSession?.queueNavigation(
|
||||
to: location,
|
||||
relativeToSpineIndex: nil,
|
||||
bookIdentifier: currentBookIdentifier
|
||||
)
|
||||
}
|
||||
|
||||
func resolvedTextLocation(forPageNumber pageNumber: Int) -> RDEPUBLocation? {
|
||||
if let resolvedPage = resolvedRuntimePage(forPageNumber: pageNumber) {
|
||||
let startOffset = resolvedPage.page.pageStartOffset
|
||||
let endOffset = max(startOffset, resolvedPage.page.pageEndOffset)
|
||||
let chapterData = makeRuntimeChapterData(from: resolvedPage)
|
||||
let location = chapterData.location(
|
||||
for: NSRange(location: startOffset, length: max(endOffset - startOffset + 1, 1)),
|
||||
bookIdentifier: currentBookIdentifier
|
||||
)
|
||||
if let publication {
|
||||
return publication.resourceResolver.normalizedLocation(
|
||||
location,
|
||||
relativeToSpineIndex: nil,
|
||||
bookIdentifier: currentBookIdentifier
|
||||
) ?? location
|
||||
}
|
||||
return location
|
||||
}
|
||||
|
||||
guard let textBook,
|
||||
let page = textBook.page(at: pageNumber) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let location = textBook.chapterData(forPageNumber: pageNumber)?.location(forPage: page, bookIdentifier: currentBookIdentifier)
|
||||
?? textBook.location(forPageNumber: pageNumber, bookIdentifier: currentBookIdentifier)
|
||||
guard let location else { return nil }
|
||||
|
||||
if let publication {
|
||||
return publication.resourceResolver.normalizedLocation(
|
||||
location,
|
||||
relativeToSpineIndex: nil,
|
||||
bookIdentifier: currentBookIdentifier
|
||||
) ?? location
|
||||
}
|
||||
return location
|
||||
}
|
||||
|
||||
func currentVisibleRangeInfo() -> String? {
|
||||
guard let pageNumber = currentPageNumber else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let resolvedPage = resolvedRuntimePage(forPageNumber: pageNumber) {
|
||||
let startOffset = resolvedPage.page.pageStartOffset
|
||||
let endOffset = max(startOffset + 1, resolvedPage.page.pageEndOffset + 1)
|
||||
return RDEPUBTextOffsetRangeInfo(
|
||||
href: resolvedPage.chapter.href,
|
||||
start: startOffset,
|
||||
end: endOffset
|
||||
).jsonString()
|
||||
}
|
||||
|
||||
guard let textBook,
|
||||
let page = textBook.page(at: pageNumber) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let startOffset = page.pageStartOffset
|
||||
let endOffset = max(startOffset + 1, page.pageEndOffset + 1)
|
||||
return RDEPUBTextOffsetRangeInfo(
|
||||
href: page.href,
|
||||
start: startOffset,
|
||||
end: endOffset
|
||||
).jsonString()
|
||||
}
|
||||
|
||||
func synchronizeTextReadingState(pageNumber: Int, location: RDEPUBLocation) {
|
||||
if let resolvedPage = resolvedRuntimePage(forPageNumber: pageNumber) {
|
||||
readingSession?.updateReadingContext(
|
||||
pageNumber: pageNumber,
|
||||
location: location,
|
||||
spineIndex: resolvedPage.page.spineIndex,
|
||||
chapterIndex: resolvedPage.chapterIndex,
|
||||
bookIdentifier: currentBookIdentifier
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
guard let textBook,
|
||||
let page = textBook.page(at: pageNumber) else {
|
||||
return
|
||||
}
|
||||
|
||||
readingSession?.updateReadingContext(
|
||||
pageNumber: pageNumber,
|
||||
location: location,
|
||||
spineIndex: page.spineIndex,
|
||||
chapterIndex: page.chapterIndex,
|
||||
bookIdentifier: currentBookIdentifier
|
||||
)
|
||||
}
|
||||
|
||||
func nativeTextSnapshot(from textBook: RDEPUBTextBook) -> RDEPUBNativeTextSnapshot {
|
||||
let chapters = textBook.chapterInfos
|
||||
let pages = textBook.pages.map {
|
||||
EPUBPage(
|
||||
spineIndex: $0.spineIndex,
|
||||
chapterIndex: $0.chapterIndex,
|
||||
pageIndexInChapter: $0.pageIndexInChapter,
|
||||
totalPagesInChapter: $0.totalPagesInChapter,
|
||||
chapterTitle: $0.chapterTitle,
|
||||
fixedSpread: nil
|
||||
)
|
||||
}
|
||||
return (pages, chapters)
|
||||
}
|
||||
|
||||
func chapterOffset(for location: RDEPUBLocation, fallbackEntry: RDEPUBBookPageMapEntry, rangeInfo: String? = nil) -> Int {
|
||||
if let decodedRangeInfo = decodedRangeInfo(from: rangeInfo, matching: location) {
|
||||
return decodedRangeInfo.start
|
||||
}
|
||||
// Native text anchors carry the exact UTF-16 chapter offset and remain
|
||||
// stable across font/layout changes. Prefer them over CFI recovery,
|
||||
// which may temporarily resolve to the chapter's first marker while a
|
||||
// new CFI map is still being prepared.
|
||||
if let anchor = location.rangeAnchor?.start {
|
||||
return anchor.chapterOffset
|
||||
}
|
||||
if let spineIndex = readerContext.normalizedSpineIndex(for: location),
|
||||
let runtimeChapter = runtime.chapterRuntimeStore.chapterData(for: spineIndex),
|
||||
let cfi = primaryLocationCFI(for: location),
|
||||
let offset = runtimeChapter.chapterOffsetMap.chapterOffset(forCFI: cfi) {
|
||||
return offset
|
||||
}
|
||||
if let rawCFI = primaryLocationCFI(for: location),
|
||||
let cfi = RDEPUBCFICompatibility.parseLossy(rawCFI),
|
||||
let cfiOffset = RDEPUBCFIResolver.resolve(cfi).chapterOffset {
|
||||
return cfiOffset
|
||||
}
|
||||
if let fragment = location.fragment,
|
||||
let offset = fallbackEntry.fragmentOffsets[fragment] {
|
||||
return offset
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func fallbackLocalPageIndex(for location: RDEPUBLocation, pageCount: Int) -> Int {
|
||||
guard pageCount > 1 else { return 0 }
|
||||
return min(
|
||||
pageCount - 1,
|
||||
max(0, Int(round(location.navigationProgression * Double(pageCount - 1))))
|
||||
)
|
||||
}
|
||||
|
||||
private func resolvedRuntimePage(forPageNumber pageNumber: Int) -> RDEPUBResolvedPage? {
|
||||
runtime.pageResolver.resolvePage(absolutePageIndex: pageNumber - 1)
|
||||
}
|
||||
|
||||
private func resolvedLocalPageIndex(
|
||||
for location: RDEPUBLocation,
|
||||
spineIndex: Int,
|
||||
fallbackEntry: RDEPUBBookPageMapEntry,
|
||||
rangeInfo: String? = nil
|
||||
) -> Int? {
|
||||
let offset = chapterOffset(for: location, fallbackEntry: fallbackEntry, rangeInfo: rangeInfo)
|
||||
|
||||
if let runtimeChapter = runtime.chapterRuntimeStore.chapterData(for: spineIndex),
|
||||
let pageIndex = runtimeChapter.chapterOffsetMap.pageIndex(forChapterOffset: offset) {
|
||||
return pageIndex
|
||||
}
|
||||
|
||||
if let summary = readerContext.chapterSummary(forSpineIndex: spineIndex) {
|
||||
return summary.pageRanges.firstIndex {
|
||||
let range = $0.nsRange
|
||||
return offset >= range.location && offset <= max(range.location + range.length - 1, range.location)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func primaryLocationCFI(for location: RDEPUBLocation) -> String? {
|
||||
if let rangeCFI = RDEPUBCFICompatibility.parseRangeLossy(location.rangeCFI) {
|
||||
return rangeCFI.start.rawValue
|
||||
}
|
||||
return location.cfi
|
||||
}
|
||||
|
||||
private func pageNumberFromRangeInfo(
|
||||
_ rangeInfo: String?,
|
||||
in textBook: RDEPUBTextBook,
|
||||
matching location: RDEPUBLocation
|
||||
) -> Int? {
|
||||
guard let decodedRangeInfo = decodedRangeInfo(from: rangeInfo, matching: location),
|
||||
let chapterData = chapterData(in: textBook, for: decodedRangeInfo),
|
||||
let page = chapterData.page(containing: decodedRangeInfo.start) else {
|
||||
return nil
|
||||
}
|
||||
return page.absolutePageIndex + 1
|
||||
}
|
||||
|
||||
private func decodedRangeInfo(
|
||||
from rangeInfo: String?,
|
||||
matching location: RDEPUBLocation
|
||||
) -> RDEPUBTextOffsetRangeInfo? {
|
||||
guard let decodedRangeInfo = RDEPUBTextOffsetRangeInfo.decode(from: rangeInfo),
|
||||
normalizedHref(decodedRangeInfo.href) == normalizedHref(location.href) else {
|
||||
return nil
|
||||
}
|
||||
return decodedRangeInfo
|
||||
}
|
||||
|
||||
private func chapterData(
|
||||
in textBook: RDEPUBTextBook,
|
||||
for rangeInfo: RDEPUBTextOffsetRangeInfo
|
||||
) -> RDEPUBChapterData? {
|
||||
if let chapterData = textBook.chapterData(for: rangeInfo.href) {
|
||||
return chapterData
|
||||
}
|
||||
|
||||
let targetHref = normalizedHref(rangeInfo.href)
|
||||
if let chapterData = textBook.chapterData(for: targetHref) {
|
||||
return chapterData
|
||||
}
|
||||
|
||||
guard let chapter = textBook.chapters.first(where: { normalizedHref($0.href) == targetHref }) else {
|
||||
return nil
|
||||
}
|
||||
return textBook.chapterData(for: chapter.href)
|
||||
}
|
||||
|
||||
private func normalizedHref(_ href: String) -> String {
|
||||
publication?.resourceResolver.normalizedHref(href)
|
||||
?? href.components(separatedBy: "#").first
|
||||
?? href
|
||||
}
|
||||
|
||||
private func nearestFragmentID(beforeOrAt offset: Int, fragmentOffsets: [String: Int]) -> String? {
|
||||
var bestID: String?
|
||||
var bestOffset = Int.min
|
||||
for (fragmentID, fragmentOffset) in fragmentOffsets where fragmentOffset <= offset && fragmentOffset > bestOffset {
|
||||
bestOffset = fragmentOffset
|
||||
bestID = fragmentID
|
||||
}
|
||||
return bestID
|
||||
}
|
||||
|
||||
private func makeRuntimeChapterData(from resolvedPage: RDEPUBResolvedPage) -> RDEPUBChapterData {
|
||||
let textChapter = RDEPUBTextChapter(
|
||||
chapterIndex: resolvedPage.chapterIndex,
|
||||
spineIndex: resolvedPage.chapter.spineIndex,
|
||||
href: resolvedPage.chapter.href,
|
||||
title: resolvedPage.chapter.title,
|
||||
attributedContent: resolvedPage.chapter.typesetAttributedString,
|
||||
fragmentOffsets: resolvedPage.chapter.chapterOffsetMap.fragmentOffsets,
|
||||
cfiMap: resolvedPage.chapter.chapterOffsetMap.cfiMap,
|
||||
pageBreakReasons: resolvedPage.chapter.pages.map(\.metadata.breakReason),
|
||||
pages: resolvedPage.chapter.pages
|
||||
)
|
||||
return RDEPUBChapterData(
|
||||
chapter: textChapter,
|
||||
indexTable: RDEPUBTextIndexTable(chapters: [textChapter])
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user