A language-neutral conformance corpus for org-mode to HTML renderers.

conformance html org-mode testing

Commit 301ff84642

301ff8464209a61964d918e1e258750e82570f2c

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-27T16:07:45Z

org-conformance: language-neutral org→HTML conformance corpus

12 cases (.org input, orgo reference .html, golden .skeleton) with a manifest,
the skeleton reduction spec, and a generator that derives the goldens from orgo.
orgo is the reference implementation; other renderers port the skeleton reducer
and assert against these goldens.
README.md added +78
@@ -0,0 +1,78 @@
1# org-conformance
2
3A language-neutral conformance corpus for org-mode → HTML renderers.
4
5The problem it solves: the same `.org` file is rendered by several independent
6codebases — a Go forge (gitbay via go-org), a Rust static-site generator (orgo), a Swift
7package (OrgSwift, shared by the iOS clients), a browser previewer (org-live). They drift.
8A footnote renders in one and vanishes in another; a `:tag:` styles here and prints raw
9there. Nobody notices until they look at the same document on two surfaces.
10
11This corpus is the shared answer to "what is the *right* output". Each implementation runs
12it in its own test suite and asserts against the same goldens.
13
14## What's here
15
16```
17cases/
18 <name>.org the input
19 <name>.html orgo's rendered HTML — the reference rendering, for humans
20 <name>.skeleton the golden semantic skeleton — the assertion target
21manifest.json every case, the constructs it exercises, and its scope (in/out)
22SKELETON.md the exact reduction each implementation must port
23tools/generate.sh regenerate .html and .skeleton from orgo
24```
25
26## The reference
27
28The goldens are produced by **orgo**, and orgo alone. orgo is not self-appointed: it
29carries an oracle test that exports every fixture with **Emacs's own `ox-html`** under
30`emacs --batch` and diffs the two semantic skeletons, so its output is anchored to org's
31reference exporter rather than to itself. When orgo and Emacs deliberately differ (orgo
32emits `<time datetime>` for a timestamp where Emacs writes plain text, `<em>` where Emacs
33writes `<i>`, and so on), those divergences are enumerated and reviewed in orgo's oracle.
34Everything downstream conforms to orgo.
35
36## How an implementation conforms
37
38Comparing raw HTML across languages is pointless — every renderer wraps and classes things
39differently. Instead, both sides are reduced to a **skeleton**: the ordered sequence of
40element opens, closes, and text runs, with `div`/`span` and all attributes except
41`href`/`src` dropped, whitespace collapsed, and entities decoded. See
42[SKELETON.md](SKELETON.md) for the exact algorithm.
43
44Each implementation:
45
461. Ports the ~150-line skeleton reduction. Verify the port first by feeding each
47 `cases/<name>.html` through it and checking it reproduces `cases/<name>.skeleton`
48 exactly — this separates a skeleton-port bug from a renderer bug.
492. Renders each `cases/<name>.org` with its own renderer, reduces the output with its
50 port, and asserts equality against `cases/<name>.skeleton`.
513. For constructs it does not yet support, keeps an explicit allowlist of expected
52 divergences (mirroring orgo's own "deliberate divergence" list against Emacs). A
53 green suite with a documented allowlist beats a red suite nobody acts on — the point
54 is that *new, unintended* drift shows up as a diff.
55
56Cases with `"scope": "out"` in the manifest are deliberately-unsupported constructs
57(babel, LaTeX, macros, arbitrary drawers). The contract there is only that they degrade
58predictably and never crash; an implementation may legitimately differ from orgo, so treat
59those as documentation rather than hard assertions.
60
61## Consumers
62
63| Implementation | Language | Engine | Status |
64|---|---|---|---|
65| orgo | Rust | own element-tree renderer | reference (generates the goldens) |
66| OrgSwift | Swift | extracted from hutch | conformance test in `swift test` |
67| gitbay | Go | go-org | planned |
68| org-live | JS | (to migrate off vendored org-js) | planned |
69
70## Regenerating
71
72The goldens are derived, not hand-written. After an intended change to orgo's renderer:
73
74```
75ORGO_DIR=../orgo tools/generate.sh
76```
77
78Review the diff, and only commit it once orgo's own oracle agrees the change is right.
SKELETON.md added +76
@@ -0,0 +1,76 @@
1# The skeleton reduction
2
3Two correct org→HTML renderers disagree on almost everything at the byte level. org
4wraps every section in `outline-container` divs keyed by generated ids; syntect emits one
5`<span>` per code token; every backend picks its own class names and id scheme. Comparing
6raw HTML across implementations measures none of the things that matter.
7
8The **skeleton** throws that away and keeps what two renderers can meaningfully agree or
9disagree about: the ordered sequence of element opens, element closes, and text runs.
10
11A conformant implementation ports this reduction and asserts that
12`skeleton(its_html) == the checked-in .skeleton file`.
13
14## The reduction, exactly
15
16Given an HTML fragment, walk it left to right and emit one line per event:
17
181. **Element open**`<name>` where `name` is lowercased. Keep only the attributes
19 `href` and `src`, in that fixed order, each rendered as ` href="value"` /
20 ` src="value"` with entities in the value decoded. Drop every other attribute (`id`,
21 `class`, `style`, `datetime`, …). A self-closing `/` is stripped from the name.
222. **Element close**`</name>`, lowercased.
233. **Text run** → the text, with entities decoded and whitespace collapsed (any run of
24 whitespace becomes a single space, leading/trailing trimmed), then emitted as a
25 **Rust debug-quoted string** — i.e. wrapped in double quotes with `"` and `\`
26 backslash-escaped. Empty runs (nothing but whitespace) emit nothing.
27
28Special rules:
29
30 **Ignored elements: `div` and `span`.** Neither emits an open or a close line. `div`
31 is pure layout. `span` matters more than it looks: a syntax highlighter emits one
32 span per token, and keeping them would turn one source block into ~60 lines of noise.
33 **Text merges across ignored tags.** Text is flushed to the output *only when a
34 non-ignored tag is emitted*. So the text on either side of a `<span>` joins into one
35 run — which is exactly why a highlighted source block compares as the single string of
36 code it is, not as a token-by-token sequence that has to line up.
37 **Void elements never emit a close:** `br hr img input meta link col area base source
38 wbr`. A `</img>` in the input is ignored.
39 **Comments and doctypes** (`<!-- … -->`, `<!DOCTYPE …>`) carry nothing and are skipped.
40
41## Entity decoding
42
43Decode the named entities `&amp; &lt; &gt; &quot; &apos; &nbsp;` and numeric entities
44(`&#160;`, `&#xA0;`). A non-breaking space (`\u{a0}`) is treated as a plain space for
45comparison. Anything that is not a recognizable entity — a bare `&` — is left as-is. Only
46consider a `&…;` an entity if the `;` is within 12 characters of the `&`.
47
48## Worked example
49
50Input HTML:
51
52```html
53<h2 id="write-parser"><span class="todo TODO">TODO</span> Write the <b>parser</b></h2>
54```
55
56Skeleton:
57
58```
59<h2>
60"TODO Write the"
61<b>
62"parser"
63</b>
64</h2>
65```
66
67The `id` is dropped, both `span`s vanish but their text `TODO` survives and merges with
68` Write the ` into one collapsed run, and `<b>` (not ignored) splits the run and emits its
69own open/close.
70
71## Reference implementation
72
73The canonical implementation is `skeleton()` in orgo's `src/skeleton.rs` (Rust). A port is
74correct when, for every case in `cases/`, feeding orgo's own `<name>.html` back through
75the port reproduces `<name>.skeleton` byte for byte. That is the first test any port
76should run — it isolates a skeleton bug from a renderer bug.
cases/blocks.html added +32
@@ -0,0 +1,32 @@
1<h2 id="quote">Quote</h2>
2<blockquote>
3<p>A quoted paragraph with <em>markup</em>.</p>
4<p>And a second paragraph.</p>
5</blockquote>
6<h2 id="center">Center</h2>
7<div class="center">
8<p>Centred text.</p>
9</div>
10<h2 id="example">Example</h2>
11<pre>Verbatim *not bold* text.
12 Indentation preserved.</pre>
13<h2 id="export">Export</h2>
14<aside class="raw">Raw HTML passes through.</aside>
15<h2 id="source">Source</h2>
16<pre><code class="language-python highlight"><span class="source python"><span class="meta function python"><span class="storage type function python">def</span> <span class="entity name function python"><span class="meta generic-name python">greet</span></span></span><span class="meta function parameters python"><span class="punctuation section parameters begin python">(</span></span><span class="meta function parameters python"><span class="variable parameter python">name</span><span class="punctuation section parameters end python">)</span></span><span class="meta function python"><span class="punctuation section function begin python">:</span></span>
17 <span class="keyword control flow return python">return</span> <span class="storage type string python">f</span><span class="meta string interpolated python"><span class="string quoted double python"><span class="punctuation definition string begin python">&quot;</span></span></span><span class="meta string interpolated python"><span class="string quoted double python">hello </span><span class="meta interpolation python"><span class="punctuation section interpolation begin python">{</span><span class="source python embedded"><span class="meta qualified-name python"><span class="meta generic-name python">name</span></span></span></span><span class="meta interpolation python"><span class="punctuation section interpolation end python">}</span></span><span class="string quoted double python"><span class="punctuation definition string end python">&quot;</span></span></span></span></code></pre>
18<pre><code class="language-none">plain block, no language</code></pre>
19<h2 id="nested">Nested</h2>
20<blockquote>
21<p>A quote containing a source block:</p>
22<pre><code class="language-sh highlight"><span class="source shell bash"><span class="meta function-call shell"><span class="support function echo shell">echo</span></span><span class="meta function-call arguments shell"> hi</span></span></code></pre>
23</blockquote>
24<h2 id="verse">Verse</h2>
25<p class="verse">
26Line breaks are the point<br>
27 and indentation survives.
28</p>
29<h2 id="a-named-special-block">A named special block</h2>
30<div class="note">
31<p>Contents are <strong>org</strong>, not literal text.</p>
32</div>
cases/blocks.org added +66
@@ -0,0 +1,66 @@
1#+TITLE: Blocks
2
3* Quote
4
5#+BEGIN_QUOTE
6A quoted paragraph with /markup/.
7
8And a second paragraph.
9#+END_QUOTE
10
11* Center
12
13#+BEGIN_CENTER
14Centred text.
15#+END_CENTER
16
17* Example
18
19#+BEGIN_EXAMPLE
20Verbatim *not bold* text.
21 Indentation preserved.
22#+END_EXAMPLE
23
24* Export
25
26#+BEGIN_EXPORT html
27<aside class="raw">Raw HTML passes through.</aside>
28#+END_EXPORT
29
30#+BEGIN_EXPORT latex
31\emph{A non-HTML backend is dropped.}
32#+END_EXPORT
33
34* Source
35
36#+BEGIN_SRC python
37def greet(name):
38 return f"hello {name}"
39#+END_SRC
40
41#+BEGIN_SRC
42plain block, no language
43#+END_SRC
44
45* Nested
46
47#+BEGIN_QUOTE
48A quote containing a source block:
49
50#+BEGIN_SRC sh
51echo hi
52#+END_SRC
53#+END_QUOTE
54
55* Verse
56
57#+BEGIN_VERSE
58Line breaks are the point
59 and indentation survives.
60#+END_VERSE
61
62* A named special block
63
64#+BEGIN_NOTE
65Contents are *org*, not literal text.
66#+END_NOTE
cases/blocks.skeleton added +77
@@ -0,0 +1,77 @@
1<h2>
2"Quote"
3</h2>
4<blockquote>
5<p>
6"A quoted paragraph with"
7<em>
8"markup"
9</em>
10"."
11</p>
12<p>
13"And a second paragraph."
14</p>
15</blockquote>
16<h2>
17"Center"
18</h2>
19<p>
20"Centred text."
21</p>
22<h2>
23"Example"
24</h2>
25<pre>
26"Verbatim *not bold* text. Indentation preserved."
27</pre>
28<h2>
29"Export"
30</h2>
31<aside>
32"Raw HTML passes through."
33</aside>
34<h2>
35"Source"
36</h2>
37<pre>
38<code>
39"def greet(name): return f\"hello {name}\""
40</code>
41</pre>
42<pre>
43<code>
44"plain block, no language"
45</code>
46</pre>
47<h2>
48"Nested"
49</h2>
50<blockquote>
51<p>
52"A quote containing a source block:"
53</p>
54<pre>
55<code>
56"echo hi"
57</code>
58</pre>
59</blockquote>
60<h2>
61"Verse"
62</h2>
63<p>
64"Line breaks are the point"
65<br>
66"and indentation survives."
67</p>
68<h2>
69"A named special block"
70</h2>
71<p>
72"Contents are"
73<strong>
74"org"
75</strong>
76", not literal text."
77</p>
cases/core.html added +15
@@ -0,0 +1,15 @@
1<p>Intro paragraph with a bare URL <a href="https://example.com">https://example.com</a> and some <code>inline code</code>.</p>
2<h2 id="ordered-and-checked">Ordered and checked</h2>
3<ol>
4<li>first item</li>
5<li>second item with <em>emphasis</em></li>
6</ol>
7<ul>
8<li class="off"><code>[&nbsp;]</code> todo item</li>
9<li class="on"><code>[X]</code> done item</li>
10</ul>
11<h2 id="links-and-code">Links and code</h2>
12<p>An external <a href="https://example.org">site</a> and a bare <a href="https://bare.example">https://bare.example</a>.</p>
13<pre><code class="language-rust highlight"><span class="source rust"><span class="meta function rust"><span class="meta function rust"><span class="storage type function rust">fn</span> </span><span class="entity name function rust">main</span></span><span class="meta function rust"><span class="meta function parameters rust"><span class="punctuation section parameters begin rust">(</span></span><span class="meta function rust"><span class="meta function parameters rust"><span class="punctuation section parameters end rust">)</span></span></span></span><span class="meta function rust"> </span><span class="meta function rust"><span class="meta block rust"><span class="punctuation section block begin rust">{</span>
14 <span class="support macro rust">println!</span><span class="meta group rust"><span class="punctuation section group begin rust">(</span></span><span class="meta group rust"><span class="string quoted double rust"><span class="punctuation definition string begin rust">&quot;</span>hello<span class="punctuation definition string end rust">&quot;</span></span></span><span class="meta group rust"><span class="punctuation section group end rust">)</span></span><span class="punctuation terminator rust">;</span>
15</span><span class="meta block rust"><span class="punctuation section block end rust">}</span></span></span></span></code></pre>
cases/core.org added +21
@@ -0,0 +1,21 @@
1#+TITLE: Core Constructs
2
3Intro paragraph with a bare URL https://example.com and some ~inline code~.
4
5* Ordered and checked
6
71. first item
82. second item with /emphasis/
9
10 [ ] todo item
11 [X] done item
12
13* Links and code
14
15An external [[https://example.org][site]] and a bare [[https://bare.example]].
16
17#+BEGIN_SRC rust
18fn main() {
19 println!("hello");
20}
21#+END_SRC
cases/core.skeleton added +58
@@ -0,0 +1,58 @@
1<p>
2"Intro paragraph with a bare URL"
3<a href="https://example.com">
4"https://example.com"
5</a>
6"and some"
7<code>
8"inline code"
9</code>
10"."
11</p>
12<h2>
13"Ordered and checked"
14</h2>
15<ol>
16<li>
17"first item"
18</li>
19<li>
20"second item with"
21<em>
22"emphasis"
23</em>
24</li>
25</ol>
26<ul>
27<li>
28<code>
29"[ ]"
30</code>
31"todo item"
32</li>
33<li>
34<code>
35"[X]"
36</code>
37"done item"
38</li>
39</ul>
40<h2>
41"Links and code"
42</h2>
43<p>
44"An external"
45<a href="https://example.org">
46"site"
47</a>
48"and a bare"
49<a href="https://bare.example">
50"https://bare.example"
51</a>
52"."
53</p>
54<pre>
55<code>
56"fn main() { println!(\"hello\"); }"
57</code>
58</pre>
cases/elements.html added +28
@@ -0,0 +1,28 @@
1<h2 id="code-and-tables">Code and tables</h2>
2<pre><code class="language-rust highlight"><span class="source rust"><span class="meta function rust"><span class="meta function rust"><span class="storage type function rust">fn</span> </span><span class="entity name function rust">main</span></span><span class="meta function rust"><span class="meta function parameters rust"><span class="punctuation section parameters begin rust">(</span></span><span class="meta function rust"><span class="meta function parameters rust"><span class="punctuation section parameters end rust">)</span></span></span></span><span class="meta function rust"> </span><span class="meta function rust"><span class="meta block rust"><span class="punctuation section block begin rust">{</span>
3 <span class="support macro rust">println!</span><span class="meta group rust"><span class="punctuation section group begin rust">(</span></span><span class="meta group rust"><span class="string quoted double rust"><span class="punctuation definition string begin rust">&quot;</span>hello<span class="punctuation definition string end rust">&quot;</span></span></span><span class="meta group rust"><span class="punctuation section group end rust">)</span></span><span class="punctuation terminator rust">;</span>
4</span><span class="meta block rust"><span class="punctuation section block end rust">}</span></span></span></span></code></pre>
5<table>
6<thead>
7<tr><th>Name</th><th>Score</th></tr>
8</thead>
9<tbody>
10<tr><td>alpha</td><td>10</td></tr>
11<tr><td>beta</td><td>20</td></tr>
12</tbody>
13</table>
14<h2 id="links-and-footnotes">Links and footnotes</h2>
15<p>An external link: <a href="https://example.com">Example</a> and an id link <a href="#abc-123">abc-123</a>.</p>
16<p>Text with a footnote reference.<sup class="footnote-ref"><a id="fnr-1" href="#fn-1">1</a></sup></p>
17<h2 id="blocks">Blocks</h2>
18<blockquote>
19<p>A quoted paragraph.</p>
20</blockquote>
21<hr>
22<section class="footnotes" aria-label="Footnotes">
23<hr>
24<ol>
25<li id="fn-1"><p>The footnote definition.</p>
26 <a class="footnote-back" href="#fnr-1" aria-label="Back to reference 1">&#8617;</a></li>
27</ol>
28</section>
cases/elements.org added +31
@@ -0,0 +1,31 @@
1#+TITLE: Element Sampler
2#+FILETAGS: :sample:
3
4* Code and tables
5
6#+BEGIN_SRC rust
7fn main() {
8 println!("hello");
9}
10#+END_SRC
11
12| Name | Score |
13|-------+-------|
14| alpha | 10 |
15| beta | 20 |
16
17* Links and footnotes
18
19An external link: [[https://example.com][Example]] and an id link [[id:abc-123]].
20
21Text with a footnote reference.[fn:1]
22
23[fn:1] The footnote definition.
24
25* Blocks
26
27#+BEGIN_QUOTE
28A quoted paragraph.
29#+END_QUOTE
30
31----
cases/elements.skeleton added +82
@@ -0,0 +1,82 @@
1<h2>
2"Code and tables"
3</h2>
4<pre>
5<code>
6"fn main() { println!(\"hello\"); }"
7</code>
8</pre>
9<table>
10<thead>
11<tr>
12<th>
13"Name"
14</th>
15<th>
16"Score"
17</th>
18</tr>
19</thead>
20<tbody>
21<tr>
22<td>
23"alpha"
24</td>
25<td>
26"10"
27</td>
28</tr>
29<tr>
30<td>
31"beta"
32</td>
33<td>
34"20"
35</td>
36</tr>
37</tbody>
38</table>
39<h2>
40"Links and footnotes"
41</h2>
42<p>
43"An external link:"
44<a href="https://example.com">
45"Example"
46</a>
47"and an id link"
48<a href="#abc-123">
49"abc-123"
50</a>
51"."
52</p>
53<p>
54"Text with a footnote reference."
55<sup>
56<a href="#fn-1">
57"1"
58</a>
59</sup>
60</p>
61<h2>
62"Blocks"
63</h2>
64<blockquote>
65<p>
66"A quoted paragraph."
67</p>
68</blockquote>
69<hr>
70<section>
71<hr>
72<ol>
73<li>
74<p>
75"The footnote definition."
76</p>
77<a href="#fnr-1">
78"↩"
79</a>
80</li>
81</ol>
82</section>
cases/footnote.html added +12
@@ -0,0 +1,12 @@
1<p>Text with a reference.<sup class="footnote-ref"><a id="fnr-1" href="#fn-1">1</a></sup> And a second one.<sup class="footnote-ref"><a id="fnr-2" href="#fn-2">2</a></sup></p>
2<p>An inline footnote.<sup class="footnote-ref"><a id="fnr-3" href="#fn-3">3</a></sup></p>
3<section class="footnotes" aria-label="Footnotes">
4<hr>
5<ol>
6<li id="fn-1"><p>The first definition.</p>
7 <a class="footnote-back" href="#fnr-1" aria-label="Back to reference 1">&#8617;</a></li>
8<li id="fn-2"><p>The second definition, with <em>emphasis</em>.</p>
9 <a class="footnote-back" href="#fnr-2" aria-label="Back to reference 2">&#8617;</a></li>
10<li id="fn-3">defined right here <a class="footnote-back" href="#fnr-3" aria-label="Back to reference 3">&#8617;</a></li>
11</ol>
12</section>
cases/footnote.org added +8
@@ -0,0 +1,8 @@
1#+TITLE: Footnote Sample
2
3Text with a reference.[fn:1] And a second one.[fn:2]
4
5An inline footnote.[fn:3:defined right here]
6
7[fn:1] The first definition.
8[fn:2] The second definition, with /emphasis/.
cases/footnote.skeleton added +53
@@ -0,0 +1,53 @@
1<p>
2"Text with a reference."
3<sup>
4<a href="#fn-1">
5"1"
6</a>
7</sup>
8"And a second one."
9<sup>
10<a href="#fn-2">
11"2"
12</a>
13</sup>
14</p>
15<p>
16"An inline footnote."
17<sup>
18<a href="#fn-3">
19"3"
20</a>
21</sup>
22</p>
23<section>
24<hr>
25<ol>
26<li>
27<p>
28"The first definition."
29</p>
30<a href="#fnr-1">
31"↩"
32</a>
33</li>
34<li>
35<p>
36"The second definition, with"
37<em>
38"emphasis"
39</em>
40"."
41</p>
42<a href="#fnr-2">
43"↩"
44</a>
45</li>
46<li>
47"defined right here"
48<a href="#fnr-3">
49"↩"
50</a>
51</li>
52</ol>
53</section>
cases/headings.html added +10
@@ -0,0 +1,10 @@
1<h2 id="write-parser"><span class="todo TODO">TODO</span> <span class="priority">[#A]</span> Write the parser <span class="tag">work</span> <span class="tag">rust</span></h2>
2<p>A heading carrying a keyword, a priority, tags and a property drawer.</p>
3<h3 id="nested-and-finished"><span class="done DONE">DONE</span> Nested and finished</h3>
4<p>Sub-headings nest by star count.</p>
5<h3 id="priority-without-a-keyword"><span class="priority">[#C]</span> Priority without a keyword</h3>
6<p>A priority cookie can stand alone.</p>
7<h2 id="todos-are-not-a-keyword">TODOs are not a keyword</h2>
8<p>The word boundary matters: this heading has no TODO keyword.</p>
9<h2><span class="done DONE">DONE</span> </h2>
10<p>A keyword with no title at all.</p>
cases/headings.org added +20
@@ -0,0 +1,20 @@
1#+TITLE: Heading Metadata
2
3* TODO [#A] Write the parser :work:rust:
4:PROPERTIES:
5:CUSTOM_ID: write-parser
6:OWNER: nobody
7:END:
8A heading carrying a keyword, a priority, tags and a property drawer.
9
10** DONE Nested and finished
11Sub-headings nest by star count.
12
13** [#C] Priority without a keyword
14A priority cookie can stand alone.
15
16* TODOs are not a keyword
17The word boundary matters: this heading has no TODO keyword.
18
19* DONE
20A keyword with no title at all.
cases/headings.skeleton added +30
@@ -0,0 +1,30 @@
1<h2>
2"TODO [#A] Write the parser work rust"
3</h2>
4<p>
5"A heading carrying a keyword, a priority, tags and a property drawer."
6</p>
7<h3>
8"DONE Nested and finished"
9</h3>
10<p>
11"Sub-headings nest by star count."
12</p>
13<h3>
14"[#C] Priority without a keyword"
15</h3>
16<p>
17"A priority cookie can stand alone."
18</p>
19<h2>
20"TODOs are not a keyword"
21</h2>
22<p>
23"The word boundary matters: this heading has no TODO keyword."
24</p>
25<h2>
26"DONE"
27</h2>
28<p>
29"A keyword with no title at all."
30</p>
cases/images.html added +10
@@ -0,0 +1,10 @@
1<h2 id="bare-image">Bare image</h2>
2<p><img src="diagram.png" alt=""></p>
3<h2 id="captioned-figure">Captioned figure</h2>
4<figure><img src="pipeline.svg" alt="The pipeline, end to end" width="640" class="diagram"><figcaption><span class="figure-number">Figure 1: </span>The pipeline, end to end</figcaption></figure>
5<h2 id="caption-with-markup">Caption with markup</h2>
6<figure><img src="chart.png" alt="A stylised chart"><figcaption><span class="figure-number">Figure 2: </span>A <em>stylised</em> chart</figcaption></figure>
7<h2 id="quoted-attribute-values">Quoted attribute values</h2>
8<figure><img src="cat.jpg" alt="a cat, sitting" loading="lazy"></figure>
9<h2 id="image-with-a-description-is-a-link">Image with a description is a link</h2>
10<p><a href="diagram.png">the diagram</a></p>
cases/images.org added +25
@@ -0,0 +1,25 @@
1#+TITLE: Images
2
3* Bare image
4
5[[file:diagram.png]]
6
7* Captioned figure
8
9#+CAPTION: The pipeline, end to end
10#+ATTR_HTML: :width 640 :class diagram
11[[file:pipeline.svg]]
12
13* Caption with markup
14
15#+CAPTION: A /stylised/ chart
16[[file:chart.png]]
17
18* Quoted attribute values
19
20#+ATTR_HTML: :alt "a cat, sitting" :loading lazy
21[[file:cat.jpg]]
22
23* Image with a description is a link
24
25[[file:diagram.png][the diagram]]
cases/images.skeleton added +42
@@ -0,0 +1,42 @@
1<h2>
2"Bare image"
3</h2>
4<p>
5<img src="diagram.png">
6</p>
7<h2>
8"Captioned figure"
9</h2>
10<figure>
11<img src="pipeline.svg">
12<figcaption>
13"Figure 1: The pipeline, end to end"
14</figcaption>
15</figure>
16<h2>
17"Caption with markup"
18</h2>
19<figure>
20<img src="chart.png">
21<figcaption>
22"Figure 2: A"
23<em>
24"stylised"
25</em>
26"chart"
27</figcaption>
28</figure>
29<h2>
30"Quoted attribute values"
31</h2>
32<figure>
33<img src="cat.jpg">
34</figure>
35<h2>
36"Image with a description is a link"
37</h2>
38<p>
39<a href="diagram.png">
40"the diagram"
41</a>
42</p>
cases/lists.html added +44
@@ -0,0 +1,44 @@
1<h2 id="nesting">Nesting</h2>
2<ul>
3<li>outer item<ul>
4<li>inner item<ul>
5<li>deepest item</li>
6</ul>
7</li>
8<li>second inner</li>
9</ul>
10</li>
11<li>second outer</li>
12</ul>
13<h2 id="ordered">Ordered</h2>
14<ol>
15<li>first</li>
16<li>second<ol>
17<li>second point one</li>
18<li>second point two</li>
19</ol>
20</li>
21<li>third</li>
22</ol>
23<h2 id="checkboxes">Checkboxes</h2>
24<ul>
25<li class="off"><code>[&nbsp;]</code> not done</li>
26<li class="on"><code>[X]</code> done</li>
27<li class="trans"><code>[-]</code> partially done</li>
28</ul>
29<h2 id="description">Description</h2>
30<dl>
31<dt>term one</dt>
32<dd>the first definition</dd>
33<dt>term two</dt>
34<dd>the second definition, which is soft-wrapped across two lines</dd>
35<dt><em>marked up</em> term</dt>
36<dd>definitions hold inline markup</dd>
37</dl>
38<h2 id="multi-paragraph-items">Multi-paragraph items</h2>
39<ul>
40<li><p>an item whose body has two paragraphs</p>
41<p>the second paragraph, indented under the bullet</p>
42</li>
43<li>a plain sibling</li>
44</ul>
cases/lists.org added +38
@@ -0,0 +1,38 @@
1#+TITLE: Lists
2
3* Nesting
4
5 outer item
6 - inner item
7 - deepest item
8 - second inner
9 second outer
10
11* Ordered
12
131. first
142. second
15 1. second point one
16 2. second point two
173. third
18
19* Checkboxes
20
21 [ ] not done
22 [X] done
23 [-] partially done
24
25* Description
26
27 term one :: the first definition
28 term two :: the second definition, which is
29 soft-wrapped across two lines
30 /marked up/ term :: definitions hold inline markup
31
32* Multi-paragraph items
33
34 an item whose body has two paragraphs
35
36 the second paragraph, indented under the bullet
37
38 a plain sibling
cases/lists.skeleton added +111
@@ -0,0 +1,111 @@
1<h2>
2"Nesting"
3</h2>
4<ul>
5<li>
6"outer item"
7<ul>
8<li>
9"inner item"
10<ul>
11<li>
12"deepest item"
13</li>
14</ul>
15</li>
16<li>
17"second inner"
18</li>
19</ul>
20</li>
21<li>
22"second outer"
23</li>
24</ul>
25<h2>
26"Ordered"
27</h2>
28<ol>
29<li>
30"first"
31</li>
32<li>
33"second"
34<ol>
35<li>
36"second point one"
37</li>
38<li>
39"second point two"
40</li>
41</ol>
42</li>
43<li>
44"third"
45</li>
46</ol>
47<h2>
48"Checkboxes"
49</h2>
50<ul>
51<li>
52<code>
53"[ ]"
54</code>
55"not done"
56</li>
57<li>
58<code>
59"[X]"
60</code>
61"done"
62</li>
63<li>
64<code>
65"[-]"
66</code>
67"partially done"
68</li>
69</ul>
70<h2>
71"Description"
72</h2>
73<dl>
74<dt>
75"term one"
76</dt>
77<dd>
78"the first definition"
79</dd>
80<dt>
81"term two"
82</dt>
83<dd>
84"the second definition, which is soft-wrapped across two lines"
85</dd>
86<dt>
87<em>
88"marked up"
89</em>
90"term"
91</dt>
92<dd>
93"definitions hold inline markup"
94</dd>
95</dl>
96<h2>
97"Multi-paragraph items"
98</h2>
99<ul>
100<li>
101<p>
102"an item whose body has two paragraphs"
103</p>
104<p>
105"the second paragraph, indented under the bullet"
106</p>
107</li>
108<li>
109"a plain sibling"
110</li>
111</ul>
cases/minimal.html added +10
@@ -0,0 +1,10 @@
1<p>A single paragraph of preamble text before any heading.</p>
2<h2 id="first">First Heading</h2>
3<p>Some body text with <strong>bold</strong>, <em>italic</em>, and <code class="verbatim">verbatim</code>.</p>
4<h3 id="a-subheading">A Subheading <span class="tag">tag1</span> <span class="tag">tag2</span></h3>
5<ul>
6<li>an unordered item</li>
7<li>another with a checkbox [ ]</li>
8</ul>
9<h2 id="second-heading">Second Heading</h2>
10<p>See <a href="#first">the first heading</a>.</p>
cases/minimal.org added +20
@@ -0,0 +1,20 @@
1#+TITLE: Minimal Fixture
2#+DATE: 2026-08-08
3#+AUTHOR: Owner
4
5A single paragraph of preamble text before any heading.
6
7* First Heading
8 :PROPERTIES:
9 :CUSTOM_ID: first
10 :END:
11
12Some body text with *bold*, /italic/, and =verbatim=.
13
14** A Subheading :tag1:tag2:
15
16 an unordered item
17 another with a checkbox [ ]
18
19* Second Heading
20 See [[#first][the first heading]].
cases/minimal.skeleton added +42
@@ -0,0 +1,42 @@
1<p>
2"A single paragraph of preamble text before any heading."
3</p>
4<h2>
5"First Heading"
6</h2>
7<p>
8"Some body text with"
9<strong>
10"bold"
11</strong>
12","
13<em>
14"italic"
15</em>
16", and"
17<code>
18"verbatim"
19</code>
20"."
21</p>
22<h3>
23"A Subheading tag1 tag2"
24</h3>
25<ul>
26<li>
27"an unordered item"
28</li>
29<li>
30"another with a checkbox [ ]"
31</li>
32</ul>
33<h2>
34"Second Heading"
35</h2>
36<p>
37"See"
38<a href="#first">
39"the first heading"
40</a>
41"."
42</p>
cases/outofscope.html added +21
@@ -0,0 +1,21 @@
1<p>Every construct here is on the explicit "Not supported" list in the org-support guide. The contract is not that we handle them — it is that they degrade predictably and never crash the build.</p>
2<h2 id="babel">Babel</h2>
3<pre><code class="language-sh highlight"><span class="source shell bash"><span class="meta function-call shell"><span class="support function echo shell">echo</span></span><span class="meta function-call arguments shell"> <span class="string quoted double shell"><span class="punctuation definition string begin shell">&quot;</span>the block renders; :results is never executed<span class="punctuation definition string end shell">&quot;</span></span></span></span></code></pre>
4<h2 id="table-formulas">Table formulas</h2>
5<table>
6<thead>
7<tr><th>item</th><th>cost</th></tr>
8</thead>
9<tbody>
10<tr><td>a</td><td>1</td></tr>
11<tr><td>b</td><td>2</td></tr>
12</tbody>
13</table>
14<h2 id="latex">LaTeX</h2>
15<p>Inline math $x^2 + y^2$ and a display block:</p>
16<p>\begin{equation} E = mc^2 \end{equation}</p>
17<h2 id="macros-and-radio-targets">Macros and radio targets</h2>
18<p>A macro call {{{author}}} and a &lt;&lt;&lt;radio target&gt;&gt;&gt; stay literal.</p>
19<h2 id="drawers">Drawers</h2>
20<h2 id="unknown-entities">Unknown entities</h2>
21<p>An entity org does not know, \notanentity, stays literal.</p>
cases/outofscope.org added +49
@@ -0,0 +1,49 @@
1#+TITLE: Out of Scope
2#+INCLUDE: "other.org"
3
4Every construct here is on the explicit "Not supported" list in the org-support guide.
5The contract is not that we handle them — it is that they degrade predictably and never
6crash the build.
7
8* Babel
9
10#+BEGIN_SRC sh :results output :exports both
11echo "the block renders; :results is never executed"
12#+END_SRC
13
14#+RESULTS:
15: stale output from a previous evaluation
16
17* Table formulas
18
19| item | cost |
20|------+------|
21| a | 1 |
22| b | 2 |
23#+TBLFM: $2=vsum(@2..@3)
24
25* LaTeX
26
27Inline math $x^2 + y^2$ and a display block:
28
29\begin{equation}
30E = mc^2
31\end{equation}
32
33* Macros and radio targets
34
35A macro call {{{author}}} and a <<<radio target>>> stay literal.
36
37* Drawers
38
39:LOGBOOK:
40CLOCK: [2024-01-15 Mon 09:00]--[2024-01-15 Mon 10:00] => 1:00
41:END:
42
43:CUSTOM_DRAWER:
44Drawer contents are captured and dropped.
45:END:
46
47* Unknown entities
48
49An entity org does not know, \notanentity, stays literal.
cases/outofscope.skeleton added +68
@@ -0,0 +1,68 @@
1<p>
2"Every construct here is on the explicit \"Not supported\" list in the org-support guide. The contract is not that we handle them — it is that they degrade predictably and never crash the build."
3</p>
4<h2>
5"Babel"
6</h2>
7<pre>
8<code>
9"echo \"the block renders; :results is never executed\""
10</code>
11</pre>
12<h2>
13"Table formulas"
14</h2>
15<table>
16<thead>
17<tr>
18<th>
19"item"
20</th>
21<th>
22"cost"
23</th>
24</tr>
25</thead>
26<tbody>
27<tr>
28<td>
29"a"
30</td>
31<td>
32"1"
33</td>
34</tr>
35<tr>
36<td>
37"b"
38</td>
39<td>
40"2"
41</td>
42</tr>
43</tbody>
44</table>
45<h2>
46"LaTeX"
47</h2>
48<p>
49"Inline math $x^2 + y^2$ and a display block:"
50</p>
51<p>
52"\\begin{equation} E = mc^2 \\end{equation}"
53</p>
54<h2>
55"Macros and radio targets"
56</h2>
57<p>
58"A macro call {{{author}}} and a <<<radio target>>> stay literal."
59</p>
60<h2>
61"Drawers"
62</h2>
63<h2>
64"Unknown entities"
65</h2>
66<p>
67"An entity org does not know, \\notanentity, stays literal."
68</p>
cases/table.html added +9
@@ -0,0 +1,9 @@
1<table>
2<thead>
3<tr><th>Name</th><th>Score</th></tr>
4</thead>
5<tbody>
6<tr><td>alpha</td><td>10</td></tr>
7<tr><td>beta</td><td>20</td></tr>
8</tbody>
9</table>
cases/table.org added +6
@@ -0,0 +1,6 @@
1#+TITLE: Table Sample
2
3| Name | Score |
4|-------+-------|
5| alpha | 10 |
6| beta | 20 |
cases/table.skeleton added +30
@@ -0,0 +1,30 @@
1<table>
2<thead>
3<tr>
4<th>
5"Name"
6</th>
7<th>
8"Score"
9</th>
10</tr>
11</thead>
12<tbody>
13<tr>
14<td>
15"alpha"
16</td>
17<td>
18"10"
19</td>
20</tr>
21<tr>
22<td>
23"beta"
24</td>
25<td>
26"20"
27</td>
28</tr>
29</tbody>
30</table>
cases/tblfm.html added +10
@@ -0,0 +1,10 @@
1<p>The cells deliberately contradict the formula: org's HTML exporter does not recalculate on export, so both exporters must emit the values as written.</p>
2<table>
3<thead>
4<tr><th>N</th><th>N<sup>2</sup></th></tr>
5</thead>
6<tbody>
7<tr><td>2</td><td>999</td></tr>
8<tr><td>3</td><td>888</td></tr>
9</tbody>
10</table>
cases/tblfm.org added +10
@@ -0,0 +1,10 @@
1#+TITLE: Table Formula
2
3The cells deliberately contradict the formula: org's HTML exporter does not
4recalculate on export, so both exporters must emit the values as written.
5
6| N | N^2 |
7|---+-----|
8| 2 | 999 |
9| 3 | 888 |
10#+TBLFM: $2=$1^2
cases/tblfm.skeleton added +36
@@ -0,0 +1,36 @@
1<p>
2"The cells deliberately contradict the formula: org's HTML exporter does not recalculate on export, so both exporters must emit the values as written."
3</p>
4<table>
5<thead>
6<tr>
7<th>
8"N"
9</th>
10<th>
11"N"
12<sup>
13"2"
14</sup>
15</th>
16</tr>
17</thead>
18<tbody>
19<tr>
20<td>
21"2"
22</td>
23<td>
24"999"
25</td>
26</tr>
27<tr>
28<td>
29"3"
30</td>
31<td>
32"888"
33</td>
34</tr>
35</tbody>
36</table>
cases/timestamps.html added +10
@@ -0,0 +1,10 @@
1<h2 id="single">Single</h2>
2<p>An active date <time class="timestamp" datetime="2024-01-15">2024-01-15</time> and an inactive one <time class="timestamp inactive" datetime="2024-01-15">2024-01-15</time>.</p>
3<p>With a time: <time class="timestamp" datetime="2024-01-15T10:30">2024-01-15 10:30</time>.</p>
4<h2 id="ranges">Ranges</h2>
5<p>A same-day time range <time class="timestamp" datetime="2024-01-15T10:00">2024-01-15 10:00</time>&#8211;<time class="timestamp" datetime="2024-01-15T11:45">11:45</time>.</p>
6<p>A multi-day range <time class="timestamp" datetime="2024-01-15">2024-01-15</time>&#8211;<time class="timestamp" datetime="2024-01-20">2024-01-20</time>.</p>
7<h2 id="ignored-decorations">Ignored decorations</h2>
8<p>A repeater is dropped: <time class="timestamp" datetime="2024-01-15">2024-01-15</time>.</p>
9<h2 id="not-timestamps">Not timestamps</h2>
10<p>Comparisons like 3 &lt; 4 and [not a stamp] stay literal text.</p>
cases/timestamps.org added +21
@@ -0,0 +1,21 @@
1#+TITLE: Timestamps
2
3* Single
4
5An active date <2024-01-15 Mon> and an inactive one [2024-01-15 Mon].
6
7With a time: <2024-01-15 Mon 10:30>.
8
9* Ranges
10
11A same-day time range <2024-01-15 Mon 10:00-11:45>.
12
13A multi-day range <2024-01-15 Mon>--<2024-01-20 Sat>.
14
15* Ignored decorations
16
17A repeater is dropped: <2024-01-15 Mon +1w>.
18
19* Not timestamps
20
21Comparisons like 3 < 4 and [not a stamp] stay literal text.
cases/timestamps.skeleton added +62
@@ -0,0 +1,62 @@
1<h2>
2"Single"
3</h2>
4<p>
5"An active date"
6<time>
7"2024-01-15"
8</time>
9"and an inactive one"
10<time>
11"2024-01-15"
12</time>
13"."
14</p>
15<p>
16"With a time:"
17<time>
18"2024-01-15 10:30"
19</time>
20"."
21</p>
22<h2>
23"Ranges"
24</h2>
25<p>
26"A same-day time range"
27<time>
28"2024-01-15 10:00"
29</time>
30"–"
31<time>
32"11:45"
33</time>
34"."
35</p>
36<p>
37"A multi-day range"
38<time>
39"2024-01-15"
40</time>
41"–"
42<time>
43"2024-01-20"
44</time>
45"."
46</p>
47<h2>
48"Ignored decorations"
49</h2>
50<p>
51"A repeater is dropped:"
52<time>
53"2024-01-15"
54</time>
55"."
56</p>
57<h2>
58"Not timestamps"
59</h2>
60<p>
61"Comparisons like 3 < 4 and [not a stamp] stay literal text."
62</p>
manifest.json added +83
@@ -0,0 +1,83 @@
1{
2 "version": 1,
3 "reference": {
4 "implementation": "orgo",
5 "repo": "krz/orgo",
6 "note": "Golden .html and .skeleton files are produced by orgo, which is validated against Emacs ox-html via its own oracle test. orgo is the reference; other implementations conform to it."
7 },
8 "comparison": "skeleton",
9 "cases": [
10 {
11 "name": "minimal",
12 "constructs": ["document-keywords", "preamble-paragraph", "heading", "property-drawer", "custom-id", "tags", "bold", "italic", "verbatim", "unordered-list", "checkbox", "custom-id-link"],
13 "scope": "in",
14 "note": "Smallest end-to-end document: metadata header, one nesting of headings, inline emphasis, a checkbox, an internal link."
15 },
16 {
17 "name": "core",
18 "constructs": ["heading", "bare-url", "inline-code", "ordered-list", "checkbox", "external-link", "src-block"],
19 "scope": "in",
20 "note": "The common README surface: links, code, lists, a source block."
21 },
22 {
23 "name": "elements",
24 "constructs": ["filetags", "src-block", "table", "external-link", "id-link", "footnote-ref", "footnote-def", "quote-block", "horizontal-rule"],
25 "scope": "in",
26 "note": "A sampler spanning most block-level elements."
27 },
28 {
29 "name": "headings",
30 "constructs": ["todo-keyword", "done-keyword", "priority", "tags", "property-drawer", "custom-id", "todo-word-boundary", "empty-title"],
31 "scope": "in",
32 "note": "Heading anatomy, including the negatives: 'TODOs' is not a keyword, a bare 'DONE' has no title."
33 },
34 {
35 "name": "lists",
36 "constructs": ["nested-unordered", "ordered-list", "nested-ordered", "checkbox", "checkbox-partial", "description-list", "inline-markup-in-term", "multi-paragraph-item"],
37 "scope": "in",
38 "note": "Every list shape, including three-deep nesting and multi-paragraph items."
39 },
40 {
41 "name": "table",
42 "constructs": ["table", "table-header-rule"],
43 "scope": "in",
44 "note": "A header band separated by a rule row."
45 },
46 {
47 "name": "blocks",
48 "constructs": ["quote-block", "center-block", "example-block", "export-html", "export-latex-dropped", "src-block", "src-block-no-lang", "nested-src-in-quote", "verse-block", "special-block"],
49 "scope": "in",
50 "note": "All standard blocks. Export blocks: html passes through raw, non-html backends are dropped. Named special blocks render as a container carrying the name."
51 },
52 {
53 "name": "timestamps",
54 "constructs": ["active-timestamp", "inactive-timestamp", "timestamp-with-time", "same-day-range", "multi-day-range", "repeater-dropped", "non-timestamp-text"],
55 "scope": "in",
56 "note": "orgo renders timestamps as <time datetime>; the last section checks that '3 < 4' and '[not a stamp]' stay literal."
57 },
58 {
59 "name": "footnote",
60 "constructs": ["footnote-ref", "footnote-def", "inline-footnote", "inline-markup-in-note"],
61 "scope": "in",
62 "note": "Numbered references, definitions, and an anonymous inline footnote."
63 },
64 {
65 "name": "images",
66 "constructs": ["bare-image", "captioned-figure", "attr-html", "caption-with-markup", "quoted-attr-values", "image-link-with-description"],
67 "scope": "in",
68 "note": "A bare image link becomes <img>; a #+CAPTION: promotes it to <figure>; a description turns it back into an ordinary link."
69 },
70 {
71 "name": "tblfm",
72 "constructs": ["table", "tblfm-inert"],
73 "scope": "in",
74 "note": "#+TBLFM: is kept inert: cells are emitted as written, never recalculated, matching org's HTML exporter."
75 },
76 {
77 "name": "outofscope",
78 "constructs": ["include-reported", "babel-results-dropped", "table-formula", "latex-literal", "macro-literal", "radio-target-literal", "logbook-drawer-dropped", "custom-drawer-dropped", "unknown-entity-literal"],
79 "scope": "out",
80 "note": "Deliberately unsupported constructs. The contract is not that they render, but that they degrade predictably and never crash. An implementation may legitimately differ from orgo here; treat this case as documentation, not a hard assertion."
81 }
82 ]
83}
tools/generate.sh added +30
@@ -0,0 +1,30 @@
1#!/usr/bin/env bash
2# Regenerate cases/<name>.html and cases/<name>.skeleton from orgo.
3#
4# The golden outputs are derived from orgo, the reference implementation — never edited by
5# hand. Point ORGO_DIR at an orgo checkout (default: ../orgo). Each case's .org input is
6# the source of truth for the input; this script only refreshes the two derived files.
7#
8# ORGO_DIR=../orgo tools/generate.sh
9set -euo pipefail
10
11here="$(cd "$(dirname "$0")/.." && pwd)"
12orgo="${ORGO_DIR:-$here/../orgo}"
13
14if [ ! -f "$orgo/Cargo.toml" ]; then
15 echo "orgo checkout not found at $orgo (set ORGO_DIR)" >&2
16 exit 1
17fi
18
19# One release-less build up front so the per-case runs are fast.
20( cd "$orgo" && cargo build -q --example skeleton )
21
22count=0
23for org in "$here"/cases/*.org; do
24 name="$(basename "$org" .org)"
25 ( cd "$orgo" && cargo run -q --example skeleton -- --html "$org" ) > "$here/cases/$name.html"
26 ( cd "$orgo" && cargo run -q --example skeleton -- "$org" ) > "$here/cases/$name.skeleton"
27 count=$((count + 1))
28 echo " regenerated $name"
29done
30echo "done: $count cases"