krz/org-swift

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

ARCHITECTURE.md

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

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.