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 { let lowerBound = page.pageStartOffset let upperBound = page.pageEndOffset + 1 return lowerBound.. Int { indexTable.absoluteIndex( fileIndex: anchor.fileIndex, row: anchor.row, column: anchor.column ) ?? anchor.chapterOffset } }