ARCHITECTURE.md
117 lines · 6547 bytes
1# Architecture
2
3OrgSwift parses org into an element tree and renders that tree. Parsing happens once; each
4output format is a walk over the result.
5
6```
7Sources/OrgSwift/ Foundation only
8 AST/
9 OrgDocument.swift the element tree
10 OrgParser.swift source → OrgDocument (blocks)
11 OrgInlineParser.swift text → [OrgObject] (inlines)
12 OrgHTMLTreeRenderer.swift OrgDocument → HTML
13 OrgAttributedStringRenderer.swift [OrgObject] → AttributedString
14 OrgFootnotes.swift document-wide numbering, shared by renderers
15 OrgURLResolver.swift typed target → safe URL, shared by renderers
16 OrgRenderer.swift the public entry point, plus OrgRenderOptions
17 Escaping.swift, Links.swift, … shared helpers: escaping, URL sanitising and
18 repository-relative resolution, line predicates
19 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
28```
29
30`OrgRenderer.renderToHTML` is a thin façade over `OrgParser.parse` + `OrgHTMLTreeRenderer`.
31It, `OrgRenderOptions`, and `CodeHighlighter` are the whole public API most callers need.
32
33## Why a tree
34
35The renderer this replaced went from source straight to an HTML string in a single pass,
36doing inline work by regex-substituting markup into escaped text and protecting the results
37with placeholder tokens. That worked, but it baked HTML into the parse: any second output
38format would have meant re-deriving the parse rather than reusing it.
39
40With a tree, `OrgAttributedStringRenderer` is roughly 140 lines and shares the parse
41entirely. It is **Foundation-only** — no SwiftUI — so it works server-side, in a CLI,
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.
48
49The types mirror orgo's `model.rs` — `OrgElement`/`OrgObject` against orgo's
50`Element`/`Object`, `OrgTableRow.{cells,rule}` against `TableRow::{Cells,Rule}`, the same
51`ListKind`/`Checkbox` vocabulary. That makes future *tree-level* conformance possible: today
52the corpus compares rendered HTML reduced to a skeleton, which is a string-level proxy for
53"do these two agree on structure", and matching trees would let that question be asked
54directly.
55
56Resolution is simpler on a tree, too. Link targets stay typed — `.external`, `.file`, `.id`
57— so repository-relative resolution applies to the `.file` case in the renderer instead of a
58resolver closure threaded through the parse. An unsafe or unresolvable target degrades to its
59text rather than becoming a bad anchor.
60
61## Conformance
62
63Measured against the [org-conformance](../org-conformance) corpus, whose goldens come from
64orgo (itself diffed against Emacs `ox-html`), reduced by the shared skeleton algorithm:
65
66**11 / 12** — every case except `outofscope`, which is `scope: out` in the corpus
67(deliberately unsupported constructs, where orgo itself may differ).
68
69`ConformanceTests` gates this: a regression fails, and `outofscope` starting to match fails
70too, forcing the record in its expectations map to be updated. `GAPS.md` carries the detail.
71
72## Notes from the migration
73
74The tree renderer replaced the single-pass one only after it was shown to be equivalent:
75every corpus case, each option combination, identical URL resolution, unsafe-scheme
76rejection, and a 28-construct battery drawn from the old renderer's own test inputs. That
77battery earned its keep — it caught three behaviours the corpus never reaches
78(`#+CAPTION:`/`#+NAME:` wrapping a non-image block in `<figure class="org-block">`, the
79nested `[[dest][[img]]]` badge form, and bare email autolinks), each of which would otherwise
80have regressed a consumer.
81
82One behaviour changed on purpose. A range whose halves are *inactive* timestamps, `[a]--[b]`,
83is now joined into one range; the old renderer joined active ranges only and left `--` as
84text. orgo applies the `--` rule to both bracket kinds, requiring only that the halves agree
85on activeness, so the new behaviour is the more correct one. It is asserted directly in
86`joinsInactiveTimestampRanges`.
87
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
114## Next
115
116The corpus measures HTML only. Tree-level conformance would let `OrgLayout` be measured
117against the same goldens instead of only its own unit tests.