README.md
150 lines · 5639 bytes
1# OrgSwift
2
3A dependency-free Swift library that renders a practical subset of
4[org-mode](https://orgmode.org). Two products:
5
6- **`OrgSwift`** — org to sanitized HTML or `AttributedString`. Pure Foundation,
7 no SwiftUI, WebKit, UIKit, or third-party packages.
8- **`OrgSwiftUI`** — org to native SwiftUI views, no web view involved.
9
10Originally extracted from the hand-rolled renderer in the Hutch iOS client so it
11could be shared across apps, and since rebuilt around an element tree: org is
12parsed once into a document model, and each output format walks it. See
13[ARCHITECTURE.md](ARCHITECTURE.md).
14
15## Supported syntax
16
17Headings (with trailing `:tags:`), paragraphs, ordered/unordered lists (with
18arbitrary-depth nesting, wrapped lines, and `[ ]`/`[x]`/`[-]` task checkboxes),
19description lists (`term :: definition` → `<dl>`), multi-paragraph list items,
20list items holding block elements (a source block, table, or quote inside an item),
21tables (with `:---:` alignment), `#+begin_src` / `example` / `quote` / `center` /
22`verse` / `export` blocks and `#+begin_<name>` special blocks,
23`#+TITLE`/`#+AUTHOR`/`#+DATE` metadata, `#+CAPTION`/`#+ATTR_HTML`/`#+NAME`
24figures, org links and images (`[[file:x.png]]` → `<img>`, `[[dest][label]]` →
25link, `[[id:target]]` → `#target`), horizontal rules, comments, timestamps
26(`<2024-01-15 Mon>`, inactive, times, and ranges → `<time>`), footnotes
27(references, inline, and definitions → a `<section class="footnotes">`), and
28inline markup (`*bold*`, `/italic/`, `~code~`, `=verbatim=`, `+strike+`,
29`_underline_`, `x^2` superscript, bare-URL and email autolinks). Other
30`#+KEYWORD:` lines are consumed as document metadata.
31
32Output is sanitized: only `http`/`https`/`mailto` link schemes and
33`http`/`https` image schemes are allowed; everything else is dropped.
34
35## Usage
36
37```swift
38import OrgSwift
39
40let html = OrgRenderer.renderToHTML(orgSource)
41```
42
43### Relative links
44
45Pass an `OrgRenderOptions` to rewrite repository-relative links and images to
46absolute `blob` URLs. When `owner`/`repositoryName` are nil (the default),
47relative links are left as-is, which the scheme allowlist then drops.
48
49```swift
50let html = OrgRenderer.renderToHTML(
51 orgSource,
52 options: OrgRenderOptions(
53 host: "git.sr.ht", // default
54 owner: "~ccleberg",
55 repositoryName: "Hutch",
56 ref: "HEAD", // default
57 readmePath: "README.org" // resolves paths relative to this file
58 )
59)
60```
61
62`./images/badge.svg` then resolves to
63`https://git.sr.ht/~ccleberg/Hutch/blob/HEAD/images/badge.svg`.
64
65The same resolution is available on its own, for a caller whose README is not org:
66
67```swift
68options.resolvedLinkURL("ARCHITECTURE.md") // .../blob/HEAD/ARCHITECTURE.md
69options.resolvedImageURL("docs/logo.png") // .../raw/HEAD/docs/logo.png
70```
71
72Both return a plain URL string, not an HTML attribute value — a caller building
73markup escapes where it interpolates.
74
75### Native SwiftUI
76
77`OrgSwiftUI` renders the same parse as views rather than HTML — selectable text,
78Dynamic Type, and VoiceOver, without a `WKWebView`.
79
80```swift
81import OrgSwiftUI
82
83OrgView(orgSource, options: options)
84```
85
86`OrgRenderOptions` means the same thing here, so links and images resolve exactly
87as they do in HTML. An image whose source will not resolve to an *absolute* URL
88shows its alt text instead: HTML can lean on a document base URL, a view cannot.
89
90Syntax highlighting for `#+begin_src` blocks is the app's to supply, which is what
91keeps this package dependency-free:
92
93```swift
94struct Highlighter: OrgCodeStyler {
95 func highlighted(code: String, language: String?) -> AttributedString? {
96 // nil means "show it as plain monospaced text"
97 }
98}
99
100OrgView(orgSource, options: options, styler: Highlighter())
101```
102
103Org's `TODO`/`DONE` keywords default to semantic colors, because a package
104cannot know your palette. Pass your own:
105
106```swift
107OrgView(orgSource, options: options,
108 keywords: OrgKeywordStyle(todo: .gbOK, done: .gbDone))
109```
110
111### Title and heading level
112
113Two presentation knobs, both defaulting to how Hutch rendered:
114
115- `metadataHeader` (default `true`) — emit a leading `<div class="org-metadata">`
116 with the `#+TITLE`/`#+AUTHOR`/`#+DATE`. Set `false` to drop it (e.g. when the
117 surrounding UI shows the title itself).
118- `headingLevelOffset` (default `0`) — added to each heading's star count, clamped
119 to `1...6`. Default renders `*` as `<h1>`; use `1` to render `*` as `<h2>`,
120 leaving `<h1>` for a document title.
121
122### Syntax highlighting
123
124Code blocks are highlighted through a protocol so the library carries no
125highlighter dependency:
126
127```swift
128public protocol CodeHighlighter {
129 func highlightedHTML(code: String, language: String?) -> String?
130}
131```
132
133The default `PlainCodeHighlighter` returns `nil`, so blocks fall back to escaped
134`<pre><code>`. Provide your own conformer to plug in a real highlighter:
135
136```swift
137let html = OrgRenderer.renderToHTML(orgSource, highlighter: MyHighlighter())
138```
139
140A conformer returning `nil` for a given block gets the same escaped fallback.
141
142## Conformance
143
144OrgSwift is tested against the shared [org-conformance](../org-conformance)
145corpus, whose golden outputs come from `orgo` (itself validated against Emacs
146`ox-html`). `swift test` locates a sibling `../org-conformance` checkout, or set
147`ORG_CONFORMANCE_DIR`. The suite reduces both this renderer's HTML and orgo's to
148a semantic skeleton and compares them; the current pass/divergence state is
149recorded per case, so a closed gap or a regression both surface as a failing
150test. See [GAPS.md](GAPS.md) for the outstanding backlog.