@minamorl/markdown-next 2.1.1 → 2.2.1
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/lib/src/parser.d.ts +2 -2
- package/lib/src/parser.js +181 -101
- package/package.json +17 -8
package/lib/src/parser.d.ts
CHANGED
package/lib/src/parser.js
CHANGED
|
@@ -10,14 +10,14 @@ class Parser {
|
|
|
10
10
|
this.rootTree = {
|
|
11
11
|
value: null,
|
|
12
12
|
children: [],
|
|
13
|
-
type:
|
|
14
|
-
parent: null
|
|
13
|
+
type: 'shadow',
|
|
14
|
+
parent: null,
|
|
15
15
|
};
|
|
16
16
|
this.currentTree = {
|
|
17
17
|
value: null,
|
|
18
18
|
children: [],
|
|
19
|
-
type:
|
|
20
|
-
parent: null
|
|
19
|
+
type: 'shadow',
|
|
20
|
+
parent: null,
|
|
21
21
|
};
|
|
22
22
|
this.create();
|
|
23
23
|
}
|
|
@@ -43,61 +43,53 @@ class Parser {
|
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
const whitespace = P.regexp(/\s+/m);
|
|
46
|
-
const asterisk = P.string(
|
|
47
|
-
const sharp = P.string(
|
|
46
|
+
const asterisk = P.string('*');
|
|
47
|
+
const sharp = P.string('#');
|
|
48
48
|
const plainStr = P.regexp(/[^`_\*\r\n]+/);
|
|
49
49
|
const codePlainStr = P.regexp(/[^`\r\n]+/);
|
|
50
|
-
const linebreak = P.string(
|
|
51
|
-
const equal = P.string(
|
|
52
|
-
const minus = P.string(
|
|
50
|
+
const linebreak = P.string('\r\n').or(P.string('\n')).or(P.string('\r'));
|
|
51
|
+
const equal = P.string('=');
|
|
52
|
+
const minus = P.string('-');
|
|
53
53
|
const join = this.opts.export.join;
|
|
54
54
|
const mapper = this.opts.export.mapper;
|
|
55
55
|
const token = (p) => {
|
|
56
56
|
return p.skip(P.regexp(/\s*/m));
|
|
57
57
|
};
|
|
58
58
|
const h1Special = P.regexp(/^(.*)\n\=+/, 1)
|
|
59
|
-
.skip(P.alt(P.eof, P.string(
|
|
60
|
-
.map(mapper(
|
|
59
|
+
.skip(P.alt(P.eof, P.string('\n')))
|
|
60
|
+
.map(mapper('h1'));
|
|
61
61
|
const h2Special = P.regexp(/^(.*)\n\-+/, 1)
|
|
62
|
-
.skip(P.alt(P.eof, P.string(
|
|
63
|
-
.map(mapper(
|
|
64
|
-
const h1 = token(P.seq(sharp, whitespace).then(plainStr)).map(mapper(
|
|
65
|
-
const h2 = token(P.seq(sharp.times(2), whitespace).then(plainStr)).map(mapper(
|
|
66
|
-
const h3 = token(P.seq(sharp.times(3), whitespace).then(plainStr)).map(mapper(
|
|
67
|
-
const h4 = token(P.seq(sharp.times(4), whitespace).then(plainStr)).map(mapper(
|
|
68
|
-
const h5 = token(P.seq(sharp.times(5), whitespace).then(plainStr)).map(mapper(
|
|
69
|
-
const h6 = token(P.seq(sharp.times(6), whitespace).then(plainStr)).map(mapper(
|
|
70
|
-
const strongStart = P.string(
|
|
62
|
+
.skip(P.alt(P.eof, P.string('\n')))
|
|
63
|
+
.map(mapper('h2'));
|
|
64
|
+
const h1 = token(P.seq(sharp, whitespace).then(plainStr)).map(mapper('h1'));
|
|
65
|
+
const h2 = token(P.seq(sharp.times(2), whitespace).then(plainStr)).map(mapper('h2'));
|
|
66
|
+
const h3 = token(P.seq(sharp.times(3), whitespace).then(plainStr)).map(mapper('h3'));
|
|
67
|
+
const h4 = token(P.seq(sharp.times(4), whitespace).then(plainStr)).map(mapper('h4'));
|
|
68
|
+
const h5 = token(P.seq(sharp.times(5), whitespace).then(plainStr)).map(mapper('h5'));
|
|
69
|
+
const h6 = token(P.seq(sharp.times(6), whitespace).then(plainStr)).map(mapper('h6'));
|
|
70
|
+
const strongStart = P.string('**').or(P.string('__'));
|
|
71
71
|
const strongEnd = strongStart;
|
|
72
|
-
const strong = strongStart
|
|
73
|
-
|
|
74
|
-
.map(mapper("strong"))
|
|
75
|
-
.skip(strongEnd);
|
|
76
|
-
const emStart = P.string("*").or(P.string("_"));
|
|
72
|
+
const strong = strongStart.then(plainStr).map(mapper('strong')).skip(strongEnd);
|
|
73
|
+
const emStart = P.string('*').or(P.string('_'));
|
|
77
74
|
const emEnd = emStart;
|
|
78
|
-
const em = emStart
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.skip(emEnd);
|
|
82
|
-
const anchor = P.seqMap(P.string("["), P.regexp(/[^\]\r\n]+/), P.string("]("), P.regexp(/[^\)\r\n]+/), P.string(")"), (_1, label, _2, href, _3) => {
|
|
83
|
-
return mapper("a", { href })(label);
|
|
75
|
+
const em = emStart.then(plainStr).map(mapper('em')).skip(emEnd);
|
|
76
|
+
const anchor = P.seqMap(P.string('['), P.regexp(/[^\]\r\n]+/), P.string(']('), P.regexp(/[^\)\r\n]+/), P.string(')'), (_1, label, _2, href, _3) => {
|
|
77
|
+
return mapper('a', { href })(label);
|
|
84
78
|
});
|
|
85
|
-
const img = P.seqMap(P.string(
|
|
86
|
-
return mapper(
|
|
79
|
+
const img = P.seqMap(P.string('!['), P.regexp(/[^\]\r\n]+/), P.string(']('), P.regexp(/[^\)\r\n]+/), P.string(')'), (_1, alt, _2, src, _3) => {
|
|
80
|
+
return mapper('img', { src, alt })(null);
|
|
87
81
|
});
|
|
88
|
-
const codeStart = P.string(
|
|
89
|
-
const codeEnd = P.string(
|
|
90
|
-
const code = codeStart
|
|
91
|
-
|
|
92
|
-
.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return this.opts.plugins && this.opts.plugins[pluginName] ?
|
|
96
|
-
this.opts.plugins[pluginName](args, null, mapper, join) : join([_1, pluginName, args, _2]);
|
|
82
|
+
const codeStart = P.string('`');
|
|
83
|
+
const codeEnd = P.string('`');
|
|
84
|
+
const code = codeStart.then(codePlainStr).map(mapper('code')).skip(codeEnd);
|
|
85
|
+
const pluginInline = P.seqMap(P.string('@['), P.regexp(/[a-zA-Z]+/), P.regexp(/:{0,1}([^\]]*)/, 1), P.string(']'), (_1, pluginName, args, _2) => {
|
|
86
|
+
return this.opts.plugins && this.opts.plugins[pluginName]
|
|
87
|
+
? this.opts.plugins[pluginName](args, null, mapper, join)
|
|
88
|
+
: join([_1, pluginName, args, _2]);
|
|
97
89
|
});
|
|
98
90
|
// Aozora bunko ruby format: |text《ruby》
|
|
99
|
-
const aozoraRuby = P.seqMap(P.string(
|
|
100
|
-
return mapper(
|
|
91
|
+
const aozoraRuby = P.seqMap(P.string('|'), P.regexp(/[^《]+/), P.string('《'), P.regexp(/[^》]+/), P.string('》'), (_pipe, base, _open, ruby, _close) => {
|
|
92
|
+
return mapper('ruby')(join([base, mapper('rt')(ruby)]));
|
|
101
93
|
});
|
|
102
94
|
// HTML element parser - converts <tag>content</tag> to AST format
|
|
103
95
|
const htmlSelfClosing = P.regexp(/<(br|hr)\s*\/?>/).map((match) => {
|
|
@@ -118,12 +110,17 @@ class Parser {
|
|
|
118
110
|
return mapper(tag)(join(children));
|
|
119
111
|
});
|
|
120
112
|
});
|
|
121
|
-
//
|
|
113
|
+
// Footnote reference: [^1], [^2], etc.
|
|
114
|
+
const footnoteRef = P.seqMap(P.string('[^'), P.regexp(/[0-9]+/), P.string(']').notFollowedBy(P.string(':')), (_1, num, _3) => {
|
|
115
|
+
return mapper('footnote-ref', { id: num })(num);
|
|
116
|
+
});
|
|
117
|
+
// Math expressions - output raw $...$ for MathJax to process
|
|
122
118
|
// Inline math: $...$
|
|
123
|
-
const mathInline = P.seqMap(P.string(
|
|
124
|
-
|
|
119
|
+
const mathInline = P.seqMap(P.string('$').notFollowedBy(P.string('$')), P.regexp(/[^\$\r\n]+/), P.string('$'), (_1, content, _3) => {
|
|
120
|
+
// Output raw $...$ for MathJax auto-detection
|
|
121
|
+
return '$' + content + '$';
|
|
125
122
|
});
|
|
126
|
-
const inline = P.alt(pluginInline, aozoraRuby, anchor, img, em, strong, code, mathInline, htmlSelfClosing, htmlElement, P.regexp(/[^\r\n<=-\[\]\*\`\@|\$]+/), P.regexp(/./));
|
|
123
|
+
const inline = P.alt(pluginInline, aozoraRuby, footnoteRef, anchor, img, em, strong, code, mathInline, htmlSelfClosing, htmlElement, P.regexp(/[^\r\n<=-\[\]\*\`\@|\$]+/), P.regexp(/./));
|
|
127
124
|
// Table cell content - supports inline elements
|
|
128
125
|
const tableCellInline = P.alt(anchor, img, em, strong, code, P.regexp(/[^\r\n\[\]\*|`]+/));
|
|
129
126
|
// Parse a single table row: |cell|cell|cell| with flexible spacing
|
|
@@ -135,7 +132,7 @@ class Parser {
|
|
|
135
132
|
}
|
|
136
133
|
// Remove first and last pipe, split by remaining pipes
|
|
137
134
|
const inner = trimmed.slice(1, -1);
|
|
138
|
-
return inner.split('|').map(cell => cell.trim());
|
|
135
|
+
return inner.split('|').map((cell) => cell.trim());
|
|
139
136
|
};
|
|
140
137
|
// Table row regex - matches |...|
|
|
141
138
|
const tableRowLine = P.regexp(/^\|[^\r\n]+\|/, 0);
|
|
@@ -168,21 +165,20 @@ class Parser {
|
|
|
168
165
|
if (headerCells.length === 0) {
|
|
169
166
|
return P.makeFailure(0, 'No header cells');
|
|
170
167
|
}
|
|
171
|
-
const headerRow = mapper(
|
|
172
|
-
const bodyRows = bodyCells.map(row => mapper(
|
|
173
|
-
return mapper(
|
|
168
|
+
const headerRow = mapper('tr')(join(headerCells.map((h) => mapper('th')(parseCellContent(h)))));
|
|
169
|
+
const bodyRows = bodyCells.map((row) => mapper('tr')(join(row.map((cell) => mapper('td')(parseCellContent(cell))))));
|
|
170
|
+
return mapper('table')(join([headerRow, ...bodyRows]));
|
|
174
171
|
});
|
|
175
172
|
const inlines = inline.atLeast(1).map(join);
|
|
176
173
|
const paragraphBegin = inlines;
|
|
177
174
|
const paragraphEnd = ignore(/```\n.*\n```/);
|
|
178
|
-
const paragraphLine = P.lazy(() => P.alt(P.seq(paragraphBegin, linebreak.skip(paragraphEnd).result(mapper(
|
|
179
|
-
const paragraph = paragraphLine
|
|
180
|
-
|
|
181
|
-
const listIndent = P.string(" ");
|
|
175
|
+
const paragraphLine = P.lazy(() => P.alt(P.seq(paragraphBegin, linebreak.skip(paragraphEnd).result(mapper('br')(null)), paragraphLine).map(join), inlines));
|
|
176
|
+
const paragraph = paragraphLine.map(mapper('p'));
|
|
177
|
+
const listIndent = P.string(' ');
|
|
182
178
|
// List item content - supports inline elements including math
|
|
183
179
|
const liInlineContent = P.alt(mathInline, anchor, img, em, strong, code, P.regexp(/[^\r\n\[\]\*\`\$]+/), P.regexp(/./));
|
|
184
180
|
const liSingleLine = liInlineContent.atLeast(0).map(join);
|
|
185
|
-
const ulStart = P.string(
|
|
181
|
+
const ulStart = P.string('- ').or(P.string('* '));
|
|
186
182
|
const olStart = P.regexp(/[0-9]+\. /);
|
|
187
183
|
let liLevel = [1];
|
|
188
184
|
let counter = 0;
|
|
@@ -190,8 +186,8 @@ class Parser {
|
|
|
190
186
|
this.rootTree = this.currentTree = {
|
|
191
187
|
value: null,
|
|
192
188
|
children: [],
|
|
193
|
-
type:
|
|
194
|
-
parent: null
|
|
189
|
+
type: 'shadow',
|
|
190
|
+
parent: null,
|
|
195
191
|
};
|
|
196
192
|
liLevel = [1];
|
|
197
193
|
counter = 0;
|
|
@@ -201,19 +197,19 @@ class Parser {
|
|
|
201
197
|
let nodeType;
|
|
202
198
|
// detect which types of content
|
|
203
199
|
liLevel.push(index.column);
|
|
204
|
-
nodeType =
|
|
200
|
+
nodeType = start == '* ' || start == '- ' ? 'ul' : 'ol';
|
|
205
201
|
counter += 1;
|
|
206
202
|
return { counter, nodeType, str, liLevel, index };
|
|
207
203
|
})
|
|
208
204
|
.skip(linebreak.atMost(1))
|
|
209
|
-
.chain(v => {
|
|
210
|
-
if (v.liLevel.filter(x => x % 2 !== 1).length > 0) {
|
|
205
|
+
.chain((v) => {
|
|
206
|
+
if (v.liLevel.filter((x) => x % 2 !== 1).length > 0) {
|
|
211
207
|
initializeList();
|
|
212
|
-
return P.fail(
|
|
208
|
+
return P.fail('Invalid indentation');
|
|
213
209
|
}
|
|
214
210
|
return P.succeed(v);
|
|
215
211
|
})
|
|
216
|
-
.map(v => {
|
|
212
|
+
.map((v) => {
|
|
217
213
|
const liLevelBefore = liLevel[v.counter - 1];
|
|
218
214
|
const liLevelCurrent = liLevel[v.counter];
|
|
219
215
|
if (liLevelBefore === liLevelCurrent) {
|
|
@@ -221,7 +217,7 @@ class Parser {
|
|
|
221
217
|
value: v.str,
|
|
222
218
|
children: [],
|
|
223
219
|
type: v.nodeType,
|
|
224
|
-
parent: this.currentTree
|
|
220
|
+
parent: this.currentTree,
|
|
225
221
|
});
|
|
226
222
|
}
|
|
227
223
|
else if (liLevelBefore < liLevelCurrent) {
|
|
@@ -231,7 +227,7 @@ class Parser {
|
|
|
231
227
|
children: [],
|
|
232
228
|
type: v.nodeType,
|
|
233
229
|
parent: this.currentTree,
|
|
234
|
-
value: v.str
|
|
230
|
+
value: v.str,
|
|
235
231
|
});
|
|
236
232
|
}
|
|
237
233
|
else if (liLevelBefore > liLevelCurrent) {
|
|
@@ -245,7 +241,7 @@ class Parser {
|
|
|
245
241
|
type: v.nodeType,
|
|
246
242
|
children: [],
|
|
247
243
|
parent: this.currentTree,
|
|
248
|
-
value: v.str
|
|
244
|
+
value: v.str,
|
|
249
245
|
});
|
|
250
246
|
}
|
|
251
247
|
const _nodeType = v.nodeType;
|
|
@@ -253,7 +249,9 @@ class Parser {
|
|
|
253
249
|
});
|
|
254
250
|
};
|
|
255
251
|
const lists = P.lazy(() => {
|
|
256
|
-
return listLineContent()
|
|
252
|
+
return listLineContent()
|
|
253
|
+
.atLeast(1)
|
|
254
|
+
.map((nodeTypes) => {
|
|
257
255
|
this.rootTree.type = nodeTypes[0];
|
|
258
256
|
const result = treeToHtml(this.rootTree);
|
|
259
257
|
// initialization
|
|
@@ -262,15 +260,15 @@ class Parser {
|
|
|
262
260
|
});
|
|
263
261
|
});
|
|
264
262
|
const treeToHtml = (treeOrNode) => {
|
|
265
|
-
if (treeOrNode.type ===
|
|
263
|
+
if (treeOrNode.type === 'shadow') {
|
|
266
264
|
return join(treeOrNode.children.map(treeToHtml));
|
|
267
265
|
}
|
|
268
266
|
else if (treeOrNode.children.length === 0 && treeOrNode.value !== null) {
|
|
269
|
-
return mapper(
|
|
267
|
+
return mapper('li')(treeOrNode.value);
|
|
270
268
|
}
|
|
271
269
|
else if (treeOrNode.children.length !== 0 && treeOrNode.value !== null) {
|
|
272
270
|
const { children } = treeOrNode;
|
|
273
|
-
return mapper(
|
|
271
|
+
return mapper('li')(join([treeOrNode.value, mapper(treeOrNode.children[0].type)(join(children.map(treeToHtml)))]));
|
|
274
272
|
}
|
|
275
273
|
else {
|
|
276
274
|
const { children } = treeOrNode;
|
|
@@ -285,17 +283,17 @@ class Parser {
|
|
|
285
283
|
if (code.length > 0) {
|
|
286
284
|
code.pop();
|
|
287
285
|
}
|
|
288
|
-
if (definition ===
|
|
289
|
-
return mapper(
|
|
290
|
-
return mapper(
|
|
286
|
+
if (definition === '')
|
|
287
|
+
return mapper('pre')(mapper('code')(join(code)));
|
|
288
|
+
return mapper('pre', { 'data-language': definition })(mapper('code')(join(code)));
|
|
291
289
|
});
|
|
292
|
-
const blockquoteBegin = P.string(
|
|
290
|
+
const blockquoteBegin = P.string('> ');
|
|
293
291
|
// Parse blockquote content using inlines to support HTML tags, ruby, and math
|
|
294
292
|
const blockquoteInline = P.alt(pluginInline, aozoraRuby, anchor, img, em, strong, code, mathInline, htmlSelfClosing, htmlElement, P.regexp(/[^\r\n<|\[\]\*\`\@\$]+/), P.regexp(/./));
|
|
295
293
|
const blockquoteContent = blockquoteInline.atLeast(1).map(join);
|
|
296
294
|
const blockquoteLine = P.lazy(() => {
|
|
297
295
|
let blockquoteLevel = 0;
|
|
298
|
-
return P.seqMap(blockquoteBegin.then(blockquoteBegin.many().map(x => blockquoteLevel = x.length)), blockquoteContent, linebreak.atMost(1), (_1, text, _2) => {
|
|
296
|
+
return P.seqMap(blockquoteBegin.then(blockquoteBegin.many().map((x) => (blockquoteLevel = x.length))), blockquoteContent, linebreak.atMost(1), (_1, text, _2) => {
|
|
299
297
|
return { text, blockquoteLevel };
|
|
300
298
|
});
|
|
301
299
|
});
|
|
@@ -336,7 +334,7 @@ class Parser {
|
|
|
336
334
|
for (const [i, v] of tree.children.entries()) {
|
|
337
335
|
if (v.text !== null) {
|
|
338
336
|
if (tree.children[i + 1] && tree.children[i + 1].text !== null) {
|
|
339
|
-
result.push(join([v.text, mapper(
|
|
337
|
+
result.push(join([v.text, mapper('br')(null)]));
|
|
340
338
|
}
|
|
341
339
|
else {
|
|
342
340
|
result.push(v.text);
|
|
@@ -346,25 +344,42 @@ class Parser {
|
|
|
346
344
|
result.push(parseBlockquoteTree(v));
|
|
347
345
|
}
|
|
348
346
|
}
|
|
349
|
-
const _result = mapper(
|
|
347
|
+
const _result = mapper('blockquote')(result.reduce((a, b) => join([a, b])));
|
|
350
348
|
return _result;
|
|
351
349
|
};
|
|
352
350
|
const blockquote = P.lazy(() => {
|
|
353
|
-
return blockquoteLine.atLeast(1).map(x => {
|
|
351
|
+
return blockquoteLine.atLeast(1).map((x) => {
|
|
354
352
|
return parseBlockquoteTree(createBlockquoteTree(x), true);
|
|
355
353
|
});
|
|
356
354
|
});
|
|
357
|
-
const pluginBlock = P.seqMap(P.string(
|
|
358
|
-
|
|
355
|
+
const pluginBlock = P.seqMap(P.string('@['), P.regexp(/[a-zA-Z]+/), P.regexp(/(:[^\]]*)*/), P.string(']\n'), P.seq(P.string(' ').result(''), P.regexp(/[^\r\n]+/), linebreak.atMost(1).result('\n'))
|
|
356
|
+
.map(join)
|
|
357
|
+
.atLeast(1)
|
|
358
|
+
.map(join), (_1, pluginName, args, _2, content) => {
|
|
359
|
+
return this.opts.plugins && this.opts.plugins[pluginName]
|
|
360
|
+
? this.opts.plugins[pluginName](args, content, mapper, join)
|
|
359
361
|
: join([_1, pluginName, args, _2, content]);
|
|
360
362
|
});
|
|
361
363
|
// Block math: $$...$$ (must be on its own line or surrounded by newlines)
|
|
362
364
|
// Content can include anything except $$ (including single $, newlines, etc.)
|
|
363
365
|
const mathBlockContent = P.regexp(/[\s\S]*?(?=\$\$)/);
|
|
364
|
-
const mathBlock = P.seqMap(P.regexp(/^\$\$/), mathBlockContent, P.string(
|
|
365
|
-
|
|
366
|
+
const mathBlock = P.seqMap(P.regexp(/^\$\$/), mathBlockContent, P.string('$$'), P.alt(linebreak, P.eof), (_1, content, _3, _4) => {
|
|
367
|
+
// Output raw $$...$$ for MathJax auto-detection
|
|
368
|
+
return '$$' + content.trim() + '$$';
|
|
369
|
+
});
|
|
370
|
+
// Footnote definition: [^1]: content (with optional indented continuation lines)
|
|
371
|
+
const footnoteDefFirstLine = P.regexp(/[^\r\n]*/);
|
|
372
|
+
const footnoteDefContLine = P.seqMap(P.regexp(/^ /), // 4 spaces or tab for continuation
|
|
373
|
+
P.regexp(/[^\r\n]*/), (_indent, content) => content);
|
|
374
|
+
const footnoteDefContLineAlt = P.seqMap(P.string('\t'), // tab for continuation
|
|
375
|
+
P.regexp(/[^\r\n]*/), (_indent, content) => content);
|
|
376
|
+
const footnoteDef = P.seqMap(P.regexp(/^\[\^([0-9]+)\]:\s*/, 1), footnoteDefFirstLine, P.seq(linebreak, P.alt(footnoteDefContLine, footnoteDefContLineAlt))
|
|
377
|
+
.map(([_br, content]) => content)
|
|
378
|
+
.many(), P.alt(linebreak, P.eof), (num, firstLine, contLines, _end) => {
|
|
379
|
+
const fullContent = [firstLine, ...contLines].join('\n').trim();
|
|
380
|
+
return mapper('footnote-def', { id: num })(fullContent);
|
|
366
381
|
});
|
|
367
|
-
const block = P.alt(P.regexp(/\s+/).result(
|
|
382
|
+
const block = P.alt(P.regexp(/\s+/).result(''), pluginBlock, h1Special, h2Special, h6, h5, h4, h3, h2, h1, table, codeBlock, mathBlock, footnoteDef, lists, blockquote, paragraph, linebreak.result(''));
|
|
368
383
|
this.acceptables = P.alt(block).many().map(join);
|
|
369
384
|
}
|
|
370
385
|
parse(s) {
|
|
@@ -372,15 +387,15 @@ class Parser {
|
|
|
372
387
|
this.rootTree = this.currentTree = {
|
|
373
388
|
value: null,
|
|
374
389
|
children: [],
|
|
375
|
-
type:
|
|
376
|
-
parent: null
|
|
390
|
+
type: 'shadow',
|
|
391
|
+
parent: null,
|
|
377
392
|
};
|
|
378
393
|
const parsed = this.acceptables.parse(s.trim());
|
|
379
|
-
if (parsed.status === true && parsed.hasOwnProperty(
|
|
394
|
+
if (parsed.status === true && parsed.hasOwnProperty('value'))
|
|
380
395
|
return this.opts.export.postprocess(parsed.value);
|
|
381
396
|
console.error(s.trim());
|
|
382
397
|
console.error(parsed);
|
|
383
|
-
throw new Error(
|
|
398
|
+
throw new Error('Parsing was failed.');
|
|
384
399
|
}
|
|
385
400
|
}
|
|
386
401
|
exports.Parser = Parser;
|
|
@@ -390,29 +405,94 @@ function escapeHtml(text) {
|
|
|
390
405
|
'<': '<',
|
|
391
406
|
'>': '>',
|
|
392
407
|
'"': '"',
|
|
393
|
-
"'": '''
|
|
408
|
+
"'": ''',
|
|
394
409
|
};
|
|
395
410
|
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
396
411
|
}
|
|
397
412
|
exports.asHTML = {
|
|
398
|
-
mapper: (tag, args) => children =>
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
413
|
+
mapper: (tag, args) => (children) => {
|
|
414
|
+
// Handle footnote reference - output as superscript link
|
|
415
|
+
if (tag === 'footnote-ref' && (args === null || args === void 0 ? void 0 : args.id)) {
|
|
416
|
+
const id = args.id;
|
|
417
|
+
return `<sup id="fnref${id}"><a href="#fn${id}">${id}</a></sup>`;
|
|
418
|
+
}
|
|
419
|
+
// Handle footnote definition - store for later collection
|
|
420
|
+
if (tag === 'footnote-def' && (args === null || args === void 0 ? void 0 : args.id)) {
|
|
421
|
+
const id = args.id;
|
|
422
|
+
return `<footnote-def data-id="${id}">${children}</footnote-def>`;
|
|
423
|
+
}
|
|
424
|
+
return [
|
|
425
|
+
'<' + tag,
|
|
426
|
+
args
|
|
427
|
+
? ' ' +
|
|
428
|
+
Object.keys(args)
|
|
429
|
+
.map((x) => `${x}="${escapeHtml(String(args[x]))}"`)
|
|
430
|
+
.join(' ')
|
|
431
|
+
: '',
|
|
432
|
+
children !== null && children !== '' ? '>' + children + '</' + tag + '>' : ' />',
|
|
433
|
+
].join('');
|
|
434
|
+
},
|
|
435
|
+
join: (x) => x.join(''),
|
|
436
|
+
postprocess: (x) => {
|
|
437
|
+
// Collect footnote definitions and move them to the end
|
|
438
|
+
const footnoteDefRegex = /<footnote-def data-id="(\d+)">([\s\S]*?)<\/footnote-def>/g;
|
|
439
|
+
const footnotes = [];
|
|
440
|
+
let match;
|
|
441
|
+
while ((match = footnoteDefRegex.exec(x)) !== null) {
|
|
442
|
+
footnotes.push({ id: match[1], content: match[2] });
|
|
443
|
+
}
|
|
444
|
+
// Remove footnote-def tags from body
|
|
445
|
+
let result = x.replace(/<footnote-def data-id="\d+">([\s\S]*?)<\/footnote-def>/g, '');
|
|
446
|
+
// Append footnotes section if any exist
|
|
447
|
+
if (footnotes.length > 0) {
|
|
448
|
+
// Sort by id
|
|
449
|
+
footnotes.sort((a, b) => parseInt(a.id) - parseInt(b.id));
|
|
450
|
+
const footnotesHtml = footnotes
|
|
451
|
+
.map((fn) => `<li id="fn${fn.id}">${fn.content} <a href="#fnref${fn.id}">↩</a></li>`)
|
|
452
|
+
.join('\n');
|
|
453
|
+
result += `\n<section class="footnotes">\n<ol>\n${footnotesHtml}\n</ol>\n</section>`;
|
|
454
|
+
}
|
|
455
|
+
return result;
|
|
456
|
+
},
|
|
405
457
|
};
|
|
406
458
|
exports.asAST = {
|
|
407
|
-
mapper: (tag, args) => children => [
|
|
459
|
+
mapper: (tag, args) => (children) => [
|
|
408
460
|
tag,
|
|
409
461
|
args ? args : null,
|
|
410
|
-
|
|
462
|
+
// Unwrap single-element arrays containing only a string for cleaner AST
|
|
463
|
+
Array.isArray(children) && children.length === 1 && typeof children[0] === 'string' ? children[0] : children,
|
|
411
464
|
],
|
|
412
|
-
join: (x) =>
|
|
465
|
+
join: (x) => {
|
|
466
|
+
// Flatten nested single-element string arrays for cleaner AST
|
|
467
|
+
if (!Array.isArray(x))
|
|
468
|
+
return x;
|
|
469
|
+
return x.map((item) => {
|
|
470
|
+
if (Array.isArray(item) && item.length === 1 && typeof item[0] === 'string') {
|
|
471
|
+
return item[0];
|
|
472
|
+
}
|
|
473
|
+
return item;
|
|
474
|
+
});
|
|
475
|
+
},
|
|
413
476
|
postprocess: (obj) => {
|
|
414
|
-
|
|
415
|
-
|
|
477
|
+
// Filter empty strings and collect footnote definitions at top level only
|
|
478
|
+
const filtered = obj.filter((x) => x !== '');
|
|
479
|
+
const footnotes = [];
|
|
480
|
+
const result = [];
|
|
481
|
+
for (const node of filtered) {
|
|
482
|
+
if (Array.isArray(node) && node[0] === 'footnote-def') {
|
|
483
|
+
footnotes.push(node);
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
result.push(node);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// Append footnotes section at the end if any exist
|
|
490
|
+
if (footnotes.length > 0) {
|
|
491
|
+
footnotes.sort((a, b) => { var _a, _b; return parseInt(((_a = a[1]) === null || _a === void 0 ? void 0 : _a.id) || '0') - parseInt(((_b = b[1]) === null || _b === void 0 ? void 0 : _b.id) || '0'); });
|
|
492
|
+
result.push(['footnotes', null, footnotes]);
|
|
493
|
+
}
|
|
494
|
+
return result;
|
|
495
|
+
},
|
|
416
496
|
};
|
|
417
497
|
const parse = (s) => {
|
|
418
498
|
const p = new Parser({
|
package/package.json
CHANGED
|
@@ -3,12 +3,6 @@
|
|
|
3
3
|
"description": "Markdown parser with Aozora bunko ruby support and HTML passthrough",
|
|
4
4
|
"main": "./lib/src/parser.js",
|
|
5
5
|
"types": "./lib/src/parser.d.ts",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"build": "tsc",
|
|
8
|
-
"example": "cd examples && webpack",
|
|
9
|
-
"prepublish": "pnpm run build",
|
|
10
|
-
"test": "pnpm run build && mocha lib/test"
|
|
11
|
-
},
|
|
12
6
|
"author": "minamorl",
|
|
13
7
|
"license": "MIT",
|
|
14
8
|
"files": [
|
|
@@ -16,11 +10,16 @@
|
|
|
16
10
|
"LICENSE"
|
|
17
11
|
],
|
|
18
12
|
"devDependencies": {
|
|
13
|
+
"@eslint/js": "^9.39.2",
|
|
19
14
|
"@types/mocha": "^10.0.10",
|
|
20
15
|
"@types/node": "^20.0.0",
|
|
21
16
|
"@types/power-assert": "^1.5.12",
|
|
17
|
+
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
18
|
+
"@typescript-eslint/parser": "^8.54.0",
|
|
19
|
+
"eslint": "^9.39.2",
|
|
22
20
|
"mocha": "^11.7.5",
|
|
23
21
|
"power-assert": "^1.6.1",
|
|
22
|
+
"prettier": "^3.8.1",
|
|
24
23
|
"typescript": "^5.9.3",
|
|
25
24
|
"webpack": "^5.102.1",
|
|
26
25
|
"webpack-cli": "^6.0.1"
|
|
@@ -37,5 +36,15 @@
|
|
|
37
36
|
"url": "https://github.com/minamorl/markdown-next/issues"
|
|
38
37
|
},
|
|
39
38
|
"homepage": "https://github.com/minamorl/markdown-next#readme",
|
|
40
|
-
"version": "2.
|
|
41
|
-
|
|
39
|
+
"version": "2.2.1",
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"example": "cd examples && webpack",
|
|
43
|
+
"prepublish": "pnpm run build",
|
|
44
|
+
"test": "pnpm run build && mocha lib/test",
|
|
45
|
+
"lint": "eslint src test",
|
|
46
|
+
"lint:fix": "eslint src test --fix",
|
|
47
|
+
"format": "prettier --write 'src/**/*.ts' 'test/**/*.ts'",
|
|
48
|
+
"format:check": "prettier --check 'src/**/*.ts' 'test/**/*.ts'"
|
|
49
|
+
}
|
|
50
|
+
}
|