@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.
- package/README.md +520 -0
- package/dist/types/ast.d.ts +181 -0
- package/dist/types/bake.d.ts +61 -0
- package/dist/types/compiler.d.ts +141 -0
- package/dist/types/component/index.d.ts +101 -0
- package/dist/types/directives.d.ts +126 -0
- package/dist/types/entities.d.ts +40 -0
- package/dist/types/footnotes.d.ts +83 -0
- package/dist/types/frontmatter.d.ts +67 -0
- package/dist/types/html.d.ts +72 -0
- package/dist/types/index.d.ts +30 -0
- package/dist/types/loader.d.ts +84 -0
- package/dist/types/mdx.d.ts +45 -0
- package/dist/types/parser.d.ts +116 -0
- package/dist/types/plugins/highlight.d.ts +64 -0
- package/dist/types/plugins/index.d.ts +64 -0
- package/dist/types/plugins/mermaid.d.ts +12 -0
- package/dist/types/scanner.d.ts +240 -0
- package/dist/types/to-html.d.ts +104 -0
- package/dist/types/to-md.d.ts +23 -0
- package/dist/types/to-vnode.d.ts +161 -0
- package/dist/types/utils.d.ts +63 -0
- package/docs/LOADER.md +92 -0
- package/docs/MD-FORMAT.md +502 -0
- package/docs/PLUGINS.md +277 -0
- package/package.json +80 -0
- package/schemas/jaren-md-ast.schema.json +296 -0
- package/src/ast.js +346 -0
- package/src/bake.js +104 -0
- package/src/compiler.js +167 -0
- package/src/component/index.js +191 -0
- package/src/directives.js +371 -0
- package/src/entities.js +107 -0
- package/src/footnotes.js +180 -0
- package/src/frontmatter.js +947 -0
- package/src/html.js +281 -0
- package/src/index.js +76 -0
- package/src/loader.js +0 -0
- package/src/mdx.js +219 -0
- package/src/parser.js +1685 -0
- package/src/plugins/highlight.js +325 -0
- package/src/plugins/index.js +75 -0
- package/src/plugins/mermaid.js +14 -0
- package/src/scanner.js +832 -0
- package/src/to-html.js +425 -0
- package/src/to-md.js +396 -0
- package/src/to-vnode.js +766 -0
- package/src/utils.js +107 -0
- 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 `©` 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`.
|