krz/org-swift

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

README.md

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

160 lines · 6002 bytes

OrgSwift

A dependency-free Swift library that renders a practical subset of org-mode. Two products:

  • OrgSwift — org to sanitized HTML or AttributedString. Pure Foundation, no SwiftUI, WebKit, UIKit, or third-party packages.
  • OrgSwiftUI — org to native SwiftUI views, no web view involved.

Originally extracted from the hand-rolled renderer in the Hutch iOS client so it could be shared across apps, and since rebuilt around an element tree: org is parsed once into a document model, and each output format walks it. See ARCHITECTURE.md.

Supported syntax

Headings (with trailing :tags:), paragraphs, ordered/unordered lists (with arbitrary-depth nesting, wrapped lines, and [ ]/[x]/[-] task checkboxes), description lists (term :: definition → <dl>), multi-paragraph list items, list items holding block elements (a source block, table, or quote inside an item), tables (with :---: alignment), #+begin_src / example / quote / center / verse / export blocks and #+begin_<name> special blocks, #+TITLE/#+AUTHOR/#+DATE metadata, #+CAPTION/#+ATTR_HTML/#+NAME figures, org links and images ([[file:x.png]] → <img>, [[dest][label]] → link, [[id:target]] → #target), horizontal rules, comments, timestamps (<2024-01-15 Mon>, inactive, times, and ranges → <time>), footnotes (references, inline, and definitions → a <section class="footnotes">), and inline markup (*bold*, /italic/, ~code~, =verbatim=, +strike+, _underline_, x^2 superscript, bare-URL and email autolinks). Other #+KEYWORD: lines are consumed as document metadata.

Output is sanitized: only http/https/mailto link schemes and http/https image schemes are allowed; everything else is dropped.

Usage

import OrgSwift

let html = OrgRenderer.renderToHTML(orgSource)

Pass an OrgRenderOptions to rewrite repository-relative links and images to absolute blob URLs. When owner/repositoryName are nil (the default), relative links are left as-is, which the scheme allowlist then drops.

let html = OrgRenderer.renderToHTML(
    orgSource,
    options: OrgRenderOptions(
        host: "git.sr.ht",       // default
        owner: "~ccleberg",
        repositoryName: "Hutch",
        ref: "HEAD",             // default
        readmePath: "README.org" // resolves paths relative to this file
    )
)

./images/badge.svg then resolves to https://git.sr.ht/~ccleberg/Hutch/blob/HEAD/images/badge.svg.

The same resolution is available on its own, for a caller whose README is not org:

options.resolvedLinkURL("ARCHITECTURE.md")   // .../blob/HEAD/ARCHITECTURE.md
options.resolvedImageURL("docs/logo.png")    // .../raw/HEAD/docs/logo.png

Both return a plain URL string, not an HTML attribute value — a caller building markup escapes where it interpolates.

Native SwiftUI

OrgSwiftUI renders the same parse as views rather than HTML — selectable text, Dynamic Type, and VoiceOver, without a WKWebView.

import OrgSwiftUI

OrgView(orgSource, options: options)

OrgRenderOptions means the same thing here, so links and images resolve exactly as they do in HTML. An image whose source will not resolve to an absolute URL shows its alt text instead: HTML can lean on a document base URL, a view cannot.

Syntax highlighting for #+begin_src blocks is the app's to supply, which is what keeps this package dependency-free:

struct Highlighter: OrgCodeStyler {
    func highlighted(code: String, language: String?) -> AttributedString? {
        // nil means "show it as plain monospaced text"
    }
}

OrgView(orgSource, options: options, styler: Highlighter())

Org's TODO/DONE keywords default to semantic colors, because a package cannot know your palette. Pass your own:

OrgView(orgSource, options: options,
        keywords: OrgKeywordStyle(todo: .gbOK, done: .gbDone))

To add links or styling of your own to prose, pass a textTransform. It gets each styled run of prose, never a code block; code and verbatim runs carry the .code inline presentation intent and links carry .link:

OrgView(orgSource, options: options, textTransform: { text in
    // e.g. link issue references outside code and existing links
})

Title and heading level

Two presentation knobs, both defaulting to how Hutch rendered:

  • metadataHeader (default true) — emit a leading <div class="org-metadata"> with the #+TITLE/#+AUTHOR/#+DATE. Set false to drop it (e.g. when the surrounding UI shows the title itself).
  • headingLevelOffset (default 0) — added to each heading's star count, clamped to 1...6. Default renders * as <h1>; use 1 to render * as <h2>, leaving <h1> for a document title.

Syntax highlighting

Code blocks are highlighted through a protocol so the library carries no highlighter dependency:

public protocol CodeHighlighter {
    func highlightedHTML(code: String, language: String?) -> String?
}

The default PlainCodeHighlighter returns nil, so blocks fall back to escaped <pre><code>. Provide your own conformer to plug in a real highlighter:

let html = OrgRenderer.renderToHTML(orgSource, highlighter: MyHighlighter())

A conformer returning nil for a given block gets the same escaped fallback.

Conformance

OrgSwift is tested against the shared org-conformance corpus, whose golden outputs come from orgo (itself validated against Emacs ox-html). swift test locates a sibling ../org-conformance checkout, or set ORG_CONFORMANCE_DIR. The suite reduces both this renderer's HTML and orgo's to a semantic skeleton and compares them; the current pass/divergence state is recorded per case, so a closed gap or a regression both surface as a failing test. See GAPS.md for the outstanding backlog.