2026-02-24 14:44:43 +00:00
|
|
|
import SwiftUI
|
|
|
|
|
import WebKit
|
|
|
|
|
|
2026-02-28 19:48:06 +00:00
|
|
|
#if os(macOS)
|
2026-03-09 16:47:50 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
/// MARK: - Types
|
|
|
|
|
|
2026-02-24 14:44:43 +00:00
|
|
|
struct MarkdownPreviewWebView: NSViewRepresentable {
|
|
|
|
|
let html: String
|
|
|
|
|
|
|
|
|
|
func makeCoordinator() -> Coordinator {
|
|
|
|
|
Coordinator()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func makeNSView(context: Context) -> WKWebView {
|
2026-02-28 19:48:06 +00:00
|
|
|
let webView = makeConfiguredWebView()
|
2026-02-24 14:44:43 +00:00
|
|
|
webView.loadHTMLString(html, baseURL: nil)
|
|
|
|
|
context.coordinator.lastHTML = html
|
|
|
|
|
return webView
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateNSView(_ webView: WKWebView, context: Context) {
|
|
|
|
|
guard context.coordinator.lastHTML != html else { return }
|
2026-03-08 13:07:50 +00:00
|
|
|
context.coordinator.reloadPreservingScroll(webView: webView, html: html)
|
2026-02-24 14:44:43 +00:00
|
|
|
context.coordinator.lastHTML = html
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final class Coordinator {
|
|
|
|
|
var lastHTML: String = ""
|
2026-03-08 13:07:50 +00:00
|
|
|
|
|
|
|
|
func reloadPreservingScroll(webView: WKWebView, html: String) {
|
|
|
|
|
let capture = "(() => { const max = Math.max(1, document.documentElement.scrollHeight - window.innerHeight); return window.scrollY / max; })();"
|
|
|
|
|
webView.evaluateJavaScript(capture) { value, _ in
|
|
|
|
|
let ratio = value as? Double ?? 0
|
|
|
|
|
webView.loadHTMLString(html, baseURL: nil)
|
|
|
|
|
let clamped = min(1.0, max(0.0, ratio))
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.06) {
|
|
|
|
|
let restore = "(() => { const max = Math.max(0, document.documentElement.scrollHeight - window.innerHeight); window.scrollTo(0, max * \(clamped)); })();"
|
|
|
|
|
webView.evaluateJavaScript(restore, completionHandler: nil)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-24 14:44:43 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-28 19:48:06 +00:00
|
|
|
#elseif os(iOS)
|
|
|
|
|
struct MarkdownPreviewWebView: UIViewRepresentable {
|
|
|
|
|
let html: String
|
|
|
|
|
|
|
|
|
|
func makeCoordinator() -> Coordinator {
|
|
|
|
|
Coordinator()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func makeUIView(context: Context) -> WKWebView {
|
|
|
|
|
let webView = makeConfiguredWebView()
|
|
|
|
|
webView.loadHTMLString(html, baseURL: nil)
|
|
|
|
|
context.coordinator.lastHTML = html
|
|
|
|
|
return webView
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateUIView(_ webView: WKWebView, context: Context) {
|
|
|
|
|
guard context.coordinator.lastHTML != html else { return }
|
2026-03-08 13:07:50 +00:00
|
|
|
context.coordinator.reloadPreservingScroll(webView: webView, html: html)
|
2026-02-28 19:48:06 +00:00
|
|
|
context.coordinator.lastHTML = html
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final class Coordinator {
|
|
|
|
|
var lastHTML: String = ""
|
2026-03-08 13:07:50 +00:00
|
|
|
|
|
|
|
|
func reloadPreservingScroll(webView: WKWebView, html: String) {
|
|
|
|
|
let capture = "(() => { const max = Math.max(1, document.documentElement.scrollHeight - window.innerHeight); return window.scrollY / max; })();"
|
|
|
|
|
webView.evaluateJavaScript(capture) { value, _ in
|
|
|
|
|
let ratio = value as? Double ?? 0
|
|
|
|
|
webView.loadHTMLString(html, baseURL: nil)
|
|
|
|
|
let clamped = min(1.0, max(0.0, ratio))
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.06) {
|
|
|
|
|
let restore = "(() => { const max = Math.max(0, document.documentElement.scrollHeight - window.innerHeight); window.scrollTo(0, max * \(clamped)); })();"
|
|
|
|
|
webView.evaluateJavaScript(restore, completionHandler: nil)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-28 19:48:06 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
private func makeConfiguredWebView() -> WKWebView {
|
|
|
|
|
let configuration = WKWebViewConfiguration()
|
|
|
|
|
configuration.defaultWebpagePreferences.allowsContentJavaScript = false
|
|
|
|
|
let webView = WKWebView(frame: .zero, configuration: configuration)
|
|
|
|
|
#if os(macOS)
|
|
|
|
|
webView.setValue(false, forKey: "drawsBackground")
|
|
|
|
|
#else
|
|
|
|
|
webView.isOpaque = false
|
|
|
|
|
webView.backgroundColor = .clear
|
|
|
|
|
webView.scrollView.backgroundColor = .clear
|
|
|
|
|
#endif
|
|
|
|
|
webView.allowsBackForwardNavigationGestures = false
|
|
|
|
|
#if os(iOS)
|
|
|
|
|
webView.scrollView.contentInsetAdjustmentBehavior = .never
|
2026-02-24 14:44:43 +00:00
|
|
|
#endif
|
2026-02-28 19:48:06 +00:00
|
|
|
return webView
|
|
|
|
|
}
|