SKELETON.md
76 lines · 3422 bytes
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 `& < > " ' ` and numeric entities
44(` `, ` `). 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.