README.md
172 lines · 6541 bytes
OrgSwift
A dependency-free Swift library that renders a practical subset of org-mode. Two products:
OrgSwift— org to sanitized HTML orAttributedString. 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)
Relative links
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
})
LaTeX fragments ($…$, $$…$$, \(…\), \[…\], \begin{…}…\end{…}) and
\begin{…} blocks parse to .math objects and .latexBlock elements, found by
go-org's rules with org's border rule for $…$. The HTML and AttributedString
renderers show their source. OrgView draws them through math, which returns a
Text (an image from a typesetter, say) or nil to keep the source:
OrgView(orgSource, options: options, math: { math in
typeset(math.tex, display: math.display).map { Text($0) }
})
Title and heading level
Two presentation knobs, both defaulting to how Hutch rendered:
metadataHeader(defaulttrue) — emit a leading<div class="org-metadata">with the#+TITLE/#+AUTHOR/#+DATE. Setfalseto drop it (e.g. when the surrounding UI shows the title itself).headingLevelOffset(default0) — added to each heading's star count, clamped to1...6. Default renders*as<h1>; use1to 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.