@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.
@@ -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,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("$").notFollowedBy(P.string("$")), P.regexp(/[^\$\r\n]+/), P.string("$"), (_1, content, _3) => {
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 "$" + content + "$";
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("tr")(join(headerCells.map(h => mapper("th")(parseCellContent(h)))));
173
- const bodyRows = bodyCells.map(row => mapper("tr")(join(row.map(cell => mapper("td")(parseCellContent(cell))))));
174
- 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]));
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("br")(null)), paragraphLine).map(join), inlines));
180
- const paragraph = paragraphLine
181
- .map(mapper("p"));
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("- ").or(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: "shadow",
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 = ((start == "* ") || (start == "- ")) ? "ul" : "ol";
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("Invalid indentation");
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().atLeast(1).map(nodeTypes => {
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 === "shadow") {
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("li")(treeOrNode.value);
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("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)))]));
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("pre")(mapper("code")(join(code)));
291
- 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)));
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("br")(null)]));
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("blockquote")(result.reduce((a, b) => join([a, b])));
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("@["), 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) => {
359
- 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)
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("$$"), P.alt(linebreak, P.eof), (_1, content, _3, _4) => {
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 "$$" + content.trim() + "$$";
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(""), 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(''));
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: "shadow",
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("value"))
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("Parsing was failed.");
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
  '<': '&lt;',
393
406
  '>': '&gt;',
394
407
  '"': '&quot;',
395
- "'": '&#39;'
408
+ "'": '&#39;',
396
409
  };
397
410
  return text.replace(/[&<>"']/g, (m) => map[m]);
398
411
  }
399
412
  exports.asHTML = {
400
- mapper: (tag, args) => children => [
401
- "<" + tag,
402
- args ? " " + Object.keys(args).map(x => `${x}="${escapeHtml(String(args[x]))}"`).join(" ") : "",
403
- children !== null && children !== "" ? ">" + children + "</" + tag + ">" : " />"
404
- ].join(""),
405
- join: (x) => x.join(""),
406
- 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
+ },
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
- 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,
413
464
  ],
414
- 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
+ },
415
476
  postprocess: (obj) => {
416
- return obj.filter((x) => (x !== ''));
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.0"
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
+ }