krz/org-swift

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

ARCHITECTURE.md

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

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