krz/org-swift

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

README.md

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

131 lines · 4952 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,
 20tables (with `:---:` alignment), `#+begin_src` / `example` / `quote` / `center` /
 21`verse` / `export` blocks and `#+begin_<name>` special blocks,
 22`#+TITLE`/`#+AUTHOR`/`#+DATE` metadata, `#+CAPTION`/`#+ATTR_HTML`/`#+NAME`
 23figures, org links and images (`[[file:x.png]]` → `<img>`, `[[dest][label]]` →
 24link, `[[id:target]]` → `#target`), horizontal rules, comments, timestamps
 25(`<2024-01-15 Mon>`, inactive, times, and ranges → `<time>`), footnotes
 26(references, inline, and definitions → a `<section class="footnotes">`), and
 27inline markup (`*bold*`, `/italic/`, `~code~`, `=verbatim=`, `+strike+`,
 28`_underline_`, `x^2` superscript, bare-URL and email autolinks). Other
 29`#+KEYWORD:` lines are consumed as document metadata.
 30
 31Output is sanitized: only `http`/`https`/`mailto` link schemes and
 32`http`/`https` image schemes are allowed; everything else is dropped.
 33
 34## Usage
 35
 36```swift
 37import OrgSwift
 38
 39let html = OrgRenderer.renderToHTML(orgSource)
 40```
 41
 42### Relative links
 43
 44Pass an `OrgRenderOptions` to rewrite repository-relative links and images to
 45absolute `blob` URLs. When `owner`/`repositoryName` are nil (the default),
 46relative links are left as-is, which the scheme allowlist then drops.
 47
 48```swift
 49let html = OrgRenderer.renderToHTML(
 50    orgSource,
 51    options: OrgRenderOptions(
 52        host: "git.sr.ht",       // default
 53        owner: "~ccleberg",
 54        repositoryName: "Hutch",
 55        ref: "HEAD",             // default
 56        readmePath: "README.org" // resolves paths relative to this file
 57    )
 58)
 59```
 60
 61`./images/badge.svg` then resolves to
 62`https://git.sr.ht/~ccleberg/Hutch/blob/HEAD/images/badge.svg`.
 63
 64### Native SwiftUI
 65
 66`OrgSwiftUI` renders the same parse as views rather than HTML — selectable text,
 67Dynamic Type, and VoiceOver, without a `WKWebView`.
 68
 69```swift
 70import OrgSwiftUI
 71
 72OrgView(orgSource, options: options)
 73```
 74
 75`OrgRenderOptions` means the same thing here, so links and images resolve exactly
 76as they do in HTML. An image whose source will not resolve to an *absolute* URL
 77shows its alt text instead: HTML can lean on a document base URL, a view cannot.
 78
 79Syntax highlighting for `#+begin_src` blocks is the app's to supply, which is what
 80keeps this package dependency-free:
 81
 82```swift
 83struct Highlighter: OrgCodeStyler {
 84    func highlighted(code: String, language: String?) -> AttributedString? {
 85        // nil means "show it as plain monospaced text"
 86    }
 87}
 88
 89OrgView(orgSource, options: options, styler: Highlighter())
 90```
 91
 92### Title and heading level
 93
 94Two presentation knobs, both defaulting to how Hutch rendered:
 95
 96- `metadataHeader` (default `true`) — emit a leading `<div class="org-metadata">`
 97  with the `#+TITLE`/`#+AUTHOR`/`#+DATE`. Set `false` to drop it (e.g. when the
 98  surrounding UI shows the title itself).
 99- `headingLevelOffset` (default `0`) — added to each heading's star count, clamped
100  to `1...6`. Default renders `*` as `<h1>`; use `1` to render `*` as `<h2>`,
101  leaving `<h1>` for a document title.
102
103### Syntax highlighting
104
105Code blocks are highlighted through a protocol so the library carries no
106highlighter dependency:
107
108```swift
109public protocol CodeHighlighter {
110    func highlightedHTML(code: String, language: String?) -> String?
111}
112```
113
114The default `PlainCodeHighlighter` returns `nil`, so blocks fall back to escaped
115`<pre><code>`. Provide your own conformer to plug in a real highlighter:
116
117```swift
118let html = OrgRenderer.renderToHTML(orgSource, highlighter: MyHighlighter())
119```
120
121A conformer returning `nil` for a given block gets the same escaped fallback.
122
123## Conformance
124
125OrgSwift is tested against the shared [org-conformance](../org-conformance)
126corpus, whose golden outputs come from `orgo` (itself validated against Emacs
127`ox-html`). `swift test` locates a sibling `../org-conformance` checkout, or set
128`ORG_CONFORMANCE_DIR`. The suite reduces both this renderer's HTML and orgo's to
129a semantic skeleton and compares them; the current pass/divergence state is
130recorded per case, so a closed gap or a regression both surface as a failing
131test. See [GAPS.md](GAPS.md) for the outstanding backlog.