// RDEPUBWebView+Search.swift // RDEPUBWebView 的搜索高亮扩展 // 从当前渲染请求中提取搜索展示信息,生成搜索高亮脚本并注入到 WebView 中执行。 import UIKit import WebKit extension RDEPUBWebView { private var normalSearchOverlayColor: UIColor { UIColor(red: 0.21, green: 0.48, blue: 0.95, alpha: 0.16) } private var activeSearchOverlayColor: UIColor { UIColor(red: 0.14, green: 0.42, blue: 0.95, alpha: 0.34) } /// 如果当前渲染请求包含搜索信息,注入搜索高亮脚本到 WebView func applySearchDecorationsIfNeeded(completion: (() -> Void)? = nil) { guard let webView else { completion?() return } let presentation: RDEPUBSearchPresentation? switch currentRenderRequest { case .reflowable(let request): presentation = request.searchPresentation case .fixed(let request): presentation = request.searchPresentation case .none: presentation = nil } let script = RDEPUBJavaScriptBridge.applySearchScript(for: presentation) webView.evaluateJavaScript(script) { [weak self] _, error in guard let self else { completion?() return } if let error { self.delegate?.epubWebView(self, didLogJavaScriptError: error.localizedDescription) } self.refreshNativeDecorationsIfNeeded(completion: completion) } } func refreshNativeDecorationsIfNeeded(completion: (() -> Void)? = nil) { guard let webView else { onDecorationsResolved?([]) completion?() return } webView.evaluateJavaScript(RDEPUBJavaScriptBridge.resolveDecorationsScript()) { [weak self] result, error in guard let self else { completion?() return } if let error { self.delegate?.epubWebView(self, didLogJavaScriptError: error.localizedDescription) self.onDecorationsResolved?([]) completion?() return } let decorations = self.nativeDecorations(from: result) self.onDecorationsResolved?(decorations) completion?() } } private func nativeDecorations(from result: Any?) -> [RDEPUBTextOverlayDecoration] { guard let payload = result as? [String: Any] else { return [] } let highlightDecorations = decorationList( from: payload["highlights"] as? [[String: Any]], defaultColor: overlayColor(hex: "#F8E16C", alpha: 0.45) ?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.45) ) let searchDecorations = decorationList( from: payload["search"] as? [[String: Any]], defaultColor: normalSearchOverlayColor ) return highlightDecorations + searchDecorations } private func decorationList( from items: [[String: Any]]?, defaultColor: UIColor ) -> [RDEPUBTextOverlayDecoration] { guard let items else { return [] } return items.enumerated().compactMap { index, item -> RDEPUBTextOverlayDecoration? in guard let kindString = item["kind"] as? String, let kind = RDEPUBTextOverlayDecoration.Kind(rawValue: kindString), let rectPayloads = item["rects"] as? [[String: Any]] else { return nil } let rects = rectPayloads.compactMap { rectPayload -> CGRect? in guard let x = (rectPayload["x"] as? NSNumber)?.doubleValue, let y = (rectPayload["y"] as? NSNumber)?.doubleValue, let width = (rectPayload["width"] as? NSNumber)?.doubleValue, let height = (rectPayload["height"] as? NSNumber)?.doubleValue else { return nil } return CGRect(x: x, y: y, width: width, height: height) } guard !rects.isEmpty else { return nil } let color: UIColor switch kind { case .underline: let hex = item["color"] as? String ?? "#F8E16C" color = overlayColor(hex: hex, alpha: 1) ?? defaultColor case .activeSearch: color = activeSearchOverlayColor case .search: color = normalSearchOverlayColor case .highlight: let hex = item["color"] as? String ?? "#F8E16C" color = overlayColor(hex: hex, alpha: 0.45) ?? defaultColor case .selection, .locate: color = defaultColor } return RDEPUBTextOverlayDecoration( kind: kind, absoluteRange: NSRange(location: index, length: 1), rects: rects, color: color ) } } private func overlayColor(hex: String, alpha: CGFloat) -> UIColor? { var value = hex.trimmingCharacters(in: .whitespacesAndNewlines) value = value.replacingOccurrences(of: "#", with: "") guard value.count == 6, let hexValue = Int(value, radix: 16) else { return nil } return UIColor( red: CGFloat((hexValue >> 16) & 0xFF) / 255, green: CGFloat((hexValue >> 8) & 0xFF) / 255, blue: CGFloat(hexValue & 0xFF) / 255, alpha: alpha ) } }