@xjtlumedia/markdown-mcp-server 2.1.2 → 2.2.0

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.
@@ -7,6 +7,29 @@ import remarkGfm from 'remark-gfm';
7
7
  import remarkRehype from 'remark-rehype';
8
8
  import rehypeStringify from 'rehype-stringify';
9
9
  export async function markdownToEmailHtml(md) {
10
+ // Pre-process highlight syntax (==text==) → <mark> since remark-gfm doesn't handle it
11
+ let processed = md.replace(/==([^=]+)==/g, '<mark>$1</mark>');
12
+ // Pre-process footnotes: collect definitions, convert refs to superscript, append endnotes
13
+ const footnoteDefRegex = /^\[\^(\w+)\]:\s*(.+)$/gm;
14
+ const footnotes = [];
15
+ let fnMatch;
16
+ while ((fnMatch = footnoteDefRegex.exec(processed)) !== null) {
17
+ footnotes.push({ label: fnMatch[1], text: fnMatch[2] });
18
+ }
19
+ if (footnotes.length > 0) {
20
+ // Remove footnote definitions from body
21
+ processed = processed.replace(/^\[\^(\w+)\]:\s*(.+)$/gm, '');
22
+ // Replace footnote references with superscript numbers
23
+ for (let idx = 0; idx < footnotes.length; idx++) {
24
+ const label = footnotes[idx].label;
25
+ processed = processed.replace(new RegExp(`\\[\\^${label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]`, 'g'), `<sup>[${idx + 1}]</sup>`);
26
+ }
27
+ // Append endnotes section
28
+ processed += '\n\n---\n\n';
29
+ for (let idx = 0; idx < footnotes.length; idx++) {
30
+ processed += `${idx + 1}. ${footnotes[idx].text}\n`;
31
+ }
32
+ }
10
33
  // First convert to basic HTML
11
34
  const htmlFile = await unified()
12
35
  .use(remarkParse)
@@ -15,7 +38,7 @@ export async function markdownToEmailHtml(md) {
15
38
  .use(remarkRehype)
16
39
  // @ts-ignore
17
40
  .use(rehypeStringify)
18
- .process(md);
41
+ .process(processed);
19
42
  let html = String(htmlFile);
20
43
  // Inline all styles for email client compatibility
21
44
  html = inlineEmailStyles(html);
@@ -90,5 +113,16 @@ function inlineEmailStyles(html) {
90
113
  // Strong and em (just ensure they work)
91
114
  out = out.replace(/<strong([^>]*)>/gi, '<strong$1 style="font-weight:bold;">');
92
115
  out = out.replace(/<em([^>]*)>/gi, '<em$1 style="font-style:italic;">');
116
+ // Strikethrough
117
+ out = out.replace(/<del([^>]*)>/gi, '<del$1 style="text-decoration:line-through;color:#999999;">');
118
+ // Highlight / mark
119
+ out = out.replace(/<mark([^>]*)>/gi, '<mark$1 style="background-color:#fff3cd;padding:1px 4px;border-radius:2px;">');
120
+ // Superscript and subscript
121
+ out = out.replace(/<sup([^>]*)>/gi, '<sup$1 style="font-size:75%;line-height:0;position:relative;vertical-align:baseline;top:-0.5em;">');
122
+ out = out.replace(/<sub([^>]*)>/gi, '<sub$1 style="font-size:75%;line-height:0;position:relative;vertical-align:baseline;bottom:-0.25em;">');
123
+ // Task list checkboxes
124
+ out = out.replace(/<input[^>]*checked[^>]*disabled[^>]*\/?>/gi, '&#9745; ');
125
+ out = out.replace(/<input[^>]*disabled[^>]*checked[^>]*\/?>/gi, '&#9745; ');
126
+ out = out.replace(/<input[^>]*type="checkbox"[^>]*\/?>/gi, '&#9744; ');
93
127
  return out;
94
128
  }
@@ -44,6 +44,15 @@ export function htmlToMarkdown(html) {
44
44
  md = md.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, '*$2*');
45
45
  // Strikethrough
46
46
  md = md.replace(/<(del|s|strike)[^>]*>([\s\S]*?)<\/\1>/gi, '~~$2~~');
47
+ // Highlight / mark
48
+ md = md.replace(/<mark[^>]*>([\s\S]*?)<\/mark>/gi, '==$1==');
49
+ // Keyboard
50
+ md = md.replace(/<kbd[^>]*>([\s\S]*?)<\/kbd>/gi, '`$1`');
51
+ // Superscript / subscript
52
+ md = md.replace(/<sup[^>]*>([\s\S]*?)<\/sup>/gi, '^$1^');
53
+ md = md.replace(/<sub[^>]*>([\s\S]*?)<\/sub>/gi, '~$1~');
54
+ // Abbreviations — just keep the text
55
+ md = md.replace(/<abbr[^>]*>([\s\S]*?)<\/abbr>/gi, '$1');
47
56
  // Code
48
57
  md = md.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, '`$1`');
49
58
  // Links
@@ -52,6 +61,40 @@ export function htmlToMarkdown(html) {
52
61
  md = md.replace(/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*\/?>/gi, '![$2]($1)');
53
62
  md = md.replace(/<img[^>]*alt="([^"]*)"[^>]*src="([^"]*)"[^>]*\/?>/gi, '![$1]($2)');
54
63
  md = md.replace(/<img[^>]*src="([^"]*)"[^>]*\/?>/gi, '![]($1)');
64
+ // Definition lists
65
+ md = md.replace(/<dl[^>]*>([\s\S]*?)<\/dl>/gi, (_m, content) => {
66
+ let result = '\n';
67
+ const dtRegex = /<dt[^>]*>([\s\S]*?)<\/dt>/gi;
68
+ const ddRegex = /<dd[^>]*>([\s\S]*?)<\/dd>/gi;
69
+ let dtMatch;
70
+ const dts = [];
71
+ const dds = [];
72
+ while ((dtMatch = dtRegex.exec(content)) !== null)
73
+ dts.push(dtMatch[1].replace(/<[^>]+>/g, '').trim());
74
+ let ddMatch;
75
+ while ((ddMatch = ddRegex.exec(content)) !== null)
76
+ dds.push(ddMatch[1].replace(/<[^>]+>/g, '').trim());
77
+ for (let i = 0; i < Math.max(dts.length, dds.length); i++) {
78
+ if (i < dts.length)
79
+ result += `${dts[i]}\n`;
80
+ if (i < dds.length)
81
+ result += `: ${dds[i]}\n`;
82
+ }
83
+ return result + '\n';
84
+ });
85
+ // Details/summary → blockquote with bold summary
86
+ md = md.replace(/<details[^>]*>\s*<summary[^>]*>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/gi, (_m, summary, content) => `\n> **${summary.replace(/<[^>]+>/g, '').trim()}**\n> ${content.replace(/<[^>]+>/g, '').trim()}\n`);
87
+ // Figure/figcaption
88
+ md = md.replace(/<figure[^>]*>([\s\S]*?)<\/figure>/gi, (_m, content) => {
89
+ const imgMatch = content.match(/<img[^>]*src="([^"]*)"[^>]*\/?>/i);
90
+ const captionMatch = content.match(/<figcaption[^>]*>([\s\S]*?)<\/figcaption>/i);
91
+ const src = imgMatch ? imgMatch[1] : '';
92
+ const caption = captionMatch ? captionMatch[1].replace(/<[^>]+>/g, '').trim() : '';
93
+ return src ? `\n![${caption}](${src})\n` : '';
94
+ });
95
+ // Task list checkboxes inside list items (already converted to - items)
96
+ md = md.replace(/- <input[^>]*checked[^>]*\/?>\s*/gi, '- [x] ');
97
+ md = md.replace(/- <input[^>]*type="checkbox"[^>]*\/?>\s*/gi, '- [ ] ');
55
98
  // Remove remaining HTML tags
56
99
  md = md.replace(/<[^>]+>/g, '');
57
100
  // Decode HTML entities
package/dist/index.js CHANGED
@@ -1143,10 +1143,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1143
1143
  return handleOutput(latex, outputPath);
1144
1144
  }
1145
1145
  if (name === "convert_to_docx") {
1146
- const elements = parseMarkdownToDocx(markdown);
1147
- const doc = new ((await import("docx")).Document)({
1146
+ const { elements, footnotes } = parseMarkdownToDocx(markdown);
1147
+ const docOptions = {
1148
1148
  sections: [{ children: elements }]
1149
- });
1149
+ };
1150
+ if (Object.keys(footnotes).length > 0) {
1151
+ docOptions.footnotes = footnotes;
1152
+ }
1153
+ const doc = new ((await import("docx")).Document)(docOptions);
1150
1154
  const buffer = await Packer.toBuffer(doc);
1151
1155
  return handleOutput(buffer, outputPath, {
1152
1156
  format: 'docx',
@@ -8,6 +8,7 @@ export function repairMarkdown(md) {
8
8
  out = repairHeadings(out);
9
9
  out = repairListIndentation(out);
10
10
  out = repairBrokenLinks(out);
11
+ out = repairBrokenTaskLists(out);
11
12
  out = normalizeWhitespace(out);
12
13
  return out;
13
14
  }
@@ -165,6 +166,17 @@ export function repairBrokenLinks(md) {
165
166
  out = out.replace(/\[([^\]]+)\(([^)]+)\)/g, '[$1]($2)');
166
167
  return out;
167
168
  }
169
+ // Fix broken task list syntax from LLM output
170
+ export function repairBrokenTaskLists(md) {
171
+ let out = md;
172
+ // Fix missing space in checkbox: - [] → - [ ]
173
+ out = out.replace(/^(\s*[-*+])\s+\[\](\s+)/gm, '$1 [ ]$2');
174
+ // Fix uppercase X: - [X] → - [x]
175
+ out = out.replace(/^(\s*[-*+]\s+)\[X\]/gm, '$1[x]');
176
+ // Fix no space after checkbox: - [x]text → - [x] text
177
+ out = out.replace(/^(\s*[-*+]\s+\[[ xX]\])([^\s])/gm, '$1 $2');
178
+ return out;
179
+ }
168
180
  // Normalize excessive whitespace
169
181
  export function normalizeWhitespace(md) {
170
182
  let out = md;
@@ -250,6 +262,14 @@ export function lintMarkdown(md) {
250
262
  fixable: true
251
263
  });
252
264
  }
265
+ // Broken task list syntax
266
+ if (/^\s*[-*+]\s+\[\]/.test(line)) {
267
+ issues.push({
268
+ line: lineNum, column: 1, severity: 'warning',
269
+ rule: 'broken-task-list', message: 'Missing space in task list checkbox: [] should be [ ]',
270
+ fixable: true
271
+ });
272
+ }
253
273
  }
254
274
  // Check for unclosed code fences
255
275
  if (inCodeBlock) {
@@ -1,6 +1,35 @@
1
+ // ── Shared pre-processing helpers ────────────────────────────────────
2
+ function collectFootnotes(text) {
3
+ const footnoteMap = {};
4
+ const cleaned = text.replace(/^\[\^(\w+)\]:\s*(.+)$/gm, (_m, label, content) => {
5
+ footnoteMap[label] = content;
6
+ return '';
7
+ });
8
+ return { cleaned, footnoteMap };
9
+ }
10
+ function appendEndnotes(text, footnoteMap, formatRef = (n) => `[${n}]`, separator = '\n\n---\n') {
11
+ const labels = Object.keys(footnoteMap);
12
+ if (labels.length === 0)
13
+ return text;
14
+ let out = text;
15
+ // Replace refs with formatted numbers
16
+ for (let i = 0; i < labels.length; i++) {
17
+ const escaped = labels[i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
18
+ out = out.replace(new RegExp(`\\[\\^${escaped}\\]`, 'g'), formatRef(i + 1));
19
+ }
20
+ // Append endnotes section
21
+ out += separator;
22
+ for (let i = 0; i < labels.length; i++) {
23
+ out += `\n${i + 1}. ${footnoteMap[labels[i]]}`;
24
+ }
25
+ return out;
26
+ }
1
27
  // ── Slack mrkdwn ─────────────────────────────────────────────────────
2
28
  export function markdownToSlack(md) {
3
- let out = md;
29
+ const { cleaned, footnoteMap } = collectFootnotes(md);
30
+ let out = cleaned;
31
+ // Highlight: ==text== → *text* (bold as closest Slack approximation)
32
+ out = out.replace(/==([^=]+)==/g, '*$1*');
4
33
  // Bold: **text** → *text*
5
34
  out = out.replace(/\*\*([^*]+)\*\*/g, '*$1*');
6
35
  // Italic: *text* or _text_ → _text_
@@ -15,32 +44,43 @@ export function markdownToSlack(md) {
15
44
  out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<$2|$1>');
16
45
  // Headers: # text → *text*
17
46
  out = out.replace(/^#{1,6}\s+(.+)$/gm, '*$1*');
47
+ // Task lists
48
+ out = out.replace(/^\s*[-*+]\s+\[x\]\s+(.+)$/gim, '☑ $1');
49
+ out = out.replace(/^\s*[-*+]\s+\[ \]\s+(.+)$/gm, '☐ $1');
18
50
  // Blockquotes: > text → > text (Slack supports >)
19
51
  // Ordered lists: 1. text stays the same
20
52
  // Unordered lists: - text stays the same
21
53
  // Horizontal rule
22
54
  out = out.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '───────────────');
23
- return out;
55
+ return appendEndnotes(out, footnoteMap);
24
56
  }
25
57
  // ── Discord markdown ─────────────────────────────────────────────────
26
58
  export function markdownToDiscord(md) {
27
- let out = md;
59
+ const { cleaned, footnoteMap } = collectFootnotes(md);
60
+ let out = cleaned;
61
+ // Highlight: ==text== → **text** (bold as closest Discord approximation)
62
+ out = out.replace(/==([^=]+)==/g, '**$1**');
28
63
  // Discord supports most standard markdown, just a few tweaks:
29
64
  // Headers: # text → **__text__** (Discord renders # only in certain contexts)
30
65
  out = out.replace(/^# (.+)$/gm, '**__$1__**');
31
66
  out = out.replace(/^## (.+)$/gm, '**$1**');
32
67
  out = out.replace(/^### (.+)$/gm, '__$1__');
33
68
  out = out.replace(/^#{4,6}\s+(.+)$/gm, '*$1*');
34
- // Spoiler: no markdown equivalent, keep as-is
69
+ // Task lists
70
+ out = out.replace(/^\s*[-*+]\s+\[x\]\s+(.+)$/gim, '- ☑ $1');
71
+ out = out.replace(/^\s*[-*+]\s+\[ \]\s+(.+)$/gm, '- ☐ $1');
35
72
  // Block quotes: > works in Discord
36
73
  // Code blocks: ``` works in Discord
37
74
  // Horizontal rules are not rendered in Discord
38
75
  out = out.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '');
39
- return out;
76
+ return appendEndnotes(out, footnoteMap);
40
77
  }
41
78
  // ── JIRA wiki markup ─────────────────────────────────────────────────
42
79
  export function markdownToJira(md) {
43
- let out = md;
80
+ const { cleaned, footnoteMap } = collectFootnotes(md);
81
+ let out = cleaned;
82
+ // Highlight: ==text== → {color:yellow}text{color}
83
+ out = out.replace(/==([^=]+)==/g, '{color:yellow}$1{color}');
44
84
  // Headers: # → h1., ## → h2., etc.
45
85
  out = out.replace(/^######\s+(.+)$/gm, 'h6. $1');
46
86
  out = out.replace(/^#####\s+(.+)$/gm, 'h5. $1');
@@ -72,6 +112,15 @@ export function markdownToJira(md) {
72
112
  const level = Math.floor(indent.length / 2) + 1;
73
113
  return '#'.repeat(level) + ' ';
74
114
  });
115
+ // Task lists
116
+ out = out.replace(/^(\s*)[-*+]\s+\[x\]\s+/gim, (_m, indent) => {
117
+ const level = Math.floor(indent.length / 2) + 1;
118
+ return '*'.repeat(level) + ' (/) ';
119
+ });
120
+ out = out.replace(/^(\s*)[-*+]\s+\[ \]\s+/gm, (_m, indent) => {
121
+ const level = Math.floor(indent.length / 2) + 1;
122
+ return '*'.repeat(level) + ' (x) ';
123
+ });
75
124
  // Blockquote: > text → {quote}text{quote}
76
125
  out = out.replace(/^>\s+(.+)$/gm, '{quote}$1{quote}');
77
126
  // Horizontal rules
@@ -101,11 +150,14 @@ export function markdownToJira(md) {
101
150
  result.push(line);
102
151
  }
103
152
  }
104
- return result.join('\n');
153
+ return appendEndnotes(result.join('\n'), footnoteMap, (n) => `^[${n}]^`, '\n\n----\n');
105
154
  }
106
155
  // ── Confluence wiki markup ───────────────────────────────────────────
107
156
  export function markdownToConfluence(md) {
108
- let out = md;
157
+ const { cleaned, footnoteMap } = collectFootnotes(md);
158
+ let out = cleaned;
159
+ // Highlight: ==text== → {color:yellow}text{color}
160
+ out = out.replace(/==([^=]+)==/g, '{color:yellow}$1{color}');
109
161
  // Headers
110
162
  out = out.replace(/^######\s+(.+)$/gm, 'h6. $1');
111
163
  out = out.replace(/^#####\s+(.+)$/gm, 'h5. $1');
@@ -138,14 +190,31 @@ export function markdownToConfluence(md) {
138
190
  out = out.replace(/^>\s+(.+)$/gm, '{quote}$1{quote}');
139
191
  // Horizontal rules
140
192
  out = out.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '----');
193
+ // Task lists
194
+ out = out.replace(/^(\s*)[-*+]\s+\[x\]\s+/gim, (_m, indent) => {
195
+ const level = Math.floor(indent.length / 2) + 1;
196
+ return '*'.repeat(level) + ' (/) ';
197
+ });
198
+ out = out.replace(/^(\s*)[-*+]\s+\[ \]\s+/gm, (_m, indent) => {
199
+ const level = Math.floor(indent.length / 2) + 1;
200
+ return '*'.repeat(level) + ' (x) ';
201
+ });
141
202
  // Info/note panels from HTML comments
142
203
  out = out.replace(/<!--\s*note:\s*([\s\S]*?)-->/gi, '{note}$1{note}');
143
204
  out = out.replace(/<!--\s*info:\s*([\s\S]*?)-->/gi, '{info}$1{info}');
144
- return out;
205
+ return appendEndnotes(out, footnoteMap, (n) => `^[${n}]^`, '\n\n----\n');
145
206
  }
146
207
  // ── AsciiDoc ─────────────────────────────────────────────────────────
147
208
  export function markdownToAsciiDoc(md) {
148
- let out = md;
209
+ const { cleaned, footnoteMap } = collectFootnotes(md);
210
+ let out = cleaned;
211
+ // Highlight: ==text== → [.mark]#text#
212
+ out = out.replace(/==([^=]+)==/g, '[.mark]#$1#');
213
+ // Footnote refs → native AsciiDoc inline footnotes
214
+ for (const [label, text] of Object.entries(footnoteMap)) {
215
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
216
+ out = out.replace(new RegExp(`\\[\\^${escaped}\\]`, 'g'), `footnote:[${text}]`);
217
+ }
149
218
  // Headers: # → =, ## → ==, etc.
150
219
  out = out.replace(/^######\s+(.+)$/gm, '====== $1');
151
220
  out = out.replace(/^#####\s+(.+)$/gm, '===== $1');
@@ -194,15 +263,58 @@ export function markdownToAsciiDoc(md) {
194
263
  out = result.join('\n');
195
264
  // Horizontal rules
196
265
  out = out.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, "'''");
266
+ // Task lists
267
+ out = out.replace(/^(\s*)[-*+]\s+\[x\]\s+/gim, '* [*] ');
268
+ out = out.replace(/^(\s*)[-*+]\s+\[ \]\s+/gm, '* [ ] ');
197
269
  // Unordered list marker stays as *
198
270
  out = out.replace(/^(\s*)[-+]\s+/gm, '* ');
199
271
  // Ordered list: 1. → .
200
272
  out = out.replace(/^\d+\.\s+/gm, '. ');
273
+ // Tables
274
+ out = convertMarkdownTableToAsciiDoc(out);
201
275
  return out;
202
276
  }
277
+ function convertMarkdownTableToAsciiDoc(text) {
278
+ const lines = text.split('\n');
279
+ const result = [];
280
+ let inTable = false;
281
+ for (const line of lines) {
282
+ const trimmed = line.trim();
283
+ if (trimmed.startsWith('|') && trimmed.endsWith('|')) {
284
+ if (/^\|[\s-:]+\|/.test(trimmed))
285
+ continue;
286
+ const cells = trimmed.split('|').filter(c => c.trim()).map(c => c.trim());
287
+ if (!inTable) {
288
+ result.push(`[cols="${cells.map(() => '1').join(',')}",options="header"]`);
289
+ result.push('|===');
290
+ inTable = true;
291
+ }
292
+ result.push(cells.map(c => '| ' + c).join(' '));
293
+ }
294
+ else {
295
+ if (inTable) {
296
+ result.push('|===');
297
+ inTable = false;
298
+ }
299
+ result.push(line);
300
+ }
301
+ }
302
+ if (inTable)
303
+ result.push('|===');
304
+ return result.join('\n');
305
+ }
203
306
  // ── reStructuredText ─────────────────────────────────────────────────
204
307
  export function markdownToRST(md) {
205
- let out = md;
308
+ const { cleaned, footnoteMap } = collectFootnotes(md);
309
+ let out = cleaned;
310
+ // Highlight: ==text== → **text** (RST has no native highlight)
311
+ out = out.replace(/==([^=]+)==/g, '**$1**');
312
+ // Footnote refs → RST native footnotes [#label]_
313
+ const fnLabels = Object.keys(footnoteMap);
314
+ for (const label of fnLabels) {
315
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
316
+ out = out.replace(new RegExp(`\\[\\^${escaped}\\]`, 'g'), ` [#fn_${label}]_`);
317
+ }
206
318
  // Code blocks: ```lang → .. code-block:: lang
207
319
  out = out.replace(/```(\w+)?\n([\s\S]*?)```/g, (_m, lang, code) => {
208
320
  const directive = lang ? `.. code-block:: ${lang}` : '.. code-block::';
@@ -248,11 +360,78 @@ export function markdownToRST(md) {
248
360
  out = out.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '----------');
249
361
  // Ordered list: keep as-is (RST uses #.)
250
362
  out = out.replace(/^(\d+)\.\s+/gm, '#. ');
363
+ // Task lists
364
+ out = out.replace(/^-\s+\[x\]\s+/gim, '- ☑ ');
365
+ out = out.replace(/^-\s+\[ \]\s+/gm, '- ☐ ');
366
+ // Tables
367
+ out = convertMarkdownTableToRST(out);
368
+ // Append RST footnote definitions
369
+ if (fnLabels.length > 0) {
370
+ out += '\n\n';
371
+ for (const label of fnLabels) {
372
+ out += `.. [#fn_${label}] ${footnoteMap[label]}\n`;
373
+ }
374
+ }
251
375
  return out;
252
376
  }
377
+ function convertMarkdownTableToRST(text) {
378
+ const lines = text.split('\n');
379
+ const result = [];
380
+ const tableRows = [];
381
+ let isCollecting = false;
382
+ for (let i = 0; i < lines.length; i++) {
383
+ const trimmed = lines[i].trim();
384
+ if (trimmed.startsWith('|') && trimmed.endsWith('|')) {
385
+ if (/^\|[\s-:]+\|/.test(trimmed))
386
+ continue;
387
+ const cells = trimmed.split('|').filter(c => c.trim()).map(c => c.trim());
388
+ tableRows.push(cells);
389
+ isCollecting = true;
390
+ }
391
+ else {
392
+ if (isCollecting && tableRows.length > 0) {
393
+ result.push(...renderRSTTable(tableRows));
394
+ tableRows.length = 0;
395
+ isCollecting = false;
396
+ }
397
+ result.push(lines[i]);
398
+ }
399
+ }
400
+ if (tableRows.length > 0)
401
+ result.push(...renderRSTTable(tableRows));
402
+ return result.join('\n');
403
+ }
404
+ function renderRSTTable(rows) {
405
+ if (rows.length === 0)
406
+ return [];
407
+ const colCount = Math.max(...rows.map(r => r.length));
408
+ const colWidths = Array(colCount).fill(3);
409
+ for (const row of rows) {
410
+ for (let j = 0; j < row.length; j++) {
411
+ colWidths[j] = Math.max(colWidths[j], (row[j] || '').length + 2);
412
+ }
413
+ }
414
+ const sep = '+' + colWidths.map(w => '-'.repeat(w)).join('+') + '+';
415
+ const headSep = '+' + colWidths.map(w => '='.repeat(w)).join('+') + '+';
416
+ const result = [sep];
417
+ for (let i = 0; i < rows.length; i++) {
418
+ const line = '|' + colWidths.map((w, j) => (' ' + (rows[i][j] || '') + ' ').padEnd(w)).join('|') + '|';
419
+ result.push(line);
420
+ result.push(i === 0 ? headSep : sep);
421
+ }
422
+ return result;
423
+ }
253
424
  // ── MediaWiki markup ─────────────────────────────────────────────────
254
425
  export function markdownToMediaWiki(md) {
255
- let out = md;
426
+ const { cleaned, footnoteMap } = collectFootnotes(md);
427
+ let out = cleaned;
428
+ // Highlight: ==text== → <mark>text</mark>
429
+ out = out.replace(/==([^=]+)==/g, '<mark>$1</mark>');
430
+ // Footnote refs → MediaWiki native <ref>text</ref>
431
+ for (const [label, text] of Object.entries(footnoteMap)) {
432
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
433
+ out = out.replace(new RegExp(`\\[\\^${escaped}\\]`, 'g'), `<ref>${text}</ref>`);
434
+ }
256
435
  // Headers: # text → == text ==
257
436
  out = out.replace(/^######\s+(.+)$/gm, '======= $1 =======');
258
437
  out = out.replace(/^#####\s+(.+)$/gm, '====== $1 ======');
@@ -319,11 +498,19 @@ export function markdownToMediaWiki(md) {
319
498
  }
320
499
  if (inTable)
321
500
  result.push('|}');
322
- return result.join('\n');
501
+ let mwOut = result.join('\n');
502
+ // Append <references/> if there were footnotes
503
+ if (Object.keys(footnoteMap).length > 0) {
504
+ mwOut += '\n\n== References ==\n<references/>';
505
+ }
506
+ return mwOut;
323
507
  }
324
508
  // ── BBCode ───────────────────────────────────────────────────────────
325
509
  export function markdownToBBCode(md) {
326
- let out = md;
510
+ const { cleaned, footnoteMap } = collectFootnotes(md);
511
+ let out = cleaned;
512
+ // Highlight: ==text== → [color=yellow]text[/color]
513
+ out = out.replace(/==([^=]+)==/g, '[color=yellow]$1[/color]');
327
514
  // Headers
328
515
  out = out.replace(/^#{1,6}\s+(.+)$/gm, '[b][size=5]$1[/size][/b]');
329
516
  // Bold
@@ -342,15 +529,66 @@ export function markdownToBBCode(md) {
342
529
  out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '[img]$2[/img]');
343
530
  // Blockquote
344
531
  out = out.replace(/^>\s+(.+)$/gm, '[quote]$1[/quote]');
532
+ // Task lists
533
+ out = out.replace(/^[-*+]\s+\[x\]\s+(.+)$/gim, '[*]☑ $1');
534
+ out = out.replace(/^[-*+]\s+\[ \]\s+(.+)$/gm, '[*]☐ $1');
345
535
  // Unordered list
346
536
  out = out.replace(/^[-*+]\s+(.+)$/gm, '[*]$1');
537
+ // Tables
538
+ out = convertMarkdownTableToBBCode(out);
347
539
  // Horizontal rules
348
540
  out = out.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '[hr]');
349
- return out;
541
+ return appendEndnotes(out, footnoteMap, (n) => `[sup][${n}][/sup]`, '\n\n[hr]\n');
542
+ }
543
+ function convertMarkdownTableToBBCode(text) {
544
+ const lines = text.split('\n');
545
+ const result = [];
546
+ let inTable = false;
547
+ let isHeader = true;
548
+ for (const line of lines) {
549
+ const trimmed = line.trim();
550
+ if (trimmed.startsWith('|') && trimmed.endsWith('|')) {
551
+ if (/^\|[\s-:]+\|/.test(trimmed)) {
552
+ isHeader = false;
553
+ continue;
554
+ }
555
+ const cells = trimmed.split('|').filter(c => c.trim()).map(c => c.trim());
556
+ if (!inTable) {
557
+ result.push('[table]');
558
+ inTable = true;
559
+ }
560
+ if (isHeader) {
561
+ result.push('[tr]' + cells.map(c => `[th]${c}[/th]`).join('') + '[/tr]');
562
+ }
563
+ else {
564
+ result.push('[tr]' + cells.map(c => `[td]${c}[/td]`).join('') + '[/tr]');
565
+ }
566
+ }
567
+ else {
568
+ if (inTable) {
569
+ result.push('[/table]');
570
+ inTable = false;
571
+ isHeader = true;
572
+ }
573
+ result.push(line);
574
+ }
575
+ }
576
+ if (inTable)
577
+ result.push('[/table]');
578
+ return result.join('\n');
350
579
  }
351
580
  // ── Textile ──────────────────────────────────────────────────────────
352
581
  export function markdownToTextile(md) {
353
- let out = md;
582
+ const { cleaned, footnoteMap } = collectFootnotes(md);
583
+ let out = cleaned;
584
+ // Highlight: ==text== → %{background:yellow}text%
585
+ out = out.replace(/==([^=]+)==/g, '%{background:yellow}$1%');
586
+ // Footnote refs → Textile native [N]
587
+ const fnLabels = Object.keys(footnoteMap);
588
+ for (let i = 0; i < fnLabels.length; i++) {
589
+ const escaped = fnLabels[i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
590
+ out = out.replace(new RegExp(`\\[\\^${escaped}\\]`, 'g'), `[${i + 1}]`);
591
+ }
354
592
  // Headers: # → h1., ## → h2.
355
593
  out = out.replace(/^######\s+(.+)$/gm, 'h6. $1');
356
594
  out = out.replace(/^#####\s+(.+)$/gm, 'h5. $1');
@@ -374,17 +612,63 @@ export function markdownToTextile(md) {
374
612
  out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '!$2($1)!');
375
613
  // Blockquote
376
614
  out = out.replace(/^>\s+(.+)$/gm, 'bq. $1');
615
+ // Task lists
616
+ out = out.replace(/^[-*+]\s+\[x\]\s+/gim, '* ☑ ');
617
+ out = out.replace(/^[-*+]\s+\[ \]\s+/gm, '* ☐ ');
377
618
  // Unordered list
378
619
  out = out.replace(/^[-+]\s+/gm, '* ');
379
620
  // Ordered list
380
621
  out = out.replace(/^\d+\.\s+/gm, '# ');
622
+ // Tables
623
+ out = convertMarkdownTableToTextile(out);
381
624
  // Horizontal rules
382
625
  out = out.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '---');
626
+ // Append Textile footnote definitions
627
+ if (fnLabels.length > 0) {
628
+ out += '\n\n';
629
+ for (let i = 0; i < fnLabels.length; i++) {
630
+ out += `fn${i + 1}. ${footnoteMap[fnLabels[i]]}\n`;
631
+ }
632
+ }
383
633
  return out;
384
634
  }
635
+ function convertMarkdownTableToTextile(text) {
636
+ const lines = text.split('\n');
637
+ const result = [];
638
+ let isHeader = true;
639
+ for (const line of lines) {
640
+ const trimmed = line.trim();
641
+ if (trimmed.startsWith('|') && trimmed.endsWith('|')) {
642
+ if (/^\|[\s-:]+\|/.test(trimmed)) {
643
+ isHeader = false;
644
+ continue;
645
+ }
646
+ const cells = trimmed.split('|').filter(c => c.trim()).map(c => c.trim());
647
+ if (isHeader) {
648
+ result.push('|_. ' + cells.join(' |_. ') + ' |');
649
+ }
650
+ else {
651
+ result.push('| ' + cells.join(' | ') + ' |');
652
+ }
653
+ }
654
+ else {
655
+ isHeader = true;
656
+ result.push(line);
657
+ }
658
+ }
659
+ return result.join('\n');
660
+ }
385
661
  // ── Org Mode ─────────────────────────────────────────────────────────
386
662
  export function markdownToOrgMode(md) {
387
- let out = md;
663
+ const { cleaned, footnoteMap } = collectFootnotes(md);
664
+ let out = cleaned;
665
+ // Highlight: ==text== → *text* (Org has no native highlight, use bold)
666
+ out = out.replace(/==([^=]+)==/g, '*$1*');
667
+ // Footnote refs → Org Mode native [fn:label]
668
+ for (const [label, text] of Object.entries(footnoteMap)) {
669
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
670
+ out = out.replace(new RegExp(`\\[\\^${escaped}\\]`, 'g'), `[fn:${label}]`);
671
+ }
388
672
  // Headers: # → *, ## → **, etc.
389
673
  out = out.replace(/^######\s+(.+)$/gm, '****** $1');
390
674
  out = out.replace(/^#####\s+(.+)$/gm, '***** $1');
@@ -409,11 +693,43 @@ export function markdownToOrgMode(md) {
409
693
  out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '[[$2]]');
410
694
  // Blockquote
411
695
  out = out.replace(/^>\s+(.+)$/gm, '#+BEGIN_QUOTE\n$1\n#+END_QUOTE');
696
+ // Task lists: - [ ] → - [ ], - [x] → - [X]
697
+ out = out.replace(/^- \[x\]/gm, '- [X]');
412
698
  // Unordered list: - → - (Org uses -)
413
699
  // Ordered list stays similar
700
+ // Tables
701
+ out = convertMarkdownTableToOrg(out);
414
702
  // Horizontal rules
415
703
  out = out.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '-----');
416
- // Task lists: - [ ] → - [ ], - [x] → - [X]
417
- out = out.replace(/^- \[x\]/gm, '- [X]');
704
+ // Append Org Mode footnote definitions
705
+ if (Object.keys(footnoteMap).length > 0) {
706
+ out += '\n\n';
707
+ for (const [label, text] of Object.entries(footnoteMap)) {
708
+ out += `[fn:${label}] ${text}\n`;
709
+ }
710
+ }
418
711
  return out;
419
712
  }
713
+ function convertMarkdownTableToOrg(text) {
714
+ const lines = text.split('\n');
715
+ const result = [];
716
+ let isFirstRow = true;
717
+ for (const line of lines) {
718
+ const trimmed = line.trim();
719
+ if (trimmed.startsWith('|') && trimmed.endsWith('|')) {
720
+ if (/^\|[\s-:]+\|/.test(trimmed)) {
721
+ // Convert separator to org separator
722
+ const cells = trimmed.split('|').filter(c => c.trim());
723
+ result.push('|' + cells.map(c => '-'.repeat(c.trim().length + 2)).join('+') + '|');
724
+ isFirstRow = false;
725
+ continue;
726
+ }
727
+ result.push(trimmed);
728
+ }
729
+ else {
730
+ isFirstRow = true;
731
+ result.push(line);
732
+ }
733
+ }
734
+ return result.join('\n');
735
+ }