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}