krz/org-swift

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

Sources/OrgSwiftUI/OrgMathText.swift

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

72 lines · 2826 bytes

6 symbols in this file
 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. The style is that of the text the
 6/// expression sits in, so math in a heading can be set at the heading's size.
 7public typealias OrgMathRenderer = @MainActor (OrgMath, Font.TextStyle) -> Text?
 8
 9/// The text style a math run is set in, recorded by ``OrgInlineStyler`` for the renderer.
10enum OrgMathStyleAttribute: AttributedStringKey {
11    typealias Value = Font.TextStyle
12    static let name = "OrgSwiftUI.mathStyle"
13}
14
15private struct OrgMathKey: EnvironmentKey {
16    static let defaultValue: OrgMathRenderer? = nil
17}
18
19extension EnvironmentValues {
20    var orgMath: OrgMathRenderer? {
21        get { self[OrgMathKey.self] }
22        set { self[OrgMathKey.self] = newValue }
23    }
24}
25
26/// A styled run as `Text`, each expression drawn by `math` where it can be. Runs are taken
27/// by their math attribute alone, so an expression a `textTransform` split is still one;
28/// two identical expressions side by side share a range, so one holding its source k times
29/// draws k.
30@MainActor
31func orgText(_ string: AttributedString, math: OrgMathRenderer?) -> Text {
32    guard let math, string.runs.contains(where: { $0[OrgMathAttribute.self] != nil }) else {
33        return Text(string)
34    }
35    var result = Text(verbatim: "")
36    for (expression, range) in string.runs[OrgMathAttribute.self] {
37        let piece = AttributedString(string[range])
38        let style = piece.runs.first?[OrgMathStyleAttribute.self] ?? .body
39        guard let expression, let drawn = math(expression, style) else {
40            result = Text("\(result)\(Text(piece))")
41            continue
42        }
43        let count = max(1, piece.characters.count / max(1, expression.source.count))
44        for _ in 0..<count { result = Text("\(result)\(drawn)") }
45    }
46    return result
47}
48
49/// Display math on its own: centred, scrolling sideways when wider than the column, or the
50/// source as a code block where nothing draws it.
51struct OrgMathBlockView: View {
52    let math: OrgMath
53    @Environment(\.orgMath) private var renderer
54
55    var body: some View {
56        if let drawn = renderer?(math, .body) {
57            ViewThatFits(in: .horizontal) {
58                drawn.frame(maxWidth: .infinity)
59                ScrollView(.horizontal, showsIndicators: false) { drawn }
60            }
61            .padding(.vertical, 4)
62        } else {
63            ScrollView(.horizontal) {
64                Text(math.source)
65                    .font(.system(.footnote, design: .monospaced))
66                    .textSelection(.enabled)
67                    .padding(10)
68            }
69            .background(Color.secondary.opacity(0.12), in: RoundedRectangle(cornerRadius: 4))
70        }
71    }
72}