# Architecture OrgSwift parses org into an element tree and renders that tree. Parsing happens once; each output format is a walk over the result. ``` Sources/OrgSwift/ Foundation only AST/ OrgDocument.swift the element tree OrgParser.swift source → OrgDocument (blocks) OrgInlineParser.swift text → [OrgObject] (inlines) OrgHTMLTreeRenderer.swift OrgDocument → HTML OrgAttributedStringRenderer.swift [OrgObject] → AttributedString OrgFootnotes.swift document-wide numbering, shared by renderers OrgURLResolver.swift typed target → safe URL, shared by renderers OrgRenderer.swift the public entry point, plus OrgRenderOptions Escaping.swift, Links.swift, … shared helpers: escaping, URL sanitising and repository-relative resolution, line predicates Skeleton.swift the HTML → semantic-skeleton reduction used by tests Sources/OrgSwiftUI/ depends on OrgSwift OrgView.swift the public entry point OrgLayout.swift the display model OrgLayoutBuilder.swift OrgDocument → OrgLayout OrgInlineStyler.swift [OrgObject] → styled AttributedString OrgBlockView.swift, OrgListView.swift, OrgTableView.swift OrgCodeStyler.swift the app's syntax highlighter, as a protocol ``` `OrgRenderer.renderToHTML` is a thin façade over `OrgParser.parse` + `OrgHTMLTreeRenderer`. It, `OrgRenderOptions`, and `CodeHighlighter` are the whole public API most callers need. ## Why a tree The renderer this replaced went from source straight to an HTML string in a single pass, doing inline work by regex-substituting markup into escaped text and protecting the results with placeholder tokens. That worked, but it baked HTML into the parse: any second output format would have meant re-deriving the parse rather than reusing it. With a tree, `OrgAttributedStringRenderer` is roughly 140 lines and shares the parse entirely. It is **Foundation-only** — no SwiftUI — so it works server-side, in a CLI, anywhere. `OrgSwiftUI` sits on top of it for the inline runs inside each block, and is a separate product so callers who want HTML never import SwiftUI. Three renderers now share one parse, one URL resolver (`OrgURLResolver`) and one footnote numbering (`OrgFootnotes`), both `package`-scoped. That sharing is the point: a link that resolves in HTML resolves natively, and note 1 is note 1 in both. The types mirror orgo's `model.rs` — `OrgElement`/`OrgObject` against orgo's `Element`/`Object`, `OrgTableRow.{cells,rule}` against `TableRow::{Cells,Rule}`, the same `ListKind`/`Checkbox` vocabulary. That makes future *tree-level* conformance possible: today the corpus compares rendered HTML reduced to a skeleton, which is a string-level proxy for "do these two agree on structure", and matching trees would let that question be asked directly. Resolution is simpler on a tree, too. Link targets stay typed — `.external`, `.file`, `.id` — so repository-relative resolution applies to the `.file` case in the renderer instead of a resolver closure threaded through the parse. An unsafe or unresolvable target degrades to its text rather than becoming a bad anchor. ## Conformance Measured against the [org-conformance](../org-conformance) corpus, whose goldens come from orgo (itself diffed against Emacs `ox-html`), reduced by the shared skeleton algorithm: **11 / 12** — every case except `outofscope`, which is `scope: out` in the corpus (deliberately unsupported constructs, where orgo itself may differ). `ConformanceTests` gates this: a regression fails, and `outofscope` starting to match fails too, forcing the record in its expectations map to be updated. `GAPS.md` carries the detail. ## Notes from the migration The tree renderer replaced the single-pass one only after it was shown to be equivalent: every corpus case, each option combination, identical URL resolution, unsafe-scheme rejection, and a 28-construct battery drawn from the old renderer's own test inputs. That battery earned its keep — it caught three behaviours the corpus never reaches (`#+CAPTION:`/`#+NAME:` wrapping a non-image block in `
`, the nested `[[dest][[img]]]` badge form, and bare email autolinks), each of which would otherwise have regressed a consumer. One behaviour changed on purpose. A range whose halves are *inactive* timestamps, `[a]--[b]`, is now joined into one range; the old renderer joined active ranges only and left `--` as text. orgo applies the `--` rule to both bracket kinds, requiring only that the halves agree on activeness, so the new behaviour is the more correct one. It is asserted directly in `joinsInactiveTimestampRanges`. ## OrgSwiftUI `OrgView(source, options:styler:)` renders org as native SwiftUI: selectable text, Dynamic Type, VoiceOver, and no web view. The walk runs **off `body`**, in a `.task` keyed to the source, producing an `OrgLayout` the views then render as pure layout. Three things force that. Footnote numbering is sequential and document-wide, so it cannot happen in a `@ViewBuilder`. Syntax highlighting is expensive and must not re-run per layout pass. And SwiftUI re-initialises views freely, so `init` is not a safe place either. Tables are the interesting part: `Grid`/`GridRow` with `.gridColumnAlignment()` set on the first row and inherited down, wrapped in a horizontal `ScrollView` for phone-width overflow. `OrgTable` already carried the rows, the rule position and the per-column alignments. Syntax highlighting arrives through `OrgCodeStyler`, the native counterpart to `CodeHighlighter` — returning `AttributedString` where the other returns HTML. Keeping it a protocol is what lets the package stay dependency-free. One behaviour deliberately differs from HTML. An image source that will not resolve to an **absolute** URL degrades to its alt text: HTML may emit a relative `src` for a document base URL to resolve, and a view has no base, so a relative source is unloadable. Not covered natively: `#+BEGIN_EXPORT html` passes through verbatim in HTML and has nowhere to go in a view, so it renders as the literal block it is. ## Next The corpus measures HTML only. Tree-level conformance would let `OrgLayout` be measured against the same goldens instead of only its own unit tests.