@jarenjs/md 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +520 -0
  2. package/dist/types/ast.d.ts +181 -0
  3. package/dist/types/bake.d.ts +61 -0
  4. package/dist/types/compiler.d.ts +141 -0
  5. package/dist/types/component/index.d.ts +101 -0
  6. package/dist/types/directives.d.ts +126 -0
  7. package/dist/types/entities.d.ts +40 -0
  8. package/dist/types/footnotes.d.ts +83 -0
  9. package/dist/types/frontmatter.d.ts +67 -0
  10. package/dist/types/html.d.ts +72 -0
  11. package/dist/types/index.d.ts +30 -0
  12. package/dist/types/loader.d.ts +84 -0
  13. package/dist/types/mdx.d.ts +45 -0
  14. package/dist/types/parser.d.ts +116 -0
  15. package/dist/types/plugins/highlight.d.ts +64 -0
  16. package/dist/types/plugins/index.d.ts +64 -0
  17. package/dist/types/plugins/mermaid.d.ts +12 -0
  18. package/dist/types/scanner.d.ts +240 -0
  19. package/dist/types/to-html.d.ts +104 -0
  20. package/dist/types/to-md.d.ts +23 -0
  21. package/dist/types/to-vnode.d.ts +161 -0
  22. package/dist/types/utils.d.ts +63 -0
  23. package/docs/LOADER.md +92 -0
  24. package/docs/MD-FORMAT.md +502 -0
  25. package/docs/PLUGINS.md +277 -0
  26. package/package.json +80 -0
  27. package/schemas/jaren-md-ast.schema.json +296 -0
  28. package/src/ast.js +346 -0
  29. package/src/bake.js +104 -0
  30. package/src/compiler.js +167 -0
  31. package/src/component/index.js +191 -0
  32. package/src/directives.js +371 -0
  33. package/src/entities.js +107 -0
  34. package/src/footnotes.js +180 -0
  35. package/src/frontmatter.js +947 -0
  36. package/src/html.js +281 -0
  37. package/src/index.js +76 -0
  38. package/src/loader.js +0 -0
  39. package/src/mdx.js +219 -0
  40. package/src/parser.js +1685 -0
  41. package/src/plugins/highlight.js +325 -0
  42. package/src/plugins/index.js +75 -0
  43. package/src/plugins/mermaid.js +14 -0
  44. package/src/scanner.js +832 -0
  45. package/src/to-html.js +425 -0
  46. package/src/to-md.js +396 -0
  47. package/src/to-vnode.js +766 -0
  48. package/src/utils.js +107 -0
  49. package/styles/md.css +238 -0
@@ -0,0 +1,502 @@
1
+ # The Jaren Markdown Format
2
+
3
+ **Version 0.1 — Specification**
4
+
5
+ This document defines the JSON document format produced by `@jarenjs/md`:
6
+ a Markdown + frontmatter engine whose output is a stable, serializable
7
+ AST that the rest of the Jaren suite consumes natively. Where JTLT
8
+ ([JSLT-FORMAT.md](../../../packages/json/docs/JSLT-FORMAT.md) §segments) turns JSON
9
+ into text, this format is the inverse arrow: Markdown text into JSON that
10
+ JSLT stylesheets match, query documents address, and
11
+ [view](../../../packages/view/docs/VIEW-FORMAT.md) renderers display.
12
+
13
+ ## 1. Introduction
14
+
15
+ ### 1.1 What this format is
16
+
17
+ An **MdDocument** is a plain JSON value — no classes, no methods, no
18
+ prototypes. It can be stringified, diffed with JSON Patch, validated with
19
+ JSON Schema (`schemas/jaren-md-ast.schema.json`), transformed with JSLT,
20
+ and generated under constrained decoding, like every other document
21
+ format in the suite.
22
+
23
+ ### 1.2 Conformance and normative language
24
+
25
+ The key words **MUST**, **MUST NOT**, **SHOULD** and **MAY** are to be
26
+ interpreted as described in RFC 2119. Two conformance roles exist:
27
+
28
+ - a **producer** creates MdDocuments (the parser in this package, a JSLT
29
+ transform, an LLM under the published schema);
30
+ - a **consumer** reads them (the vnode emitter, the canonical printer,
31
+ a stylesheet, application code).
32
+
33
+ ### 1.3 Dialect
34
+
35
+ The parser recognizes the CommonMark core constructs (ATX headings,
36
+ setext headings, paragraphs, thematic breaks, fenced and indented code,
37
+ blockquotes, ordered/unordered lists, inline emphasis/links/images/code,
38
+ hard and soft breaks, backslash escapes, autolinks, raw HTML blocks and
39
+ spans) plus the GFM extensions in universal use: **tables**,
40
+ **strikethrough**, **task lists**, **footnotes** (§4.6) and **literal
41
+ autolinks** (§4.7). The parser passes **every example in the CommonMark
42
+ specification** through the string emitter (§4.4a), and the benchmark
43
+ workspace scores both emitters against the official corpus — and against
44
+ the GFM specification's extension sections — on every run.
45
+
46
+ This specification remains normative for the package: it covers the
47
+ frontmatter, the AST, and the extensions neither spec describes
48
+ (footnotes are GitHub's, documented nowhere but here). Where it and
49
+ CommonMark speak about the same construct they agree. Two places where
50
+ this package deliberately differs from the GFM reference implementation
51
+ are stated with their reasons in the package README's scorecard section;
52
+ neither is a dialect gap a document can fall into.
53
+
54
+ ## 2. The document envelope
55
+
56
+ ```json
57
+ {
58
+ "$md": "0.1",
59
+ "frontmatter": { "title": "Hello" },
60
+ "ast": [ { "type": "heading", "depth": 1, "children": [ { "type": "text", "value": "Hello" } ] } ],
61
+ "meta": { "sourceUrl": null, "hash": "1f9a2k3", "frontmatterLang": "yaml" }
62
+ }
63
+ ```
64
+
65
+ - `$md` — the format version. Producers MUST write `"0.1"`.
66
+ - `frontmatter` — the parsed frontmatter value, or `null` when the
67
+ document has none. Whatever the source syntax (§3), it normalizes to
68
+ plain JSON here.
69
+ - `ast` — the ordered array of block nodes (§4).
70
+ - `meta.sourceUrl` — the URL the document was loaded from, or `null`.
71
+ - `meta.hash` — a content hash of the source (FNV-1a 32-bit, base 36):
72
+ the identity primitive for caches and vnode keys.
73
+ - `meta.frontmatterLang` — `"yaml"`, `"json"`, `"toml"` or `null`.
74
+
75
+ Consumers MUST ignore `meta` members they do not know; producers MAY add
76
+ members there (the envelope is minimal, extensible).
77
+
78
+ ## 3. Frontmatter
79
+
80
+ Frontmatter is detected **at the very top of the source only** (byte 0):
81
+
82
+ | Opener (first line) | Syntax | Closer |
83
+ |---|---|---|
84
+ | `---` | YAML subset (§3.1) | a line `---` or `...` |
85
+ | `---json` | JSON | a line `---` |
86
+ | `{` (first character) | JSON object | a line that is exactly `}` |
87
+ | `+++` | TOML (§3.3) | a line `+++` |
88
+
89
+ When the closing line is missing, the document has no frontmatter and
90
+ the opener is ordinary Markdown (a `---` first line followed by text is
91
+ a setext heading candidate, exactly as CommonMark treats it). Malformed
92
+ content *inside* a properly closed fence raises `MdFrontmatterError`.
93
+
94
+ ### 3.1 The YAML subset (normative limits)
95
+
96
+ The built-in parser handles, from scratch and dependency-free:
97
+
98
+ - **Scalars** — `null`/`Null`/`NULL`/`~`, `true`/`false` (with `True`
99
+ etc. capitalizations), decimal integers and floats (optional sign and
100
+ exponent), and plain strings. Anything else — including YAML 1.1
101
+ `yes`/`no`, sexagesimals, hex ints and timestamps — is a **string**.
102
+ - **Quoted strings** — double quotes with JSON-style escapes
103
+ (`\n \t \r \b \f \0 \\ \" \uXXXX`), single quotes with `''` → `'`.
104
+ - **Block maps and sequences** by indentation (spaces only). A sequence
105
+ MAY sit at the same indent as its parent key. `- key: value` opens an
106
+ inline map item.
107
+ - **Flow collections** — `[a, b]` and `{a: 1}`, nesting freely, spanning
108
+ multiple lines while brackets remain open.
109
+ - **Block scalars** — literal `|` and folded `>`, each with the `-`
110
+ chomp (drop the final newline). The `+` chomp and explicit indent
111
+ indicators are not supported.
112
+ - **Comments** — `#` to end of line, outside quotes.
113
+
114
+ Deliberately outside the subset: anchors/aliases (`&`/`*`), tags (`!!`),
115
+ multi-document streams (`---` separators inside the block), complex
116
+ (non-scalar) keys, and the `?` key indicator. Duplicate keys: last one
117
+ wins. A key named `__proto__` becomes an ordinary own property.
118
+
119
+ ### 3.2 JSON frontmatter
120
+
121
+ The `---json` fence body and the leading-`{` form are parsed with
122
+ `JSON.parse`. The leading-`{` form closes at the **first line that is
123
+ exactly `}`**; if that slice fails to parse the opener is ordinary
124
+ Markdown text, not an error.
125
+
126
+ ### 3.3 TOML frontmatter
127
+
128
+ The caller MAY inject a full TOML parser — `parseToml` from
129
+ [`@jarenjs/josl`](../../../packages/josl/README.md) is the intended companion —
130
+ through `options.toml`; the package itself stays dependency-free. The
131
+ built-in fallback subset handles `[table]`/`[[array-of-tables]]` headers
132
+ with dotted paths, bare/quoted/dotted keys, basic and literal strings,
133
+ integers (decimal/hex/octal/binary with `_`), floats, booleans, flow
134
+ arrays (multi-line), inline tables and comments. Datetimes and any other
135
+ unrecognized value are kept as verbatim **strings**; multi-line strings
136
+ are not supported.
137
+
138
+ ### 3.4 Frontmatter as ambient variables
139
+
140
+ When the AST flows through a JSLT/JTLT stylesheet or a query document,
141
+ frontmatter members bind as **externals**: the compiled document exposes
142
+ `externals()`, a flat object of the frontmatter's top-level members
143
+ (minus the engine-reserved names `root` and `path`), passed as the
144
+ second argument of a compiled transform — bodies reference them as
145
+ `"$title"`, `"$date"`, and so on.
146
+
147
+ ## 4. The node vocabulary
148
+
149
+ Every node is a plain object with a `"type"` string discriminator.
150
+ Container nodes hold their ordered content in `children`; literal nodes
151
+ hold text in `value`. Producers MUST NOT emit other container member
152
+ names; consumers MUST ignore unknown members on known types (so plugins
153
+ and future versions can annotate) and MUST pass unknown *types* through
154
+ unchanged where possible.
155
+
156
+ ### 4.1 Block nodes
157
+
158
+ | type | members | notes |
159
+ |---|---|---|
160
+ | `paragraph` | `children` | inline content |
161
+ | `heading` | `depth` (1–6), `children` | ATX and setext |
162
+ | `thematicBreak` | — | `***`, `---`, `___` |
163
+ | `blockquote` | `children` | block content |
164
+ | `list` | `ordered` (boolean), `start` (number or `null`), `tight` (boolean), `children` | children are `listItem`s |
165
+ | `listItem` | `checked` (`true`/`false`/`null`), `children` | `checked` non-null only for task-list items |
166
+ | `code` | `lang` (string or `null`), `meta` (string or `null`), `value` | fenced and indented; `lang` is the info string's first word, `meta` the rest |
167
+ | `html` | `value` | a raw HTML block, verbatim |
168
+ | `table` | `align` (array of `"left"`/`"right"`/`"center"`/`null`), `children` | children are `tableRow`s; the first row is the header |
169
+ | `tableRow` | `children` | children are `tableCell`s |
170
+ | `tableCell` | `children` | inline content |
171
+ | `footnoteDefinition` | `identifier`, `label`, `children` | GFM; block content, collected rather than rendered in place (§4.6) |
172
+
173
+ ### 4.2 Inline nodes
174
+
175
+ | type | members | notes |
176
+ |---|---|---|
177
+ | `text` | `value` | literal text, escapes resolved |
178
+ | `emphasis` | `children` | `*x*` / `_x_` |
179
+ | `strong` | `children` | `**x**` / `__x__` |
180
+ | `strikethrough` | `children` | `~~x~~` (GFM) |
181
+ | `link` | `url`, `title` (string or `null`), `children` | inline links and autolinks; `url` is verbatim, filtered at render (§4.3). A GFM literal autolink additionally carries `auto: true` and a RESOLVED `url` (§4.7) |
182
+ | `image` | `url`, `title` (string or `null`), `alt` (string) | `alt` is plain text; `url` as for `link` |
183
+ | `inlineCode` | `value` | backtick spans |
184
+ | `break` | — | hard break (two spaces or `\` before newline) |
185
+ | `softBreak` | — | an in-paragraph newline |
186
+ | `html` | `value` | a raw inline HTML span |
187
+ | `footnoteReference` | `identifier`, `label` | GFM; the citation mark (§4.6) |
188
+
189
+ ### 4.3 URL safety (normative)
190
+
191
+ A `link`/`image` `url` in the AST is **verbatim**: whatever the author
192
+ wrote, so that `toMarkdown` round-trips it (§5) and a transformation can
193
+ inspect or rewrite it. Filtering happens one step later, when the AST is
194
+ projected to vnodes.
195
+
196
+ An emitter MUST NOT write a `url` into an `href`/`src` when its scheme can
197
+ execute script (`javascript:`, `vbscript:`) or stand in for a document of
198
+ its own (`file:`, and `data:` other than a raster image type). On
199
+ rejection it MUST drop **only that attribute**, keeping the element and
200
+ its children, so no authored text is lost. A scheme-less relative
201
+ reference (`image.png`, `docs/guide.md`) is not a scheme and MUST pass.
202
+
203
+ The scheme MUST be read the way a browser reads it, not as a literal
204
+ prefix: ASCII whitespace and control characters inside it are ignored, so
205
+ a tab spliced into `javascript:` does not get a URL through.
206
+
207
+ A `url` that survives the filter MUST be **percent-encoded** for the
208
+ attribute: an authored destination may hold spaces, backslashes,
209
+ backticks or any non-ASCII character, and a browser resolves those
210
+ differently than the author wrote them. Characters carrying URL
211
+ structure (`/?:@&=+$,#`) stay literal, and an existing `%XX` is not
212
+ re-encoded. The AST still holds the destination verbatim — only the
213
+ attribute is encoded.
214
+
215
+ `mdToVnode` implements this with `sanitizeUrl` from
216
+ `@jarenjs/view/helpers`. `options.sanitizeUrl` replaces the policy
217
+ wholesale — `(url) => string | null` — which is how a host widens it for a
218
+ custom scheme in trusted content. **A plugin `render` bypasses the core
219
+ emitter entirely, so a plugin that writes its own `href`/`src` owns this
220
+ rule for the URLs it emits.**
221
+
222
+ ### 4.4 Plugin and custom nodes
223
+
224
+ A compiled-in plugin (see [PLUGINS.md](PLUGINS.md)) MAY emit nodes of
225
+ any `type` it declares — e.g. `{ "type": "mermaid", "value": "...",
226
+ "meta": null }`. For document interchange without a plugin vocabulary,
227
+ the generic escape hatch is:
228
+
229
+ ```json
230
+ { "type": "custom", "name": "callout", "data": { "kind": "warning" }, "children": [] }
231
+ ```
232
+
233
+ Consumers that do not know a type SHOULD fall back gracefully: the vnode
234
+ emitter renders unknown literal nodes as plain `pre` text and unknown
235
+ containers by their children; without the mermaid plugin, a
236
+ ` ```mermaid ` fence is just a `code` node with `lang: "mermaid"`.
237
+
238
+ ### 4.4a Raw HTML and the two emitters (normative)
239
+
240
+ An `html` node holds markup verbatim (§4.1, §4.2). What a consumer may do
241
+ with it depends on what the consumer emits, and this format recognizes
242
+ exactly two emission targets:
243
+
244
+ - a **vnode tree** (`mdToVnode`) has no representation for unbalanced or
245
+ unknown markup — a lone `</div>`, a never-closed `<span>` — because a
246
+ tree node is an element or it is nothing. An emitter to vnodes MUST
247
+ therefore drop (`html: 'skip'`, the default), show as literal text
248
+ (`'text'`), or PARSE through an allow-list (`'vnode'`). It MUST NOT
249
+ have a mode that emits author markup unescaped, and that impossibility
250
+ — not a filtering promise — is what makes the vnode path safe for
251
+ Markdown a host did not write;
252
+ - a **string** (`toHtml`) can hold any byte sequence. A string emitter
253
+ MUST offer `'escape'` (the default: the markup is shown, escaped),
254
+ `'skip'` (parity with the vnode default) and `'raw'` (verbatim). `'raw'`
255
+ is the mode CommonMark specifies and the mode a conformance scorecard
256
+ measures; it MUST be per-call, MUST NOT be the default, and MUST be
257
+ documented as trusted-input-only.
258
+
259
+ The URL rule of §4.3 is **orthogonal to all of this** and applies in
260
+ every mode of both emitters: `'raw'` states that a document's HTML blocks
261
+ are trusted, not that its markdown links are.
262
+
263
+ Neither emitter may be implemented in terms of the other: a string
264
+ emitter built on vnodes inherits the tree's structural limit, and a vnode
265
+ emitter built on strings would have to re-parse its own output. For every
266
+ document whose markup a vnode CAN express, the two MUST agree
267
+ byte-for-byte, which is the property that keeps them one dialect with two
268
+ targets rather than two dialects.
269
+
270
+ ### 4.5 Heading identifiers (normative)
271
+
272
+ A `heading` node carries no identifier: an `id` is a *rendering*
273
+ decision, so the AST stays the text the author wrote and two emitters can
274
+ disagree about anchors without disagreeing about the document.
275
+
276
+ An emitter that offers heading ids MUST compute one from the heading's
277
+ plain text (`textOf`, §4.2 — image `alt` counts, formatting does not) by
278
+ this algorithm, which is GitHub's, so a document anchors identically on
279
+ GitHub, in an editor preview and in this renderer:
280
+
281
+ 1. lower-case the text;
282
+ 2. drop every character that is not a letter, a digit, a combining mark,
283
+ `-` or `_` — so punctuation, symbols (`§`, `—`) and emoji go, while
284
+ non-ASCII letters and digits stay;
285
+ 3. replace each remaining whitespace character with `-`, one for one (a
286
+ run of two spaces yields `--`).
287
+
288
+ The result MAY be empty (`## ***`); an emitter MUST then substitute
289
+ `section`, because a heading with no landing place cannot be linked.
290
+ Within one document, the **second** heading yielding a given identifier
291
+ MUST get `-1` appended, the third `-2`, and so on, counted over the
292
+ substituted value so `***` twice yields `section` and `section-1`. The
293
+ counter is per emission and MUST NOT be shared with any other numbering
294
+ in the emitter.
295
+
296
+ An emitter MUST offer a prefix (`slugPrefix`) prepended to every emitted
297
+ identifier and to every anchor href it writes, and its default MUST be
298
+ empty. A host rendering a document it did not author into a page it owns
299
+ sets the prefix (GitHub's own answer is `user-content-`) so an author
300
+ cannot mint an identifier that collides with the host's own DOM.
301
+
302
+ Emitting ids MUST be opt-in and off by default: CommonMark renders a
303
+ heading as `<h1>Foo</h1>`, and a default that adds an attribute would put
304
+ this package's conformance score at odds with the documents it produces.
305
+
306
+ `mdToVnode` implements this as `headingIds`, `slugPrefix` and
307
+ `headingAnchors`; the slug transform itself is `slugify` from
308
+ `@jarenjs/core/string`, the suite's only one.
309
+
310
+ ### 4.6 Footnotes (normative)
311
+
312
+ Footnotes are a GFM extension the GFM *specification* never described —
313
+ GitHub ships them, the spec has no section for them — so their behaviour
314
+ is pinned here, matching GitHub's rendering.
315
+
316
+ **Two node types.** A producer MUST emit a `footnoteDefinition` for
317
+ `[^label]: …` and a `footnoteReference` for `[^label]`, both carrying an
318
+ `identifier` (the label under the normalization of §Link reference
319
+ definitions — trim, collapse whitespace, case fold) and a `label` (the
320
+ text as written). Both are recognized only when `gfm` is on.
321
+
322
+ **One label grammar, both sides.** A label is `[^` followed by one or
323
+ more characters that are not `]`, `[` or whitespace, then `]`. The same
324
+ grammar decides a definition and a reference, so `[^my note]` is neither
325
+ rather than one without the other.
326
+
327
+ **A definition is a container block**, not a leading definition like
328
+ `[foo]:`. It therefore MAY interrupt a paragraph, its continuation lines
329
+ are indented four columns, it takes lazy continuation, and — like a list
330
+ item — a blank line ends it while it is still empty. A producer MUST
331
+ record only the FIRST definition of an identifier.
332
+
333
+ **A definition stays where it was written.** A producer MUST NOT hoist
334
+ definitions: the AST is the document, and moving them would make
335
+ `toMarkdown` print a document the author did not write. Collecting them
336
+ is the *emitter's* job (§4.4a: what a consumer may do depends on what it
337
+ emits).
338
+
339
+ An emitter that renders footnotes MUST:
340
+
341
+ 1. **number by first reference** — not by definition order, not by label.
342
+ References inside a rendered footnote count, and are numbered after
343
+ the references in the document body;
344
+ 2. **render nothing for an uncited definition**, in place or at the end;
345
+ 3. **render a reference with no definition as the literal text it was
346
+ written as** (`[^nope]`), never as a link to a missing anchor. (A
347
+ parser cannot produce such a node — the inline rule requires a
348
+ definition — but a transform can.);
349
+ 4. **terminate on a cycle.** A footnote MAY cite another, itself
350
+ included; an emitter MUST render each definition at most once, which
351
+ makes termination a property of the algorithm rather than a depth
352
+ limit;
353
+ 5. **give each citation its own identifier**, so a footnote cited *n*
354
+ times has *n* landing places and *n* back-references. The
355
+ identifiers are `<prefix>fn-<number>` for the definition and
356
+ `<prefix>fnref-<number>` for the first citation, `-2`, `-3` … for the
357
+ rest;
358
+ 6. **default the prefix to `user-content-`**, and use `slugPrefix` when
359
+ it is given. This differs from §4.5 on purpose: a heading id is
360
+ opt-in and asserted bare by CommonMark, while a footnote id is
361
+ emitted by the default rendering of a feature that is *nothing but* a
362
+ link between two places on one page. No corpus example asserts a bare
363
+ `fn-1`, so the safe default costs nothing;
364
+ 7. **append one section after the last block**, inside whatever fragment
365
+ or wrapper it is producing. A consumer concatenating fragments
366
+ receives one footnotes section per fragment, which is the only
367
+ placement a fragment emitter can offer.
368
+
369
+ ### 4.7 Literal autolinks (normative)
370
+
371
+ With `gfm` on, a producer MUST recognize GFM's extended autolinks — bare
372
+ `www.…`, `http://…`, `https://…`, `ftp://…` and email addresses — under
373
+ the grammar of GFM §Autolinks (extension), including:
374
+
375
+ - a match MAY begin only at the start of the text, after whitespace, or
376
+ after one of `*`, `_`, `~`, `(`. The start of a `text` node counts:
377
+ what precedes it is a sibling node, not a character;
378
+ - matching is **case-sensitive**. `WWW.EXAMPLE.COM` is not a link, here
379
+ or on GitHub;
380
+ - trailing `?`, `!`, `.`, `,`, `:`, `*`, `_`, `~` are excluded from the
381
+ link, though they MAY appear in its interior;
382
+ - a trailing `)` is excluded only while the link holds more `)` than
383
+ `(`, so `(www.a.test/x)` links `www.a.test/x` and
384
+ `www.a.test/x(y)` links all of it;
385
+ - a trailing `;` is excluded only as part of an entity-shaped tail
386
+ (`&` + alphanumerics + `;`), and then the whole tail goes, not the
387
+ semicolon;
388
+ - a `<` ends the link at that character.
389
+
390
+ **The node type is `link`.** A literal autolink IS a link, and inventing
391
+ a second type would make every consumer, plugin and schema learn two
392
+ spellings of one thing. The `url` holds the RESOLVED destination — a
393
+ `www.` link gets `http://`, an address gets `mailto:` — so no consumer
394
+ re-derives it. A producer MUST set `auto: true` on such a node; exactly
395
+ one consumer reads it, the canonical printer, which prints the link back
396
+ bare (§5). A consumer that does not know the flag renders a correct
397
+ link, which is why it is a flag and not a type.
398
+
399
+ Recognition happens **after** inline parsing, over `text` nodes, and MUST
400
+ NOT descend into a `link` — links do not nest. Consequently a literal
401
+ autolink is never found inside a code span, raw HTML or link text, and
402
+ the entity rule above is meaningful: by then `&copy;` has become `©`, and
403
+ the only `&…;` left to exclude is one that was never an entity.
404
+
405
+ ### 4.8 Directives (normative)
406
+
407
+ A **directive** is a value in a document that a machine derives and a
408
+ human reads:
409
+
410
+ ```markdown
411
+ Jaren is <!--bm:jsonpath.ctsRatio-->23.1<!--/bm-->x faster on the CTS mean.
412
+ ```
413
+
414
+ The carrier is an HTML comment, and the choice is the whole design:
415
+ every markdown renderer drops comments, so a directive is invisible on
416
+ GitHub, in an editor preview and on npm, while the baked text between the
417
+ markers stays readable, correct and static.
418
+
419
+ - A directive is `<!--<ns>:<payload>-->` … `<!--/<ns>-->`. `ns` matches
420
+ `[a-z][a-z0-9-]*`.
421
+ - **The payload is opaque.** This format assigns it no meaning: `bm` puts
422
+ a derivation key there, `mdx` puts a query expression. A consumer owns
423
+ its own vocabulary, and a producer MUST NOT interpret another
424
+ namespace's payload.
425
+ - **Block scope** is an opener and closer that are their own `html` block
426
+ nodes, with block nodes between them. **Inline scope** is all three
427
+ inside one inline container. Markers pair **within one children array**:
428
+ an opener inside `**bold**` and a closer outside it are two unpaired
429
+ markers, not one directive.
430
+ - **Unpaired markers MUST be reported, not dropped.** An opener with no
431
+ closer, a stray closer, and same-`ns` nesting are each diagnostics. A
432
+ marker nobody matched is how a stale figure hides.
433
+ - A consumer MUST NOT require directive support to read the document:
434
+ with the markers ignored, the text between them is the document's
435
+ content, and that is what every renderer shows.
436
+
437
+ **An inline marker MUST NOT begin a line.** A comment at the start of a
438
+ line opens a CommonMark HTML block (§HTML blocks, type 2), which consumes
439
+ the rest of that line — so the marker, its value *and the prose after it*
440
+ leave the document. This is a property of CommonMark, not of this
441
+ format, and it applies to every renderer including GitHub's. Put text
442
+ before the marker, or give the directive a block of its own.
443
+
444
+ Writing a directive's current value into the source is **baking**. It is
445
+ a build-time operation on trusted input, and it differs from mdx
446
+ interpolation on exactly one axis that matters: a baked body is spliced
447
+ into the source and WILL be re-parsed as markdown (a whole table is a
448
+ legitimate body), whereas an interpolated value lands in a `text` node
449
+ and is never re-read. A baker MUST rewrite only the spans between
450
+ markers — a document with no directives MUST come back byte-identical —
451
+ because canonical re-printing (§5) would reformat every hand-written
452
+ document it touched.
453
+
454
+ ## 5. Canonical Markdown and round-trips
455
+
456
+ `toMarkdown(doc)` prints **canonical Markdown**: ATX headings, `-`
457
+ bullets, `1.` ordered markers (renumbered from `start`), fenced code
458
+ with backticks, `*emphasis*`/`**strong**`, reference-free inline links,
459
+ `|`-piped tables with an alignment row, and blank lines between blocks.
460
+ A footnote definition prints where it stands, its later blocks indented
461
+ four columns; a literal autolink prints bare, escaped so that a `_` or
462
+ `&` in a destination survives the trip back.
463
+
464
+ The normative round-trip guarantees:
465
+
466
+ 1. `parseMarkdown(toMarkdown(doc))` produces an AST deep-equal to
467
+ `doc.ast` for any document this package produced, and printing that
468
+ AST again reproduces the same text (canonical form is a fixed
469
+ point). Both halves are checked over the whole CommonMark example
470
+ corpus, not a sample: a printer that cannot represent a construct it
471
+ just parsed — an autolink, a heading carrying a soft break, two
472
+ adjacent lists, a text run holding a literal newline — shows up
473
+ there and nowhere else. The same check runs over the GFM
474
+ specification's 672 examples with the extensions ON, which is the
475
+ only place tables, task lists, strikethrough, footnotes and literal
476
+ autolinks are printed at corpus scale;
477
+ 2. a document produced by a JTLT stylesheet parses into an AST that an
478
+ identity JSLT stylesheet (`[]`) maps back — by reference — to the
479
+ same AST, which prints to the canonical equivalent of the JTLT
480
+ output.
481
+
482
+ ## 6. Structural sharing (normative)
483
+
484
+ The AST is designed for the suite's reference-equality fast paths:
485
+
486
+ - a parser MUST return a fresh document per call but MUST NOT mutate a
487
+ document after returning it — consumers MAY freeze, cache and share;
488
+ - an identity JSLT transform of a document returns the input by
489
+ reference (the engine's proof-of-no-change sharing), and any partial
490
+ transform keeps unmatched subtrees `===` — the vnode emitter's
491
+ per-block cache and the view patcher's `oldVnode === newVnode` check
492
+ then skip unchanged blocks in O(1);
493
+ - the vnode emitter MUST key block vnodes by content hash so the keyed
494
+ patcher reorders instead of rebuilding when blocks move.
495
+
496
+ ## 7. The JSON Schema
497
+
498
+ `schemas/jaren-md-ast.schema.json` (draft 2020-12,
499
+ `$id: https://jarenjs.dev/schemas/jaren-md-ast/0.1`) publishes this
500
+ vocabulary for validators and LLM structured output, in the same spirit
501
+ as the [query and JSLT grammar twins](../../../packages/json/README.md). The test
502
+ suite validates parser output against it with `@jarenjs/validate`.