// RDEPUBReaderController+PublicAPI.swift // RDEPUBReaderController 的公开 API 扩展,提供书签、高亮、目录跳转、搜索等操作 import UIKit // MARK: - Public Reader Commands extension RDEPUBReaderController { /// 重新加载书籍,重置所有状态并重新解析 EPUB public func reloadBook() { runtime.reloadBook() } /// 跳转到指定阅读位置 /// - Parameter location: 目标阅读位置 public func go(to location: RDEPUBLocation) { guard publication != nil else { return } _ = runtime.go(to: location) } /// 跳转到指定页码 /// - Parameters: /// - pageNumber: 目标页码(从 1 开始) /// - animated: 是否动画过渡 /// - Returns: 是否跳转成功 @discardableResult public func go(toPageNumber pageNumber: Int, animated: Bool = false) -> Bool { runtime.go(toPageNumber: pageNumber, animated: animated) } /// 清除当前文本选中状态 public func clearSelection() { runtime.clearSelection() } /// 根据唯一标识获取书签 /// - Parameter id: 书签 ID /// - Returns: 匹配的书签对象,不存在时返回 nil public func bookmark(withID id: String) -> RDEPUBBookmark? { runtime.bookmark(withID: id) } /// 根据唯一标识获取高亮标注 /// - Parameter id: 高亮 ID /// - Returns: 匹配的高亮对象,不存在时返回 nil public func highlight(withID id: String) -> RDEPUBHighlight? { runtime.highlight(withID: id) } /// 获取当前页的原生文本语义摘要,用于调试和可访问性 /// - Returns: 包含页码、断行原因、块类型等信息的摘要字符串,无内容时返回 nil public func nativeTextSemanticSummary() -> String? { let resolvedPage: RDEPUBTextPage? if let textBook { resolvedPage = textBook.page(at: max(readerView.currentPage + 1, 1)) ?? textBook.pages.first } else if readerContext.bookPageMap != nil { let absolutePageIndex = max(readerView.currentPage, 0) _ = runtime.prepareOnDemandChapter(forAbsolutePageNumber: absolutePageIndex + 1) resolvedPage = runtime.pageResolver.resolvePage(absolutePageIndex: absolutePageIndex)?.page } else { resolvedPage = nil } guard let page = resolvedPage else { return nil } let metadata = page.metadata var parts = [ "page \(page.absolutePageIndex + 1)", "break \(metadata.breakReason.rawValue)", metadata.blockKinds.isEmpty ? nil : "block kinds [\(metadata.blockKinds.map(\.rawValue).joined(separator: ","))]", metadata.semanticHints.isEmpty ? nil : "hints [\(metadata.semanticHints.map(\.rawValue).joined(separator: ","))]", metadata.attachmentPlacements.isEmpty ? nil : "placements [\(metadata.attachmentPlacements.map(\.rawValue).joined(separator: ","))]" ].compactMap { $0 } if let firstDiagnostic = metadata.diagnostics.first { parts.append(firstDiagnostic) } return parts.joined(separator: " · ") } /// 添加高亮标注,从当前选中文本或指定选区创建 /// - Parameters: /// - selection: 文本选区,默认使用 currentSelection /// - color: 高亮颜色(CSS 格式),默认黄色 /// - note: 可选批注文字 /// - Returns: 创建的高亮对象,重复时返回 nil @discardableResult public func addHighlight( from selection: RDEPUBSelection? = nil, color: String = "#F8E16C", note: String? = nil ) -> RDEPUBHighlight? { runtime.addHighlight(from: selection, color: color, note: note) } /// 添加指定样式的标注,从当前选中文本或指定选区创建 /// - Parameters: /// - selection: 文本选区,默认使用 currentSelection /// - style: 标注样式(下划线、高亮等) /// - color: 标注颜色(CSS 格式),默认黄色 /// - note: 可选批注文字 /// - Returns: 创建的高亮对象,重复时返回 nil @discardableResult public func addAnnotation( from selection: RDEPUBSelection? = nil, style: RDEPUBHighlightStyle, color: String = "#F8E16C", note: String? = nil ) -> RDEPUBHighlight? { runtime.addAnnotation(from: selection, style: style, color: color, note: note) } /// 插入或更新高亮标注(存在则更新,不存在则插入) /// - Parameter highlight: 要写入的高亮对象 /// - Returns: 写入后的高亮对象 @discardableResult public func upsertHighlight(_ highlight: RDEPUBHighlight) -> RDEPUBHighlight? { runtime.upsertHighlight(highlight) } /// 移除指定高亮标注 /// - Parameter id: 高亮 ID /// - Returns: 被移除的高亮对象,不存在时返回 nil @discardableResult public func removeHighlight(id: String) -> RDEPUBHighlight? { runtime.removeHighlight(id: id) } /// 更新高亮标注的批注内容 /// - Parameters: /// - id: 高亮 ID /// - note: 新的批注文字,传 nil 清除批注 /// - Returns: 更新后的高亮对象,不存在时返回 nil @discardableResult public func updateHighlightNote(id: String, note: String?) -> RDEPUBHighlight? { runtime.updateHighlightNote(id: id, note: note) } /// 跳转到指定高亮标注所在位置 /// - Parameters: /// - id: 高亮 ID /// - animated: 是否动画过渡 /// - Returns: 是否跳转成功 @discardableResult public func go(toHighlightID id: String, animated: Bool = true) -> Bool { runtime.go(toHighlightID: id, animated: animated) } /// 移除当前书籍的所有高亮标注 public func removeAllHighlights() { runtime.removeAllHighlights() } /// 跳转到目录项对应的阅读位置 /// - Parameters: /// - item: EPUB 原生目录项 /// - animated: 是否动画过渡 /// - Returns: 是否跳转成功 @discardableResult public func go(toTableOfContentsItem item: EPUBTableOfContentsItem, animated: Bool = true) -> Bool { go(toTableOfContentsHref: item.href, animated: animated) } /// 跳转到目录项对应的阅读位置 /// - Parameters: /// - item: RDReader 目录项 /// - animated: 是否动画过渡 /// - Returns: 是否跳转成功 @discardableResult public func go(toTableOfContentsItem item: RDEPUBReaderTableOfContentsItem, animated: Bool = true) -> Bool { go(toTableOfContentsHref: item.href, animated: animated) } /// 通过目录 href 跳转到对应阅读位置,支持带锚点的 href /// - Parameters: /// - href: 目录资源路径(可含 #fragment) /// - animated: 是否动画过渡 /// - Returns: 是否跳转成功 @discardableResult public func go(toTableOfContentsHref href: String, animated: Bool = true) -> Bool { guard publication != nil else { return false } let components = href.components(separatedBy: "#") let baseHref = components.first ?? href let fragment = components.count > 1 ? components[1] : nil let location = RDEPUBLocation( bookIdentifier: currentBookIdentifier, href: baseHref, progression: 0, fragment: fragment ) return restoreReadingLocation(location, animated: animated) } /// 在当前位置添加书签 /// - Parameter note: 可选批注文字 /// - Returns: 创建的书签对象,位置已存在书签时返回 nil @discardableResult public func addBookmark(note: String? = nil) -> RDEPUBBookmark? { runtime.addBookmark(note: note) } /// 切换当前位置的书签状态(有则移除,无则添加) /// - Parameter note: 添加时可附带的批注文字 /// - Returns: 添加时返回新书签,移除时返回被移除的书签 @discardableResult public func toggleBookmark(note: String? = nil) -> RDEPUBBookmark? { runtime.toggleBookmark(note: note) } /// 移除指定书签 /// - Parameter id: 书签 ID /// - Returns: 被移除的书签对象,不存在时返回 nil @discardableResult public func removeBookmark(id: String) -> RDEPUBBookmark? { runtime.removeBookmark(id: id) } /// 跳转到指定书签所在位置 /// - Parameters: /// - id: 书签 ID /// - animated: 是否动画过渡 /// - Returns: 是否跳转成功 @discardableResult public func go(toBookmarkID id: String, animated: Bool = true) -> Bool { runtime.go(toBookmarkID: id, animated: animated) } /// 执行全文搜索,匹配结果非空时自动跳转到第一个匹配项,无匹配时清除搜索状态 /// - Parameter keyword: 搜索关键词,空字符串会清除搜索 public func search(keyword: String) { runtime.search(keyword: keyword) } /// 跳转到下一个搜索匹配项 /// - Returns: 是否存在下一个匹配项并跳转成功 @discardableResult public func searchNext() -> Bool { runtime.searchNext() } /// 跳转到上一个搜索匹配项 /// - Returns: 是否存在上一个匹配项并跳转成功 @discardableResult public func searchPrevious() -> Bool { runtime.searchPrevious() } /// 清除搜索状态和高亮 public func clearSearch() { runtime.clearSearch() } }