ARCHITECTURE.md
123 lines · 6985 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 OrgKeywordStyle.swift the app's colors for TODO/DONE
29```
30
31`OrgRenderer.renderToHTML` is a thin façade over `OrgParser.parse` + `OrgHTMLTreeRenderer`.
32It, `OrgRenderOptions`, and `CodeHighlighter` are the whole public API most callers need.
33
34## Why a tree
35
36The renderer this replaced went from source straight to an HTML string in a single pass,
37doing inline work by regex-substituting markup into escaped text and protecting the results
38with placeholder tokens. That worked, but it baked HTML into the parse: any second output
39format would have meant re-deriving the parse rather than reusing it.
40
41With a tree, `OrgAttributedStringRenderer` is roughly 140 lines and shares the parse
42entirely. It is **Foundation-only** — no SwiftUI — so it works server-side, in a CLI,
43anywhere. `OrgSwiftUI` sits on top of it for the inline runs inside each block, and is a
44separate product so callers who want HTML never import SwiftUI.
45
46Three renderers now share one parse, one URL resolver (`OrgURLResolver`) and one footnote
47numbering (`OrgFootnotes`), both `package`-scoped. That sharing is the point: a link that
48resolves in HTML resolves natively, and note 1 is note 1 in both.
49
50The types mirror orgo's `model.rs` — `OrgElement`/`OrgObject` against orgo's
51`Element`/`Object`, `OrgTableRow.{cells,rule}` against `TableRow::{Cells,Rule}`, the same
52`ListKind`/`Checkbox` vocabulary. That makes future *tree-level* conformance possible: today
53the corpus compares rendered HTML reduced to a skeleton, which is a string-level proxy for
54"do these two agree on structure", and matching trees would let that question be asked
55directly.
56
57Resolution is simpler on a tree, too. Link targets stay typed — `.external`, `.file`, `.id`
58— so repository-relative resolution applies to the `.file` case in the renderer instead of a
59resolver closure threaded through the parse. An unsafe or unresolvable target degrades to its
60text rather than becoming a bad anchor.
61
62## Conformance
63
64Measured against the [org-conformance](../org-conformance) corpus, whose goldens come from
65orgo (itself diffed against Emacs `ox-html`), reduced by the shared skeleton algorithm:
66
67**11 / 12** — every case except `outofscope`, which is `scope: out` in the corpus
68(deliberately unsupported constructs, where orgo itself may differ).
69
70`ConformanceTests` gates this: a regression fails, and `outofscope` starting to match fails
71too, forcing the record in its expectations map to be updated. `GAPS.md` carries the detail.
72
73## Notes from the migration
74
75The tree renderer replaced the single-pass one only after it was shown to be equivalent:
76every corpus case, each option combination, identical URL resolution, unsafe-scheme
77rejection, and a 28-construct battery drawn from the old renderer's own test inputs. That
78battery earned its keep — it caught three behaviours the corpus never reaches
79(`#+CAPTION:`/`#+NAME:` wrapping a non-image block in `<figure class="org-block">`, the
80nested `[[dest][[img]]]` badge form, and bare email autolinks), each of which would otherwise
81have regressed a consumer.
82
83One behaviour changed on purpose. A range whose halves are *inactive* timestamps, `[a]--[b]`,
84is now joined into one range; the old renderer joined active ranges only and left `--` as
85text. orgo applies the `--` rule to both bracket kinds, requiring only that the halves agree
86on activeness, so the new behaviour is the more correct one. It is asserted directly in
87`joinsInactiveTimestampRanges`.
88
89## OrgSwiftUI
90
91`OrgView(source, options:styler:)` renders org as native SwiftUI: selectable text, Dynamic
92Type, VoiceOver, and no web view.
93
94The walk runs **off `body`**, in a `.task` keyed to the source, producing an `OrgLayout` the
95views then render as pure layout. Three things force that. Footnote numbering is sequential
96and document-wide, so it cannot happen in a `@ViewBuilder`. Syntax highlighting is expensive
97and must not re-run per layout pass. And SwiftUI re-initialises views freely, so `init` is
98not a safe place either.
99
100Tables are the interesting part: `Grid`/`GridRow` with `.gridColumnAlignment()` set on the
101first row and inherited down, wrapped in a horizontal `ScrollView` for phone-width overflow.
102`OrgTable` already carried the rows, the rule position and the per-column alignments.
103
104Syntax highlighting arrives through `OrgCodeStyler`, the native counterpart to
105`CodeHighlighter` — returning `AttributedString` where the other returns HTML. Keeping it a
106protocol is what lets the package stay dependency-free.
107
108Color follows the same principle. Everything the renderer draws uses a semantic color, so it
109inherits the embedding app's appearance; the one place org carries status rather than prose —
110`TODO`/`DONE` — is configurable through `OrgKeywordStyle` and defaults to semantic too. A
111package that picks concrete hues puts colors on screen that match nothing around them.
112
113One behaviour deliberately differs from HTML. An image source that will not resolve to an
114**absolute** URL degrades to its alt text: HTML may emit a relative `src` for a document base
115URL to resolve, and a view has no base, so a relative source is unloadable.
116
117Not covered natively: `#+BEGIN_EXPORT html` passes through verbatim in HTML and has nowhere
118to go in a view, so it renders as the literal block it is.
119
120## Next
121
122The corpus measures HTML only. Tree-level conformance would let `OrgLayout` be measured
123against the same goldens instead of only its own unit tests.