feat(wxread): align pagination, rendering, and docs
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
import UIKit
|
||||
|
||||
public final class RDEPUBChapterData {
|
||||
public let chapter: RDEPUBTextChapter
|
||||
public let indexTable: RDEPUBTextIndexTable
|
||||
|
||||
public init(chapter: RDEPUBTextChapter, indexTable: RDEPUBTextIndexTable) {
|
||||
self.chapter = chapter
|
||||
self.indexTable = indexTable
|
||||
}
|
||||
|
||||
public var chapterIndex: Int { chapter.chapterIndex }
|
||||
public var spineIndex: Int { chapter.spineIndex }
|
||||
public var href: String { chapter.href }
|
||||
public var title: String { chapter.title }
|
||||
public var attributedContent: NSAttributedString { chapter.attributedContent }
|
||||
public var pages: [RDEPUBTextPage] { chapter.pages }
|
||||
public var fragmentOffsets: [String: Int] { chapter.fragmentOffsets }
|
||||
|
||||
public func page(containing absoluteOffset: Int) -> RDEPUBTextPage? {
|
||||
chapter.pages.first { NSLocationInRange(absoluteOffset, $0.contentRange) }
|
||||
}
|
||||
|
||||
public func pageNumber(containing absoluteOffset: Int) -> Int? {
|
||||
page(containing: absoluteOffset)?.absolutePageIndex
|
||||
}
|
||||
|
||||
public func page(atAbsolutePageIndex absolutePageIndex: Int) -> RDEPUBTextPage? {
|
||||
chapter.pages.first { $0.absolutePageIndex == absolutePageIndex }
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
public func location(for absoluteRange: NSRange, bookIdentifier: String?) -> RDEPUBLocation {
|
||||
indexTable.location(
|
||||
for: rangeAnchor(for: absoluteRange),
|
||||
in: chapter,
|
||||
bookIdentifier: bookIdentifier
|
||||
)
|
||||
}
|
||||
|
||||
public func location(forPage page: RDEPUBTextPage, bookIdentifier: String?) -> RDEPUBLocation {
|
||||
location(for: page.contentRange, bookIdentifier: bookIdentifier)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
public func pageNumber(for location: RDEPUBLocation) -> Int? {
|
||||
if let range = absoluteRange(for: location) {
|
||||
return pageNumber(containing: range.location).map { $0 + 1 }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func absoluteOffsetRange(for page: RDEPUBTextPage) -> Range<Int> {
|
||||
let lowerBound = page.pageStartOffset
|
||||
let upperBound = page.pageEndOffset + 1
|
||||
return lowerBound..<max(upperBound, lowerBound)
|
||||
}
|
||||
|
||||
private func absoluteOffset(for anchor: RDEPUBTextAnchor) -> Int {
|
||||
indexTable.absoluteIndex(
|
||||
fileIndex: anchor.fileIndex,
|
||||
row: anchor.row,
|
||||
column: anchor.column
|
||||
) ?? anchor.chapterOffset
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user