@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.
@@ -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
- 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(" ");
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("- ").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;
@@ -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
- const codeBlockStr = P.regexp(/[^\r\n]+/);
285
- const codeBlock = P.seqMap(codeBlockBegin, codeBlockDefinitionStr, linebreak, linebreak.or(codeBlockStr.lookahead(linebreak)).many(), codeBlockEnd, (_1, definition, _2, code, _3) => {
286
- if (code.length > 0) {
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("pre")(mapper("code")(join(code)));
291
- return mapper("pre", { "data-language": definition })(mapper("code")(join(code)));
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("br")(null)]));
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("blockquote")(result.reduce((a, b) => join([a, b])));
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("@["), 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)
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("$$"), P.alt(linebreak, P.eof), (_1, content, _3, _4) => {
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 "$$" + content.trim() + "$$";
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(""), pluginBlock, h1Special, h2Special, h6, h5, h4, h3, h2, h1, table, codeBlock, mathBlock, lists, blockquote, paragraph, linebreak.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: "shadow",
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("value"))
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("Parsing was failed.");
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
  '<': '&lt;',
393
408
  '>': '&gt;',
394
409
  '"': '&quot;',
395
- "'": '&#39;'
410
+ "'": '&#39;',
396
411
  };
397
412
  return text.replace(/[&<>"']/g, (m) => map[m]);
398
413
  }
399
414
  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
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
- children
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) => x, // identical
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
- return obj.filter((x) => (x !== ''));
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.0"
49
+ "version": "2.2.2"
41
50
  }