@tabnas/markdown 0.4.2 → 0.5.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 +78 -12
- package/dist/ast.d.ts +83 -0
- package/dist/ast.js +248 -0
- package/dist/ast.js.map +1 -0
- package/dist/block.d.ts +10 -0
- package/dist/block.js +971 -0
- package/dist/block.js.map +1 -0
- package/dist/common.d.ts +49 -0
- package/dist/common.js +155 -0
- package/dist/common.js.map +1 -0
- package/dist/commonmark.d.ts +14 -0
- package/dist/commonmark.js +31 -0
- package/dist/commonmark.js.map +1 -0
- package/dist/entities.d.ts +1 -0
- package/dist/entities.js +14 -0
- package/dist/entities.js.map +1 -0
- package/dist/html.d.ts +10 -0
- package/dist/html.js +270 -0
- package/dist/html.js.map +1 -0
- package/dist/inline.d.ts +13 -0
- package/dist/inline.js +902 -0
- package/dist/inline.js.map +1 -0
- package/dist/markdown.d.ts +18 -84
- package/dist/markdown.js +80 -687
- package/dist/markdown.js.map +1 -1
- package/dist/node.d.ts +74 -0
- package/dist/node.js +167 -0
- package/dist/node.js.map +1 -0
- package/dist/options.d.ts +21 -0
- package/dist/options.js +16 -0
- package/dist/options.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/src/ast.ts +352 -0
- package/src/block.ts +1094 -0
- package/src/common.ts +166 -0
- package/src/commonmark.ts +34 -0
- package/src/entities.ts +14 -0
- package/src/html.ts +321 -0
- package/src/inline.ts +1034 -0
- package/src/markdown.ts +126 -770
- package/src/node.ts +240 -0
- package/src/options.ts +39 -0
- package/src/tsconfig.json +2 -0
package/dist/markdown.js
CHANGED
|
@@ -1,26 +1,48 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/* Copyright (c) 2021-2026 Richard Rodger, MIT License */
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.Markdown = void 0;
|
|
4
|
+
exports.renderHTML = exports.MdNode = exports.grammarText = exports.Markdown = void 0;
|
|
5
5
|
exports.parseDocument = parseDocument;
|
|
6
6
|
exports.parseInline = parseInline;
|
|
7
|
+
exports.toHtml = toHtml;
|
|
8
|
+
exports.parseTree = parseTree;
|
|
9
|
+
const commonmark_ts_1 = require("./commonmark.js");
|
|
10
|
+
const ast_ts_1 = require("./ast.js");
|
|
11
|
+
const html_ts_1 = require("./html.js");
|
|
12
|
+
const options_ts_1 = require("./options.js");
|
|
7
13
|
// ---------------------------------------------------------------------------
|
|
8
|
-
// Grammar source
|
|
9
|
-
//
|
|
10
|
-
// The
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
+
// Grammar source.
|
|
15
|
+
//
|
|
16
|
+
// The prose grammar is intentionally trivial: block structure is decided by
|
|
17
|
+
// the line-oriented algorithm in `block.ts`, not by declarative alts, so the
|
|
18
|
+
// `markdown` rule exists only to consume the token stream. The file is kept
|
|
19
|
+
// because `ts/embed-grammar.js` embeds it verbatim into both runtimes; see
|
|
20
|
+
// AGENTS.md. It is documentation of the entry rule, not a source of behaviour.
|
|
14
21
|
// --- BEGIN EMBEDDED markdown-grammar.jsonic ---
|
|
15
22
|
const grammarText = `
|
|
16
|
-
# Markdown
|
|
17
|
-
#
|
|
18
|
-
# This grammar is
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
23
|
+
# Markdown entry rule — CommonMark 0.31.2
|
|
24
|
+
#
|
|
25
|
+
# This grammar is deliberately trivial, and it is NOT where the parser
|
|
26
|
+
# lives. Block structure is decided by the two-phase line algorithm of
|
|
27
|
+
# spec Appendix A, implemented in block.ts / block.go, and inline
|
|
28
|
+
# structure by the delimiter and bracket stacks in inline.ts / inline.go.
|
|
29
|
+
# None of that is expressible as declarative alts, which is why none of
|
|
30
|
+
# it is here.
|
|
31
|
+
#
|
|
32
|
+
# The markdown rule exists only to give the engine an entry point and to
|
|
33
|
+
# consume the token stream so its trailing-content check passes. The bo
|
|
34
|
+
# action reads the whole source via ctx.src() and hands it to the parser.
|
|
35
|
+
# Everything the rule does is in those two lines below.
|
|
36
|
+
#
|
|
37
|
+
# Consequences worth knowing rather than discovering:
|
|
38
|
+
# - The railroad diagram generated from this file is empty: a bare
|
|
39
|
+
# track with no boxes on it. That is accurate, not a rendering fault.
|
|
40
|
+
# - Editing this file changes documentation, not behaviour. The one
|
|
41
|
+
# exception is the open/close alts, which really are the rule.
|
|
42
|
+
#
|
|
43
|
+
# The file is kept because ts/embed-grammar.js embeds it verbatim into
|
|
44
|
+
# both runtimes as grammarText (see AGENTS.md). It may not contain
|
|
45
|
+
# backticks: the Go copy is a raw string literal.
|
|
24
46
|
{
|
|
25
47
|
rule: markdown: open: [
|
|
26
48
|
{ s: '#ZZ' }
|
|
@@ -30,667 +52,45 @@ const grammarText = `
|
|
|
30
52
|
]
|
|
31
53
|
}
|
|
32
54
|
`;
|
|
55
|
+
exports.grammarText = grammarText;
|
|
33
56
|
// --- END EMBEDDED markdown-grammar.jsonic ---
|
|
34
57
|
// ---------------------------------------------------------------------------
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// Angle-bracketed URL: <http://example.com>
|
|
41
|
-
if (s.startsWith('<')) {
|
|
42
|
-
const close = s.indexOf('>', 1);
|
|
43
|
-
if (-1 !== close) {
|
|
44
|
-
const url = s.slice(1, close);
|
|
45
|
-
const rest = s.slice(close + 1).trim();
|
|
46
|
-
if ('' === rest)
|
|
47
|
-
return { url, title: null };
|
|
48
|
-
if ((rest.startsWith('"') && rest.endsWith('"') && 1 < rest.length) ||
|
|
49
|
-
(rest.startsWith("'") && rest.endsWith("'") && 1 < rest.length) ||
|
|
50
|
-
(rest.startsWith('(') && rest.endsWith(')') && 1 < rest.length)) {
|
|
51
|
-
return { url, title: rest.slice(1, -1) };
|
|
52
|
-
}
|
|
53
|
-
return { url: s, title: null }; // malformed title → treat whole inside as url
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
// Unbracketed URL: first token is url, remainder (if quoted) is title
|
|
57
|
-
// Split on whitespace not inside quotes.
|
|
58
|
-
let url = '';
|
|
59
|
-
let title = null;
|
|
60
|
-
let i = 0;
|
|
61
|
-
while (i < s.length && !/\s/.test(s[i]))
|
|
62
|
-
i++;
|
|
63
|
-
url = s.slice(0, i);
|
|
64
|
-
const rest = s.slice(i).trim();
|
|
65
|
-
if ('' !== rest) {
|
|
66
|
-
if ((rest.startsWith('"') && rest.endsWith('"') && 1 < rest.length) ||
|
|
67
|
-
(rest.startsWith("'") && rest.endsWith("'") && 1 < rest.length) ||
|
|
68
|
-
(rest.startsWith('(') && rest.endsWith(')') && 1 < rest.length)) {
|
|
69
|
-
title = rest.slice(1, -1);
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
// Malformed — treat whole inside as url
|
|
73
|
-
url = s;
|
|
74
|
-
title = null;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return { url, title };
|
|
78
|
-
}
|
|
79
|
-
function findClosingBracket(text, start) {
|
|
80
|
-
let depth = 0;
|
|
81
|
-
for (let i = start; i < text.length; i++) {
|
|
82
|
-
const c = text[i];
|
|
83
|
-
if ('\\' === c && i + 1 < text.length) {
|
|
84
|
-
i++;
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
if ('[' === c)
|
|
88
|
-
depth++;
|
|
89
|
-
else if (']' === c) {
|
|
90
|
-
if (0 === depth)
|
|
91
|
-
return i;
|
|
92
|
-
depth--;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return -1;
|
|
96
|
-
}
|
|
97
|
-
function findClosingParen(text, start) {
|
|
98
|
-
let depth = 0;
|
|
99
|
-
for (let i = start; i < text.length; i++) {
|
|
100
|
-
const c = text[i];
|
|
101
|
-
if ('\\' === c && i + 1 < text.length) {
|
|
102
|
-
i++;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
if ('(' === c)
|
|
106
|
-
depth++;
|
|
107
|
-
else if (')' === c) {
|
|
108
|
-
if (0 === depth)
|
|
109
|
-
return i;
|
|
110
|
-
depth--;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return -1;
|
|
58
|
+
// Public parse API (engine-free)
|
|
59
|
+
/** Parse Markdown to the mdast-adjacent JSON AST. */
|
|
60
|
+
function parseDocument(src, opts) {
|
|
61
|
+
const options = (0, options_ts_1.resolveOptions)(opts);
|
|
62
|
+
return (0, ast_ts_1.toAst)((0, commonmark_ts_1.parse)(src, options), options);
|
|
114
63
|
}
|
|
115
|
-
|
|
116
|
-
// Inline scanner — produces Inline[] from a text string.
|
|
117
|
-
// `text` may contain '\n' for soft breaks (paragraph lines joined with \n).
|
|
64
|
+
/** Parse a single run of inline Markdown to an `Inline[]`. */
|
|
118
65
|
function parseInline(text, opts) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const flush = () => {
|
|
123
|
-
if ('' !== buf) {
|
|
124
|
-
nodes.push({ type: 'text', value: buf });
|
|
125
|
-
buf = '';
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
while (i < text.length) {
|
|
129
|
-
const ch = text[i];
|
|
130
|
-
// Escapes — \* \_ \~ \` etc.
|
|
131
|
-
if ('\\' === ch && i + 1 < text.length && /[\\`*_\[\]{}()#+\-.!|>~]/.test(text[i + 1])) {
|
|
132
|
-
buf += text[i + 1];
|
|
133
|
-
i += 2;
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
// Inline code — `code` or ``code with ` inside`` etc.
|
|
137
|
-
if ('`' === ch) {
|
|
138
|
-
let j = i;
|
|
139
|
-
while (j < text.length && '`' === text[j])
|
|
140
|
-
j++;
|
|
141
|
-
const ticks = j - i;
|
|
142
|
-
const fence = '`'.repeat(ticks);
|
|
143
|
-
const close = text.indexOf(fence, j);
|
|
144
|
-
if (-1 !== close) {
|
|
145
|
-
const raw = text.slice(j, close);
|
|
146
|
-
// Collapse newlines/spaces: trim at ends, internal newlines → space
|
|
147
|
-
const code = raw.replace(/\r?\n/g, ' ').replace(/^\s+|\s+$/g, '');
|
|
148
|
-
// Per CommonMark, `` `` → ``? Already handled by fence length.
|
|
149
|
-
flush();
|
|
150
|
-
nodes.push({ type: 'inlineCode', value: code });
|
|
151
|
-
i = close + ticks;
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
// Image — 
|
|
156
|
-
if (text.startsWith('![', i)) {
|
|
157
|
-
const closeAlt = findClosingBracket(text, i + 2);
|
|
158
|
-
if (-1 !== closeAlt && '(' === text[closeAlt + 1]) {
|
|
159
|
-
const closeParen = findClosingParen(text, closeAlt + 2);
|
|
160
|
-
if (-1 !== closeParen) {
|
|
161
|
-
const alt = text.slice(i + 2, closeAlt);
|
|
162
|
-
const inside = text.slice(closeAlt + 2, closeParen);
|
|
163
|
-
const { url, title } = parseLinkDestination(inside);
|
|
164
|
-
if ('' !== url) {
|
|
165
|
-
flush();
|
|
166
|
-
nodes.push({ type: 'image', url, title, alt });
|
|
167
|
-
i = closeParen + 1;
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
// Autolink — <http://…> or <https://…>
|
|
174
|
-
if ('<' === ch) {
|
|
175
|
-
const close = text.indexOf('>', i);
|
|
176
|
-
if (-1 !== close) {
|
|
177
|
-
const url = text.slice(i + 1, close);
|
|
178
|
-
if (/^https?:\/\/\S+$/.test(url) || /^mailto:[^\s@]+@[^\s@]+\.[^\s@]+$/.test(url)) {
|
|
179
|
-
flush();
|
|
180
|
-
const href = url.startsWith('mailto:') ? url : url;
|
|
181
|
-
nodes.push({ type: 'link', url: href, title: null, children: [{ type: 'text', value: url }] });
|
|
182
|
-
i = close + 1;
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
// Email autolink <a@b.com> etc. — treat same as http for now (already covered)
|
|
186
|
-
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(url)) {
|
|
187
|
-
flush();
|
|
188
|
-
nodes.push({ type: 'link', url: 'mailto:' + url, title: null, children: [{ type: 'text', value: url }] });
|
|
189
|
-
i = close + 1;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
// Link — [label](url "title")
|
|
195
|
-
if ('[' === ch) {
|
|
196
|
-
const closeBracket = findClosingBracket(text, i + 1);
|
|
197
|
-
if (-1 !== closeBracket) {
|
|
198
|
-
const label = text.slice(i + 1, closeBracket);
|
|
199
|
-
if ('(' === text[closeBracket + 1]) {
|
|
200
|
-
const closeParen = findClosingParen(text, closeBracket + 2);
|
|
201
|
-
if (-1 !== closeParen) {
|
|
202
|
-
const inside = text.slice(closeBracket + 2, closeParen);
|
|
203
|
-
const { url, title } = parseLinkDestination(inside);
|
|
204
|
-
if ('' !== url) {
|
|
205
|
-
flush();
|
|
206
|
-
nodes.push({ type: 'link', url, title, children: parseInline(label, opts) });
|
|
207
|
-
i = closeParen + 1;
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
// Strong — **text** or __text__ (must be non-empty)
|
|
215
|
-
if (text.startsWith('**', i) || text.startsWith('__', i)) {
|
|
216
|
-
const delim = text.slice(i, i + 2);
|
|
217
|
-
const close = text.indexOf(delim, i + 2);
|
|
218
|
-
if (-1 !== close && close > i + 2) {
|
|
219
|
-
const inner = text.slice(i + 2, close);
|
|
220
|
-
if ('' !== inner.trim()) {
|
|
221
|
-
flush();
|
|
222
|
-
nodes.push({ type: 'strong', children: parseInline(inner, opts) });
|
|
223
|
-
i = close + 2;
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
// Strikethrough GFM — ~~text~~
|
|
229
|
-
if (opts.gfm && text.startsWith('~~', i)) {
|
|
230
|
-
const close = text.indexOf('~~', i + 2);
|
|
231
|
-
if (-1 !== close && close > i + 2) {
|
|
232
|
-
const inner = text.slice(i + 2, close);
|
|
233
|
-
if ('' !== inner.trim()) {
|
|
234
|
-
flush();
|
|
235
|
-
nodes.push({ type: 'delete', children: parseInline(inner, opts) });
|
|
236
|
-
i = close + 2;
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
// Emphasis — *text* or _text_ (single char)
|
|
242
|
-
if ('*' === ch || '_' === ch) {
|
|
243
|
-
// Avoid handling ** as *: already handled above, but single * inside ** shouldn't reach here alone
|
|
244
|
-
// If next char is same delim, we would have taken strong branch; so this is single.
|
|
245
|
-
// For '_' inside a word like foo_bar_baz, CommonMark says not emphasis — we simplify to not split inside alphanum_ sequence.
|
|
246
|
-
if ('_' === ch) {
|
|
247
|
-
const prevIsAlnum = i > 0 && /[\w]/.test(text[i - 1]);
|
|
248
|
-
const nextIsAlnum = i + 1 < text.length && /[\w]/.test(text[i + 1]);
|
|
249
|
-
if (prevIsAlnum && nextIsAlnum) {
|
|
250
|
-
buf += ch;
|
|
251
|
-
i++;
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
// also check close side will have same word-boundary issue, but we check at open and close
|
|
255
|
-
}
|
|
256
|
-
const delim = ch;
|
|
257
|
-
let close = -1;
|
|
258
|
-
// Find closing delim not escaped and with word-boundary handling for _
|
|
259
|
-
for (let k = i + 1; k < text.length; k++) {
|
|
260
|
-
if ('\\' === text[k] && k + 1 < text.length) {
|
|
261
|
-
k++;
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
if (delim === text[k]) {
|
|
265
|
-
if ('_' === delim) {
|
|
266
|
-
const beforeIsAlnum = k > 0 && /[\w]/.test(text[k - 1]);
|
|
267
|
-
const afterIsAlnum = k + 1 < text.length && /[\w]/.test(text[k + 1]);
|
|
268
|
-
if (beforeIsAlnum && afterIsAlnum)
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
// Ensure not part of strong's double
|
|
272
|
-
if (k + 1 < text.length && delim === text[k + 1])
|
|
273
|
-
continue;
|
|
274
|
-
if (k > 0 && delim === text[k - 1] && text.startsWith(delim + delim, k - 1))
|
|
275
|
-
continue;
|
|
276
|
-
close = k;
|
|
277
|
-
break;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
if (-1 !== close && close > i + 1) {
|
|
281
|
-
const inner = text.slice(i + 1, close);
|
|
282
|
-
if ('' !== inner.trim()) {
|
|
283
|
-
flush();
|
|
284
|
-
nodes.push({ type: 'emphasis', children: parseInline(inner, opts) });
|
|
285
|
-
i = close + 1;
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
// Hard break — two spaces + \n or backslash + \n, or plain \n when breaks:true
|
|
291
|
-
if ('\n' === ch) {
|
|
292
|
-
const prevTwoSpaces = i >= 2 && ' ' === text[i - 1] && ' ' === text[i - 2];
|
|
293
|
-
if (prevTwoSpaces) {
|
|
294
|
-
// Hard break via trailing spaces: remove the two spaces from buffered text
|
|
295
|
-
// They are still in buf (not yet flushed), so trim them there.
|
|
296
|
-
if (buf.endsWith(' '))
|
|
297
|
-
buf = buf.slice(0, -2);
|
|
298
|
-
else if (buf.endsWith(' '))
|
|
299
|
-
buf = buf.slice(0, -1);
|
|
300
|
-
flush();
|
|
301
|
-
nodes.push({ type: 'break' });
|
|
302
|
-
}
|
|
303
|
-
else if (opts.breaks) {
|
|
304
|
-
flush();
|
|
305
|
-
nodes.push({ type: 'break' });
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
// Soft break → single space inside the current text run
|
|
309
|
-
buf += ' ';
|
|
310
|
-
}
|
|
311
|
-
i++;
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
// Fallthrough — plain char
|
|
315
|
-
buf += ch;
|
|
316
|
-
i++;
|
|
317
|
-
}
|
|
318
|
-
flush();
|
|
319
|
-
// If nothing was produced but non-empty text, return single text node (preserve empty paragraph case ? — caller handles)
|
|
320
|
-
return nodes;
|
|
321
|
-
}
|
|
322
|
-
// ---------------------------------------------------------------------------
|
|
323
|
-
// Block helpers
|
|
324
|
-
function isBlank(line) {
|
|
325
|
-
return /^[\t ]*$/.test(line);
|
|
326
|
-
}
|
|
327
|
-
function isATXHeading(line) {
|
|
328
|
-
// Up to 3 leading spaces, 1-6 #'s, then space, then content, then optional trailing #'s
|
|
329
|
-
return line.match(/^ {0,3}(#{1,6})[ \t]+(.+?)(?:[ \t]+#+)?[ \t]*$/);
|
|
330
|
-
}
|
|
331
|
-
function isSetextUnderline(line) {
|
|
332
|
-
return line.match(/^ {0,3}(=+|-+)[ \t]*$/);
|
|
333
|
-
}
|
|
334
|
-
function isThematicBreak(line) {
|
|
335
|
-
if (!/^ {0,3}((\*([ \t]*\*)*)|(-([ \t]*-)*)|(_([ \t]*_)*))[ \t]*$/.test(line))
|
|
336
|
-
return false;
|
|
337
|
-
const stripped = line.replace(/[ \t]/g, '');
|
|
338
|
-
if (3 > stripped.length)
|
|
339
|
-
return false;
|
|
340
|
-
const c = stripped[0];
|
|
341
|
-
return [...stripped].every(ch => c === ch);
|
|
66
|
+
const doc = parseDocument(text, opts);
|
|
67
|
+
const first = doc.children[0];
|
|
68
|
+
return first && 'paragraph' === first.type ? first.children : [];
|
|
342
69
|
}
|
|
343
|
-
|
|
344
|
-
|
|
70
|
+
/** Parse Markdown and render it to CommonMark-conformant HTML. */
|
|
71
|
+
function toHtml(src, opts) {
|
|
72
|
+
const options = (0, options_ts_1.resolveOptions)(opts);
|
|
73
|
+
return (0, html_ts_1.renderHTML)((0, commonmark_ts_1.parse)(src, options), options);
|
|
345
74
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
function isBlockquoteLine(line) {
|
|
350
|
-
return /^ {0,3}>\s?/.test(line);
|
|
351
|
-
}
|
|
352
|
-
function isUnorderedListItem(line) {
|
|
353
|
-
return line.match(/^ {0,3}([*+-])[ \t]+(.*)$/);
|
|
354
|
-
}
|
|
355
|
-
function isOrderedListItem(line) {
|
|
356
|
-
return line.match(/^ {0,3}(\d+)([.)])[ \t]+(.*)$/);
|
|
357
|
-
}
|
|
358
|
-
function isHtmlBlockLine(line) {
|
|
359
|
-
const t = line.trimStart();
|
|
360
|
-
return t.startsWith('<') && (/^<(?:!--|!\[CDATA\[|!DOCTYPE|\?[a-z])/i.test(t) ||
|
|
361
|
-
/^<\/?[a-zA-Z][\w-]*(?:\s[^>]*)?>/.test(t));
|
|
362
|
-
}
|
|
363
|
-
// ---------------------------------------------------------------------------
|
|
364
|
-
// Block parsing
|
|
365
|
-
function parseDocument(src, opts) {
|
|
366
|
-
// Normalize newlines, keep trailing newline behaviour: split on \r?\n
|
|
367
|
-
const lines = src.split(/\r?\n/);
|
|
368
|
-
// CommonMark: final newline not significant for block parsing — but an
|
|
369
|
-
// empty trailing line from split should not produce an extra block.
|
|
370
|
-
const { nodes } = parseBlocks(lines, 0, opts, 0);
|
|
371
|
-
return { type: 'document', children: nodes };
|
|
372
|
-
}
|
|
373
|
-
function parseBlocks(lines, start, opts, baseIndent) {
|
|
374
|
-
const nodes = [];
|
|
375
|
-
let i = start;
|
|
376
|
-
while (i < lines.length) {
|
|
377
|
-
let line = lines[i];
|
|
378
|
-
// End if dedented below baseIndent for list handling — caller manages.
|
|
379
|
-
// Here we just skip blanks that are not meaningful inside prose.
|
|
380
|
-
if (isBlank(line)) {
|
|
381
|
-
i++;
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
// Fenced code — takes precedence over other block types
|
|
385
|
-
const fenced = isFencedCodeStart(line);
|
|
386
|
-
if (fenced) {
|
|
387
|
-
const fence = fenced[1];
|
|
388
|
-
const fenceChar = fence[0];
|
|
389
|
-
const langAndMeta = line.slice(line.indexOf(fence) + fence.length).trim();
|
|
390
|
-
let lang = null;
|
|
391
|
-
let meta = null;
|
|
392
|
-
if ('' !== langAndMeta) {
|
|
393
|
-
const spaceIdx = langAndMeta.search(/\s/);
|
|
394
|
-
if (-1 === spaceIdx)
|
|
395
|
-
lang = langAndMeta;
|
|
396
|
-
else {
|
|
397
|
-
lang = langAndMeta.slice(0, spaceIdx);
|
|
398
|
-
meta = langAndMeta.slice(spaceIdx + 1).trim() || null;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
const closingRe = new RegExp('^ {0,3}' + fenceChar + '{' + fence.length + ',}[ \\t]*$');
|
|
402
|
-
const content = [];
|
|
403
|
-
i++;
|
|
404
|
-
while (i < lines.length && !closingRe.test(lines[i])) {
|
|
405
|
-
content.push(lines[i]);
|
|
406
|
-
i++;
|
|
407
|
-
}
|
|
408
|
-
if (i < lines.length)
|
|
409
|
-
i++; // consume closing fence
|
|
410
|
-
nodes.push({ type: 'code', lang: lang || null, meta: meta || null, value: content.join('\n') });
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
// Indented code — line indented by ≥4 spaces and not already handled as list
|
|
414
|
-
if (isIndentedCodeLine(line) && !isUnorderedListItem(line) && !isOrderedListItem(line)) {
|
|
415
|
-
const content = [];
|
|
416
|
-
while (i < lines.length && (isIndentedCodeLine(lines[i]) || isBlank(lines[i]))) {
|
|
417
|
-
if (isBlank(lines[i])) {
|
|
418
|
-
// Blank line inside indented code is kept as empty line
|
|
419
|
-
// but we must peek ahead for next indented line to decide if blank is part of code
|
|
420
|
-
// Look ahead to next non-blank
|
|
421
|
-
let j = i + 1;
|
|
422
|
-
while (j < lines.length && isBlank(lines[j]))
|
|
423
|
-
j++;
|
|
424
|
-
if (j < lines.length && isIndentedCodeLine(lines[j])) {
|
|
425
|
-
content.push('');
|
|
426
|
-
i++;
|
|
427
|
-
}
|
|
428
|
-
else
|
|
429
|
-
break;
|
|
430
|
-
}
|
|
431
|
-
else {
|
|
432
|
-
const raw = lines[i];
|
|
433
|
-
content.push(raw.startsWith('\t') ? raw.slice(1) : raw.slice(4));
|
|
434
|
-
i++;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
nodes.push({ type: 'code', lang: null, meta: null, value: content.join('\n') });
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
// ATX heading
|
|
441
|
-
const atx = isATXHeading(line);
|
|
442
|
-
if (atx) {
|
|
443
|
-
const depth = atx[1].length;
|
|
444
|
-
const content = atx[2].trim();
|
|
445
|
-
nodes.push({ type: 'heading', depth, children: parseInline(content, opts) });
|
|
446
|
-
i++;
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
// Thematic break — before paragraph/setext because --- can be both
|
|
450
|
-
if (isThematicBreak(line)) {
|
|
451
|
-
// But `---` after a paragraph line could be a setext underline for heading level 2.
|
|
452
|
-
// Handle setext case one line lookahead before emitting thematic break.
|
|
453
|
-
// Setext check is done inside paragraph path below via peek.
|
|
454
|
-
// Here we only emit thematic break if not a setext context.
|
|
455
|
-
// For simplicity, if previous block was a paragraph's single-line source that could be setext,
|
|
456
|
-
// paragraph collector would have already consumed it.
|
|
457
|
-
// So safe to emit.
|
|
458
|
-
nodes.push({ type: 'thematicBreak' });
|
|
459
|
-
i++;
|
|
460
|
-
continue;
|
|
461
|
-
}
|
|
462
|
-
// Blockquote — collect consecutive >-prefixed lines
|
|
463
|
-
if (isBlockquoteLine(line)) {
|
|
464
|
-
const bqLines = [];
|
|
465
|
-
while (i < lines.length && (isBlockquoteLine(lines[i]) || isBlank(lines[i]))) {
|
|
466
|
-
// Blank line inside blockquote is kept, but only if next line is also blockquoted or we are continuing quote
|
|
467
|
-
if (isBlank(lines[i])) {
|
|
468
|
-
// Peek next: if next is blockquote line, this blank is inside quote
|
|
469
|
-
const nxt = i + 1 < lines.length ? lines[i + 1] : '';
|
|
470
|
-
if (isBlockquoteLine(nxt)) {
|
|
471
|
-
bqLines.push('');
|
|
472
|
-
i++;
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
|
-
// else blank ends blockquote when followed by non-quote content
|
|
476
|
-
// but per CommonMark blank inside quote is allowed even without > marker if we are inside
|
|
477
|
-
// we treat blank as part of quote for recursion
|
|
478
|
-
bqLines.push('');
|
|
479
|
-
i++;
|
|
480
|
-
// If next line is blank and then non-quote, break to let outer parse handle
|
|
481
|
-
let j = i;
|
|
482
|
-
while (j < lines.length && isBlank(lines[j]))
|
|
483
|
-
j++;
|
|
484
|
-
if (j < lines.length && !isBlockquoteLine(lines[j]) && !isBlank(lines[j]))
|
|
485
|
-
break;
|
|
486
|
-
continue;
|
|
487
|
-
}
|
|
488
|
-
const m = lines[i].match(/^ {0,3}>[ \t]?(.*)$/);
|
|
489
|
-
bqLines.push(m ? m[1] : '');
|
|
490
|
-
i++;
|
|
491
|
-
}
|
|
492
|
-
// Trim trailing blank
|
|
493
|
-
while (bqLines.length && '' === bqLines[bqLines.length - 1])
|
|
494
|
-
bqLines.pop();
|
|
495
|
-
const inner = parseBlocks(bqLines, 0, opts, 0).nodes;
|
|
496
|
-
nodes.push({ type: 'blockquote', children: inner });
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
// HTML block — raw line starting with < until blank line
|
|
500
|
-
if (isHtmlBlockLine(line)) {
|
|
501
|
-
const htmlLines = [];
|
|
502
|
-
while (i < lines.length && !isBlank(lines[i])) {
|
|
503
|
-
htmlLines.push(lines[i]);
|
|
504
|
-
i++;
|
|
505
|
-
// HTML blocks typically end at blank line; we let blank consume break outer
|
|
506
|
-
if (i < lines.length && isBlank(lines[i]))
|
|
507
|
-
break;
|
|
508
|
-
// Also break if next line is a clear block start (heading, fence, thematic)
|
|
509
|
-
const nxt = i < lines.length ? lines[i] : '';
|
|
510
|
-
if (isATXHeading(nxt) || isFencedCodeStart(nxt) || isThematicBreak(nxt) || isBlockquoteLine(nxt) || isUnorderedListItem(nxt) || isOrderedListItem(nxt))
|
|
511
|
-
break;
|
|
512
|
-
}
|
|
513
|
-
nodes.push({ type: 'html', value: htmlLines.join('\n') });
|
|
514
|
-
continue;
|
|
515
|
-
}
|
|
516
|
-
// Lists — must be handled together to group consecutive items
|
|
517
|
-
const isUL = isUnorderedListItem(line);
|
|
518
|
-
const isOL = isOrderedListItem(line);
|
|
519
|
-
if (isUL || isOL) {
|
|
520
|
-
const ordered = !!isOL;
|
|
521
|
-
const startNum = isOL ? parseInt(isOL[1], 10) : null;
|
|
522
|
-
const items = [];
|
|
523
|
-
let spread = false;
|
|
524
|
-
// Collect items while next line is list marker of similar type or indented continuation
|
|
525
|
-
while (i < lines.length) {
|
|
526
|
-
const cur = lines[i];
|
|
527
|
-
if (isBlank(cur)) {
|
|
528
|
-
i++;
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
const ulM = isUnorderedListItem(cur);
|
|
532
|
-
const olM = isOrderedListItem(cur);
|
|
533
|
-
const markerMatch = ordered ? olM : ulM;
|
|
534
|
-
// If next line is not the right list kind, see if it's continuation of previous item
|
|
535
|
-
if (!markerMatch) {
|
|
536
|
-
// Continuation: indented by ≥2 spaces or belongs to last item's blocks?
|
|
537
|
-
// Check if last item exists and current line is indented (2+ spaces) and not another block start
|
|
538
|
-
if (items.length && /^ {2,}/.test(cur) && !isATXHeading(cur) && !isFencedCodeStart(cur) && !isThematicBreak(cur) && !isBlockquoteLine(cur) && !isHtmlBlockLine(cur)) {
|
|
539
|
-
// Continuation belongs to last item — append to its raw lines before re-parsing
|
|
540
|
-
// We will instead handle continuations inside item parsing loop below.
|
|
541
|
-
}
|
|
542
|
-
break;
|
|
543
|
-
}
|
|
544
|
-
// Parse one item
|
|
545
|
-
const markerLen = cur.match(/^ {0,3}(?:[*+-]|\d+[.)])[ \t]+/)[0].length;
|
|
546
|
-
const firstContent = ordered ? olM[3] : ulM[2];
|
|
547
|
-
const itemLines = [];
|
|
548
|
-
itemLines.push(firstContent);
|
|
549
|
-
i++;
|
|
550
|
-
// Collect following lines belonging to this item: blank lines, indented content, paragraph continuation
|
|
551
|
-
let blankRun = 0;
|
|
552
|
-
while (i < lines.length) {
|
|
553
|
-
const nxt = lines[i];
|
|
554
|
-
if (isBlank(nxt)) {
|
|
555
|
-
// Blank inside list item is kept — but peek if next line starts a new item
|
|
556
|
-
const peek = i + 1 < lines.length ? lines[i + 1] : null;
|
|
557
|
-
const peekIsList = peek && (isUnorderedListItem(peek) || isOrderedListItem(peek));
|
|
558
|
-
if (peekIsList)
|
|
559
|
-
break; // blank before next item → end of this item
|
|
560
|
-
// If loose, keep blank
|
|
561
|
-
itemLines.push('');
|
|
562
|
-
blankRun++;
|
|
563
|
-
i++;
|
|
564
|
-
continue;
|
|
565
|
-
}
|
|
566
|
-
// If next line is a list marker at same base indent, it is the next item
|
|
567
|
-
if ((isUnorderedListItem(nxt) || isOrderedListItem(nxt)) && !/^ {4,}/.test(nxt))
|
|
568
|
-
break;
|
|
569
|
-
// If line is indented ≥2 spaces beyond base? Actually CommonMark list content indent = markerLen or 4?
|
|
570
|
-
// Simplify: if line starts with spaces and not a blockquote/thematic/heading/fenced, treat as item continuation.
|
|
571
|
-
if (/^ {2,}/.test(nxt) || /^\t/.test(nxt)) {
|
|
572
|
-
// Strip one level of indentation (up to 4 spaces or 1 tab) but keep as content
|
|
573
|
-
let stripped = nxt;
|
|
574
|
-
if (stripped.startsWith('\t'))
|
|
575
|
-
stripped = stripped.slice(1);
|
|
576
|
-
else {
|
|
577
|
-
// Remove up to markerLen or 4 spaces? Use 2 as common
|
|
578
|
-
const lead = stripped.match(/^ +/)[0].length;
|
|
579
|
-
stripped = stripped.slice(Math.min(4, lead));
|
|
580
|
-
}
|
|
581
|
-
// Check if stripped line looks like an inner blockquote/list etc. — keep as is for inner parse
|
|
582
|
-
itemLines.push(stripped);
|
|
583
|
-
i++;
|
|
584
|
-
blankRun = 0;
|
|
585
|
-
continue;
|
|
586
|
-
}
|
|
587
|
-
// If line looks like another block start (heading, code, thematic, blockquote, html) at base, break?
|
|
588
|
-
if (isATXHeading(nxt) || isFencedCodeStart(nxt) || isThematicBreak(nxt) || isBlockquoteLine(nxt) || isHtmlBlockLine(nxt)) {
|
|
589
|
-
// Inside list items, these are content, not outer blocks, if indented earlier they'd have been taken.
|
|
590
|
-
// Unindented block inside list: check if we already have content — if so it may terminate list.
|
|
591
|
-
// For tight lists, indented blocks are needed. Simplify: break to let items close, next iteration will handle as outer block.
|
|
592
|
-
break;
|
|
593
|
-
}
|
|
594
|
-
// Otherwise treat as loose paragraph continuation? Only if previous line wasn't blank?
|
|
595
|
-
if (blankRun)
|
|
596
|
-
break;
|
|
597
|
-
// Unindented paragraph-like line inside item (tight list paragraph extension)
|
|
598
|
-
// Only if item already has one line and next line is not a list marker: join as continuation
|
|
599
|
-
itemLines.push(nxt);
|
|
600
|
-
i++;
|
|
601
|
-
}
|
|
602
|
-
// Determine if item had blank line inside → affects list spread (loose)
|
|
603
|
-
const itemHadBlank = itemLines.includes('');
|
|
604
|
-
if (itemHadBlank)
|
|
605
|
-
spread = true;
|
|
606
|
-
// Trim trailing blanks
|
|
607
|
-
while (itemLines.length && '' === itemLines[itemLines.length - 1])
|
|
608
|
-
itemLines.pop();
|
|
609
|
-
// Parse item content as blocks (recursive)
|
|
610
|
-
// If itemLines is single line and contains only inline, it will become a paragraph. CommonMark wraps tight items' paragraph directly.
|
|
611
|
-
const strippedItemSrc = itemLines.join('\n');
|
|
612
|
-
// Avoid empty item
|
|
613
|
-
let itemBlocks = [];
|
|
614
|
-
if (strippedItemSrc.trim()) {
|
|
615
|
-
itemBlocks = parseBlocks(strippedItemSrc.split(/\r?\n/), 0, opts, 0).nodes;
|
|
616
|
-
}
|
|
617
|
-
// Tight lists: single paragraph items are collapsed? Our AST keeps paragraph wrapper regardless — callers handle spread.
|
|
618
|
-
// Record spread on item if its trailing had blank
|
|
619
|
-
items.push({ type: 'listItem', spread: itemHadBlank, children: itemBlocks });
|
|
620
|
-
}
|
|
621
|
-
nodes.push({ type: 'list', ordered, start: startNum, spread, children: items });
|
|
622
|
-
continue;
|
|
623
|
-
}
|
|
624
|
-
// Paragraph — collect until blank or block start, handling setext underline
|
|
625
|
-
{
|
|
626
|
-
const paraLines = [];
|
|
627
|
-
while (i < lines.length && !isBlank(lines[i])) {
|
|
628
|
-
const nxt = lines[i];
|
|
629
|
-
// Break if next line is a block that cannot be inside a paragraph without blank line.
|
|
630
|
-
// Setext underline (=== or ---) looks like a thematic break but must be kept
|
|
631
|
-
// as the last para line so the post-loop Setext check can turn the paragraph
|
|
632
|
-
// into a heading. Only break on thematic breaks that are NOT Setext underlines.
|
|
633
|
-
const isSetext = null !== isSetextUnderline(nxt);
|
|
634
|
-
const isThematicButNotSetext = isThematicBreak(nxt) && !isSetext;
|
|
635
|
-
if (paraLines.length && (isATXHeading(nxt) ||
|
|
636
|
-
isFencedCodeStart(nxt) ||
|
|
637
|
-
isThematicButNotSetext ||
|
|
638
|
-
isBlockquoteLine(nxt) ||
|
|
639
|
-
isUnorderedListItem(nxt) ||
|
|
640
|
-
isOrderedListItem(nxt) ||
|
|
641
|
-
isHtmlBlockLine(nxt)))
|
|
642
|
-
break;
|
|
643
|
-
// Also check for indented code (not part of para)
|
|
644
|
-
if (paraLines.length && isIndentedCodeLine(nxt))
|
|
645
|
-
break;
|
|
646
|
-
paraLines.push(nxt);
|
|
647
|
-
i++;
|
|
648
|
-
}
|
|
649
|
-
// Check for Setext heading: last line could be underline if para is 1-2 lines of text
|
|
650
|
-
// A setext underline must not be thematic break-duplicate. Pattern: === or ---
|
|
651
|
-
if (paraLines.length) {
|
|
652
|
-
const last = paraLines[paraLines.length - 1];
|
|
653
|
-
const underline = isSetextUnderline(last);
|
|
654
|
-
if (underline && paraLines.length >= 2) {
|
|
655
|
-
const depth = '=' === underline[1][0] ? 1 : 2;
|
|
656
|
-
const hdText = paraLines.slice(0, -1).join('\n');
|
|
657
|
-
nodes.push({ type: 'heading', depth, children: parseInline(hdText.trim(), opts) });
|
|
658
|
-
continue;
|
|
659
|
-
}
|
|
660
|
-
// Single-line setext: need preceding para line + underline on next line? Our peek included underline line already as part of paraLines.
|
|
661
|
-
// If paraLines is 2 lines where second is underline, the above would have depth. If paraLines is 1 line underline alone, it was already handled as thematic break earlier? But `---` alone is thematic break, not setext with no para above.
|
|
662
|
-
// So ok.
|
|
663
|
-
}
|
|
664
|
-
const paraText = paraLines.join('\n').trimEnd();
|
|
665
|
-
if ('' !== paraText) {
|
|
666
|
-
nodes.push({ type: 'paragraph', children: parseInline(paraText, opts) });
|
|
667
|
-
}
|
|
668
|
-
continue;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
return { nodes, next: i };
|
|
75
|
+
/** Parse Markdown to the native CommonMark node tree. */
|
|
76
|
+
function parseTree(src, opts) {
|
|
77
|
+
return (0, commonmark_ts_1.parse)(src, (0, options_ts_1.resolveOptions)(opts));
|
|
672
78
|
}
|
|
673
79
|
// ---------------------------------------------------------------------------
|
|
674
|
-
// Plugin wiring
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
//
|
|
80
|
+
// Plugin wiring
|
|
81
|
+
//
|
|
82
|
+
// The `markdown` rule's `bo` action reads the whole source via `ctx.src()` and
|
|
83
|
+
// hands it to the parser; the alts then consume the token stream so the
|
|
84
|
+
// engine's trailing-content check (lex must end at #ZZ) passes. `#AA` is the
|
|
85
|
+
// ANY-token wildcard and `r:'markdown'` loops at the same depth, consuming one
|
|
86
|
+
// token per iteration until only #ZZ remains.
|
|
678
87
|
const Markdown = (tn, options) => {
|
|
679
|
-
const opts =
|
|
680
|
-
gfm: options?.gfm ?? true,
|
|
681
|
-
breaks: options?.breaks ?? false,
|
|
682
|
-
};
|
|
683
|
-
// No custom token descriptions needed for prose — the block scanner
|
|
684
|
-
// reads ctx.src() directly; railroad will show the single `markdown`
|
|
685
|
-
// rule as the entry point.
|
|
686
|
-
// Start rule
|
|
88
|
+
const opts = (0, options_ts_1.resolveOptions)(options);
|
|
687
89
|
tn.options({ rule: { start: 'markdown' } });
|
|
688
|
-
//
|
|
689
|
-
//
|
|
690
|
-
//
|
|
691
|
-
//
|
|
692
|
-
// - comment: `# heading` must not be swallowed as a comment line
|
|
693
|
-
// - number/value: `1. list` must stay as text, not a number/keyword
|
|
90
|
+
// The parser reads `ctx.src()` directly rather than the token stream, so
|
|
91
|
+
// disable the lexers that would corrupt Markdown syntax before it gets
|
|
92
|
+
// there: backtick code spans lex as unterminated strings, `# heading` as a
|
|
93
|
+
// comment, and `1. list` as a number.
|
|
694
94
|
tn.options({
|
|
695
95
|
string: { lex: false },
|
|
696
96
|
comment: { lex: false },
|
|
@@ -698,31 +98,12 @@ const Markdown = (tn, options) => {
|
|
|
698
98
|
value: { lex: false },
|
|
699
99
|
lex: { emptyResult: { type: 'document', children: [] } },
|
|
700
100
|
});
|
|
701
|
-
// Grammar is intentionally tiny — see `grammarText` above for the
|
|
702
|
-
// railroad-visible spec. Install it first so `rule('markdown')` can
|
|
703
|
-
// clear/override it cleanly.
|
|
704
|
-
// We parse the embedded JSON via a throwaway Tabnas to get a GrammarSpec,
|
|
705
|
-
// then install it with the ref map.
|
|
706
|
-
// To avoid a jsonic dependency, install inline via tn.grammar instead of
|
|
707
|
-
// parsing jsonic text — simpler: just define the rule directly.
|
|
708
101
|
const refs = {
|
|
709
102
|
'@markdown-bo': (r, ctx) => {
|
|
710
103
|
const src = ctx.src();
|
|
711
|
-
// Preserve empty handling: "" → empty document
|
|
712
|
-
if ('' === src || /^[\r\n\t ]*$/.test(src)) {
|
|
713
|
-
r.node = { type: 'document', children: [] };
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
104
|
r.node = parseDocument(src, opts);
|
|
717
105
|
},
|
|
718
106
|
};
|
|
719
|
-
// Define the `markdown` rule: `bo` builds the document from
|
|
720
|
-
// `ctx.src()`; the alts then consume the underlying token stream so
|
|
721
|
-
// the engine's trailing-content check (lex must end at #ZZ) passes.
|
|
722
|
-
// `#AA` is the ANY-token wildcard and `r:'markdown'` loops at the
|
|
723
|
-
// same depth, consuming one token per iteration until only #ZZ
|
|
724
|
-
// remains. The empty alt `{}`
|
|
725
|
-
// then falls through to the close phase, which matches #ZZ.
|
|
726
107
|
tn.rule('markdown', (rs) => {
|
|
727
108
|
return rs
|
|
728
109
|
.clear()
|
|
@@ -730,9 +111,21 @@ const Markdown = (tn, options) => {
|
|
|
730
111
|
.open([{ s: '#AA', r: 'markdown' }, {}])
|
|
731
112
|
.close([{ s: '#ZZ' }, {}]);
|
|
732
113
|
});
|
|
733
|
-
tn.markdown = {
|
|
114
|
+
tn.markdown = {
|
|
115
|
+
parseDocument: (t) => parseDocument(t, opts),
|
|
116
|
+
parseInline: (t) => parseInline(t, opts),
|
|
117
|
+
toHtml: (t) => toHtml(t, opts),
|
|
118
|
+
parseTree: (t) => parseTree(t, opts),
|
|
119
|
+
};
|
|
734
120
|
};
|
|
735
121
|
exports.Markdown = Markdown;
|
|
736
|
-
// Defaults for `new Tabnas().use(Markdown, opts?)` merging
|
|
737
122
|
Markdown.defaults = { gfm: true, breaks: false };
|
|
123
|
+
var node_ts_1 = require("./node.js");
|
|
124
|
+
Object.defineProperty(exports, "MdNode", { enumerable: true, get: function () { return node_ts_1.MdNode; } });
|
|
125
|
+
// Re-exported so the package entry offers the same pair as the Go package,
|
|
126
|
+
// where `RenderHTML` is exported alongside `ToHTML`. Without this, rendering a
|
|
127
|
+
// tree you built or mutated yourself meant a deep import of `commonmark.ts`,
|
|
128
|
+
// and the two runtimes' public surfaces did not match.
|
|
129
|
+
var html_ts_2 = require("./html.js");
|
|
130
|
+
Object.defineProperty(exports, "renderHTML", { enumerable: true, get: function () { return html_ts_2.renderHTML; } });
|
|
738
131
|
//# sourceMappingURL=markdown.js.map
|