@thi.ng/hiccup-markdown 2.1.49 → 3.0.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/CHANGELOG.md +106 -1
- package/README.md +261 -169
- package/api.d.ts +348 -17
- package/emoji.d.ts +11 -0
- package/emoji.js +1914 -0
- package/package.json +8 -7
- package/parse.d.ts +73 -5
- package/parse.js +521 -211
- package/parser.d.ts +65 -0
- package/parser.js +379 -0
- package/serialize.d.ts +2 -1
- package/serialize.js +43 -32
package/parse.js
CHANGED
|
@@ -1,223 +1,533 @@
|
|
|
1
|
+
// thing:no-export
|
|
1
2
|
import { peek } from "@thi.ng/arrays/peek";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
3
|
+
import { isArray } from "@thi.ng/checks/is-array";
|
|
4
|
+
import { isPlainObject } from "@thi.ng/checks/is-plain-object";
|
|
5
|
+
import { isPrimitive } from "@thi.ng/checks/is-primitive";
|
|
6
|
+
import { isString } from "@thi.ng/checks/is-string";
|
|
7
|
+
import { DEFAULT, defmulti } from "@thi.ng/defmulti";
|
|
8
|
+
import { EMOJI } from "@thi.ng/emoji/emoji";
|
|
9
|
+
import { defContext } from "@thi.ng/parse/context";
|
|
10
|
+
import { defGrammar } from "@thi.ng/parse/grammar";
|
|
11
|
+
import { escapeEntities } from "@thi.ng/strings/entities";
|
|
12
|
+
import { slugifyGH } from "@thi.ng/strings/slugify";
|
|
13
|
+
export const GRAMMAR = defGrammar(`
|
|
14
|
+
DNL1: <DNL>+ => discard ;
|
|
15
|
+
DNL2: <NL>{2,} ;
|
|
16
|
+
lbr: '\\\\'! <NL>! ;
|
|
17
|
+
esc: '\\\\'! ( '<' | '[' | '_' | '*' | '\`' | '~' | ':' ) ;
|
|
18
|
+
inlinedelim: ( "![" | '[' | "**" | '_' | '\`' | '<' | '\\\\' | "~~" | " :" | <lbr> ) ;
|
|
19
|
+
delim: ( <inlinedelim> | <DNL2> ) ;
|
|
20
|
+
delim1: ( <inlinedelim> | <NL> ) ;
|
|
21
|
+
body: .(?-<delim>!) => join ;
|
|
22
|
+
body1: .(?-<delim1>!) => join ;
|
|
23
|
+
|
|
24
|
+
wikiref: "[["! .(?+"]]"!) => join ;
|
|
25
|
+
fnref: "[^"! <label> ;
|
|
26
|
+
fnote: <LSTART> "[^"! <fnlabel> <WS1> <para> ;
|
|
27
|
+
fnlabel: .(?+"]:"!) => join ;
|
|
28
|
+
label: .(?+']'!) => join ;
|
|
29
|
+
target: .(?+')'!) => join ;
|
|
30
|
+
link: '['! <linklabel> '('! <target> ;
|
|
31
|
+
linkref: '['! <linklabel> '['! <label> ;
|
|
32
|
+
linkdef: <LSTART> '['! <label> ':'! <WS1> <ldtarget> ;
|
|
33
|
+
ldtarget: .(?+<DNL1>) => join ;
|
|
34
|
+
linklabel: (<img> | <bold> | <italic> | <strike> | <code> | <emoji> | <linkbody>)* ']'! ;
|
|
35
|
+
linkdelim: ( "![" | '[' | "**" | '_' | "~~" | '\`' | ']') ;
|
|
36
|
+
linkbody: .(?-<linkdelim>!) => join ;
|
|
37
|
+
img: "!["! <label> '('! <target> ;
|
|
38
|
+
bold: "**"! (<wikiref> | <img> | <fnref> | <linkref> | <link> | <italic> | <strike> | <code> | <emoji> | <body1>)* "**"! ;
|
|
39
|
+
italic: '_'! (<wikiref> | <img> | <fnref> | <linkref> | <link> | <bold> | <strike> | <code> | <emoji> | <body1>)* '_'! ;
|
|
40
|
+
strike: "~~"! (<wikiref> | <img> | <fnref> | <linkref> | <link> | <bold> | <italic> | <code> | <emoji> | <body1>)* "~~"! ;
|
|
41
|
+
code: '\`'! .(?+'\`'!) => join ;
|
|
42
|
+
kbd: "<kbd>"! .(?+"</kbd>"!) => join ;
|
|
43
|
+
emoji: ' '? ':'! (<ALPHA_NUM> | '+' | '-')(?+':'!) => join ;
|
|
44
|
+
para: (<wikiref> | <img> | <fnref> | <linkref> | <link> | <bold> | <italic> | <strike> | <code> | <kbd> | <emoji> | <lbr> | <esc> | <body>)* <DNL2>! ;
|
|
45
|
+
|
|
46
|
+
hdlevel: '#'+ => count ;
|
|
47
|
+
hd: <LSTART> <hdlevel> <WS0>
|
|
48
|
+
(<wikiref> | <img> | <fnref> | <link> | <bold> | <italic> | <strike> | <code> | <emoji> | <body1> )* <DNL1> ;
|
|
49
|
+
|
|
50
|
+
lilevel: ' '* => count ;
|
|
51
|
+
uint: <DIGIT>+ => int ;
|
|
52
|
+
ulid: <DNL> <WS0> '-'! ;
|
|
53
|
+
olid: <DNL> <WS0> <DIGIT>+! '.'! ;
|
|
54
|
+
lidelim: ( <delim> | <ulid> | <olid> ) ;
|
|
55
|
+
libody: .(?-<lidelim>!) => join ;
|
|
56
|
+
todo: '['! [ xX] ']'! <WS1> => hoistR ;
|
|
57
|
+
ulitem: <LSTART> <lilevel> "- "! <todo>?
|
|
58
|
+
(<wikiref> | <img> | <fnref> | <link> | <bold> | <italic> | <strike> | <code> | <emoji> | <libody> )* <DNL> ;
|
|
59
|
+
olitem: <LSTART> <lilevel> <uint> ". "! <todo>?
|
|
60
|
+
(<wikiref> | <img> | <fnref> | <link> | <bold> | <italic> | <strike> | <code> | <emoji> | <libody> )* <DNL> ;
|
|
61
|
+
list: (<ulitem> | <olitem>)+ <DNL1> ;
|
|
62
|
+
|
|
63
|
+
cbdelim: <LSTART> "\`\`\`"! ;
|
|
64
|
+
codeblock: <cbdelim>! <codemeta> <codebody> <DNL1> ;
|
|
65
|
+
codemeta: .(?+<NL>!) => join ;
|
|
66
|
+
codebody: .(?+<cbdelim>) => join ;
|
|
67
|
+
|
|
68
|
+
customdelim: <LSTART> ":::"! ;
|
|
69
|
+
customblock: <customdelim>! <custommeta> <custombody> <DNL1> ;
|
|
70
|
+
custommeta: .(?+<NL>!) => join ;
|
|
71
|
+
custombody: .(?+<customdelim>) => join ;
|
|
72
|
+
|
|
73
|
+
metablock: <LSTART> "{{{"! <metabody> <DNL1> ;
|
|
74
|
+
metabody: .(?+<metaend>!) => join ;
|
|
75
|
+
metaend: "}}}" <LEND> ;
|
|
76
|
+
|
|
77
|
+
bqlevel: '>'+ => count ;
|
|
78
|
+
bqline: <LSTART> <bqlevel> ' '?!
|
|
79
|
+
(<wikiref> | <img> | <fnref> | <link> | <bold> | <italic> | <strike> | <code> | <emoji> | <bqlbr> | <body1>)* <DNL> ;
|
|
80
|
+
bqlbr: '\\\\'!(?-<DNL>) ;
|
|
81
|
+
bquote: <bqline>+ <DNL1> ;
|
|
82
|
+
|
|
83
|
+
tdelim: (<inlinedelim> | '|' ) ;
|
|
84
|
+
tbody: .(?-<tdelim>!) => join ;
|
|
85
|
+
tcell: <WS0> (<wikiref> | <img> | <fnref> | <link> | <bold> | <italic> | <strike> | <code> | <emoji> | <tbody> )* '|'! ;
|
|
86
|
+
trow: <LSTART> '|'! <tcell>(?+<DNL>) ;
|
|
87
|
+
table: <trow>+ <DNL1> ;
|
|
88
|
+
|
|
89
|
+
hr: "--" '-'+ <DNL1> => join ;
|
|
90
|
+
|
|
91
|
+
main: <WS0> (<hd> | <list> | <bquote> | <codeblock> | <customblock> | <metablock> | <table> | <hr> | <fnote> | <linkdef> | <para>)* ;
|
|
92
|
+
`);
|
|
93
|
+
export const DEFAULT_TAG_TRANSFORMS = {
|
|
94
|
+
bold: (_, body) => ["strong", {}, ...body],
|
|
95
|
+
blockquote: (_, body, meta) => ["blockquote", withMeta({}, meta), ...body],
|
|
96
|
+
br: () => ["br", {}],
|
|
97
|
+
code: (_, body) => ["code", {}, body],
|
|
98
|
+
codeblock: (_, lang, __head, body, meta) => [
|
|
99
|
+
"pre",
|
|
100
|
+
withMeta({ data: { lang }, __head }, meta),
|
|
101
|
+
["code", {}, body],
|
|
102
|
+
],
|
|
103
|
+
custom: (_, type, __head, body, meta) => [
|
|
104
|
+
"custom",
|
|
105
|
+
withMeta({ type, __head }, meta),
|
|
106
|
+
body,
|
|
107
|
+
],
|
|
108
|
+
emoji: (_, id) => EMOJI[id] || id,
|
|
109
|
+
footnote: (_, id, body, meta) => [
|
|
110
|
+
"li",
|
|
111
|
+
withMeta({ id: `fn-${id}` }, meta),
|
|
112
|
+
["sup", {}, `[${id}] `],
|
|
113
|
+
...body,
|
|
114
|
+
" ",
|
|
115
|
+
["a", { href: `#fnref-${id}` }, "↩︎"],
|
|
116
|
+
],
|
|
117
|
+
footnoteRef: (_, id) => [
|
|
118
|
+
"sup",
|
|
119
|
+
{},
|
|
120
|
+
["a", { id: `fnref-${id}`, href: `#fn-${id}` }, `[${id}]`],
|
|
121
|
+
],
|
|
122
|
+
footnoteWrapper: (_, notes) => [
|
|
123
|
+
"ul",
|
|
124
|
+
{ id: "footnotes" },
|
|
125
|
+
...Object.keys(notes)
|
|
126
|
+
.sort()
|
|
127
|
+
.map((id) => notes[id]),
|
|
128
|
+
],
|
|
129
|
+
heading: (_, level, id, body, meta) => [
|
|
130
|
+
level > 6 ? "p" : `h${level}`,
|
|
131
|
+
withMeta({ id }, meta),
|
|
132
|
+
...body,
|
|
133
|
+
],
|
|
134
|
+
hr: (_, __length, meta) => ["hr", withMeta({ __length }, meta)],
|
|
135
|
+
img: (_, alt, src, title) => ["img", { src, alt, title }],
|
|
136
|
+
italic: (_, body) => ["em", {}, ...body],
|
|
137
|
+
kbd: (_, key) => ["kbd", {}, key],
|
|
138
|
+
link: (_, href, title, body) => ["a", { href, title }, ...body],
|
|
139
|
+
linkRef: (ctx, refID, body) => [
|
|
140
|
+
"a",
|
|
141
|
+
{
|
|
142
|
+
href: () => ctx.linkRefs[refID]?.[0],
|
|
143
|
+
title: () => ctx.linkRefs[refID]?.[1],
|
|
144
|
+
},
|
|
145
|
+
...body,
|
|
146
|
+
],
|
|
147
|
+
meta: (_, body) => body,
|
|
148
|
+
olitem: (_, attribs, index, body) => [
|
|
149
|
+
"li",
|
|
150
|
+
{ ...attribs, __index: index },
|
|
151
|
+
...body,
|
|
152
|
+
],
|
|
153
|
+
ol: (_, items, meta) => ["ol", withMeta({}, meta), ...items],
|
|
154
|
+
para: (_, body, meta) => ["p", withMeta({}, meta), ...body],
|
|
155
|
+
strike: (_, body) => ["s", {}, ...body],
|
|
156
|
+
table: (_, __align, head, rows, meta) => [
|
|
157
|
+
"table",
|
|
158
|
+
withMeta({ __align }, meta),
|
|
159
|
+
["thead", {}, head],
|
|
160
|
+
["tbody", {}, ...rows],
|
|
161
|
+
],
|
|
162
|
+
tableCell: (_, body) => ["td", {}, ...body],
|
|
163
|
+
tableHead: (_, body) => ["th", {}, ...body],
|
|
164
|
+
tableRow: (_, __, cells) => ["tr", {}, ...cells],
|
|
165
|
+
ul: (_, items, meta) => ["ul", withMeta({}, meta), ...items],
|
|
166
|
+
ulitem: (_, attribs, body) => ["li", attribs, ...body],
|
|
167
|
+
wikiref: (_, id, label) => [
|
|
168
|
+
"a",
|
|
169
|
+
{ class: "wikiref", href: encodeURI(id.replace(/\s+/g, "_")) },
|
|
170
|
+
label || id,
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
export class ParseError extends Error {
|
|
174
|
+
constructor(state) {
|
|
175
|
+
super(state
|
|
176
|
+
? `stopped line: ${state.l}, col: ${state.c} (pos: ${state.p})`
|
|
177
|
+
: undefined);
|
|
178
|
+
this.state = state;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
12
181
|
/**
|
|
13
|
-
*
|
|
182
|
+
* Parses given Markdown source string into a tree structure defined by given
|
|
183
|
+
* {@link TagTransforms}.
|
|
184
|
+
*
|
|
185
|
+
* @remarks
|
|
186
|
+
* The tag transforms are optional and the default implementations can be
|
|
187
|
+
* overwritten on tag-by-tag basis. The default transforms yield a simple hiccup
|
|
188
|
+
* tree (aka each tag will be an array in the form: `["tagname", {...},
|
|
189
|
+
* ...body]`).
|
|
190
|
+
*
|
|
191
|
+
* See [thi.ng/hiccup](https://thi.ng/hiccup) and related packages for further
|
|
192
|
+
* details.
|
|
193
|
+
*
|
|
194
|
+
* @param src
|
|
195
|
+
* @param opts
|
|
14
196
|
*/
|
|
15
|
-
|
|
16
|
-
(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
State[State["IMG"] = 10] = "IMG";
|
|
28
|
-
State[State["LINK"] = 11] = "LINK";
|
|
29
|
-
State[State["LI"] = 12] = "LI";
|
|
30
|
-
State[State["PARA"] = 13] = "PARA";
|
|
31
|
-
State[State["START"] = 14] = "START";
|
|
32
|
-
State[State["START_CODEBLOCK"] = 15] = "START_CODEBLOCK";
|
|
33
|
-
State[State["STRIKE"] = 16] = "STRIKE";
|
|
34
|
-
State[State["STRONG"] = 17] = "STRONG";
|
|
35
|
-
State[State["TABLE"] = 18] = "TABLE";
|
|
36
|
-
})(State || (State = {}));
|
|
197
|
+
export const parse = (src, { tags, opts, linkRefs, logger, } = {}) => {
|
|
198
|
+
const parseCtx = parseRaw(src, opts?.retain);
|
|
199
|
+
const mdCtx = defTransformContext(tags, opts, linkRefs, logger);
|
|
200
|
+
const result = [];
|
|
201
|
+
transformScope(parseCtx.root, mdCtx, result);
|
|
202
|
+
return {
|
|
203
|
+
result,
|
|
204
|
+
ctx: mdCtx,
|
|
205
|
+
complete: !!parseCtx.done,
|
|
206
|
+
state: parseCtx.state,
|
|
207
|
+
};
|
|
208
|
+
};
|
|
37
209
|
/**
|
|
38
|
-
*
|
|
210
|
+
* 1st stage of the parsing (with out result transformations). This calls the
|
|
211
|
+
* `main` rule of the provided parse {@link GRAMMAR} and returns a parse
|
|
212
|
+
* context, incl. the raw abstract syntax tree of the parsed document. If
|
|
213
|
+
* parsing failed entirely (due to invalid input), throws a {@link ParseError}.
|
|
214
|
+
*
|
|
215
|
+
* @remarks
|
|
216
|
+
* Note: Even if the function returns a result, parsing might only have
|
|
217
|
+
* partially successful (can be checked via the [`.done`
|
|
218
|
+
* flag](https://docs.thi.ng/umbrella/parse/classes/ParseContext.html#done)).
|
|
219
|
+
*
|
|
220
|
+
* This function is only for advanced use. Mostly you'll probably want to use
|
|
221
|
+
* the main {@link parse} function instead.
|
|
222
|
+
*
|
|
223
|
+
* @param src
|
|
224
|
+
* @param retain
|
|
39
225
|
*/
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
heading: (level, xs) => [level < 7 ? `h${level}` : "p", {}, ...xs],
|
|
46
|
-
hr: () => ["hr", {}],
|
|
47
|
-
img: (src, alt) => ["img", { src, alt }],
|
|
48
|
-
li: (xs) => ["li", {}, ...xs],
|
|
49
|
-
link: (href, body) => ["a", { href }, body],
|
|
50
|
-
list: (type, xs) => [type, {}, ...xs],
|
|
51
|
-
paragraph: (xs) => ["p", {}, ...xs],
|
|
52
|
-
strong: (body) => ["strong", {}, body],
|
|
53
|
-
strike: (body) => ["del", {}, body],
|
|
54
|
-
table: (rows) => ["table", {}, ["tbody", {}, ...rows]],
|
|
55
|
-
td: (_, xs) => ["td", {}, ...xs],
|
|
56
|
-
tr: (_, xs) => ["tr", {}, ...xs],
|
|
57
|
-
};
|
|
58
|
-
const BQUOTE = ">";
|
|
59
|
-
const CODE = "`";
|
|
60
|
-
const CODEBLOCK = "```";
|
|
61
|
-
const CODEBLOCK_END = "\n```\n";
|
|
62
|
-
const EM = "_";
|
|
63
|
-
const HD = "#";
|
|
64
|
-
const HR = "-";
|
|
65
|
-
const IMG = "![";
|
|
66
|
-
const LI = "- ";
|
|
67
|
-
const LINK_LABEL = "[";
|
|
68
|
-
const LINK_LABEL_END = "]";
|
|
69
|
-
const LINK_HREF = "(";
|
|
70
|
-
const LINK_HREF_END = ")";
|
|
71
|
-
const NL = "\n";
|
|
72
|
-
const STRIKE = "~~";
|
|
73
|
-
const STRONG = "**";
|
|
74
|
-
const TD = "|";
|
|
75
|
-
// state / context handling helpers
|
|
76
|
-
const transition = (ctx, id) => {
|
|
77
|
-
ctx.children = [];
|
|
78
|
-
ctx.body = "";
|
|
79
|
-
return [id];
|
|
80
|
-
};
|
|
81
|
-
const push = (id, next) => (ctx) => {
|
|
82
|
-
ctx.stack.push({ id, children: ctx.children.concat(ctx.body) });
|
|
83
|
-
return transition(ctx, next);
|
|
226
|
+
export const parseRaw = (src, retain = false) => {
|
|
227
|
+
const ctx = defContext(src + "\n\n", { retain });
|
|
228
|
+
if (!GRAMMAR.rules.main(ctx))
|
|
229
|
+
throw new ParseError(ctx.state);
|
|
230
|
+
return ctx;
|
|
84
231
|
};
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
232
|
+
export const defTransformContext = (tags, opts, linkRefs, logger) => ({
|
|
233
|
+
footnotes: {},
|
|
234
|
+
headings: [],
|
|
235
|
+
linkRefs: linkRefs || {},
|
|
236
|
+
hasFootnotes: false,
|
|
237
|
+
logger,
|
|
238
|
+
meta: null,
|
|
239
|
+
row: 0,
|
|
240
|
+
opts: {
|
|
241
|
+
escape: false,
|
|
242
|
+
retain: false,
|
|
243
|
+
...opts,
|
|
244
|
+
},
|
|
245
|
+
tags: { ...DEFAULT_TAG_TRANSFORMS, ...tags },
|
|
246
|
+
});
|
|
247
|
+
/**
|
|
248
|
+
* Polymorphic & recursive parse scope/node transformation function. Takes a
|
|
249
|
+
* single scope, context and accumulator array, then calls itself recursively
|
|
250
|
+
* for any child scopes and passes relevant data to its user defined
|
|
251
|
+
* {@link TagTransforms} handler and adds result to the accumulator array.
|
|
252
|
+
*
|
|
253
|
+
*/
|
|
254
|
+
export const transformScope = defmulti((x, ctx) => {
|
|
255
|
+
ctx.logger && ctx.logger.debug(x);
|
|
256
|
+
return x.id;
|
|
257
|
+
}, {
|
|
258
|
+
body1: "body",
|
|
259
|
+
bqlbr: "lbr",
|
|
260
|
+
bqline: "repeat0",
|
|
261
|
+
label: "body",
|
|
262
|
+
libody: "body",
|
|
263
|
+
linkbody: "body",
|
|
264
|
+
main: "root",
|
|
265
|
+
repeat1: "repeat0",
|
|
266
|
+
tbody: "body",
|
|
267
|
+
}, {
|
|
268
|
+
[DEFAULT]: (scope, ctx) => {
|
|
269
|
+
throw new Error(`unknown ID: ${scope.id}, ctx: ${JSON.stringify(ctx)}`);
|
|
270
|
+
},
|
|
271
|
+
root: ({ children }, ctx, acc) => {
|
|
272
|
+
if (!children)
|
|
273
|
+
return;
|
|
274
|
+
transformScope(children[0], ctx, acc);
|
|
275
|
+
if (ctx.hasFootnotes) {
|
|
276
|
+
__collect(acc, ctx.tags.footnoteWrapper(ctx, ctx.footnotes));
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
main: (scope, ctx, acc) => transformScope(scope.children[0], ctx, acc),
|
|
280
|
+
repeat0: (scope, ctx, acc) => scope.children && __children(ctx, scope.children, acc),
|
|
281
|
+
body: (scope, ctx, acc) => __collect(acc, __escape(ctx, scope.result)),
|
|
282
|
+
bold: (scope, ctx, acc) => __collect(acc, ctx.tags.bold(ctx, __children(ctx, scope.children))),
|
|
283
|
+
bquote: (scope, ctx, acc) => {
|
|
284
|
+
const stack = [[]];
|
|
285
|
+
const children = scope.children[0].children;
|
|
286
|
+
const $unwind = (level) => {
|
|
287
|
+
while (level < stack.length) {
|
|
288
|
+
const nested = stack.pop();
|
|
289
|
+
__collect(peek(stack), ctx.tags.blockquote(ctx, __trimBody(nested)));
|
|
290
|
+
}
|
|
291
|
+
return peek(stack);
|
|
292
|
+
};
|
|
293
|
+
for (let i = 0, n = children.length - 1; i <= n; i++) {
|
|
294
|
+
const [{ result: level }, bqline] = children[i].children;
|
|
295
|
+
let body = peek(stack);
|
|
296
|
+
if (level > stack.length) {
|
|
297
|
+
while (level > stack.length)
|
|
298
|
+
stack.push((body = []));
|
|
299
|
+
}
|
|
300
|
+
else if (level < stack.length) {
|
|
301
|
+
body = $unwind(level);
|
|
302
|
+
}
|
|
303
|
+
else if (body.length) {
|
|
304
|
+
const prev = children[i - 1].children[1];
|
|
305
|
+
if (!bqline.children) {
|
|
306
|
+
__collect(body, ctx.tags.br(ctx));
|
|
307
|
+
__collect(body, ctx.tags.br(ctx));
|
|
308
|
+
}
|
|
309
|
+
else if (prev.children &&
|
|
310
|
+
peek(prev.children).id !== "bqlbr") {
|
|
311
|
+
body.push(" ");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
transformScope(bqline, ctx, body);
|
|
315
|
+
}
|
|
316
|
+
__collect(acc, ctx.tags.blockquote(ctx, __trimBody($unwind(1)), ctx.meta));
|
|
317
|
+
ctx.meta = null;
|
|
318
|
+
},
|
|
319
|
+
code: (scope, ctx, acc) => __collect(acc, ctx.tags.code(ctx, __escape(ctx, scope.result))),
|
|
320
|
+
codeblock: ({ children }, ctx, acc) => {
|
|
321
|
+
const [lang, ...head] = children[0].result.split(" ");
|
|
322
|
+
const body = children[1].result.trim();
|
|
323
|
+
__collect(acc, ctx.tags.codeblock(ctx, lang, head, __escape(ctx, body), ctx.meta));
|
|
324
|
+
ctx.meta = null;
|
|
325
|
+
},
|
|
326
|
+
customblock: ({ children }, ctx, acc) => {
|
|
327
|
+
const [type, ...head] = children[0].result.split(" ");
|
|
328
|
+
__collect(acc, ctx.tags.custom(ctx, type, head, children[1].result.trim(), ctx.meta));
|
|
329
|
+
ctx.meta = null;
|
|
330
|
+
},
|
|
331
|
+
emoji: ({ result }, ctx, acc) => {
|
|
332
|
+
if (result[0] === " ") {
|
|
333
|
+
__collect(acc, " ");
|
|
334
|
+
result = result.substring(1);
|
|
335
|
+
}
|
|
336
|
+
__collect(acc, ctx.tags.emoji(ctx, result));
|
|
337
|
+
},
|
|
338
|
+
esc: (scope, ctx, acc) => acc.push(__escape(ctx, scope.children[0].result)),
|
|
339
|
+
fnote: ({ children }, ctx) => {
|
|
340
|
+
const body = [];
|
|
341
|
+
const id = children[0].result;
|
|
342
|
+
transformScope(children[1].children[0], ctx, body);
|
|
343
|
+
const res = ctx.tags.footnote(ctx, id, body, ctx.meta);
|
|
344
|
+
if (res != null) {
|
|
345
|
+
ctx.hasFootnotes = true;
|
|
346
|
+
ctx.footnotes[id] = res;
|
|
347
|
+
}
|
|
348
|
+
ctx.meta = null;
|
|
349
|
+
},
|
|
350
|
+
fnref: (scope, ctx, acc) => __collect(acc, ctx.tags.footnoteRef(ctx, scope.children[0].result)),
|
|
351
|
+
hd: ({ children }, ctx, acc) => {
|
|
352
|
+
const body = [];
|
|
353
|
+
const level = children[0].result;
|
|
354
|
+
transformScope(children[1], ctx, body);
|
|
355
|
+
ctx.headings.push({ level, body });
|
|
356
|
+
__trimBody(body);
|
|
357
|
+
__collect(acc, ctx.tags.heading(ctx, level, slugifyGH(extractBody(body).join("")), body, ctx.meta));
|
|
358
|
+
ctx.meta = null;
|
|
359
|
+
},
|
|
360
|
+
hr: (scope, ctx, acc) => {
|
|
361
|
+
__collect(acc, ctx.tags.hr(ctx, scope.result.length, ctx.meta));
|
|
362
|
+
ctx.meta = null;
|
|
363
|
+
},
|
|
364
|
+
img: ({ children }, ctx, acc) => __collect(acc, ctx.tags.img(ctx, __escape(ctx, children[0].result.trim()), ...__linkTitle(children[1].result))),
|
|
365
|
+
italic: (scope, ctx, acc) => __collect(acc, ctx.tags.italic(ctx, __children(ctx, scope.children))),
|
|
366
|
+
kbd: (scope, ctx, acc) => __collect(acc, ctx.tags.kbd(ctx, scope.result)),
|
|
367
|
+
lbr: (_, ctx, acc) => __collect(acc, ctx.tags.br(ctx)),
|
|
368
|
+
link: ({ children }, ctx, acc) => __collect(acc, ctx.tags.link(ctx, ...__linkTitle(children[1].result), __children(ctx, children[0].children))),
|
|
369
|
+
linkdef: ({ children }, ctx) => {
|
|
370
|
+
ctx.linkRefs[children[0].result] = __linkTitle(children[1].result);
|
|
371
|
+
},
|
|
372
|
+
linkref: ({ children }, ctx, acc) => __collect(acc, ctx.tags.linkRef(ctx, children[1].result, __children(ctx, children[0].children))),
|
|
373
|
+
list: (scope, ctx, acc) => {
|
|
374
|
+
const children = scope.children[0].children;
|
|
375
|
+
const stack = [
|
|
376
|
+
[children[0].id === "ulitem" ? "ul" : "ol"],
|
|
377
|
+
];
|
|
378
|
+
const levels = [0];
|
|
379
|
+
for (let item of children) {
|
|
380
|
+
const currLevel = item.children[0].result;
|
|
381
|
+
if (currLevel > peek(levels)) {
|
|
382
|
+
const sublist = [item.id === "ulitem" ? "ul" : "ol"];
|
|
383
|
+
const parent = peek(stack);
|
|
384
|
+
parent.length > 1
|
|
385
|
+
? peek(parent).push(sublist)
|
|
386
|
+
: parent.push([
|
|
387
|
+
parent[0] === "ul" ? "ulitem" : "olitem",
|
|
388
|
+
{},
|
|
389
|
+
sublist,
|
|
390
|
+
]);
|
|
391
|
+
stack.push(sublist);
|
|
392
|
+
levels.push(currLevel);
|
|
393
|
+
}
|
|
394
|
+
else if (currLevel < peek(levels)) {
|
|
395
|
+
while (currLevel < peek(levels)) {
|
|
396
|
+
stack.pop();
|
|
397
|
+
levels.pop();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
transformScope(item, ctx, peek(stack));
|
|
401
|
+
}
|
|
402
|
+
const $list = (root, isRoot = false) => ctx.tags[root[0]](ctx, root.slice(1).map($item), isRoot ? ctx.meta : null);
|
|
403
|
+
const $item = (item) => {
|
|
404
|
+
let last = item[item.length - 1];
|
|
405
|
+
if (last[0] === "ul" || last[0] === "ol")
|
|
406
|
+
item[item.length - 1] = $list(last);
|
|
407
|
+
return item[0] === "ulitem"
|
|
408
|
+
? ctx.tags.ulitem(ctx, item[1], item.slice(2))
|
|
409
|
+
: ctx.tags.olitem(ctx, item[1], item[2], item.slice(3));
|
|
410
|
+
};
|
|
411
|
+
__collect(acc, $list(stack[0], true));
|
|
412
|
+
ctx.meta = null;
|
|
413
|
+
},
|
|
414
|
+
metablock: ({ children }, ctx) => {
|
|
415
|
+
ctx.meta = ctx.tags.meta(ctx, children[0].result.trim());
|
|
416
|
+
},
|
|
417
|
+
olitem: ({ children }, ctx, acc) => {
|
|
418
|
+
const body = [];
|
|
419
|
+
transformScope(children[3], ctx, body);
|
|
420
|
+
__collect(acc, [
|
|
421
|
+
"olitem",
|
|
422
|
+
__listItemAttribs(children[2]),
|
|
423
|
+
children[1].result,
|
|
424
|
+
...__trimBody(body),
|
|
425
|
+
]);
|
|
426
|
+
},
|
|
427
|
+
para: (scope, ctx, acc) => {
|
|
428
|
+
__collect(acc, ctx.tags.para(ctx, __trimBody(__children(ctx, scope.children)), ctx.meta));
|
|
429
|
+
ctx.meta = null;
|
|
430
|
+
},
|
|
431
|
+
strike: (scope, ctx, acc) => __collect(acc, ctx.tags.strike(ctx, __children(ctx, scope.children))),
|
|
432
|
+
table: (scope, ctx, acc) => {
|
|
433
|
+
const children = scope.children[0].children;
|
|
434
|
+
const head = [];
|
|
435
|
+
const rows = [];
|
|
436
|
+
ctx.row = 0;
|
|
437
|
+
transformScope(children[0], ctx, head);
|
|
438
|
+
let align;
|
|
439
|
+
if (children.length > 1) {
|
|
440
|
+
align = __columnAlignments(children[1].children[0].children);
|
|
441
|
+
for (let i = 2, n = children.length; i < n; i++) {
|
|
442
|
+
ctx.row = i - 1;
|
|
443
|
+
transformScope(children[i], ctx, rows);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
align = new Array(children[0].children[0].children.length).fill("left");
|
|
448
|
+
}
|
|
449
|
+
__collect(acc, ctx.tags.table(ctx, align, head[0], rows, ctx.meta));
|
|
450
|
+
ctx.meta = null;
|
|
451
|
+
},
|
|
452
|
+
tcell: ({ children }, ctx, acc) => __collect(acc, (ctx.row > 0 ? ctx.tags.tableCell : ctx.tags.tableHead)(ctx, __trimBody(__children(ctx, children)))),
|
|
453
|
+
trow: ({ children }, ctx, acc) => __collect(acc, ctx.tags.tableRow(ctx, ctx.row, __children(ctx, children[0].children))),
|
|
454
|
+
ulitem: ({ children }, ctx, acc) => {
|
|
455
|
+
const body = [];
|
|
456
|
+
transformScope(children[2], ctx, body);
|
|
457
|
+
__collect(acc, [
|
|
458
|
+
"ulitem",
|
|
459
|
+
__listItemAttribs(children[1]),
|
|
460
|
+
...__trimBody(body),
|
|
461
|
+
]);
|
|
462
|
+
},
|
|
463
|
+
wikiref: (scope, ctx, acc) => {
|
|
464
|
+
const [id, label] = scope.result.split("|");
|
|
465
|
+
__collect(acc, ctx.tags.wikiref(ctx, id, label));
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
/**
|
|
469
|
+
* Takes an attributes object and optional metadata. If `meta` is not nullish,
|
|
470
|
+
* assigns it as `__meta` key in given object. Returns object.
|
|
471
|
+
*
|
|
472
|
+
* @param target
|
|
473
|
+
* @param meta
|
|
474
|
+
*/
|
|
475
|
+
export const withMeta = (target, meta) => {
|
|
476
|
+
if (meta != null)
|
|
477
|
+
target.__meta = meta;
|
|
478
|
+
return target;
|
|
107
479
|
};
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
480
|
+
/**
|
|
481
|
+
* Takes a hiccup tree and extracts only the primitive body values (strings,
|
|
482
|
+
* numbers, booleans) and returns them as array.
|
|
483
|
+
*
|
|
484
|
+
* @param body
|
|
485
|
+
* @param acc
|
|
486
|
+
*/
|
|
487
|
+
export const extractBody = (body, acc = []) => {
|
|
488
|
+
for (let x of isPlainObject(body[1]) ? body.slice(2) : body) {
|
|
489
|
+
if (isPrimitive(x))
|
|
490
|
+
acc.push(x);
|
|
491
|
+
else if (isArray(x))
|
|
492
|
+
extractBody(x, acc);
|
|
493
|
+
}
|
|
494
|
+
return acc;
|
|
112
495
|
};
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
496
|
+
/** @internal */
|
|
497
|
+
const __collect = (acc, x) => x != null && acc.push(x);
|
|
498
|
+
/** @internal */
|
|
499
|
+
const __children = (ctx, children, acc = []) => {
|
|
500
|
+
for (let c of children)
|
|
501
|
+
transformScope(c, ctx, acc);
|
|
502
|
+
return acc;
|
|
118
503
|
};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
504
|
+
/** @internal */
|
|
505
|
+
const __escape = (ctx, x) => ctx.opts.escape ? escapeEntities(x) : x;
|
|
506
|
+
/** @internal */
|
|
507
|
+
const __listItemAttribs = (scope) => scope?.id === "todo"
|
|
508
|
+
? {
|
|
509
|
+
__todo: true,
|
|
510
|
+
__done: scope.result === "x",
|
|
511
|
+
}
|
|
512
|
+
: {};
|
|
513
|
+
/** @internal */
|
|
514
|
+
const __trimBody = (body) => {
|
|
515
|
+
if (body.length === 1 && isString(body[0]))
|
|
516
|
+
body[0] = body[0].trim();
|
|
517
|
+
return body;
|
|
123
518
|
};
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const matchLink = (result) => seq([
|
|
135
|
-
untilStr(LINK_LABEL_END, (ctx, body) => ((ctx.title = body), undefined)),
|
|
136
|
-
str(LINK_HREF),
|
|
137
|
-
untilStr(LINK_HREF_END, (ctx, body) => ((ctx.href = body), undefined)),
|
|
138
|
-
], pop((ctx) => result(ctx.href, ctx.title)));
|
|
139
|
-
const matchPara = (id, next) => alts([
|
|
140
|
-
...matchInline(id),
|
|
141
|
-
str(NL, (ctx) => ((ctx.body += " "), [next])),
|
|
142
|
-
], collect(id));
|
|
143
|
-
const newPara = (ctx, buf) => ((ctx.body = buf.join("")), (ctx.children = []), [13 /* State.PARA */]);
|
|
144
|
-
const newParaInline = (next) => (ctx) => {
|
|
145
|
-
ctx.stack.push({ id: 13 /* State.PARA */, children: [] });
|
|
146
|
-
return transition(ctx, next);
|
|
519
|
+
/** @internal */
|
|
520
|
+
const __columnAlignments = (children) => {
|
|
521
|
+
const align = [];
|
|
522
|
+
for (let c of children) {
|
|
523
|
+
const raw = c.children[0].children[0].result.trim();
|
|
524
|
+
const isLeft = raw.startsWith(":-") ? 1 : 0;
|
|
525
|
+
const isRight = raw.endsWith("-:") ? 2 : 0;
|
|
526
|
+
align.push((["default", "left", "right", "center"][isLeft | isRight]));
|
|
527
|
+
}
|
|
528
|
+
return align;
|
|
147
529
|
};
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
[1
|
|
151
|
-
const newList = (ctx) => ((ctx.container = []), transition(ctx, 12 /* State.LI */));
|
|
152
|
-
const newTable = (ctx) => (ctx.stack.push({ id: 18 /* State.TABLE */, container: [] }),
|
|
153
|
-
(ctx.container = []),
|
|
154
|
-
transition(ctx, 18 /* State.TABLE */));
|
|
155
|
-
/**
|
|
156
|
-
* Main parser / transducer. Defines state map with the various Markdown
|
|
157
|
-
* syntax matchers and state transition handlers. The returned parser
|
|
158
|
-
* itself is only used in `index.ts`.
|
|
159
|
-
*/
|
|
160
|
-
export const parse = (_tags) => {
|
|
161
|
-
const tags = { ...DEFAULT_TAGS, ..._tags };
|
|
162
|
-
return comp(filter((x) => x !== "\r"), fsm({
|
|
163
|
-
[14 /* State.START */]: alts([
|
|
164
|
-
whitespace(() => [14 /* State.START */]),
|
|
165
|
-
repeat(str(HD), 1, Infinity, heading),
|
|
166
|
-
str(BQUOTE, (ctx) => transition(ctx, 0 /* State.BLOCKQUOTE */)),
|
|
167
|
-
str(LI, newList),
|
|
168
|
-
alts([
|
|
169
|
-
seq([str(CODE), not(str(CODE))], newParaCode),
|
|
170
|
-
str(CODEBLOCK, () => [15 /* State.START_CODEBLOCK */]),
|
|
171
|
-
], undefined, (_, next) => next),
|
|
172
|
-
seq([repeat(str(HR), 3, Infinity), str(NL)], () => [
|
|
173
|
-
14 /* State.START */,
|
|
174
|
-
[tags.hr()],
|
|
175
|
-
]),
|
|
176
|
-
str(IMG, newParaInline(10 /* State.IMG */)),
|
|
177
|
-
str(LINK_LABEL, newParaInline(11 /* State.LINK */)),
|
|
178
|
-
str(STRONG, newParaInline(17 /* State.STRONG */)),
|
|
179
|
-
str(STRIKE, newParaInline(16 /* State.STRIKE */)),
|
|
180
|
-
str(EM, newParaInline(3 /* State.EMPHASIS */)),
|
|
181
|
-
str(TD, newTable),
|
|
182
|
-
], newPara),
|
|
183
|
-
[13 /* State.PARA */]: matchPara(13 /* State.PARA */, 6 /* State.END_PARA */),
|
|
184
|
-
[6 /* State.END_PARA */]: alts([
|
|
185
|
-
...matchInline(13 /* State.PARA */),
|
|
186
|
-
str(NL, collectAndRestart(tags.paragraph)),
|
|
187
|
-
], collect(13 /* State.PARA */)),
|
|
188
|
-
[0 /* State.BLOCKQUOTE */]: matchPara(0 /* State.BLOCKQUOTE */, 4 /* State.END_BLOCKQUOTE */),
|
|
189
|
-
[4 /* State.END_BLOCKQUOTE */]: alts([
|
|
190
|
-
...matchInline(0 /* State.BLOCKQUOTE */),
|
|
191
|
-
str(BQUOTE, collectBlockQuote),
|
|
192
|
-
str(NL, collectAndRestart(tags.blockquote)),
|
|
193
|
-
], collect(0 /* State.BLOCKQUOTE */)),
|
|
194
|
-
[9 /* State.HEADING */]: matchPara(9 /* State.HEADING */, 7 /* State.END_HEADING */),
|
|
195
|
-
[7 /* State.END_HEADING */]: alts([
|
|
196
|
-
...matchInline(9 /* State.HEADING */),
|
|
197
|
-
str(NL, collectHeading(tags.heading)),
|
|
198
|
-
], collect(9 /* State.HEADING */)),
|
|
199
|
-
[15 /* State.START_CODEBLOCK */]: untilStr(NL, (ctx, lang) => ((ctx.lang = lang), [2 /* State.CODEBLOCK */])),
|
|
200
|
-
[2 /* State.CODEBLOCK */]: untilStr(CODEBLOCK_END, collectCodeBlock(tags.codeblock)),
|
|
201
|
-
[12 /* State.LI */]: matchPara(12 /* State.LI */, 5 /* State.END_LI */),
|
|
202
|
-
[5 /* State.END_LI */]: alts([
|
|
203
|
-
str(NL, collectList("ul", tags.list, tags.li)),
|
|
204
|
-
str(LI, (ctx) => (collectLi(ctx, tags.li),
|
|
205
|
-
transition(ctx, 12 /* State.LI */))),
|
|
206
|
-
], collect(12 /* State.LI */)),
|
|
207
|
-
[11 /* State.LINK */]: matchLink(tags.link),
|
|
208
|
-
[10 /* State.IMG */]: matchLink(tags.img),
|
|
209
|
-
[17 /* State.STRONG */]: untilStr(STRONG, collectInline(tags.strong)),
|
|
210
|
-
[16 /* State.STRIKE */]: untilStr(STRIKE, collectInline(tags.strike)),
|
|
211
|
-
[3 /* State.EMPHASIS */]: untilStr(EM, collectInline(tags.em)),
|
|
212
|
-
[1 /* State.CODE */]: untilStr(CODE, collectInline(tags.code)),
|
|
213
|
-
[18 /* State.TABLE */]: alts([
|
|
214
|
-
...matchInline(18 /* State.TABLE */),
|
|
215
|
-
str(TD, collectTD(tags.td)),
|
|
216
|
-
str(NL, collectTR(tags.tr)),
|
|
217
|
-
], collect(18 /* State.TABLE */)),
|
|
218
|
-
[8 /* State.END_TABLE */]: alts([
|
|
219
|
-
str(NL, collectTable(tags.table)),
|
|
220
|
-
str(TD, () => [18 /* State.TABLE */]),
|
|
221
|
-
]),
|
|
222
|
-
}, { stack: [] }, 14 /* State.START */));
|
|
530
|
+
const __linkTitle = (src) => {
|
|
531
|
+
const match = /\s"(.+)"$/.exec(src);
|
|
532
|
+
return match ? [src.substring(0, match.index), match[1]] : [src, undefined];
|
|
223
533
|
};
|