krz/org-swift

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

README.md

799d4ba18ece2bd259bd4760cca46faeae0fd1e1
org-swift/README.md rendered · source · history · blame · raw

173 lines · 6624 bytes

8 symbols in this file
  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
111To add links or styling of your own to prose, pass a `textTransform`. It gets each
112styled run of prose, never a code block; code and verbatim runs carry the `.code`
113inline presentation intent and links carry `.link`:
114
115```swift
116OrgView(orgSource, options: options, textTransform: { text in
117    // e.g. link issue references outside code and existing links
118})
119```
120
121LaTeX fragments (`$…$`, `$$…$$`, `\(…\)`, `\[…\]`, `\begin{…}…\end{…}`) and
122`\begin{…}` blocks parse to `.math` objects and `.latexBlock` elements, found by
123go-org's rules with org's border rule for `$…$`. The HTML and `AttributedString`
124renderers show their source. `OrgView` draws them through `math`, which is given the
125text style of the run the expression sits in and returns a `Text` (an image from a
126typesetter, say) or nil to keep the source:
127
128```swift
129OrgView(orgSource, options: options, math: { math, style in
130    typeset(math.tex, display: math.display, style: style).map { Text($0) }
131})
132```
133
134### Title and heading level
135
136Two presentation knobs, both defaulting to how Hutch rendered:
137
138- `metadataHeader` (default `true`) — emit a leading `<div class="org-metadata">`
139  with the `#+TITLE`/`#+AUTHOR`/`#+DATE`. Set `false` to drop it (e.g. when the
140  surrounding UI shows the title itself).
141- `headingLevelOffset` (default `0`) — added to each heading's star count, clamped
142  to `1...6`. Default renders `*` as `<h1>`; use `1` to render `*` as `<h2>`,
143  leaving `<h1>` for a document title.
144
145### Syntax highlighting
146
147Code blocks are highlighted through a protocol so the library carries no
148highlighter dependency:
149
150```swift
151public protocol CodeHighlighter {
152    func highlightedHTML(code: String, language: String?) -> String?
153}
154```
155
156The default `PlainCodeHighlighter` returns `nil`, so blocks fall back to escaped
157`<pre><code>`. Provide your own conformer to plug in a real highlighter:
158
159```swift
160let html = OrgRenderer.renderToHTML(orgSource, highlighter: MyHighlighter())
161```
162
163A conformer returning `nil` for a given block gets the same escaped fallback.
164
165## Conformance
166
167OrgSwift is tested against the shared [org-conformance](../org-conformance)
168corpus, whose golden outputs come from `orgo` (itself validated against Emacs
169`ox-html`). `swift test` locates a sibling `../org-conformance` checkout, or set
170`ORG_CONFORMANCE_DIR`. The suite reduces both this renderer's HTML and orgo's to
171a semantic skeleton and compares them; the current pass/divergence state is
172recorded per case, so a closed gap or a regression both surface as a failing
173test. See [GAPS.md](GAPS.md) for the outstanding backlog.