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}