~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
215 lines · 8.9 KB Raw History
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)"
    }
}