@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.
@@ -1,6 +1,6 @@
1
- import P = require("parsimmon");
1
+ import P = require('parsimmon');
2
2
  export interface ListTree {
3
- type: "ul" | "ol" | "shadow";
3
+ type: 'ul' | 'ol' | 'shadow';
4
4
  children: Array<ListTree>;
5
5
  value: string | null;
6
6
  parent: ListTree | null;
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: "shadow",
14
- parent: null
13
+ type: 'shadow',
14
+ parent: null,
15
15
  };
16
16
  this.currentTree = {
17
17
  value: null,
18
18
  children: [],
19
- type: "shadow",
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("\r\n").or(P.string("\n")).or(P.string("\r"));
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("\n")))
60
- .map(mapper("h1"));
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("\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("__"));
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
- .then(plainStr)
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
- .then(plainStr)
80
- .map(mapper("em"))
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("!["), P.regexp(/[^\]\r\n]+/), P.string("]("), P.regexp(/[^\)\r\n]+/), P.string(")"), (_1, alt, _2, src, _3) => {
86
- return mapper("img", { src, alt })(null);
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
- .then(codePlainStr)
92
- .map(mapper("code"))
93
- .skip(codeEnd);
94
- const pluginInline = P.seqMap(P.string("@["), P.regexp(/[a-zA-Z]+/), P.regexp(/:{0,1}([^\]]*)/, 1), P.string("]"), (_1, pluginName, args, _2) => {
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(""), P.regexp(/[^《]+/), P.string(""), P.regexp(/[^》]+/), P.string(""), (_pipe, base, _open, ruby, _close) => {
100
- return mapper("ruby")(join([base, mapper("rt")(ruby)]));
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
- // Math expressions (KaTeX-compatible)
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("$").notFollowedBy(P.string("$")), P.regexp(/[^\$\r\n]+/), P.string("$"), (_1, content, _3) => {
124
- return mapper("span", { class: "math math-inline", "data-math": content })(content);
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("tr")(join(headerCells.map(h => mapper("th")(parseCellContent(h)))));
172
- const bodyRows = bodyCells.map(row => mapper("tr")(join(row.map(cell => mapper("td")(parseCellContent(cell))))));
173
- return mapper("table")(join([headerRow, ...bodyRows]));
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("br")(null)), paragraphLine).map(join), inlines));
179
- const paragraph = paragraphLine
180
- .map(mapper("p"));
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("- ").or(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: "shadow",
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 = ((start == "* ") || (start == "- ")) ? "ul" : "ol";
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("Invalid indentation");
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().atLeast(1).map(nodeTypes => {
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 === "shadow") {
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("li")(treeOrNode.value);
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("li")(join([treeOrNode.value, mapper(treeOrNode.children[0].type)(join(children.map(treeToHtml)))]));
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("pre")(mapper("code")(join(code)));
290
- return mapper("pre", { "data-language": definition })(mapper("code")(join(code)));
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("br")(null)]));
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("blockquote")(result.reduce((a, b) => join([a, b])));
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("@["), P.regexp(/[a-zA-Z]+/), P.regexp(/(:[^\]]*)*/), P.string("]\n"), P.seq(P.string(" ").result(""), P.regexp(/[^\r\n]+/), linebreak.atMost(1).result("\n")).map(join).atLeast(1).map(join), (_1, pluginName, args, _2, content) => {
358
- return this.opts.plugins && this.opts.plugins[pluginName] ? this.opts.plugins[pluginName](args, content, mapper, join)
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("$$"), P.alt(linebreak, P.eof), (_1, content, _3, _4) => {
365
- return mapper("div", { class: "math math-display", "data-math": content.trim() })(content.trim());
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(""), pluginBlock, h1Special, h2Special, h6, h5, h4, h3, h2, h1, table, codeBlock, mathBlock, lists, blockquote, paragraph, linebreak.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: "shadow",
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("value"))
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("Parsing was failed.");
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
  '<': '&lt;',
391
406
  '>': '&gt;',
392
407
  '"': '&quot;',
393
- "'": '&#39;'
408
+ "'": '&#39;',
394
409
  };
395
410
  return text.replace(/[&<>"']/g, (m) => map[m]);
396
411
  }
397
412
  exports.asHTML = {
398
- mapper: (tag, args) => children => [
399
- "<" + tag,
400
- args ? " " + Object.keys(args).map(x => `${x}="${escapeHtml(String(args[x]))}"`).join(" ") : "",
401
- children !== null && children !== "" ? ">" + children + "</" + tag + ">" : " />"
402
- ].join(""),
403
- join: (x) => x.join(""),
404
- postprocess: (x) => x
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
- children
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) => x, // identical
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
- return obj.filter((x) => (x !== ''));
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.1.1"
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
+ }