@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
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 ` `, and printed
|
|
36
|
+
// literally it would split the paragraph in two.
|
|
37
|
+
return escaped.indexOf('\n') === -1 ? escaped : escaped.replace(/\n/g, ' ');
|
|
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 (`	foo`) and how it goes back out.
|
|
51
|
+
if (line.charCodeAt(0) === 0x09) return '	' + line.slice(1);
|
|
52
|
+
if (line.startsWith(' ')) return ' ' + 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) => ' + ')',
|
|
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
|
+
}
|