ARCHITECTURE.md
123 lines · 6985 bytes
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
OrgKeywordStyle.swift the app's colors for TODO/DONE
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 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 <figure class="org-block">, 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.
Color follows the same principle. Everything the renderer draws uses a semantic color, so it
inherits the embedding app's appearance; the one place org carries status rather than prose —
TODO/DONE — is configurable through OrgKeywordStyle and defaults to semantic too. A
package that picks concrete hues puts colors on screen that match nothing around them.
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.