krz/org-swift

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

Sources/OrgSwiftUI/OrgView.swift

79 lines · 2820 bytes

 1import OrgSwift
 2import SwiftUI
 3
 4/// Renders org-mode as native SwiftUI.
 5///
 6/// ```swift
 7/// OrgView(source, options: OrgRenderOptions(host: "gitbay.org", owner: "krz",
 8///                                           repositoryName: "org-swift"))
 9/// ```
10///
11/// The parse, footnote numbering, URL resolution and syntax highlighting run once off
12/// `body`, in a task keyed to the source *and the color scheme*, so scrolling and Dynamic
13/// Type changes do not re-derive them — but switching to dark mode does, because a
14/// ``OrgCodeStyler`` bakes its theme's colors into the text it returns. Text is selectable,
15/// links open through the environment's `openURL`, and everything scales with Dynamic Type.
16public struct OrgView: View {
17    private let source: String
18    private let options: OrgRenderOptions
19    private let styler: OrgCodeStyler
20
21    @Environment(\.colorScheme) private var colorScheme
22    @State private var layout = OrgLayout()
23
24    public init(
25        _ source: String,
26        options: OrgRenderOptions = .init(),
27        styler: OrgCodeStyler = PlainOrgCodeStyler()
28    ) {
29        self.source = source
30        self.options = options
31        self.styler = styler
32    }
33
34    public var body: some View {
35        VStack(alignment: .leading, spacing: 12) {
36            OrgBlockListView(blocks: layout.blocks)
37            if !layout.footnotes.isEmpty {
38                OrgFootnotesView(footnotes: layout.footnotes)
39            }
40        }
41        .frame(maxWidth: .infinity, alignment: .leading)
42        .textSelection(.enabled)
43        .task(id: LayoutKey(source: source, colorScheme: colorScheme)) {
44            layout = OrgLayoutBuilder(options: options, styler: styler)
45                .layout(OrgParser.parse(source))
46        }
47    }
48}
49
50/// What the prepared layout depends on. Highlighted code carries baked-in colors, so the
51/// scheme belongs in the key alongside the source.
52private struct LayoutKey: Equatable {
53    var source: String
54    var colorScheme: ColorScheme
55}
56
57/// The notes section org's exporter puts at the end of a document.
58struct OrgFootnotesView: View {
59    let footnotes: [OrgFootnote]
60
61    var body: some View {
62        VStack(alignment: .leading, spacing: 6) {
63            Divider()
64            ForEach(footnotes) { note in
65                HStack(alignment: .firstTextBaseline, spacing: 6) {
66                    Text("\(note.number).")
67                        .font(.footnote)
68                        .monospacedDigit()
69                        .foregroundStyle(.secondary)
70                    Text(note.text)
71                        .fixedSize(horizontal: false, vertical: true)
72                        .frame(maxWidth: .infinity, alignment: .leading)
73                }
74            }
75        }
76        .padding(.top, 8)
77        .accessibilityLabel("Footnotes")
78    }
79}