# 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/ AST/ OrgDocument.swift the element tree OrgParser.swift source → OrgDocument (blocks) OrgInlineParser.swift text → [OrgObject] (inlines) OrgHTMLTreeRenderer.swift OrgDocument → HTML OrgAttributedStringRenderer.swift [OrgObject] → AttributedString 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 ``` `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; a SwiftUI layer would sit on top of it for the inline runs inside each block. 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`. ## Next A SwiftUI renderer belongs in a **separate product** depending on this one, so the parser and HTML renderer stay Foundation-only and callers who want HTML never import SwiftUI. Tables are the interesting part: `Grid`/`GridRow` with `.gridColumnAlignment()`, wrapped in a horizontal `ScrollView` for phone-width overflow — the approach MarkdownUI uses. `OrgTable` already carries the rows, the rule position, and the per-column alignments that needs.