@minamorl/markdown-next 2.2.0 → 2.2.2
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 +184 -104
- package/package.json +11 -2
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
|
-
const paragraphEnd = ignore(
|
|
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(" ");
|
|
174
|
+
const paragraphEnd = ignore(/```[^`\r\n]*\n[\s\S]*?\n```/);
|
|
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;
|
|
@@ -281,22 +278,24 @@ class Parser {
|
|
|
281
278
|
const codeBlockBegin = P.regexp(/^```/);
|
|
282
279
|
const codeBlockEnd = P.regexp(/^```/);
|
|
283
280
|
const codeBlockDefinitionStr = P.regexp(/[^`\r\n]*/);
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
281
|
+
// Match a code line that is NOT a closing fence (``` at start of line)
|
|
282
|
+
const codeBlockStr = P.regexp(/(?!```)[^\r\n]+/);
|
|
283
|
+
const codeBlock = P.seqMap(codeBlockBegin, codeBlockDefinitionStr, linebreak, linebreak.or(codeBlockStr).many(), linebreak.atMost(1), codeBlockEnd, (_1, definition, _2, code, _3, _4) => {
|
|
284
|
+
// Remove trailing linebreak consumed by .many() before the closing fence
|
|
285
|
+
while (code.length > 0 && (code[code.length - 1] === '\n' || code[code.length - 1] === '\r\n' || code[code.length - 1] === '\r' || code[code.length - 1] === '')) {
|
|
287
286
|
code.pop();
|
|
288
287
|
}
|
|
289
|
-
if (definition ===
|
|
290
|
-
return mapper(
|
|
291
|
-
return mapper(
|
|
288
|
+
if (definition === '')
|
|
289
|
+
return mapper('pre')(mapper('code')(join(code)));
|
|
290
|
+
return mapper('pre', { 'data-language': definition })(mapper('code')(join(code)));
|
|
292
291
|
});
|
|
293
|
-
const blockquoteBegin = P.string(
|
|
292
|
+
const blockquoteBegin = P.string('> ');
|
|
294
293
|
// Parse blockquote content using inlines to support HTML tags, ruby, and math
|
|
295
294
|
const blockquoteInline = P.alt(pluginInline, aozoraRuby, anchor, img, em, strong, code, mathInline, htmlSelfClosing, htmlElement, P.regexp(/[^\r\n<|\[\]\*\`\@\$]+/), P.regexp(/./));
|
|
296
295
|
const blockquoteContent = blockquoteInline.atLeast(1).map(join);
|
|
297
296
|
const blockquoteLine = P.lazy(() => {
|
|
298
297
|
let blockquoteLevel = 0;
|
|
299
|
-
return P.seqMap(blockquoteBegin.then(blockquoteBegin.many().map(x => blockquoteLevel = x.length)), blockquoteContent, linebreak.atMost(1), (_1, text, _2) => {
|
|
298
|
+
return P.seqMap(blockquoteBegin.then(blockquoteBegin.many().map((x) => (blockquoteLevel = x.length))), blockquoteContent, linebreak.atMost(1), (_1, text, _2) => {
|
|
300
299
|
return { text, blockquoteLevel };
|
|
301
300
|
});
|
|
302
301
|
});
|
|
@@ -337,7 +336,7 @@ class Parser {
|
|
|
337
336
|
for (const [i, v] of tree.children.entries()) {
|
|
338
337
|
if (v.text !== null) {
|
|
339
338
|
if (tree.children[i + 1] && tree.children[i + 1].text !== null) {
|
|
340
|
-
result.push(join([v.text, mapper(
|
|
339
|
+
result.push(join([v.text, mapper('br')(null)]));
|
|
341
340
|
}
|
|
342
341
|
else {
|
|
343
342
|
result.push(v.text);
|
|
@@ -347,26 +346,42 @@ class Parser {
|
|
|
347
346
|
result.push(parseBlockquoteTree(v));
|
|
348
347
|
}
|
|
349
348
|
}
|
|
350
|
-
const _result = mapper(
|
|
349
|
+
const _result = mapper('blockquote')(result.reduce((a, b) => join([a, b])));
|
|
351
350
|
return _result;
|
|
352
351
|
};
|
|
353
352
|
const blockquote = P.lazy(() => {
|
|
354
|
-
return blockquoteLine.atLeast(1).map(x => {
|
|
353
|
+
return blockquoteLine.atLeast(1).map((x) => {
|
|
355
354
|
return parseBlockquoteTree(createBlockquoteTree(x), true);
|
|
356
355
|
});
|
|
357
356
|
});
|
|
358
|
-
const pluginBlock = P.seqMap(P.string(
|
|
359
|
-
|
|
357
|
+
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'))
|
|
358
|
+
.map(join)
|
|
359
|
+
.atLeast(1)
|
|
360
|
+
.map(join), (_1, pluginName, args, _2, content) => {
|
|
361
|
+
return this.opts.plugins && this.opts.plugins[pluginName]
|
|
362
|
+
? this.opts.plugins[pluginName](args, content, mapper, join)
|
|
360
363
|
: join([_1, pluginName, args, _2, content]);
|
|
361
364
|
});
|
|
362
365
|
// Block math: $$...$$ (must be on its own line or surrounded by newlines)
|
|
363
366
|
// Content can include anything except $$ (including single $, newlines, etc.)
|
|
364
367
|
const mathBlockContent = P.regexp(/[\s\S]*?(?=\$\$)/);
|
|
365
|
-
const mathBlock = P.seqMap(P.regexp(/^\$\$/), mathBlockContent, P.string(
|
|
368
|
+
const mathBlock = P.seqMap(P.regexp(/^\$\$/), mathBlockContent, P.string('$$'), P.alt(linebreak, P.eof), (_1, content, _3, _4) => {
|
|
366
369
|
// Output raw $$...$$ for MathJax auto-detection
|
|
367
|
-
return
|
|
370
|
+
return '$$' + content.trim() + '$$';
|
|
371
|
+
});
|
|
372
|
+
// Footnote definition: [^1]: content (with optional indented continuation lines)
|
|
373
|
+
const footnoteDefFirstLine = P.regexp(/[^\r\n]*/);
|
|
374
|
+
const footnoteDefContLine = P.seqMap(P.regexp(/^ /), // 4 spaces or tab for continuation
|
|
375
|
+
P.regexp(/[^\r\n]*/), (_indent, content) => content);
|
|
376
|
+
const footnoteDefContLineAlt = P.seqMap(P.string('\t'), // tab for continuation
|
|
377
|
+
P.regexp(/[^\r\n]*/), (_indent, content) => content);
|
|
378
|
+
const footnoteDef = P.seqMap(P.regexp(/^\[\^([0-9]+)\]:\s*/, 1), footnoteDefFirstLine, P.seq(linebreak, P.alt(footnoteDefContLine, footnoteDefContLineAlt))
|
|
379
|
+
.map(([_br, content]) => content)
|
|
380
|
+
.many(), P.alt(linebreak, P.eof), (num, firstLine, contLines, _end) => {
|
|
381
|
+
const fullContent = [firstLine, ...contLines].join('\n').trim();
|
|
382
|
+
return mapper('footnote-def', { id: num })(fullContent);
|
|
368
383
|
});
|
|
369
|
-
const block = P.alt(P.regexp(/\s+/).result(
|
|
384
|
+
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
385
|
this.acceptables = P.alt(block).many().map(join);
|
|
371
386
|
}
|
|
372
387
|
parse(s) {
|
|
@@ -374,15 +389,15 @@ class Parser {
|
|
|
374
389
|
this.rootTree = this.currentTree = {
|
|
375
390
|
value: null,
|
|
376
391
|
children: [],
|
|
377
|
-
type:
|
|
378
|
-
parent: null
|
|
392
|
+
type: 'shadow',
|
|
393
|
+
parent: null,
|
|
379
394
|
};
|
|
380
395
|
const parsed = this.acceptables.parse(s.trim());
|
|
381
|
-
if (parsed.status === true && parsed.hasOwnProperty(
|
|
396
|
+
if (parsed.status === true && parsed.hasOwnProperty('value'))
|
|
382
397
|
return this.opts.export.postprocess(parsed.value);
|
|
383
398
|
console.error(s.trim());
|
|
384
399
|
console.error(parsed);
|
|
385
|
-
throw new Error(
|
|
400
|
+
throw new Error('Parsing was failed.');
|
|
386
401
|
}
|
|
387
402
|
}
|
|
388
403
|
exports.Parser = Parser;
|
|
@@ -392,29 +407,94 @@ function escapeHtml(text) {
|
|
|
392
407
|
'<': '<',
|
|
393
408
|
'>': '>',
|
|
394
409
|
'"': '"',
|
|
395
|
-
"'": '''
|
|
410
|
+
"'": ''',
|
|
396
411
|
};
|
|
397
412
|
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
398
413
|
}
|
|
399
414
|
exports.asHTML = {
|
|
400
|
-
mapper: (tag, args) => children =>
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
415
|
+
mapper: (tag, args) => (children) => {
|
|
416
|
+
// Handle footnote reference - output as superscript link
|
|
417
|
+
if (tag === 'footnote-ref' && (args === null || args === void 0 ? void 0 : args.id)) {
|
|
418
|
+
const id = args.id;
|
|
419
|
+
return `<sup id="fnref${id}"><a href="#fn${id}">${id}</a></sup>`;
|
|
420
|
+
}
|
|
421
|
+
// Handle footnote definition - store for later collection
|
|
422
|
+
if (tag === 'footnote-def' && (args === null || args === void 0 ? void 0 : args.id)) {
|
|
423
|
+
const id = args.id;
|
|
424
|
+
return `<footnote-def data-id="${id}">${children}</footnote-def>`;
|
|
425
|
+
}
|
|
426
|
+
return [
|
|
427
|
+
'<' + tag,
|
|
428
|
+
args
|
|
429
|
+
? ' ' +
|
|
430
|
+
Object.keys(args)
|
|
431
|
+
.map((x) => `${x}="${escapeHtml(String(args[x]))}"`)
|
|
432
|
+
.join(' ')
|
|
433
|
+
: '',
|
|
434
|
+
children !== null && children !== '' ? '>' + children + '</' + tag + '>' : ' />',
|
|
435
|
+
].join('');
|
|
436
|
+
},
|
|
437
|
+
join: (x) => x.join(''),
|
|
438
|
+
postprocess: (x) => {
|
|
439
|
+
// Collect footnote definitions and move them to the end
|
|
440
|
+
const footnoteDefRegex = /<footnote-def data-id="(\d+)">([\s\S]*?)<\/footnote-def>/g;
|
|
441
|
+
const footnotes = [];
|
|
442
|
+
let match;
|
|
443
|
+
while ((match = footnoteDefRegex.exec(x)) !== null) {
|
|
444
|
+
footnotes.push({ id: match[1], content: match[2] });
|
|
445
|
+
}
|
|
446
|
+
// Remove footnote-def tags from body
|
|
447
|
+
let result = x.replace(/<footnote-def data-id="\d+">([\s\S]*?)<\/footnote-def>/g, '');
|
|
448
|
+
// Append footnotes section if any exist
|
|
449
|
+
if (footnotes.length > 0) {
|
|
450
|
+
// Sort by id
|
|
451
|
+
footnotes.sort((a, b) => parseInt(a.id) - parseInt(b.id));
|
|
452
|
+
const footnotesHtml = footnotes
|
|
453
|
+
.map((fn) => `<li id="fn${fn.id}">${fn.content} <a href="#fnref${fn.id}">↩</a></li>`)
|
|
454
|
+
.join('\n');
|
|
455
|
+
result += `\n<section class="footnotes">\n<ol>\n${footnotesHtml}\n</ol>\n</section>`;
|
|
456
|
+
}
|
|
457
|
+
return result;
|
|
458
|
+
},
|
|
407
459
|
};
|
|
408
460
|
exports.asAST = {
|
|
409
|
-
mapper: (tag, args) => children => [
|
|
461
|
+
mapper: (tag, args) => (children) => [
|
|
410
462
|
tag,
|
|
411
463
|
args ? args : null,
|
|
412
|
-
|
|
464
|
+
// Unwrap single-element arrays containing only a string for cleaner AST
|
|
465
|
+
Array.isArray(children) && children.length === 1 && typeof children[0] === 'string' ? children[0] : children,
|
|
413
466
|
],
|
|
414
|
-
join: (x) =>
|
|
467
|
+
join: (x) => {
|
|
468
|
+
// Flatten nested single-element string arrays for cleaner AST
|
|
469
|
+
if (!Array.isArray(x))
|
|
470
|
+
return x;
|
|
471
|
+
return x.map((item) => {
|
|
472
|
+
if (Array.isArray(item) && item.length === 1 && typeof item[0] === 'string') {
|
|
473
|
+
return item[0];
|
|
474
|
+
}
|
|
475
|
+
return item;
|
|
476
|
+
});
|
|
477
|
+
},
|
|
415
478
|
postprocess: (obj) => {
|
|
416
|
-
|
|
417
|
-
|
|
479
|
+
// Filter empty strings and collect footnote definitions at top level only
|
|
480
|
+
const filtered = obj.filter((x) => x !== '');
|
|
481
|
+
const footnotes = [];
|
|
482
|
+
const result = [];
|
|
483
|
+
for (const node of filtered) {
|
|
484
|
+
if (Array.isArray(node) && node[0] === 'footnote-def') {
|
|
485
|
+
footnotes.push(node);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
result.push(node);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// Append footnotes section at the end if any exist
|
|
492
|
+
if (footnotes.length > 0) {
|
|
493
|
+
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'); });
|
|
494
|
+
result.push(['footnotes', null, footnotes]);
|
|
495
|
+
}
|
|
496
|
+
return result;
|
|
497
|
+
},
|
|
418
498
|
};
|
|
419
499
|
const parse = (s) => {
|
|
420
500
|
const p = new Parser({
|
package/package.json
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
"build": "tsc",
|
|
8
8
|
"example": "cd examples && webpack",
|
|
9
9
|
"prepublish": "pnpm run build",
|
|
10
|
-
"test": "pnpm run build && mocha lib/test"
|
|
10
|
+
"test": "pnpm run build && mocha lib/test",
|
|
11
|
+
"lint": "eslint src test",
|
|
12
|
+
"lint:fix": "eslint src test --fix",
|
|
13
|
+
"format": "prettier --write 'src/**/*.ts' 'test/**/*.ts'",
|
|
14
|
+
"format:check": "prettier --check 'src/**/*.ts' 'test/**/*.ts'"
|
|
11
15
|
},
|
|
12
16
|
"author": "minamorl",
|
|
13
17
|
"license": "MIT",
|
|
@@ -16,11 +20,16 @@
|
|
|
16
20
|
"LICENSE"
|
|
17
21
|
],
|
|
18
22
|
"devDependencies": {
|
|
23
|
+
"@eslint/js": "^9.39.2",
|
|
19
24
|
"@types/mocha": "^10.0.10",
|
|
20
25
|
"@types/node": "^20.0.0",
|
|
21
26
|
"@types/power-assert": "^1.5.12",
|
|
27
|
+
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
28
|
+
"@typescript-eslint/parser": "^8.54.0",
|
|
29
|
+
"eslint": "^9.39.2",
|
|
22
30
|
"mocha": "^11.7.5",
|
|
23
31
|
"power-assert": "^1.6.1",
|
|
32
|
+
"prettier": "^3.8.1",
|
|
24
33
|
"typescript": "^5.9.3",
|
|
25
34
|
"webpack": "^5.102.1",
|
|
26
35
|
"webpack-cli": "^6.0.1"
|
|
@@ -37,5 +46,5 @@
|
|
|
37
46
|
"url": "https://github.com/minamorl/markdown-next/issues"
|
|
38
47
|
},
|
|
39
48
|
"homepage": "https://github.com/minamorl/markdown-next#readme",
|
|
40
|
-
"version": "2.2.
|
|
49
|
+
"version": "2.2.2"
|
|
41
50
|
}
|