@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,277 @@
1
+ # The Jaren Markdown Plugin Contract
2
+
3
+ **Version 0.1 — Specification**
4
+
5
+ Plugins are the extensibility story of `@jarenjs/md`, and they follow
6
+ the package's one rule: **everything is declared up front and baked into
7
+ compiled closures**. There is no runtime registration, no mutation of a
8
+ live parser, no dynamic lookup on the hot path. A plugin is data; the
9
+ compiler turns it into table entries.
10
+
11
+ ## 1. The plugin object
12
+
13
+ A plugin is a plain frozen object created by `definePlugin(spec)`:
14
+
15
+ ```js
16
+ definePlugin({
17
+ name: 'mermaid', // unique, kebab-case (required)
18
+ fences: ['mermaid'], // fenced-code claims by info-string first word
19
+ blocks: [ /* block rule descriptors, §3 */ ],
20
+ inlines: [ /* inline rule descriptors, §4 */ ],
21
+ node: 'mermaid', // the AST type this plugin emits
22
+ render: (node, h, ctx) => vnode, // pure, synchronous vnode renderer
23
+ toHtml: (node, ctx) => '<figure>…', // pure HTML-string renderer, for toHtml
24
+ hydrate: async (el, node, ctx) => {}, // optional browser-only upgrade
25
+ });
26
+ ```
27
+
28
+ `definePlugin` validates the shape (name present and kebab-case, rule
29
+ descriptors well-formed, `render` and `toHtml` functions when present)
30
+ and returns `Object.freeze`d data. Every member except `name` is
31
+ optional.
32
+
33
+ Plugins are passed as `options.plugins: MdPlugin[]` to `parseMarkdown`,
34
+ `compileMarkdown` and `loadMarkdown`. At compile time they merge into
35
+ six prebuilt tables:
36
+
37
+ | table | indexed by | consulted |
38
+ |---|---|---|
39
+ | fence claims | info-string first word | when a fenced code block closes |
40
+ | block starts | first non-space character | once per unclaimed line |
41
+ | inline scans | trigger character | from the inline scanner's dispatch |
42
+ | renders | AST `type` | by the vnode emitter |
43
+ | html renderers | AST `type` | by the string emitter (`toHtml`) |
44
+ | hydrators | AST `type` | by `createMdRenderer` after mount |
45
+
46
+ All six are built once per compile; the hot loops do only indexed
47
+ lookups. Two plugins claiming the same fence word, block character
48
+ *and* matching the same line, inline character, or node type: the
49
+ **first plugin in the array wins** (deterministic, documented, no
50
+ merging magic).
51
+
52
+ ## 2. Fence claims
53
+
54
+ `fences: ['mermaid']` claims fenced code blocks whose info-string first
55
+ word matches. The block is scanned exactly like a code fence (the
56
+ scanner owns fence semantics — nesting, indentation, closing rules);
57
+ only the resulting node changes:
58
+
59
+ ```
60
+ ```mermaid → { type: 'mermaid', value: 'graph TD; …', meta: null }
61
+ graph TD; A-->B
62
+ ```
63
+ ```
64
+
65
+ A claimed fence MUST produce `{ type: plugin.node, value, meta }` where
66
+ `meta` is the info string after the first word (or `null`). Without the
67
+ plugin, the same source is a plain `code` node with `lang: 'mermaid'` —
68
+ **degradation is always graceful**.
69
+
70
+ ## 3. Block rule descriptors
71
+
72
+ For syntax that is not a fence (callout blocks, directives, sidenotes):
73
+
74
+ ```js
75
+ {
76
+ chars: ':', // trigger characters (first non-space char)
77
+ start: (line, ctx) => node|null, // claim the line: return a fresh node or null
78
+ continue: (node, line, ctx) => boolean, // does this line still belong?
79
+ close: (node, ctx) => void, // finalize (parse buffered text, etc.)
80
+ }
81
+ ```
82
+
83
+ - `start` runs only when the line's first non-space character is in
84
+ `chars` and no core construct with higher precedence claimed it. It
85
+ MUST return a new AST node (which the parser appends to the current
86
+ container) or `null` to decline.
87
+ - `continue` runs per subsequent line while the block is open. Return
88
+ `true` to consume the line and stay open, `'end'` to consume the line
89
+ *and* close (a closing fence), or `false` to close without consuming
90
+ — the line then re-processes as a fresh block start.
91
+ - `close` runs once when the block closes (end of input included).
92
+ - `ctx` is `{ frontmatter, options }` — read-only compile context.
93
+
94
+ ## 4. Inline rule descriptors
95
+
96
+ ```js
97
+ {
98
+ char: '$', // single trigger character
99
+ scan: (src, pos, ctx) => ({ node, end }) | null,
100
+ }
101
+ ```
102
+
103
+ `scan` is called when the inline scanner meets `char` at `pos` in the
104
+ raw inline text `src`. Return the inline AST node and the exclusive end
105
+ offset, or `null` to decline (the character then flows into plain text).
106
+ `scan` MUST NOT look behind `pos` except to inspect delimiter context
107
+ and MUST NOT allocate on the decline path.
108
+
109
+ ## 5. Rendering and hydration
110
+
111
+ `render(node, h, ctx) => vnode` maps the plugin's node type to a
112
+ [view vnode](../../../packages/view/docs/VIEW-FORMAT.md). It MUST be pure and
113
+ synchronous — SSR through `renderToString` and the DOM patcher both
114
+ call it, and its output for equal input SHOULD be reference-equal
115
+ (cache by `node` reference or content hash) so re-patches hit the O(1)
116
+ fast path. `h` is `@jarenjs/view`'s element constructor; `ctx` carries
117
+ `{ options, hash, sanitizeUrl }` where `hash(str)` is the package content
118
+ hash.
119
+
120
+ A plugin `render` **shadows the core emitter for its node type**, so the
121
+ core's URL filtering does not run for it. A plugin that writes an
122
+ `href`/`src` from document content MUST put it through
123
+ `ctx.sanitizeUrl(url)` — the active policy, host override included — and
124
+ drop the attribute when it returns `null` (MD-FORMAT §4.3). A URL the
125
+ plugin composes itself from a trusted constant needs no filtering.
126
+
127
+ ### 5.1 Rendering to a string
128
+
129
+ `toHtml(node, ctx) => string` is the same contract for the string
130
+ emitter: pure, synchronous, and shadowing the core emitter for its node
131
+ type — so the URL rule above applies unchanged, and a plugin that writes
132
+ markup MUST escape document content itself (`escapeText`/`escapeAttribute`
133
+ from `@jarenjs/view`). `ctx` is the emission context: `{ options, html,
134
+ sanitizeUrl, headingIds, slugPrefix, headingAnchors }`, where `html` is
135
+ the raw-HTML policy in force.
136
+
137
+ The two hooks are independent. A plugin MAY provide either, and the
138
+ emitters degrade differently when one is missing:
139
+
140
+ - no `toHtml`, and the node type is one the CORE emitter knows (the
141
+ highlight plugin claims `code`): the core emitter prints it — the
142
+ content is never lost, it simply arrives without the plugin's
143
+ decoration;
144
+ - no `toHtml`, and the type is the plugin's own (`mermaid`): `toHtml`
145
+ emits `<!-- unsupported plugin node: <type> -->`. The gap is SHOWN,
146
+ because a silently dropped diagram looks like a document that never
147
+ had one.
148
+
149
+ Anything asynchronous or DOM-dependent goes in
150
+ `hydrate(el, node, ctx)`, which `createMdRenderer` invokes **after**
151
+ the patcher mounts the element. A hydratable render marks its root element with
152
+ `'data-md-hydrate': plugin.name` and `'data-md-hash': contentHash`; the
153
+ renderer finds marked elements, skips those whose hash it already
154
+ hydrated, and calls the plugin. `hydrate` MAY be async; failures are
155
+ contained per element (reported through `options.onHydrateError`,
156
+ default `console.error`).
157
+
158
+ ## 6. The reference plugins
159
+
160
+ Both ship from `@jarenjs/md/plugins` and are the canonical templates
161
+ for third-party plugins (math, callouts/admonitions, embeds). GFM
162
+ footnotes are NOT a plugin — they are part of the dialect
163
+ ([MD-FORMAT.md](MD-FORMAT.md) §4.6), gated on `gfm` like tables are.
164
+
165
+ ### 6.1 mermaidPlugin({ theme })
166
+
167
+ The **native** plugin, re-exported from `@jarenjs/mermaid/plugin`. It
168
+ parses the fence source with the in-house headless Mermaid engine and
169
+ emits **pure-vnode SVG** synchronously — no injected `mermaid` instance,
170
+ no CDN global, no `innerHTML`.
171
+
172
+ - `render` claims `mermaid`/`mmd` fences and returns
173
+ `div.md-mermaid.mermaid-block > svg`, keyed by content hash. Because it
174
+ is pure and synchronous, a Markdown document containing a `mermaid`
175
+ fence renders to a full SVG string through **SSR with no browser**.
176
+ Text and attribute values are escaped by the view serializer, and
177
+ only `http(s)`/relative link `href`s survive, so there is no
178
+ raw-`innerHTML` injection surface.
179
+ - There is **no `hydrate`** — the render is already complete.
180
+ Optional client-only enhancements (pan/zoom) are reserved for a future
181
+ interactivity plugin.
182
+ - The dependency arrow is **md → mermaid**:
183
+ `@jarenjs/mermaid/plugin` returns a self-frozen `MdPlugin`-shaped
184
+ object *without* importing `definePlugin`, so there is no cycle;
185
+ `@jarenjs/md` re-exports it and adds `@jarenjs/mermaid` to its
186
+ dependencies. Consumers who never use it tree-shake it away
187
+ (`sideEffects:false`). Mermaid stays **opt-in** — it is not in
188
+ `DEFAULT_PLUGINS`.
189
+
190
+ **Transformed-diagram round-trip.** There is no per-plugin `toMarkdown`
191
+ hook; a `mermaid` fence round-trips through `toMarkdown` generically as
192
+ long as its source lives in `node.value` (which it does). So a JSLT
193
+ transform that *rewrites* a diagram must refresh the fence source with
194
+ the canonical printer. `@jarenjs/mermaid/plugin` exports the primitive
195
+ for exactly this:
196
+
197
+ ```js
198
+ import { parseMermaid, toMermaid } from '@jarenjs/mermaid';
199
+ import { refreshMermaidFence } from '@jarenjs/mermaid/plugin';
200
+
201
+ const doc = parseMermaid(fenceNode.value);
202
+ const edited = transform(doc); // JSLT / hand edit of the AST
203
+ const fresh = refreshMermaidFence(fenceNode, edited); // { type:'mermaid', value: toMermaid(edited) }
204
+ ```
205
+
206
+ The refreshed `value` re-emits through the core fence printer
207
+ (`to-md.js`), so `toMarkdown` prints the new diagram.
208
+
209
+ ### 6.2 highlightPlugin({ grammars, adapter })
210
+
211
+ Claims nothing — it **renders** `code` nodes, replacing the default
212
+ code renderer through the render table.
213
+
214
+ **Built-in mode**: a zero-dependency single-pass tokenizer driven by
215
+ compact grammar tables (keyword set, string/comment/number delimiters,
216
+ punctuation classes), compiled to closures once per compile. Shipped
217
+ grammars: `js`/`ts` (+`jsx`/`mjs`), `json`, `josl`/`toml`, `html`/`xml`,
218
+ `css`, `md`/`markdown`, `bash`/`sh`/`shell`.
219
+
220
+ **Adapter mode**: `adapter: (code, lang) => Token[] | null` plugs
221
+ shiki/prism/highlight.js behind the same token contract; `null` falls
222
+ back to the built-in grammar for that `lang`, then to plain text. The
223
+ adapter is captured at compile time, never re-registered.
224
+
225
+ **The token contract** (normative): a flat array of
226
+ `{ kind, value }` covering the source exactly, in order. Kinds and
227
+ their CSS classes:
228
+
229
+ | kind | class | meaning |
230
+ |---|---|---|
231
+ | `kw` | `tok-kw` | keyword |
232
+ | `str` | `tok-str` | string literal |
233
+ | `num` | `tok-num` | number literal |
234
+ | `com` | `tok-com` | comment |
235
+ | `pun` | `tok-pun` | punctuation/brackets |
236
+ | `id` | `tok-id` | identifier |
237
+ | `op` | `tok-op` | operator |
238
+ | `lit` | `tok-lit` | language literal (`true`, `null`, …) |
239
+
240
+ Tokens render as `<span class="tok-{kind}">` children of the `<code>`
241
+ element (plain `id` runs render as bare text nodes to keep the vnode
242
+ small); the fence language renders as `class="language-{lang}"` on the
243
+ `code` element, claimed fences aside.
244
+
245
+ ## 7. Writing your own (non-normative)
246
+
247
+ A callout plugin in full:
248
+
249
+ ```js
250
+ import { definePlugin } from '@jarenjs/md';
251
+
252
+ export const calloutPlugin = () => definePlugin({
253
+ name: 'callout',
254
+ node: 'callout',
255
+ blocks: [{
256
+ chars: ':',
257
+ start: (line) => {
258
+ const m = /^:::\s*(\w+)\s*$/.exec(line);
259
+ return m ? { type: 'callout', kind: m[1], lines: [] } : null;
260
+ },
261
+ continue: (node, line) => {
262
+ if (/^\s*:::\s*$/.test(line)) return 'end';
263
+ node.lines.push(line);
264
+ return true;
265
+ },
266
+ close: (node) => {
267
+ node.value = node.lines.join('\n');
268
+ delete node.lines;
269
+ },
270
+ }],
271
+ render: (node, h) =>
272
+ h('aside', { class: `md-callout md-callout-${node.kind}` }, node.value),
273
+ });
274
+ ```
275
+
276
+ Everything the core constructs get, a plugin gets: table dispatch, SSR
277
+ purity, graceful degradation, and content-hash keys.
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@jarenjs/md",
3
+ "private": false,
4
+ "version": "0.34.0",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./dist/types/index.d.ts",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/types/index.d.ts",
12
+ "default": "./src/index.js"
13
+ },
14
+ "./plugins": {
15
+ "types": "./dist/types/plugins/index.d.ts",
16
+ "default": "./src/plugins/index.js"
17
+ },
18
+ "./html": {
19
+ "types": "./dist/types/html.d.ts",
20
+ "default": "./src/html.js"
21
+ },
22
+ "./mdx": {
23
+ "types": "./dist/types/mdx.d.ts",
24
+ "default": "./src/mdx.js"
25
+ },
26
+ "./directives": {
27
+ "types": "./dist/types/directives.d.ts",
28
+ "default": "./src/directives.js"
29
+ },
30
+ "./component": {
31
+ "types": "./dist/types/component/index.d.ts",
32
+ "default": "./src/component/index.js"
33
+ },
34
+ "./styles/md.css": "./styles/md.css",
35
+ "./schemas/*": "./schemas/*",
36
+ "./package.json": "./package.json"
37
+ },
38
+ "files": [
39
+ "dist/types/",
40
+ "src/",
41
+ "docs/",
42
+ "schemas/",
43
+ "styles/"
44
+ ],
45
+ "description": "Markdown + frontmatter as JSON documents: a compile-to-closures parser whose AST feeds the Jaren query, JSLT, view and forms engines natively",
46
+ "author": "joham",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/jklarenbeek/jarenjs.git",
50
+ "directory": "components/md"
51
+ },
52
+ "license": "MIT",
53
+ "engines": {
54
+ "node": ">=24"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public",
58
+ "registry": "https://registry.npmjs.org/"
59
+ },
60
+ "keywords": [
61
+ "jaren",
62
+ "json",
63
+ "markdown",
64
+ "commonmark",
65
+ "gfm",
66
+ "frontmatter",
67
+ "ast",
68
+ "parser"
69
+ ],
70
+ "scripts": {
71
+ "build": "npm run build:types",
72
+ "build:types": "tsc -p tsconfig.json",
73
+ "prepack": "npm run build:types"
74
+ },
75
+ "dependencies": {
76
+ "@jarenjs/core": "^0.34.0",
77
+ "@jarenjs/mermaid": "^0.34.0",
78
+ "@jarenjs/view": "^0.34.0"
79
+ }
80
+ }
@@ -0,0 +1,296 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://jarenjs.dev/schemas/jaren-md-ast/0.1",
4
+ "title": "Jaren Markdown document",
5
+ "description": "The MdDocument envelope and AST node vocabulary of @jarenjs/md (normative prose in docs/MD-FORMAT.md). Core node types validate strictly; unknown types pass as extension nodes so compiled-in plugins can extend the vocabulary.",
6
+ "type": "object",
7
+ "required": ["$md", "frontmatter", "ast", "meta"],
8
+ "properties": {
9
+ "$md": { "const": "0.1" },
10
+ "frontmatter": true,
11
+ "ast": {
12
+ "type": "array",
13
+ "items": { "$ref": "#/$defs/blockNode" }
14
+ },
15
+ "meta": {
16
+ "type": "object",
17
+ "required": ["sourceUrl", "hash"],
18
+ "properties": {
19
+ "sourceUrl": { "type": ["string", "null"] },
20
+ "hash": { "type": "string" },
21
+ "frontmatterLang": { "enum": ["yaml", "json", "toml", null] }
22
+ }
23
+ }
24
+ },
25
+ "$defs": {
26
+ "blockNode": {
27
+ "anyOf": [
28
+ { "$ref": "#/$defs/paragraph" },
29
+ { "$ref": "#/$defs/heading" },
30
+ { "$ref": "#/$defs/thematicBreak" },
31
+ { "$ref": "#/$defs/blockquote" },
32
+ { "$ref": "#/$defs/list" },
33
+ { "$ref": "#/$defs/code" },
34
+ { "$ref": "#/$defs/html" },
35
+ { "$ref": "#/$defs/table" },
36
+ { "$ref": "#/$defs/footnoteDefinition" },
37
+ { "$ref": "#/$defs/custom" },
38
+ { "$ref": "#/$defs/extensionNode" }
39
+ ]
40
+ },
41
+ "inlineNode": {
42
+ "anyOf": [
43
+ { "$ref": "#/$defs/text" },
44
+ { "$ref": "#/$defs/emphasis" },
45
+ { "$ref": "#/$defs/strong" },
46
+ { "$ref": "#/$defs/strikethrough" },
47
+ { "$ref": "#/$defs/link" },
48
+ { "$ref": "#/$defs/image" },
49
+ { "$ref": "#/$defs/inlineCode" },
50
+ { "$ref": "#/$defs/break" },
51
+ { "$ref": "#/$defs/softBreak" },
52
+ { "$ref": "#/$defs/html" },
53
+ { "$ref": "#/$defs/footnoteReference" },
54
+ { "$ref": "#/$defs/custom" },
55
+ { "$ref": "#/$defs/extensionNode" }
56
+ ]
57
+ },
58
+ "inlineChildren": {
59
+ "type": "array",
60
+ "items": { "$ref": "#/$defs/inlineNode" }
61
+ },
62
+ "blockChildren": {
63
+ "type": "array",
64
+ "items": { "$ref": "#/$defs/blockNode" }
65
+ },
66
+ "paragraph": {
67
+ "type": "object",
68
+ "required": ["type", "children"],
69
+ "properties": {
70
+ "type": { "const": "paragraph" },
71
+ "children": { "$ref": "#/$defs/inlineChildren" }
72
+ }
73
+ },
74
+ "heading": {
75
+ "type": "object",
76
+ "required": ["type", "depth", "children"],
77
+ "properties": {
78
+ "type": { "const": "heading" },
79
+ "depth": { "type": "integer", "minimum": 1, "maximum": 6 },
80
+ "children": { "$ref": "#/$defs/inlineChildren" }
81
+ }
82
+ },
83
+ "thematicBreak": {
84
+ "type": "object",
85
+ "required": ["type"],
86
+ "properties": { "type": { "const": "thematicBreak" } }
87
+ },
88
+ "blockquote": {
89
+ "type": "object",
90
+ "required": ["type", "children"],
91
+ "properties": {
92
+ "type": { "const": "blockquote" },
93
+ "children": { "$ref": "#/$defs/blockChildren" }
94
+ }
95
+ },
96
+ "list": {
97
+ "type": "object",
98
+ "required": ["type", "ordered", "start", "tight", "children"],
99
+ "properties": {
100
+ "type": { "const": "list" },
101
+ "ordered": { "type": "boolean" },
102
+ "start": { "type": ["integer", "null"] },
103
+ "tight": { "type": "boolean" },
104
+ "children": {
105
+ "type": "array",
106
+ "items": { "$ref": "#/$defs/listItem" }
107
+ }
108
+ }
109
+ },
110
+ "listItem": {
111
+ "type": "object",
112
+ "required": ["type", "checked", "children"],
113
+ "properties": {
114
+ "type": { "const": "listItem" },
115
+ "checked": { "type": ["boolean", "null"] },
116
+ "children": { "$ref": "#/$defs/blockChildren" }
117
+ }
118
+ },
119
+ "code": {
120
+ "type": "object",
121
+ "required": ["type", "lang", "meta", "value"],
122
+ "properties": {
123
+ "type": { "const": "code" },
124
+ "lang": { "type": ["string", "null"] },
125
+ "meta": { "type": ["string", "null"] },
126
+ "value": { "type": "string" }
127
+ }
128
+ },
129
+ "html": {
130
+ "type": "object",
131
+ "required": ["type", "value"],
132
+ "properties": {
133
+ "type": { "const": "html" },
134
+ "value": { "type": "string" }
135
+ }
136
+ },
137
+ "table": {
138
+ "type": "object",
139
+ "required": ["type", "align", "children"],
140
+ "properties": {
141
+ "type": { "const": "table" },
142
+ "align": {
143
+ "type": "array",
144
+ "items": { "enum": ["left", "right", "center", null] }
145
+ },
146
+ "children": {
147
+ "type": "array",
148
+ "items": { "$ref": "#/$defs/tableRow" }
149
+ }
150
+ }
151
+ },
152
+ "tableRow": {
153
+ "type": "object",
154
+ "required": ["type", "children"],
155
+ "properties": {
156
+ "type": { "const": "tableRow" },
157
+ "children": {
158
+ "type": "array",
159
+ "items": { "$ref": "#/$defs/tableCell" }
160
+ }
161
+ }
162
+ },
163
+ "tableCell": {
164
+ "type": "object",
165
+ "required": ["type", "children"],
166
+ "properties": {
167
+ "type": { "const": "tableCell" },
168
+ "children": { "$ref": "#/$defs/inlineChildren" }
169
+ }
170
+ },
171
+ "text": {
172
+ "type": "object",
173
+ "required": ["type", "value"],
174
+ "properties": {
175
+ "type": { "const": "text" },
176
+ "value": { "type": "string" }
177
+ }
178
+ },
179
+ "emphasis": {
180
+ "type": "object",
181
+ "required": ["type", "children"],
182
+ "properties": {
183
+ "type": { "const": "emphasis" },
184
+ "children": { "$ref": "#/$defs/inlineChildren" }
185
+ }
186
+ },
187
+ "strong": {
188
+ "type": "object",
189
+ "required": ["type", "children"],
190
+ "properties": {
191
+ "type": { "const": "strong" },
192
+ "children": { "$ref": "#/$defs/inlineChildren" }
193
+ }
194
+ },
195
+ "strikethrough": {
196
+ "type": "object",
197
+ "required": ["type", "children"],
198
+ "properties": {
199
+ "type": { "const": "strikethrough" },
200
+ "children": { "$ref": "#/$defs/inlineChildren" }
201
+ }
202
+ },
203
+ "link": {
204
+ "type": "object",
205
+ "required": ["type", "url", "title", "children"],
206
+ "properties": {
207
+ "type": { "const": "link" },
208
+ "url": { "type": "string" },
209
+ "title": { "type": ["string", "null"] },
210
+ "auto": {
211
+ "type": "boolean",
212
+ "description": "present and true on a GFM literal autolink (bare www./http:///email); the canonical printer prints such a link back bare, and nothing else reads it"
213
+ },
214
+ "children": { "$ref": "#/$defs/inlineChildren" }
215
+ }
216
+ },
217
+ "footnoteDefinition": {
218
+ "type": "object",
219
+ "required": ["type", "identifier", "label", "children"],
220
+ "properties": {
221
+ "type": { "const": "footnoteDefinition" },
222
+ "identifier": { "type": "string" },
223
+ "label": { "type": "string" },
224
+ "children": { "$ref": "#/$defs/blockChildren" }
225
+ }
226
+ },
227
+ "footnoteReference": {
228
+ "type": "object",
229
+ "required": ["type", "identifier", "label"],
230
+ "properties": {
231
+ "type": { "const": "footnoteReference" },
232
+ "identifier": { "type": "string" },
233
+ "label": { "type": "string" }
234
+ }
235
+ },
236
+ "image": {
237
+ "type": "object",
238
+ "required": ["type", "url", "title", "alt"],
239
+ "properties": {
240
+ "type": { "const": "image" },
241
+ "url": { "type": "string" },
242
+ "title": { "type": ["string", "null"] },
243
+ "alt": { "type": "string" }
244
+ }
245
+ },
246
+ "inlineCode": {
247
+ "type": "object",
248
+ "required": ["type", "value"],
249
+ "properties": {
250
+ "type": { "const": "inlineCode" },
251
+ "value": { "type": "string" }
252
+ }
253
+ },
254
+ "break": {
255
+ "type": "object",
256
+ "required": ["type"],
257
+ "properties": { "type": { "const": "break" } }
258
+ },
259
+ "softBreak": {
260
+ "type": "object",
261
+ "required": ["type"],
262
+ "properties": { "type": { "const": "softBreak" } }
263
+ },
264
+ "custom": {
265
+ "type": "object",
266
+ "required": ["type", "name", "data"],
267
+ "properties": {
268
+ "type": { "const": "custom" },
269
+ "name": { "type": "string" },
270
+ "data": true,
271
+ "children": {
272
+ "type": "array",
273
+ "items": { "$ref": "#/$defs/blockNode" }
274
+ }
275
+ }
276
+ },
277
+ "extensionNode": {
278
+ "type": "object",
279
+ "required": ["type"],
280
+ "properties": {
281
+ "type": {
282
+ "type": "string",
283
+ "not": {
284
+ "enum": [
285
+ "paragraph", "heading", "thematicBreak", "blockquote",
286
+ "list", "listItem", "code", "html", "table", "tableRow",
287
+ "tableCell", "text", "emphasis", "strong", "strikethrough",
288
+ "link", "image", "inlineCode", "break", "softBreak", "custom",
289
+ "footnoteDefinition", "footnoteReference"
290
+ ]
291
+ }
292
+ }
293
+ }
294
+ }
295
+ }
296
+ }