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

html library org-mode swift

Parse org into an element tree, and render the tree !1

merged cmc wants to merge krz/org-swift:org-tree into main

cmc

Parse once into a document model, then render that model. Replaces the single-pass source-to-HTML renderer this package started as.

  • Why

The old renderer went straight from source to an HTML string, doing inline work by regex-substituting markup into escaped text and protecting results with placeholder tokens. It worked, but HTML was baked into the parse: a second output format would have meant re-deriving the parse rather than reusing it.

With a tree, =OrgAttributedStringRenderer= is ~140 lines and shares the parse entirely. It is Foundation-only — no SwiftUI — so it works server-side or in a CLI too. A SwiftUI renderer would sit on top of it, in a separate product, so callers wanting HTML never import SwiftUI.

The types mirror orgo's =model.rs= (=OrgElement=/=OrgObject=, =TableRow::{Cells,Rule}=, the same =ListKind=/=Checkbox= vocabulary), which is what would later allow tree-level conformance instead of comparing rendered HTML.

  • Consumer impact

None expected. =OrgRenderer.renderToHTML= is now a façade over =OrgParser.parse= plus =OrgHTMLTreeRenderer=; the public API is purely additive (56 new symbols, nothing removed), and both consumers — krz/gitbay-ios and krz/hutch — reference only =OrgRenderer=, =OrgRenderOptions= and =CodeHighlighter=. Both pin this package to =main=, so they pick the change up on their next resolve.

  • Evidence

Conformance is unchanged at 11/12 against krz/org-conformance — every case but =outofscope=, which is =scope: out= in the corpus. The gate gained nothing and lost nothing.

Before swapping, the tree renderer was shown equivalent to the one it replaces: every corpus case, each render-option combination, identical URL resolution, unsafe-scheme rejection, and a 28-construct battery drawn from the old renderer's own test inputs. That battery caught three behaviours the corpus never reaches, each since fixed and each of which would otherwise have regressed a consumer:

  • =#+CAPTION:=/=#+NAME:= on a non-image block wraps it in ==
  • the nested =[[dest][[img]]]= badge form
  • bare email autolinks
  • One intentional behaviour change

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 literal text. orgo applies the =--= rule to both bracket kinds, requiring only that the halves agree on activeness, so this is the more correct behaviour rather than a regression. Asserted directly in =joinsInactiveTimestampRanges=. In the corpus it appears only inside =outofscope='s LOGBOOK drawer.

  • Size

~1,357 lines deleted against ~1,481 added. =Inline.swift= and =Timestamps.swift= are gone; =Lists.swift= 188 → 16, =Footnotes.swift= 76 → 10, keeping only the helpers the tree parser shares. 63 tests, all passing.

=ARCHITECTURE.md= documents the design and the migration notes.