krz/org-swift

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

Commit 032cfe34d5

032cfe34d52200909d6ebcdc67c28898457c8710

parent: dedfbafd51

Unsigned

cmc <hello@cleberg.net> · 2026-08-31 17:38 UTC

Document OrgSwiftUI

Layout: unified · split

ARCHITECTURE.md +45 −7
@@ -4,17 +4,27 @@ OrgSwift parses org into an element tree and renders that tree. Parsing happens
44output format is a walk over the result.
55
66```
7Sources/OrgSwift/
7Sources/OrgSwift/ Foundation only
88 AST/
99 OrgDocument.swift the element tree
1010 OrgParser.swift source → OrgDocument (blocks)
1111 OrgInlineParser.swift text → [OrgObject] (inlines)
1212 OrgHTMLTreeRenderer.swift OrgDocument → HTML
1313 OrgAttributedStringRenderer.swift [OrgObject] → AttributedString
14 OrgFootnotes.swift document-wide numbering, shared by renderers
15 OrgURLResolver.swift typed target → safe URL, shared by renderers
1416 OrgRenderer.swift the public entry point, plus OrgRenderOptions
1517 Escaping.swift, Links.swift, … shared helpers: escaping, URL sanitising and
1618 repository-relative resolution, line predicates
1719 Skeleton.swift the HTML → semantic-skeleton reduction used by tests
20
21Sources/OrgSwiftUI/ depends on OrgSwift
22 OrgView.swift the public entry point
23 OrgLayout.swift the display model
24 OrgLayoutBuilder.swift OrgDocument → OrgLayout
25 OrgInlineStyler.swift [OrgObject] → styled AttributedString
26 OrgBlockView.swift, OrgListView.swift, OrgTableView.swift
27 OrgCodeStyler.swift the app's syntax highlighter, as a protocol
1828```
1929
2030`OrgRenderer.renderToHTML` is a thin façade over `OrgParser.parse` + `OrgHTMLTreeRenderer`.
@@ -29,7 +39,12 @@ format would have meant re-deriving the parse rather than reusing it.
2939
3040With a tree, `OrgAttributedStringRenderer` is roughly 140 lines and shares the parse
3141entirely. It is **Foundation-only** — no SwiftUI — so it works server-side, in a CLI,
32anywhere; a SwiftUI layer would sit on top of it for the inline runs inside each block.
42anywhere. `OrgSwiftUI` sits on top of it for the inline runs inside each block, and is a
43separate product so callers who want HTML never import SwiftUI.
44
45Three renderers now share one parse, one URL resolver (`OrgURLResolver`) and one footnote
46numbering (`OrgFootnotes`), both `package`-scoped. That sharing is the point: a link that
47resolves in HTML resolves natively, and note 1 is note 1 in both.
3348
3449The types mirror orgo's `model.rs` — `OrgElement`/`OrgObject` against orgo's
3550`Element`/`Object`, `OrgTableRow.{cells,rule}` against `TableRow::{Cells,Rule}`, the same
@@ -70,10 +85,33 @@ text. orgo applies the `--` rule to both bracket kinds, requiring only that the
7085on activeness, so the new behaviour is the more correct one. It is asserted directly in
7186`joinsInactiveTimestampRanges`.
7287
88## OrgSwiftUI
89
90`OrgView(source, options:styler:)` renders org as native SwiftUI: selectable text, Dynamic
91Type, VoiceOver, and no web view.
92
93The walk runs **off `body`**, in a `.task` keyed to the source, producing an `OrgLayout` the
94views then render as pure layout. Three things force that. Footnote numbering is sequential
95and document-wide, so it cannot happen in a `@ViewBuilder`. Syntax highlighting is expensive
96and must not re-run per layout pass. And SwiftUI re-initialises views freely, so `init` is
97not a safe place either.
98
99Tables are the interesting part: `Grid`/`GridRow` with `.gridColumnAlignment()` set on the
100first row and inherited down, wrapped in a horizontal `ScrollView` for phone-width overflow.
101`OrgTable` already carried the rows, the rule position and the per-column alignments.
102
103Syntax highlighting arrives through `OrgCodeStyler`, the native counterpart to
104`CodeHighlighter` — returning `AttributedString` where the other returns HTML. Keeping it a
105protocol is what lets the package stay dependency-free.
106
107One behaviour deliberately differs from HTML. An image source that will not resolve to an
108**absolute** URL degrades to its alt text: HTML may emit a relative `src` for a document base
109URL to resolve, and a view has no base, so a relative source is unloadable.
110
111Not covered natively: `#+BEGIN_EXPORT html` passes through verbatim in HTML and has nowhere
112to go in a view, so it renders as the literal block it is.
113
73114## Next
74115
75A SwiftUI renderer belongs in a **separate product** depending on this one, so the parser and
76HTML renderer stay Foundation-only and callers who want HTML never import SwiftUI. Tables are
77the interesting part: `Grid`/`GridRow` with `.gridColumnAlignment()`, wrapped in a horizontal
78`ScrollView` for phone-width overflow — the approach MarkdownUI uses. `OrgTable` already
79carries the rows, the rule position, and the per-column alignments that needs.
116The corpus measures HTML only. Tree-level conformance would let `OrgLayout` be measured
117against the same goldens instead of only its own unit tests.
README.md +33 −2
@@ -1,8 +1,11 @@
11# OrgSwift
22
33A dependency-free Swift library that renders a practical subset of
4[org-mode](https://orgmode.org) to sanitized HTML. Pure Foundation — no
5SwiftUI, WebKit, UIKit, or third-party packages.
4[org-mode](https://orgmode.org). Two products:
5
6- **`OrgSwift`** — org to sanitized HTML or `AttributedString`. Pure Foundation,
7 no SwiftUI, WebKit, UIKit, or third-party packages.
8- **`OrgSwiftUI`** — org to native SwiftUI views, no web view involved.
69
710Originally extracted from the hand-rolled renderer in the Hutch iOS client so it
811could be shared across apps, and since rebuilt around an element tree: org is
@@ -58,6 +61,34 @@ let html = OrgRenderer.renderToHTML(
5861`./images/badge.svg` then resolves to
5962`https://git.sr.ht/~ccleberg/Hutch/blob/HEAD/images/badge.svg`.
6063
64### Native SwiftUI
65
66`OrgSwiftUI` renders the same parse as views rather than HTML — selectable text,
67Dynamic Type, and VoiceOver, without a `WKWebView`.
68
69```swift
70import OrgSwiftUI
71
72OrgView(orgSource, options: options)
73```
74
75`OrgRenderOptions` means the same thing here, so links and images resolve exactly
76as they do in HTML. An image whose source will not resolve to an *absolute* URL
77shows its alt text instead: HTML can lean on a document base URL, a view cannot.
78
79Syntax highlighting for `#+begin_src` blocks is the app's to supply, which is what
80keeps this package dependency-free:
81
82```swift
83struct Highlighter: OrgCodeStyler {
84 func highlighted(code: String, language: String?) -> AttributedString? {
85 // nil means "show it as plain monospaced text"
86 }
87}
88
89OrgView(orgSource, options: options, styler: Highlighter())
90```
91
6192### Title and heading level
6293
6394Two presentation knobs, both defaulting to how Hutch rendered: