krz/org-swift

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

Sources/OrgSwiftUI/OrgView.swift

82 lines · 2960 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    private let keywords: OrgKeywordStyle
21
22    @Environment(\.colorScheme) private var colorScheme
23    @State private var layout = OrgLayout()
24
25    public init(
26        _ source: String,
27        options: OrgRenderOptions = .init(),
28        styler: OrgCodeStyler = PlainOrgCodeStyler(),
29        keywords: OrgKeywordStyle = .init()
30    ) {
31        self.source = source
32        self.options = options
33        self.styler = styler
34        self.keywords = keywords
35    }
36
37    public var body: some View {
38        VStack(alignment: .leading, spacing: 12) {
39            OrgBlockListView(blocks: layout.blocks)
40            if !layout.footnotes.isEmpty {
41                OrgFootnotesView(footnotes: layout.footnotes)
42            }
43        }
44        .frame(maxWidth: .infinity, alignment: .leading)
45        .textSelection(.enabled)
46        .task(id: LayoutKey(source: source, colorScheme: colorScheme)) {
47            layout = OrgLayoutBuilder(options: options, styler: styler, keywords: keywords)
48                .layout(OrgParser.parse(source))
49        }
50    }
51}
52
53/// What the prepared layout depends on. Highlighted code carries baked-in colors, so the
54/// scheme belongs in the key alongside the source.
55private struct LayoutKey: Equatable {
56    var source: String
57    var colorScheme: ColorScheme
58}
59
60/// The notes section org's exporter puts at the end of a document.
61struct OrgFootnotesView: View {
62    let footnotes: [OrgFootnote]
63
64    var body: some View {
65        VStack(alignment: .leading, spacing: 6) {
66            Divider()
67            ForEach(footnotes) { note in
68                HStack(alignment: .firstTextBaseline, spacing: 6) {
69                    Text("\(note.number).")
70                        .font(.footnote)
71                        .monospacedDigit()
72                        .foregroundStyle(.secondary)
73                    Text(note.text)
74                        .fixedSize(horizontal: false, vertical: true)
75                        .frame(maxWidth: .infinity, alignment: .leading)
76                }
77            }
78        }
79        .padding(.top, 8)
80        .accessibilityLabel("Footnotes")
81    }
82}