@minamorl/markdown-next 2.2.0 → 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 +178 -100
- 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,13 +110,17 @@ class Parser {
|
|
|
118
110
|
return mapper(tag)(join(children));
|
|
119
111
|
});
|
|
120
112
|
});
|
|
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
|
+
});
|
|
121
117
|
// Math expressions - output raw $...$ for MathJax to process
|
|
122
118
|
// Inline math: $...$
|
|
123
|
-
const mathInline = P.seqMap(P.string(
|
|
119
|
+
const mathInline = P.seqMap(P.string('$').notFollowedBy(P.string('$')), P.regexp(/[^\$\r\n]+/), P.string('$'), (_1, content, _3) => {
|
|
124
120
|
// Output raw $...$ for MathJax auto-detection
|
|
125
|
-
return
|
|
121
|
+
return '$' + content + '$';
|
|
126
122
|
});
|
|
127
|
-
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(/./));
|
|
128
124
|
// Table cell content - supports inline elements
|
|
129
125
|
const tableCellInline = P.alt(anchor, img, em, strong, code, P.regexp(/[^\r\n\[\]\*|`]+/));
|
|
130
126
|
// Parse a single table row: |cell|cell|cell| with flexible spacing
|
|
@@ -136,7 +132,7 @@ class Parser {
|
|
|
136
132
|
}
|
|
137
133
|
// Remove first and last pipe, split by remaining pipes
|
|
138
134
|
const inner = trimmed.slice(1, -1);
|
|
139
|
-
return inner.split('|').map(cell => cell.trim());
|
|
135
|
+
return inner.split('|').map((cell) => cell.trim());
|
|
140
136
|
};
|
|
141
137
|
// Table row regex - matches |...|
|
|
142
138
|
const tableRowLine = P.regexp(/^\|[^\r\n]+\|/, 0);
|
|
@@ -169,21 +165,20 @@ class Parser {
|
|
|
169
165
|
if (headerCells.length === 0) {
|
|
170
166
|
return P.makeFailure(0, 'No header cells');
|
|
171
167
|
}
|
|
172
|
-
const headerRow = mapper(
|
|
173
|
-
const bodyRows = bodyCells.map(row => mapper(
|
|
174
|
-
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]));
|
|
175
171
|
});
|
|
176
172
|
const inlines = inline.atLeast(1).map(join);
|
|
177
173
|
const paragraphBegin = inlines;
|
|
178
174
|
const paragraphEnd = ignore(/```\n.*\n```/);
|
|
179
|
-
const paragraphLine = P.lazy(() => P.alt(P.seq(paragraphBegin, linebreak.skip(paragraphEnd).result(mapper(
|
|
180
|
-
const paragraph = paragraphLine
|
|
181
|
-
|
|
182
|
-
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(' ');
|
|
183
178
|
// List item content - supports inline elements including math
|
|
184
179
|
const liInlineContent = P.alt(mathInline, anchor, img, em, strong, code, P.regexp(/[^\r\n\[\]\*\`\$]+/), P.regexp(/./));
|
|
185
180
|
const liSingleLine = liInlineContent.atLeast(0).map(join);
|
|
186
|
-
const ulStart = P.string(
|
|
181
|
+
const ulStart = P.string('- ').or(P.string('* '));
|
|
187
182
|
const olStart = P.regexp(/[0-9]+\. /);
|
|
188
183
|
let liLevel = [1];
|
|
189
184
|
let counter = 0;
|
|
@@ -191,8 +186,8 @@ class Parser {
|
|
|
191
186
|
this.rootTree = this.currentTree = {
|
|
192
187
|
value: null,
|
|
193
188
|
children: [],
|
|
194
|
-
type:
|
|
195
|
-
parent: null
|
|
189
|
+
type: 'shadow',
|
|
190
|
+
parent: null,
|
|
196
191
|
};
|
|
197
192
|
liLevel = [1];
|
|
198
193
|
counter = 0;
|
|
@@ -202,19 +197,19 @@ class Parser {
|
|
|
202
197
|
let nodeType;
|
|
203
198
|
// detect which types of content
|
|
204
199
|
liLevel.push(index.column);
|
|
205
|
-
nodeType =
|
|
200
|
+
nodeType = start == '* ' || start == '- ' ? 'ul' : 'ol';
|
|
206
201
|
counter += 1;
|
|
207
202
|
return { counter, nodeType, str, liLevel, index };
|
|
208
203
|
})
|
|
209
204
|
.skip(linebreak.atMost(1))
|
|
210
|
-
.chain(v => {
|
|
211
|
-
if (v.liLevel.filter(x => x % 2 !== 1).length > 0) {
|
|
205
|
+
.chain((v) => {
|
|
206
|
+
if (v.liLevel.filter((x) => x % 2 !== 1).length > 0) {
|
|
212
207
|
initializeList();
|
|
213
|
-
return P.fail(
|
|
208
|
+
return P.fail('Invalid indentation');
|
|
214
209
|
}
|
|
215
210
|
return P.succeed(v);
|
|
216
211
|
})
|
|
217
|
-
.map(v => {
|
|
212
|
+
.map((v) => {
|
|
218
213
|
const liLevelBefore = liLevel[v.counter - 1];
|
|
219
214
|
const liLevelCurrent = liLevel[v.counter];
|
|
220
215
|
if (liLevelBefore === liLevelCurrent) {
|
|
@@ -222,7 +217,7 @@ class Parser {
|
|
|
222
217
|
value: v.str,
|
|
223
218
|
children: [],
|
|
224
219
|
type: v.nodeType,
|
|
225
|
-
parent: this.currentTree
|
|
220
|
+
parent: this.currentTree,
|
|
226
221
|
});
|
|
227
222
|
}
|
|
228
223
|
else if (liLevelBefore < liLevelCurrent) {
|
|
@@ -232,7 +227,7 @@ class Parser {
|
|
|
232
227
|
children: [],
|
|
233
228
|
type: v.nodeType,
|
|
234
229
|
parent: this.currentTree,
|
|
235
|
-
value: v.str
|
|
230
|
+
value: v.str,
|
|
236
231
|
});
|
|
237
232
|
}
|
|
238
233
|
else if (liLevelBefore > liLevelCurrent) {
|
|
@@ -246,7 +241,7 @@ class Parser {
|
|
|
246
241
|
type: v.nodeType,
|
|
247
242
|
children: [],
|
|
248
243
|
parent: this.currentTree,
|
|
249
|
-
value: v.str
|
|
244
|
+
value: v.str,
|
|
250
245
|
});
|
|
251
246
|
}
|
|
252
247
|
const _nodeType = v.nodeType;
|
|
@@ -254,7 +249,9 @@ class Parser {
|
|
|
254
249
|
});
|
|
255
250
|
};
|
|
256
251
|
const lists = P.lazy(() => {
|
|
257
|
-
return listLineContent()
|
|
252
|
+
return listLineContent()
|
|
253
|
+
.atLeast(1)
|
|
254
|
+
.map((nodeTypes) => {
|
|
258
255
|
this.rootTree.type = nodeTypes[0];
|
|
259
256
|
const result = treeToHtml(this.rootTree);
|
|
260
257
|
// initialization
|
|
@@ -263,15 +260,15 @@ class Parser {
|
|
|
263
260
|
});
|
|
264
261
|
});
|
|
265
262
|
const treeToHtml = (treeOrNode) => {
|
|
266
|
-
if (treeOrNode.type ===
|
|
263
|
+
if (treeOrNode.type === 'shadow') {
|
|
267
264
|
return join(treeOrNode.children.map(treeToHtml));
|
|
268
265
|
}
|
|
269
266
|
else if (treeOrNode.children.length === 0 && treeOrNode.value !== null) {
|
|
270
|
-
return mapper(
|
|
267
|
+
return mapper('li')(treeOrNode.value);
|
|
271
268
|
}
|
|
272
269
|
else if (treeOrNode.children.length !== 0 && treeOrNode.value !== null) {
|
|
273
270
|
const { children } = treeOrNode;
|
|
274
|
-
return mapper(
|
|
271
|
+
return mapper('li')(join([treeOrNode.value, mapper(treeOrNode.children[0].type)(join(children.map(treeToHtml)))]));
|
|
275
272
|
}
|
|
276
273
|
else {
|
|
277
274
|
const { children } = treeOrNode;
|
|
@@ -286,17 +283,17 @@ class Parser {
|
|
|
286
283
|
if (code.length > 0) {
|
|
287
284
|
code.pop();
|
|
288
285
|
}
|
|
289
|
-
if (definition ===
|
|
290
|
-
return mapper(
|
|
291
|
-
return mapper(
|
|
286
|
+
if (definition === '')
|
|
287
|
+
return mapper('pre')(mapper('code')(join(code)));
|
|
288
|
+
return mapper('pre', { 'data-language': definition })(mapper('code')(join(code)));
|
|
292
289
|
});
|
|
293
|
-
const blockquoteBegin = P.string(
|
|
290
|
+
const blockquoteBegin = P.string('> ');
|
|
294
291
|
// Parse blockquote content using inlines to support HTML tags, ruby, and math
|
|
295
292
|
const blockquoteInline = P.alt(pluginInline, aozoraRuby, anchor, img, em, strong, code, mathInline, htmlSelfClosing, htmlElement, P.regexp(/[^\r\n<|\[\]\*\`\@\$]+/), P.regexp(/./));
|
|
296
293
|
const blockquoteContent = blockquoteInline.atLeast(1).map(join);
|
|
297
294
|
const blockquoteLine = P.lazy(() => {
|
|
298
295
|
let blockquoteLevel = 0;
|
|
299
|
-
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) => {
|
|
300
297
|
return { text, blockquoteLevel };
|
|
301
298
|
});
|
|
302
299
|
});
|
|
@@ -337,7 +334,7 @@ class Parser {
|
|
|
337
334
|
for (const [i, v] of tree.children.entries()) {
|
|
338
335
|
if (v.text !== null) {
|
|
339
336
|
if (tree.children[i + 1] && tree.children[i + 1].text !== null) {
|
|
340
|
-
result.push(join([v.text, mapper(
|
|
337
|
+
result.push(join([v.text, mapper('br')(null)]));
|
|
341
338
|
}
|
|
342
339
|
else {
|
|
343
340
|
result.push(v.text);
|
|
@@ -347,26 +344,42 @@ class Parser {
|
|
|
347
344
|
result.push(parseBlockquoteTree(v));
|
|
348
345
|
}
|
|
349
346
|
}
|
|
350
|
-
const _result = mapper(
|
|
347
|
+
const _result = mapper('blockquote')(result.reduce((a, b) => join([a, b])));
|
|
351
348
|
return _result;
|
|
352
349
|
};
|
|
353
350
|
const blockquote = P.lazy(() => {
|
|
354
|
-
return blockquoteLine.atLeast(1).map(x => {
|
|
351
|
+
return blockquoteLine.atLeast(1).map((x) => {
|
|
355
352
|
return parseBlockquoteTree(createBlockquoteTree(x), true);
|
|
356
353
|
});
|
|
357
354
|
});
|
|
358
|
-
const pluginBlock = P.seqMap(P.string(
|
|
359
|
-
|
|
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)
|
|
360
361
|
: join([_1, pluginName, args, _2, content]);
|
|
361
362
|
});
|
|
362
363
|
// Block math: $$...$$ (must be on its own line or surrounded by newlines)
|
|
363
364
|
// Content can include anything except $$ (including single $, newlines, etc.)
|
|
364
365
|
const mathBlockContent = P.regexp(/[\s\S]*?(?=\$\$)/);
|
|
365
|
-
const mathBlock = P.seqMap(P.regexp(/^\$\$/), mathBlockContent, P.string(
|
|
366
|
+
const mathBlock = P.seqMap(P.regexp(/^\$\$/), mathBlockContent, P.string('$$'), P.alt(linebreak, P.eof), (_1, content, _3, _4) => {
|
|
366
367
|
// Output raw $$...$$ for MathJax auto-detection
|
|
367
|
-
return
|
|
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);
|
|
368
381
|
});
|
|
369
|
-
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(''));
|
|
370
383
|
this.acceptables = P.alt(block).many().map(join);
|
|
371
384
|
}
|
|
372
385
|
parse(s) {
|
|
@@ -374,15 +387,15 @@ class Parser {
|
|
|
374
387
|
this.rootTree = this.currentTree = {
|
|
375
388
|
value: null,
|
|
376
389
|
children: [],
|
|
377
|
-
type:
|
|
378
|
-
parent: null
|
|
390
|
+
type: 'shadow',
|
|
391
|
+
parent: null,
|
|
379
392
|
};
|
|
380
393
|
const parsed = this.acceptables.parse(s.trim());
|
|
381
|
-
if (parsed.status === true && parsed.hasOwnProperty(
|
|
394
|
+
if (parsed.status === true && parsed.hasOwnProperty('value'))
|
|
382
395
|
return this.opts.export.postprocess(parsed.value);
|
|
383
396
|
console.error(s.trim());
|
|
384
397
|
console.error(parsed);
|
|
385
|
-
throw new Error(
|
|
398
|
+
throw new Error('Parsing was failed.');
|
|
386
399
|
}
|
|
387
400
|
}
|
|
388
401
|
exports.Parser = Parser;
|
|
@@ -392,29 +405,94 @@ function escapeHtml(text) {
|
|
|
392
405
|
'<': '<',
|
|
393
406
|
'>': '>',
|
|
394
407
|
'"': '"',
|
|
395
|
-
"'": '''
|
|
408
|
+
"'": ''',
|
|
396
409
|
};
|
|
397
410
|
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
398
411
|
}
|
|
399
412
|
exports.asHTML = {
|
|
400
|
-
mapper: (tag, args) => children =>
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
+
},
|
|
407
457
|
};
|
|
408
458
|
exports.asAST = {
|
|
409
|
-
mapper: (tag, args) => children => [
|
|
459
|
+
mapper: (tag, args) => (children) => [
|
|
410
460
|
tag,
|
|
411
461
|
args ? args : null,
|
|
412
|
-
|
|
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,
|
|
413
464
|
],
|
|
414
|
-
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
|
+
},
|
|
415
476
|
postprocess: (obj) => {
|
|
416
|
-
|
|
417
|
-
|
|
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
|
+
},
|
|
418
496
|
};
|
|
419
497
|
const parse = (s) => {
|
|
420
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.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
|
+
}
|