@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
package/src/to-md.js ADDED
@@ -0,0 +1,396 @@
1
+ //@ts-check
2
+ /**
3
+ * @file Canonical Markdown printer: AST → text (MD-FORMAT.md §5).
4
+ *
5
+ * The printer is the round-trip half of the package: canonical output
6
+ * re-parses to a deep-equal AST. Canonical choices: ATX headings, `-`
7
+ * bullets, `1.`/`2.` ordered markers renumbered from `start`, backtick
8
+ * fences, `*`/`**` emphasis, inline links, backslash hard breaks and
9
+ * piped tables. Dispatch is one prebuilt table per node class — no
10
+ * per-node type chains.
11
+ */
12
+
13
+ /**
14
+ * @typedef {import('./ast.js').MdNode} MdNode
15
+ */
16
+
17
+ /** Characters that are backslash-escaped in canonical text output. */
18
+ const RE_ESCAPE = /[\\`*_[\]<>~|#&]/g;
19
+ /** Line starts that would re-parse as a block construct: a list marker,
20
+ * a blockquote, or a setext underline under the line before it. */
21
+ const RE_DANGEROUS_LINE = /^(?:(?:[-+=>]|\d{1,9}[.)])(?: |$)|[-=]{2,}[ \t]*$)/;
22
+ /** The ordinal of such a line, when it is an ordered-list marker. */
23
+ const RE_LEADING_DIGITS = /^\d{1,9}/;
24
+ /** The indent a footnote definition's later blocks are written at. */
25
+ const FOOTNOTE_CONTINUATION = ' ';
26
+
27
+ /**
28
+ * Escape inline text so it re-parses as the same literal text.
29
+ * @param {string} value
30
+ * @returns {string}
31
+ */
32
+ function escapeText(value) {
33
+ const escaped = value.replace(RE_ESCAPE, '\\$&');
34
+ // A newline inside a text VALUE is not a line break — the parser gives
35
+ // those their own `softBreak` node — it came from `&#10;`, and printed
36
+ // literally it would split the paragraph in two.
37
+ return escaped.indexOf('\n') === -1 ? escaped : escaped.replace(/\n/g, '&#10;');
38
+ }
39
+
40
+ /**
41
+ * Escape a printed paragraph/cell line that would otherwise open a
42
+ * block construct at line start.
43
+ * @param {string} line
44
+ * @returns {string}
45
+ */
46
+ function guardLineStart(line) {
47
+ // Leading whitespace cannot be escaped with a backslash, and four
48
+ // spaces (or one tab) of it would re-parse as indented code, so the
49
+ // first whitespace character prints as a character reference — which
50
+ // is how the text came in (`&#9;foo`) and how it goes back out.
51
+ if (line.charCodeAt(0) === 0x09) return '&#9;' + line.slice(1);
52
+ if (line.startsWith(' ')) return '&#32;' + line.slice(1);
53
+ if (!RE_DANGEROUS_LINE.test(line)) return line;
54
+ // A backslash escapes ASCII PUNCTUATION and nothing else, so an
55
+ // ordered-list line is disarmed at its delimiter (`1\. text`): `\1.`
56
+ // would print a literal backslash, re-parse as one, and print itself
57
+ // again next time — the canonical form would never settle.
58
+ const digits = RE_LEADING_DIGITS.exec(line);
59
+ return digits === null
60
+ ? '\\' + line
61
+ : digits[0] + '\\' + line.slice(digits[0].length);
62
+ }
63
+
64
+ // ------------------------------------------------------------------
65
+ // Inline printing
66
+ // ------------------------------------------------------------------
67
+
68
+ /**
69
+ * Print a list of inline nodes.
70
+ * @param {MdNode[]} nodes
71
+ * @returns {string}
72
+ */
73
+ function printInlines(nodes) {
74
+ let out = '';
75
+ for (let i = 0; i < nodes.length; i++) {
76
+ const printed = printInline(nodes[i]);
77
+ // A trailing `!` in front of a link is what makes an image, so text
78
+ // that genuinely ends in one keeps its escape (`\\![foo]`).
79
+ const next = nodes[i + 1];
80
+ out += printed.endsWith('!') && next !== undefined && next.type === 'link'
81
+ ? printed.slice(0, -1) + '\\!'
82
+ : printed;
83
+ }
84
+ return out;
85
+ }
86
+
87
+ /** @type {Record<string, (node: MdNode) => string>} */
88
+ const INLINE_PRINTERS = {
89
+ text: (node) => escapeText(node.value),
90
+ emphasis: (node) => {
91
+ const inner = printInlines(node.children);
92
+ // `*x*` directly inside `*…*` runs the two markers together into
93
+ // `**` — strong — so emphasis nested in emphasis alternates to `_`.
94
+ // Emphasis around STRONG does not: `***x***` is unambiguous, and
95
+ // `_` would be worse, since it does not open inside a word.
96
+ const first = node.children[0];
97
+ const last = node.children[node.children.length - 1];
98
+ const alternate = (first !== undefined && first.type === 'emphasis')
99
+ || (last !== undefined && last.type === 'emphasis');
100
+ return alternate ? '_' + inner + '_' : '*' + inner + '*';
101
+ },
102
+ strong: (node) => '**' + printInlines(node.children) + '**',
103
+ strikethrough: (node) => '~~' + printInlines(node.children) + '~~',
104
+ inlineCode: (node) => printCodeSpan(node.value),
105
+ // A GFM literal autolink prints as the bare text it was written as
106
+ // (escaped, so `*` in a query string cannot open emphasis on the way
107
+ // back in) — the one place the `auto` flag earns its keep.
108
+ link: (node) => (node.auto === true
109
+ ? printInlines(node.children)
110
+ : isAutolink(node)
111
+ ? '<' + node.url.replace(/^mailto:/, '') + '>'
112
+ : '[' + printInlines(node.children) + '](' + printLinkTarget(node) + ')'),
113
+ image: (node) => '![' + escapeText(node.alt) + '](' + printLinkTarget(node) + ')',
114
+ break: () => '\\\n',
115
+ softBreak: () => '\n',
116
+ html: (node) => node.value,
117
+ footnoteReference: (node) => '[^' + (node.label ?? node.identifier) + ']',
118
+ };
119
+
120
+ /**
121
+ * Print one inline node (unknown types degrade to their text content).
122
+ * @param {MdNode} node
123
+ * @returns {string}
124
+ */
125
+ function printInline(node) {
126
+ const printer = INLINE_PRINTERS[node.type];
127
+ if (printer !== undefined) return printer(node);
128
+ if (Array.isArray(node.children)) return printInlines(node.children);
129
+ return typeof node.value === 'string' ? escapeText(node.value) : '';
130
+ }
131
+
132
+ /**
133
+ * Is this link one an autolink produced — its text IS its destination?
134
+ * Printing such a link as `[text](url)` would escape the text but not
135
+ * the destination, and the two spellings drift apart on the next
136
+ * round trip; `<url>` is both shorter and stable.
137
+ * @param {MdNode} node
138
+ * @returns {boolean}
139
+ */
140
+ function isAutolink(node) {
141
+ if (node.title != null || node.children.length !== 1) return false;
142
+ const only = node.children[0];
143
+ if (only.type !== 'text') return false;
144
+ const url = /** @type {string} */ (node.url);
145
+ const shown = url.startsWith('mailto:') ? url.slice(7) : url;
146
+ return only.value === shown && !/[\s<>]/.test(shown);
147
+ }
148
+
149
+ /**
150
+ * Wrap a code span in a backtick run longer than any run inside it.
151
+ * @param {string} value
152
+ * @returns {string}
153
+ */
154
+ function printCodeSpan(value) {
155
+ let longest = 0;
156
+ let run = 0;
157
+ for (let i = 0; i < value.length; i++) {
158
+ run = value.charCodeAt(i) === 0x60 ? run + 1 : 0;
159
+ if (run > longest) longest = run;
160
+ }
161
+ const fence = '`'.repeat(longest + 1);
162
+ const pad = value.length === 0
163
+ || value.charCodeAt(0) === 0x60
164
+ || value.charCodeAt(value.length - 1) === 0x60
165
+ || (value.charCodeAt(0) === 0x20 && value.charCodeAt(value.length - 1) === 0x20
166
+ && value.trim() !== '')
167
+ ? ' ' : '';
168
+ return fence + pad + value + pad + fence;
169
+ }
170
+
171
+ /**
172
+ * Print a link/image destination (+ optional title).
173
+ * @param {MdNode} node
174
+ * @returns {string}
175
+ */
176
+ function printLinkTarget(node) {
177
+ const url = /** @type {string} */ (node.url);
178
+ const wrapped = url === '' || /[\s()]/.test(url) ? '<' + url.replace(/[<>]/g, '\\$&') + '>' : url;
179
+ return node.title == null
180
+ ? wrapped
181
+ : wrapped + ' "' + node.title.replace(/"/g, '\\"') + '"';
182
+ }
183
+
184
+ // ------------------------------------------------------------------
185
+ // Block printing
186
+ // ------------------------------------------------------------------
187
+
188
+ /**
189
+ * Print inline content and guard every printed line's start.
190
+ * @param {MdNode[]} children
191
+ * @returns {string}
192
+ */
193
+ function printFlow(children) {
194
+ const out = printInlines(children);
195
+ if (out.indexOf('\n') === -1) return guardLineStart(out);
196
+ const lines = out.split('\n');
197
+ for (let i = 0; i < lines.length; i++) lines[i] = guardLineStart(lines[i]);
198
+ return lines.join('\n');
199
+ }
200
+
201
+ /** @type {Record<string, (node: MdNode) => string>} */
202
+ const BLOCK_PRINTERS = {
203
+ paragraph: (node) => printFlow(node.children),
204
+
205
+ heading: (node) => {
206
+ const text = printInlines(node.children);
207
+ // An ATX heading is one line, so a heading whose text carries a soft
208
+ // break has to print in its setext form or lose the break — which
209
+ // only depths 1 and 2 have. Deeper headings cannot hold one: they
210
+ // can only come from ATX in the first place.
211
+ if (node.depth <= 2 && text.indexOf('\n') !== -1) {
212
+ return printFlow(node.children) + '\n' + (node.depth === 1 ? '===' : '---');
213
+ }
214
+ return '#'.repeat(node.depth) + ' ' + text;
215
+ },
216
+
217
+ // `***`, not `---`: the printer's bullet is `-`, and `- ---` is a
218
+ // thematic break in its own right rather than an item containing one.
219
+ thematicBreak: () => '***',
220
+
221
+ blockquote: (node) => {
222
+ const inner = printBlocks(node.children, false);
223
+ const lines = inner.split('\n');
224
+ for (let i = 0; i < lines.length; i++) {
225
+ lines[i] = lines[i] === '' ? '>' : '> ' + lines[i];
226
+ }
227
+ return lines.join('\n');
228
+ },
229
+
230
+ list: (node, alt) => {
231
+ const items = node.children;
232
+ const parts = [];
233
+ let ordinal = node.ordered ? (node.start ?? 1) : 0;
234
+ for (let i = 0; i < items.length; i++) {
235
+ // Two lists in a row are two lists only because their markers
236
+ // differ: printed with the same marker they re-parse as one. The
237
+ // second of an adjacent pair therefore switches (`-`→`*`, `.`→`)`).
238
+ const marker = node.ordered
239
+ ? `${ordinal + i}${alt === true ? ')' : '.'} `
240
+ : (alt === true ? '* ' : '- ');
241
+ const indent = ' '.repeat(marker.length);
242
+ const check = items[i].checked === null || items[i].checked === undefined
243
+ ? ''
244
+ : items[i].checked ? '[x] ' : '[ ] ';
245
+ const body = printBlocks(items[i].children, node.tight);
246
+ const lines = body.split('\n');
247
+ let item = marker + check + lines[0];
248
+ for (let k = 1; k < lines.length; k++) {
249
+ item += '\n' + (lines[k] === '' ? '' : indent + lines[k]);
250
+ }
251
+ parts.push(item);
252
+ }
253
+ return parts.join(node.tight ? '\n' : '\n\n');
254
+ },
255
+
256
+ code: (node) => printFence(node.lang, node.meta, node.value),
257
+
258
+ html: (node) => node.value,
259
+
260
+ // A definition prints where it stands — the AST is the document, not
261
+ // the rendering, so moving them all to the end would change the
262
+ // document to match one renderer's idea of it.
263
+ footnoteDefinition: (node) => {
264
+ const body = printBlocks(node.children, false);
265
+ const lines = body.split('\n');
266
+ let out = '[^' + (node.label ?? node.identifier) + ']: ' + lines[0];
267
+ for (let k = 1; k < lines.length; k++) {
268
+ out += '\n' + (lines[k] === '' ? '' : FOOTNOTE_CONTINUATION + lines[k]);
269
+ }
270
+ return out;
271
+ },
272
+
273
+ table: (node) => {
274
+ const rows = node.children;
275
+ const out = [];
276
+ for (let r = 0; r < rows.length; r++) {
277
+ const cells = rows[r].children;
278
+ let line = '|';
279
+ for (let c = 0; c < cells.length; c++) {
280
+ line += ' ' + printInlines(cells[c].children).replace(/\n/g, ' ') + ' |';
281
+ }
282
+ out.push(line);
283
+ if (r === 0) {
284
+ let delim = '|';
285
+ for (let c = 0; c < node.align.length; c++) {
286
+ const a = node.align[c];
287
+ delim += a === 'center' ? ' :---: |'
288
+ : a === 'right' ? ' ---: |'
289
+ : a === 'left' ? ' :--- |'
290
+ : ' --- |';
291
+ }
292
+ out.push(delim);
293
+ }
294
+ }
295
+ return out.join('\n');
296
+ },
297
+
298
+ custom: (node) => (Array.isArray(node.children) ? printBlocks(node.children, false) : ''),
299
+ };
300
+
301
+ /**
302
+ * Print a code fence, growing the fence beyond any backtick run in the
303
+ * value.
304
+ * @param {string|null} lang
305
+ * @param {string|null} meta
306
+ * @param {string} value
307
+ * @returns {string}
308
+ */
309
+ function printFence(lang, meta, value) {
310
+ const info = lang === null ? '' : meta === null ? lang : lang + ' ' + meta;
311
+ // A backtick info string cannot sit on a backtick fence (the spec
312
+ // forbids it, precisely so the fence stays findable), so such a block
313
+ // prints on a tilde fence instead.
314
+ const marker = info.indexOf('`') === -1 ? '`' : '~';
315
+ const code = marker.charCodeAt(0);
316
+ let longest = 2;
317
+ let run = 0;
318
+ for (let i = 0; i < value.length; i++) {
319
+ run = value.charCodeAt(i) === code ? run + 1 : 0;
320
+ if (run > longest) longest = run;
321
+ }
322
+ const fence = marker.repeat(longest + 1);
323
+ const body = value === '' ? '' : value.endsWith('\n') ? value : value + '\n';
324
+ return fence + info + '\n' + body + fence;
325
+ }
326
+
327
+ /**
328
+ * Print one block node. Unknown types with a string `value` print as a
329
+ * fence tagged with the type (a compiled-in plugin's fence claim then
330
+ * round-trips); container-shaped unknowns print their children.
331
+ * @param {MdNode} node
332
+ * @param {boolean} [alt] use the alternate list marker (see printBlocks)
333
+ * @returns {string}
334
+ */
335
+ function printBlock(node, alt) {
336
+ const printer = BLOCK_PRINTERS[node.type];
337
+ if (printer !== undefined) return printer(node, alt);
338
+ if (typeof node.value === 'string') {
339
+ return printFence(node.type, node.meta ?? null, node.value);
340
+ }
341
+ if (Array.isArray(node.children)) return printBlocks(node.children, false);
342
+ return '';
343
+ }
344
+
345
+ /**
346
+ * Print a block sequence. In a tight list the child paragraphs join
347
+ * with single newlines; everywhere else blocks separate with a blank
348
+ * line.
349
+ * @param {MdNode[]} blocks
350
+ * @param {boolean} tight
351
+ * @returns {string}
352
+ */
353
+ function printBlocks(blocks, tight) {
354
+ const parts = [];
355
+ let alt = false;
356
+ for (let i = 0; i < blocks.length; i++) {
357
+ const node = blocks[i];
358
+ // alternate the marker across a RUN of adjacent lists of the same
359
+ // kind; anything else between them resets it
360
+ const previous = i > 0 ? blocks[i - 1] : null;
361
+ alt = node.type === 'list' && previous !== null && previous.type === 'list'
362
+ && previous.ordered === node.ordered
363
+ ? !alt
364
+ : false;
365
+ const printed = printBlock(node, alt);
366
+ if (printed !== '') parts.push(printed);
367
+ }
368
+ return parts.join(tight ? '\n' : '\n\n');
369
+ }
370
+
371
+ /**
372
+ * Print an MdDocument (or a bare AST array / single node) to canonical
373
+ * Markdown. Frontmatter re-emits as a `---json` block by default —
374
+ * exact, syntax-neutral round-trips (MD-FORMAT.md §5).
375
+ *
376
+ * @param {any} docOrAst
377
+ * @param {{ frontmatter?: boolean }} [options]
378
+ * @returns {string}
379
+ */
380
+ export function toMarkdown(docOrAst, options = {}) {
381
+ /** @type {MdNode[]} */
382
+ let ast;
383
+ let frontmatter = null;
384
+ if (Array.isArray(docOrAst)) ast = docOrAst;
385
+ else if (docOrAst !== null && typeof docOrAst === 'object' && Array.isArray(docOrAst.ast)) {
386
+ ast = docOrAst.ast;
387
+ frontmatter = docOrAst.frontmatter ?? null;
388
+ }
389
+ else ast = [docOrAst];
390
+ let out = '';
391
+ if (frontmatter !== null && options.frontmatter !== false) {
392
+ out += '---json\n' + JSON.stringify(frontmatter, null, 2) + '\n---\n\n';
393
+ }
394
+ const body = printBlocks(ast, false);
395
+ return body === '' ? out : out + body + '\n';
396
+ }