krz/org-swift

A dependency-free Swift library that renders org-mode to sanitized HTML. html library org-mode swift

Sources/OrgSwiftUI/OrgMathText.swift

v0.8.0
org-swift/Sources/OrgSwiftUI/OrgMathText.swift history · blame · raw

64 lines · 2382 bytes

 1import OrgSwift
 2import SwiftUI
 3
 4/// Draws one TeX expression for ``OrgView``: a `Text` holding it, typically an image set on
 5/// the baseline, or nil to show the source as written.
 6public typealias OrgMathRenderer = @MainActor (OrgMath) -> Text?
 7
 8private struct OrgMathKey: EnvironmentKey {
 9    static let defaultValue: OrgMathRenderer? = nil
10}
11
12extension EnvironmentValues {
13    var orgMath: OrgMathRenderer? {
14        get { self[OrgMathKey.self] }
15        set { self[OrgMathKey.self] = newValue }
16    }
17}
18
19/// A styled run as `Text`, each expression drawn by `math` where it can be. Runs are taken
20/// by their math attribute alone, so an expression a `textTransform` split is still one;
21/// two identical expressions side by side share a range, so one holding its source k times
22/// draws k.
23@MainActor
24func orgText(_ string: AttributedString, math: OrgMathRenderer?) -> Text {
25    guard let math, string.runs.contains(where: { $0[OrgMathAttribute.self] != nil }) else {
26        return Text(string)
27    }
28    var result = Text(verbatim: "")
29    for (expression, range) in string.runs[OrgMathAttribute.self] {
30        let piece = AttributedString(string[range])
31        guard let expression, let drawn = math(expression) else {
32            result = Text("\(result)\(Text(piece))")
33            continue
34        }
35        let count = max(1, piece.characters.count / max(1, expression.source.count))
36        for _ in 0..<count { result = Text("\(result)\(drawn)") }
37    }
38    return result
39}
40
41/// Display math on its own: centred, scrolling sideways when wider than the column, or the
42/// source as a code block where nothing draws it.
43struct OrgMathBlockView: View {
44    let math: OrgMath
45    @Environment(\.orgMath) private var renderer
46
47    var body: some View {
48        if let drawn = renderer?(math) {
49            ViewThatFits(in: .horizontal) {
50                drawn.frame(maxWidth: .infinity)
51                ScrollView(.horizontal, showsIndicators: false) { drawn }
52            }
53            .padding(.vertical, 4)
54        } else {
55            ScrollView(.horizontal) {
56                Text(math.source)
57                    .font(.system(.footnote, design: .monospaced))
58                    .textSelection(.enabled)
59                    .padding(10)
60            }
61            .background(Color.secondary.opacity(0.12), in: RoundedRectangle(cornerRadius: 4))
62        }
63    }
64}