johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
import JavaScriptCore
import SwiftUI
import UIKit
import WebKit
/// A Mermaid block drawn as the pages draw it, by Pele (https://pele.run). Pele
/// lays it out in a JavaScript context of its own, with no page, network or
/// anything else to reach, measuring its words with the system font. The SVG it
/// makes is then shown in a web view that runs no script. Until it's drawn, and
/// if it can't be, the block shows its source, as `code` draws it.
struct DiagramView<Source: View>: View {
let source: String
@ViewBuilder let code: Source
@State private var width: CGFloat = 0
@State private var drawn: Diagrams.Drawn?
private struct Ask: Equatable {
let source: String
let width: Int
}
var body: some View {
Group {
if let drawn {
ScrollView(.horizontal, showsIndicators: false) {
DiagramPage(svg: drawn.svg)
.frame(width: drawn.width, height: drawn.height)
}
.scrollDisabled(drawn.width <= width)
.accessibilityElement()
.accessibilityLabel("Diagram")
} else {
code
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.onGeometryChange(for: CGFloat.self) { $0.size.width } action: { width = $0 }
.task(id: Ask(source: source, width: Int(width))) {
guard width > 0 else { return }
drawn = await Diagrams.shared.draw(source, width: width)
}
}
}
/// Pele, loaded once, and what it has drawn.
actor Diagrams {
static let shared = Diagrams()
struct Drawn: Sendable, Hashable {
let svg: String
let width: CGFloat
let height: CGFloat
}
static let font = "-apple-system, system-ui, sans-serif"
static let mono = "ui-monospace, Menlo, monospace"
private var context: JSContext?
private var broken = false
private var drawn: [String: Drawn?] = [:]
func draw(_ source: String, width: CGFloat) -> Drawn? {
let key = "\(Int(width))\n\(source)"
if let known = drawn[key] { return known }
let made = render(source, width: width)
if drawn.count > 64 { drawn.removeAll() }
drawn[key] = made
return made
}
private func render(_ source: String, width: CGFloat) -> Drawn? {
guard let context = loaded(), let draw = context.objectForKeyedSubscript("knotlessDraw"),
let answer = draw.call(withArguments: [source, Double(width)]), answer.isString,
let data = answer.toString().data(using: .utf8),
let out = try? JSONDecoder().decode(Out.self, from: data), out.width > 0, out.height > 0
else { return nil }
return Drawn(svg: out.svg, width: CGFloat(out.width), height: CGFloat(out.height))
}
private struct Out: Decodable {
let svg: String
let width: Double
let height: Double
}
private func loaded() -> JSContext? {
if let context { return context }
guard !broken else { return nil }
broken = true
guard let url = Bundle.main.url(forResource: "pele", withExtension: "js"),
let script = try? String(contentsOf: url, encoding: .utf8),
let context = JSContext()
else { return nil }
let measure: @convention(block) (String, Double, Int32) -> Double = { text, size, style in
Diagrams.width(of: text, size: size, style: Int(style))
}
context.setObject(measure, forKeyedSubscript: "knotlessMeasure" as NSString)
context.evaluateScript(script, withSourceURL: url)
context.evaluateScript("""
function knotlessDraw(source, width) {
try {
var r = pele.render(source, {
measurer: { width: knotlessMeasure },
fontFamily: \(Self.quoted(Self.font)),
fontFamilyMono: \(Self.quoted(Self.mono)),
maxWidth: width,
linkRel: 'noopener nofollow ugc'
});
return JSON.stringify({ svg: r.svg, width: r.width, height: r.height });
} catch (e) {
return null;
}
}
""")
guard context.exception == nil, context.objectForKeyedSubscript("pele")?.isObject == true else { return nil }
broken = false
self.context = context
return context
}
private static func quoted(_ s: String) -> String {
"'" + s.replacing("\\", with: "\\\\").replacing("'", with: "\\'") + "'"
}
/// How wide Pele's words are in the font the web view draws them in: its
/// style is bits, 1 bold, 2 italic, 4 monospaced.
static func width(of text: String, size: Double, style: Int) -> Double {
let weight: UIFont.Weight = style & 1 != 0 ? .bold : .regular
var font = style & 4 != 0
? UIFont.monospacedSystemFont(ofSize: size, weight: weight)
: UIFont.systemFont(ofSize: size, weight: weight)
if style & 2 != 0, let italic = font.fontDescriptor.withSymbolicTraits(font.fontDescriptor.symbolicTraits.union(.traitItalic)) {
font = UIFont(descriptor: italic, size: size)
}
let line = CTLineCreateWithAttributedString(NSAttributedString(string: text, attributes: [.font: font]))
return CTLineGetTypographicBounds(line, nil, nil, nil)
}
}
/// An SVG Pele made, in a web view with no script, no store and no way off the page.
private struct DiagramPage: UIViewRepresentable {
let svg: String
/// Read, so a change of theme draws it again, in the theme's colours.
@Environment(\.themeGeneration) private var generation
func makeCoordinator() -> Coordinator { Coordinator() }
func makeUIView(context: Context) -> WKWebView {
let configuration = WKWebViewConfiguration()
configuration.defaultWebpagePreferences.allowsContentJavaScript = false
configuration.websiteDataStore = .nonPersistent()
configuration.dataDetectorTypes = []
let view = WKWebView(frame: .zero, configuration: configuration)
view.isOpaque = false
view.backgroundColor = .clear
view.scrollView.backgroundColor = .clear
view.scrollView.isScrollEnabled = false
view.scrollView.contentInsetAdjustmentBehavior = .never
view.navigationDelegate = context.coordinator
return view
}
func updateUIView(_ view: WKWebView, context: Context) {
let shown = "\(generation) \(svg)"
guard context.coordinator.shown != shown else { return }
context.coordinator.shown = shown
view.loadHTMLString(Self.page(svg), baseURL: nil)
}
final class Coordinator: NSObject, WKNavigationDelegate {
var shown: String?
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
navigationAction.request.url?.absoluteString == "about:blank" ? .allow : .cancel
}
}
static func page(_ svg: String) -> String {
"""
<!doctype html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<style>
:root{color-scheme:light dark;\(palette(.light))}
@media (prefers-color-scheme: dark){:root{\(palette(.dark))}}
html,body{margin:0;padding:0;background:transparent;-webkit-user-select:none;-webkit-touch-callout:none;-webkit-text-size-adjust:none}
svg{display:block}
</style></head><body>\(svg)</body></html>
"""
}
/// The app's colours as Pele's, as the pages give it theirs.
private static func palette(_ style: UIUserInterfaceStyle) -> String {
let traits = UITraitCollection(userInterfaceStyle: style)
func css(_ color: UIColor) -> String {
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
color.resolvedColor(with: traits).getRed(&r, green: &g, blue: &b, alpha: &a)
return "rgba(\(Int((r * 255).rounded())),\(Int((g * 255).rounded())),\(Int((b * 255).rounded())),\(a))"
}
return [
("bg", UIColor(Color.card)),
("fg", UIColor(Color.fg)),
("muted", UIColor(Color.mute)),
("line", UIColor(Color(themed: \.mute, otherwise: .systemGray))),
// Fills a step off the card behind, in dark as in light.
("surface", UIColor(Color.inset)),
("surface-alt", UIColor(Color(themed: { Palette.mix($0.fg, 0.14, $0.bg) }, light: 0xe5e5ea, dark: 0x3a3a3c))),
("border", UIColor(Color(themed: \.line, otherwise: .systemGray2))),
("accent", UIColor(Color.accent)),
("critical", UIColor(Color.danger)),
("error", UIColor(Color.danger)),
].map { "--pele-\($0.0):\(css($0.1))" }.joined(separator: ";")
+ ";--pele-font:\(Diagrams.font);--pele-font-mono:\(Diagrams.mono)"
}
}