import OrgSwift import SwiftUI /// Renders org-mode as native SwiftUI. /// /// ```swift /// OrgView(source, options: OrgRenderOptions(host: "gitbay.org", owner: "krz", /// repositoryName: "org-swift")) /// ``` /// /// The parse, footnote numbering, URL resolution and syntax highlighting run once off /// `body`, in a task keyed to the source *and the color scheme*, so scrolling and Dynamic /// Type changes do not re-derive them — but switching to dark mode does, because a /// ``OrgCodeStyler`` bakes its theme's colors into the text it returns. Text is selectable, /// links open through the environment's `openURL`, and everything scales with Dynamic Type. public struct OrgView: View { private let source: String private let options: OrgRenderOptions private let styler: OrgCodeStyler private let keywords: OrgKeywordStyle private let textTransform: ((inout AttributedString) -> Void)? @Environment(\.colorScheme) private var colorScheme @State private var layout = OrgLayout() /// - Parameter textTransform: runs on each run of prose after styling: paragraphs, /// headings, list terms, table cells, verse, captions and footnotes, never a code /// block. Code and verbatim runs carry the `.code` inline presentation intent and /// links carry `.link`, so a transform can leave them alone. It is not part of the /// layout key; changing it alone does not re-render. public init( _ source: String, options: OrgRenderOptions = .init(), styler: OrgCodeStyler = PlainOrgCodeStyler(), keywords: OrgKeywordStyle = .init(), textTransform: ((inout AttributedString) -> Void)? = nil ) { self.source = source self.options = options self.styler = styler self.keywords = keywords self.textTransform = textTransform } public var body: some View { VStack(alignment: .leading, spacing: 12) { OrgBlockListView(blocks: layout.blocks) if !layout.footnotes.isEmpty { OrgFootnotesView(footnotes: layout.footnotes) } } .frame(maxWidth: .infinity, alignment: .leading) .textSelection(.enabled) .task(id: LayoutKey(source: source, colorScheme: colorScheme)) { layout = OrgLayoutBuilder(options: options, styler: styler, keywords: keywords, textTransform: textTransform) .layout(OrgParser.parse(source)) } } } /// What the prepared layout depends on. Highlighted code carries baked-in colors, so the /// scheme belongs in the key alongside the source. private struct LayoutKey: Equatable { var source: String var colorScheme: ColorScheme } /// The notes section org's exporter puts at the end of a document. struct OrgFootnotesView: View { let footnotes: [OrgFootnote] var body: some View { VStack(alignment: .leading, spacing: 6) { Divider() ForEach(footnotes) { note in HStack(alignment: .firstTextBaseline, spacing: 6) { Text("\(note.number).") .font(.footnote) .monospacedDigit() .foregroundStyle(.secondary) Text(note.text) .fixedSize(horizontal: false, vertical: true) .frame(maxWidth: .infinity, alignment: .leading) } } } .padding(.top, 8) .accessibilityLabel("Footnotes") } }