import Foundation import WebKit extension RDEPUBWebView { func internalLocation(from url: URL) -> RDEPUBLocation? { let href = url.path.removingPercentEncoding?.trimmingCharacters(in: CharacterSet(charactersIn: "/")) ?? "" let fragment = url.fragment guard !href.isEmpty || fragment != nil else { return nil } return RDEPUBLocation(href: href, progression: 0, fragment: fragment) } func currentLocation(from body: [String: Any]) -> RDEPUBLocation { let progression = (body["progression"] as? NSNumber)?.doubleValue ?? 0 let lastProgression = (body["lastProgression"] as? NSNumber)?.doubleValue let fragment = body["fragment"] as? String let href = body["href"] as? String ?? currentHref return RDEPUBLocation(href: href, progression: progression, lastProgression: lastProgression, fragment: fragment) } } extension RDEPUBWebView: WKNavigationDelegate { public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didStart", url: webView.url) } public func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didCommit", url: webView.url) } public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didFinish", url: webView.url) applyPresentation() } public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "processTerminated", url: webView.url) } public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didFail", url: webView.url, error: error) } public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "didFailProvisional", url: webView.url, error: error) } public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "decidePolicy", url: navigationAction.request.url) guard navigationAction.navigationType == .linkActivated, let url = navigationAction.request.url else { decisionHandler(.allow) return } if url.scheme == RDEPUBResourceURLSchemeHandler.scheme, let location = internalLocation(from: url) { delegate?.epubWebView(self, didActivateInternalLink: location, fromSpineIndex: currentSpineIndex) decisionHandler(.cancel) return } if let scheme = url.scheme?.lowercased(), ["http", "https", "mailto", "tel"].contains(scheme) { delegate?.epubWebView(self, didActivateExternalLink: url) decisionHandler(.cancel) return } decisionHandler(.allow) } } extension RDEPUBWebView: WKScriptMessageHandler { public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { RDEPUBWebViewDebug.logMessage(debugScope, webView: message.webView, name: message.name, body: message.body) switch message.name { case RDEPUBJavaScriptBridgeMessage.progressionChanged.rawValue: guard let body = message.body as? [String: Any] else { return } delegate?.epubWebView(self, didUpdateLocation: currentLocation(from: body), spineIndex: currentSpineIndex) case RDEPUBJavaScriptBridgeMessage.selectionChanged.rawValue: if let body = message.body as? [String: Any], let text = body["text"] as? String, let rangeInfo = body["rangeInfo"] as? String { let location = currentLocation(from: body) let selection = RDEPUBSelection(location: location, text: text, rangeInfo: rangeInfo) delegate?.epubWebView(self, didChangeSelection: selection, spineIndex: currentSpineIndex) } else { delegate?.epubWebView(self, didChangeSelection: nil, spineIndex: currentSpineIndex) } case RDEPUBJavaScriptBridgeMessage.internalLink.rawValue: guard let body = message.body as? [String: Any], let href = body["href"] as? String else { return } let location = RDEPUBLocation(href: href, progression: 0) delegate?.epubWebView(self, didActivateInternalLink: location, fromSpineIndex: currentSpineIndex) case RDEPUBJavaScriptBridgeMessage.externalLink.rawValue: guard let body = message.body as? [String: Any], let urlString = body["url"] as? String, let url = URL(string: urlString) else { return } delegate?.epubWebView(self, didActivateExternalLink: url) case RDEPUBJavaScriptBridgeMessage.javaScriptError.rawValue: delegate?.epubWebView(self, didLogJavaScriptError: "\(message.body)") case RDEPUBJavaScriptBridgeMessage.fixedLayoutReady.rawValue: applyFixedPresentationIfNeeded { self.refreshNativeDecorationsIfNeeded { self.rendered() } } case RDEPUBJavaScriptBridgeMessage.imageDidTap.rawValue: guard let body = message.body as? [String: Any], let src = body["src"] as? String else { return } let sourceRect = sourceRect(from: body["rect"]) delegate?.epubWebView(self, didTapImageWithSource: src, sourceRect: sourceRect) case RDEPUBJavaScriptBridgeMessage.footnoteDidTap.rawValue: guard let body = message.body as? [String: Any], let altText = body["alt"] as? String else { return } let sourceRect = sourceRect(from: body["rect"]) delegate?.epubWebView(self, didTapFootnoteWithAltText: altText, sourceRect: sourceRect) default: break } } private func sourceRect(from value: Any?) -> CGRect? { guard let rectDict = value as? [String: Any] else { return nil } return CGRect( x: cgFloatValue(rectDict["x"]), y: cgFloatValue(rectDict["y"]), width: cgFloatValue(rectDict["width"]), height: cgFloatValue(rectDict["height"]) ) } private func cgFloatValue(_ value: Any?) -> CGFloat { switch value { case let number as NSNumber: return CGFloat(truncating: number) case let double as Double: return CGFloat(double) case let int as Int: return CGFloat(int) case let string as String: return CGFloat(Double(string) ?? 0) default: return 0 } } }