krz/org-swift

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

ARCHITECTURE.md

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

79 lines · 4424 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/
  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 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.

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.