@xjtlumedia/markdown-mcp-server 2.1.1 → 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.
- package/dist/core-exports.js +418 -48
- package/dist/email-html.js +35 -1
- package/dist/html-import.js +43 -0
- package/dist/index.js +7 -3
- package/dist/markdown-repair.js +20 -0
- package/dist/platform-converters.js +335 -19
- package/package.json +1 -1
package/dist/email-html.js
CHANGED
|
@@ -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(
|
|
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, '☑ ');
|
|
125
|
+
out = out.replace(/<input[^>]*disabled[^>]*checked[^>]*\/?>/gi, '☑ ');
|
|
126
|
+
out = out.replace(/<input[^>]*type="checkbox"[^>]*\/?>/gi, '☐ ');
|
|
93
127
|
return out;
|
|
94
128
|
}
|
package/dist/html-import.js
CHANGED
|
@@ -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, '');
|
|
53
62
|
md = md.replace(/<img[^>]*alt="([^"]*)"[^>]*src="([^"]*)"[^>]*\/?>/gi, '');
|
|
54
63
|
md = md.replace(/<img[^>]*src="([^"]*)"[^>]*\/?>/gi, '');
|
|
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\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
|
|
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',
|
package/dist/markdown-repair.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
417
|
-
|
|
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
|
+
}
|