ReadViewSDK/Sources/RDReaderView/EPUBCore/RDEPUBWebView+JavaScriptBridge.swift
shenlei d7f28c3f10 fix: 修复右对齐短文本显示不完整及多项功能改进
右对齐文本显示修复(如"巫鸿"只显示"鸿"的问题):
- RDEPUBChapterTailNormalizer: 短尾页合并时检查 avoidPageBreakInside 语义,
  避免将带有此标记的短文本(如 right-info 署名)错误合并回上一页
- RDEPUBTextContentView: 续段归一化时跳过右对齐/居中对齐的段落,
  防止错误修改 firstLineHeadIndent 导致首字不可见
- RDEPUBCSSCompatibilityLayer: 为含 text-align:right/center 的 CSS 块
  自动注入 text-indent:0 !important,确保右对齐/居中文本无首行缩进
- RDEPUBTextRendererSupport: 排版属性归一化时将右对齐/居中段落的
  firstLineHeadIndent 重置为 0

图片查看器及脚注点击功能:
- 新增 RDEPUBImageViewController 和 RDEPUBImageViewerCoordinator,
  支持从 WebView 和 TextPage 两种模式查看图片
- epub-bridge.js: 检测图片和脚注图片的点击事件,脚注图片显示弹窗
- RDEPUBJavaScriptBridge: 新增 imageDidTap/footnoteDidTap 桥接消息
- RDEPUBWebView: 新增图片和脚注点击的 delegate 方法
- RDEPUBAttachmentNormalizer: 改进脚注检测,优先使用 alt 文本判断
- RDEPUBPaginationModels: 新增 .footnote 附件类型
- RDEPUBPageLayoutSnapshot: 运行时动态解析附件类型,脚注优先级高于图片

位置解析改进:
- RDEPUBReaderController+LocationResolution: 利用 rangeInfo 提升页码定位精度

其他:
- RDEPUBTextBookCache: schema 版本升级至 13
- RDEPUBReaderTheme: 主题更新
- Pod 项目文件更新

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-25 15:39:58 +08:00

150 lines
7.1 KiB
Swift

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
}
}
}