krz/org-swift

A dependency-free Swift library that renders org-mode to sanitized HTML. html library org-mode swift

ARCHITECTURE.md

v0.7.0
org-swift/ARCHITECTURE.md rendered · source · history · blame · raw

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.