@xjtlumedia/markdown-mcp-server 1.0.4 → 2.0.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.
@@ -0,0 +1,263 @@
1
+ // ── Markdown Repair / Linting ────────────────────────────────────────
2
+ // Fixes common broken markdown from LLM output
3
+ export function repairMarkdown(md) {
4
+ let out = md;
5
+ out = repairCodeFences(out);
6
+ out = repairBrokenTables(out);
7
+ out = repairStrayMarkers(out);
8
+ out = repairHeadings(out);
9
+ out = repairListIndentation(out);
10
+ out = repairBrokenLinks(out);
11
+ out = normalizeWhitespace(out);
12
+ return out;
13
+ }
14
+ // Fix unclosed or mismatched code fences
15
+ export function repairCodeFences(md) {
16
+ const lines = md.split('\n');
17
+ const result = [];
18
+ let inFence = false;
19
+ let fenceChar = '`';
20
+ let fenceCount = 0;
21
+ for (let i = 0; i < lines.length; i++) {
22
+ const line = lines[i];
23
+ const backtickMatch = line.match(/^(`{3,})([\w]*)\s*$/);
24
+ const tildeMatch = line.match(/^(~{3,})([\w]*)\s*$/);
25
+ const match = backtickMatch || tildeMatch;
26
+ if (match) {
27
+ const char = match[1][0];
28
+ const count = match[1].length;
29
+ if (!inFence) {
30
+ inFence = true;
31
+ fenceChar = char;
32
+ fenceCount = count;
33
+ result.push(line);
34
+ }
35
+ else if (char === fenceChar && count >= fenceCount) {
36
+ inFence = false;
37
+ result.push(fenceChar.repeat(fenceCount));
38
+ }
39
+ else {
40
+ result.push(line);
41
+ }
42
+ }
43
+ else {
44
+ result.push(line);
45
+ }
46
+ }
47
+ // Close unclosed fence at end
48
+ if (inFence) {
49
+ result.push(fenceChar.repeat(fenceCount));
50
+ }
51
+ return result.join('\n');
52
+ }
53
+ // Fix broken tables (mismatched columns, missing separators)
54
+ export function repairBrokenTables(md) {
55
+ const lines = md.split('\n');
56
+ const result = [];
57
+ let i = 0;
58
+ while (i < lines.length) {
59
+ const line = lines[i];
60
+ // Detect table start: line with pipes
61
+ if (line.trim().includes('|') && line.trim().startsWith('|')) {
62
+ const tableLines = [];
63
+ while (i < lines.length && lines[i].trim().includes('|') && lines[i].trim().startsWith('|')) {
64
+ tableLines.push(lines[i]);
65
+ i++;
66
+ }
67
+ if (tableLines.length >= 1) {
68
+ const repaired = repairTableBlock(tableLines);
69
+ result.push(...repaired);
70
+ }
71
+ continue;
72
+ }
73
+ result.push(line);
74
+ i++;
75
+ }
76
+ return result.join('\n');
77
+ }
78
+ function repairTableBlock(lines) {
79
+ // Parse cells and find max column count
80
+ const parsed = lines
81
+ .filter(l => !/^\|[\s-:]+\|[\s-:|]*$/.test(l.trim())) // remove existing separators
82
+ .map(l => l.split('|').slice(1, -1).map(c => c.trim()));
83
+ if (parsed.length === 0)
84
+ return lines;
85
+ const maxCols = Math.max(...parsed.map(r => r.length));
86
+ // Pad rows to have consistent column count
87
+ const padded = parsed.map(row => {
88
+ while (row.length < maxCols)
89
+ row.push('');
90
+ return row;
91
+ });
92
+ // Reconstruct table
93
+ const result = [];
94
+ result.push('| ' + padded[0].join(' | ') + ' |');
95
+ result.push('| ' + padded[0].map(() => '---').join(' | ') + ' |');
96
+ for (let i = 1; i < padded.length; i++) {
97
+ result.push('| ' + padded[i].join(' | ') + ' |');
98
+ }
99
+ return result;
100
+ }
101
+ // Fix stray emphasis markers (* or _ at start/end without matching)
102
+ export function repairStrayMarkers(md) {
103
+ let out = md;
104
+ // Fix unmatched bold markers at line level
105
+ out = out.replace(/^(\*\*[^*]+)$/gm, (m) => {
106
+ if (!m.endsWith('**'))
107
+ return m + '**';
108
+ return m;
109
+ });
110
+ out = out.replace(/^([^*]+\*\*)$/gm, (m) => {
111
+ if (!m.startsWith('**'))
112
+ return '**' + m;
113
+ return m;
114
+ });
115
+ // Fix solo backticks (odd count of backticks on a line)
116
+ out = out.replace(/^((?:[^`]*`[^`]*){1,})$/gm, (line) => {
117
+ const count = (line.match(/`/g) || []).length;
118
+ if (count % 2 !== 0) {
119
+ // Find the last backtick and close it
120
+ return line + '`';
121
+ }
122
+ return line;
123
+ });
124
+ return out;
125
+ }
126
+ // Fix heading spacing and format
127
+ export function repairHeadings(md) {
128
+ let out = md;
129
+ // Fix missing space after #: #Heading → # Heading
130
+ out = out.replace(/^(#{1,6})([^\s#])/gm, '$1 $2');
131
+ // Fix trailing hashes: # Heading # → # Heading
132
+ out = out.replace(/^(#{1,6}\s+.+?)\s*#+\s*$/gm, '$1');
133
+ // Ensure blank line before headings (unless at start of doc)
134
+ const lines = out.split('\n');
135
+ const result = [];
136
+ for (let i = 0; i < lines.length; i++) {
137
+ if (i > 0 && /^#{1,6}\s/.test(lines[i]) && lines[i - 1].trim() !== '') {
138
+ result.push('');
139
+ }
140
+ result.push(lines[i]);
141
+ }
142
+ return result.join('\n');
143
+ }
144
+ // Fix inconsistent list indentation
145
+ export function repairListIndentation(md) {
146
+ const lines = md.split('\n');
147
+ const result = [];
148
+ for (const line of lines) {
149
+ // Normalize tab-based indentation to spaces
150
+ let fixed = line.replace(/\t/g, ' ');
151
+ // Fix mixed list markers at same level to use consistent marker
152
+ if (/^\s*[+]\s/.test(fixed)) {
153
+ fixed = fixed.replace(/^(\s*)[+]\s/, '$1- ');
154
+ }
155
+ result.push(fixed);
156
+ }
157
+ return result.join('\n');
158
+ }
159
+ // Fix broken link syntax
160
+ export function repairBrokenLinks(md) {
161
+ let out = md;
162
+ // Fix missing closing paren: [text](url → [text](url)
163
+ out = out.replace(/\[([^\]]+)\]\(([^)\s]+)(?=\s|$)/gm, '[$1]($2)');
164
+ // Fix missing closing bracket: [text(url) → [text](url)
165
+ out = out.replace(/\[([^\]]+)\(([^)]+)\)/g, '[$1]($2)');
166
+ return out;
167
+ }
168
+ // Normalize excessive whitespace
169
+ export function normalizeWhitespace(md) {
170
+ let out = md;
171
+ // Remove trailing whitespace (except intentional line breaks: 2+ spaces)
172
+ out = out.replace(/([^ \n]) +$/gm, '$1');
173
+ // Collapse 3+ blank lines to 2
174
+ out = out.replace(/\n{4,}/g, '\n\n\n');
175
+ // Ensure file ends with single newline
176
+ out = out.replace(/\n*$/, '\n');
177
+ return out;
178
+ }
179
+ // ── Markdown Linting ─────────────────────────────────────────────────
180
+ export function lintMarkdown(md) {
181
+ const issues = [];
182
+ const lines = md.split('\n');
183
+ let inCodeBlock = false;
184
+ for (let i = 0; i < lines.length; i++) {
185
+ const line = lines[i];
186
+ const lineNum = i + 1;
187
+ // Track code blocks
188
+ if (/^```/.test(line.trim())) {
189
+ inCodeBlock = !inCodeBlock;
190
+ continue;
191
+ }
192
+ if (inCodeBlock)
193
+ continue;
194
+ // Missing space after heading marker
195
+ if (/^#{1,6}[^\s#]/.test(line)) {
196
+ issues.push({
197
+ line: lineNum, column: 1, severity: 'error',
198
+ rule: 'heading-space', message: 'Missing space after heading marker (#)',
199
+ fixable: true
200
+ });
201
+ }
202
+ // Trailing whitespace
203
+ if (/\S +$/.test(line) && !/ $/.test(line)) {
204
+ issues.push({
205
+ line: lineNum, column: line.length, severity: 'warning',
206
+ rule: 'trailing-whitespace', message: 'Trailing whitespace',
207
+ fixable: true
208
+ });
209
+ }
210
+ // Inconsistent list markers
211
+ if (/^\s*[+]\s/.test(line)) {
212
+ issues.push({
213
+ line: lineNum, column: 1, severity: 'warning',
214
+ rule: 'list-marker', message: 'Inconsistent list marker (+), prefer - or *',
215
+ fixable: true
216
+ });
217
+ }
218
+ // Hard tabs
219
+ if (line.includes('\t')) {
220
+ issues.push({
221
+ line: lineNum, column: line.indexOf('\t') + 1, severity: 'warning',
222
+ rule: 'no-hard-tabs', message: 'Hard tab found, prefer spaces',
223
+ fixable: true
224
+ });
225
+ }
226
+ // Multiple blank lines
227
+ if (i > 1 && line.trim() === '' && lines[i - 1].trim() === '' && lines[i - 2]?.trim() === '') {
228
+ issues.push({
229
+ line: lineNum, column: 1, severity: 'info',
230
+ rule: 'no-multiple-blanks', message: 'Multiple consecutive blank lines',
231
+ fixable: true
232
+ });
233
+ }
234
+ // Bare URLs (not inside links or code)
235
+ if (/(?<!\(|<)https?:\/\/[^\s)>]+/.test(line) && !/\[.*\]\(/.test(line) && !/`/.test(line)) {
236
+ issues.push({
237
+ line: lineNum,
238
+ column: line.search(/https?:\/\//) + 1,
239
+ severity: 'info',
240
+ rule: 'bare-url', message: 'Bare URL found, consider wrapping in link syntax',
241
+ fixable: false
242
+ });
243
+ }
244
+ // Unclosed emphasis (simple heuristic)
245
+ const boldCount = (line.match(/\*\*/g) || []).length;
246
+ if (boldCount % 2 !== 0) {
247
+ issues.push({
248
+ line: lineNum, column: 1, severity: 'warning',
249
+ rule: 'unclosed-emphasis', message: 'Possible unclosed bold (**) markers',
250
+ fixable: true
251
+ });
252
+ }
253
+ }
254
+ // Check for unclosed code fences
255
+ if (inCodeBlock) {
256
+ issues.push({
257
+ line: lines.length, column: 1, severity: 'error',
258
+ rule: 'unclosed-code-fence', message: 'Unclosed code fence (```) at end of document',
259
+ fixable: true
260
+ });
261
+ }
262
+ return issues;
263
+ }
@@ -0,0 +1,419 @@
1
+ // ── Slack mrkdwn ─────────────────────────────────────────────────────
2
+ export function markdownToSlack(md) {
3
+ let out = md;
4
+ // Bold: **text** → *text*
5
+ out = out.replace(/\*\*([^*]+)\*\*/g, '*$1*');
6
+ // Italic: *text* or _text_ → _text_
7
+ out = out.replace(/(?<!\*)\*(?!\*)([^*]+)(?<!\*)\*(?!\*)/g, '_$1_');
8
+ // Strikethrough: ~~text~~ → ~text~
9
+ out = out.replace(/~~([^~]+)~~/g, '~$1~');
10
+ // Inline code stays as backtick
11
+ // Code blocks: ```lang\n...\n``` stays the same (Slack supports it)
12
+ // Links: [text](url) → <url|text>
13
+ out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<$2|$1>');
14
+ // Images: ![alt](url) → <url|alt>
15
+ out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<$2|$1>');
16
+ // Headers: # text → *text*
17
+ out = out.replace(/^#{1,6}\s+(.+)$/gm, '*$1*');
18
+ // Blockquotes: > text → > text (Slack supports >)
19
+ // Ordered lists: 1. text stays the same
20
+ // Unordered lists: - text stays the same
21
+ // Horizontal rule
22
+ out = out.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '───────────────');
23
+ return out;
24
+ }
25
+ // ── Discord markdown ─────────────────────────────────────────────────
26
+ export function markdownToDiscord(md) {
27
+ let out = md;
28
+ // Discord supports most standard markdown, just a few tweaks:
29
+ // Headers: # text → **__text__** (Discord renders # only in certain contexts)
30
+ out = out.replace(/^# (.+)$/gm, '**__$1__**');
31
+ out = out.replace(/^## (.+)$/gm, '**$1**');
32
+ out = out.replace(/^### (.+)$/gm, '__$1__');
33
+ out = out.replace(/^#{4,6}\s+(.+)$/gm, '*$1*');
34
+ // Spoiler: no markdown equivalent, keep as-is
35
+ // Block quotes: > works in Discord
36
+ // Code blocks: ``` works in Discord
37
+ // Horizontal rules are not rendered in Discord
38
+ out = out.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '');
39
+ return out;
40
+ }
41
+ // ── JIRA wiki markup ─────────────────────────────────────────────────
42
+ export function markdownToJira(md) {
43
+ let out = md;
44
+ // Headers: # → h1., ## → h2., etc.
45
+ out = out.replace(/^######\s+(.+)$/gm, 'h6. $1');
46
+ out = out.replace(/^#####\s+(.+)$/gm, 'h5. $1');
47
+ out = out.replace(/^####\s+(.+)$/gm, 'h4. $1');
48
+ out = out.replace(/^###\s+(.+)$/gm, 'h3. $1');
49
+ out = out.replace(/^##\s+(.+)$/gm, 'h2. $1');
50
+ out = out.replace(/^#\s+(.+)$/gm, 'h1. $1');
51
+ // Bold: **text** → *text*
52
+ out = out.replace(/\*\*([^*]+)\*\*/g, '*$1*');
53
+ // Italic: *text* → _text_
54
+ out = out.replace(/(?<!\*)\*(?!\*)([^*]+)(?<!\*)\*(?!\*)/g, '_$1_');
55
+ // Strikethrough: ~~text~~ → -text-
56
+ out = out.replace(/~~([^~]+)~~/g, '-$1-');
57
+ // Inline code: `code` → {{code}}
58
+ out = out.replace(/`([^`]+)`/g, '{{$1}}');
59
+ // Code blocks: ```lang → {code:lang} / ``` → {code}
60
+ out = out.replace(/```(\w+)?\n([\s\S]*?)```/g, (_m, lang, code) => lang ? `{code:${lang}}\n${code.trimEnd()}\n{code}` : `{code}\n${code.trimEnd()}\n{code}`);
61
+ // Links: [text](url) → [text|url]
62
+ out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '[$1|$2]');
63
+ // Images: ![alt](url) → !url!
64
+ out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '!$2!');
65
+ // Unordered list: - text → * text (JIRA uses *)
66
+ out = out.replace(/^(\s*)[-+]\s+/gm, (_m, indent) => {
67
+ const level = Math.floor(indent.length / 2) + 1;
68
+ return '*'.repeat(level) + ' ';
69
+ });
70
+ // Ordered list: 1. text → # text
71
+ out = out.replace(/^(\s*)\d+\.\s+/gm, (_m, indent) => {
72
+ const level = Math.floor(indent.length / 2) + 1;
73
+ return '#'.repeat(level) + ' ';
74
+ });
75
+ // Blockquote: > text → {quote}text{quote}
76
+ out = out.replace(/^>\s+(.+)$/gm, '{quote}$1{quote}');
77
+ // Horizontal rules
78
+ out = out.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '----');
79
+ // Tables: | A | B | → || A || B || for header, | a | b | for body
80
+ const lines = out.split('\n');
81
+ const result = [];
82
+ let headerDone = false;
83
+ for (let i = 0; i < lines.length; i++) {
84
+ const line = lines[i];
85
+ if (line.trim().startsWith('|') && line.trim().endsWith('|')) {
86
+ // Skip separator rows
87
+ if (/^\|[\s-:]+\|/.test(line.trim())) {
88
+ headerDone = true;
89
+ continue;
90
+ }
91
+ if (!headerDone) {
92
+ // Convert header cells: | A | B | → || A || B ||
93
+ result.push(line.replace(/\|/g, '||'));
94
+ }
95
+ else {
96
+ result.push(line);
97
+ }
98
+ }
99
+ else {
100
+ headerDone = false;
101
+ result.push(line);
102
+ }
103
+ }
104
+ return result.join('\n');
105
+ }
106
+ // ── Confluence wiki markup ───────────────────────────────────────────
107
+ export function markdownToConfluence(md) {
108
+ let out = md;
109
+ // Headers
110
+ out = out.replace(/^######\s+(.+)$/gm, 'h6. $1');
111
+ out = out.replace(/^#####\s+(.+)$/gm, 'h5. $1');
112
+ out = out.replace(/^####\s+(.+)$/gm, 'h4. $1');
113
+ out = out.replace(/^###\s+(.+)$/gm, 'h3. $1');
114
+ out = out.replace(/^##\s+(.+)$/gm, 'h2. $1');
115
+ out = out.replace(/^#\s+(.+)$/gm, 'h1. $1');
116
+ // Bold, Italic, Strikethrough (same as JIRA)
117
+ out = out.replace(/\*\*([^*]+)\*\*/g, '*$1*');
118
+ out = out.replace(/(?<!\*)\*(?!\*)([^*]+)(?<!\*)\*(?!\*)/g, '_$1_');
119
+ out = out.replace(/~~([^~]+)~~/g, '-$1-');
120
+ // Inline code
121
+ out = out.replace(/`([^`]+)`/g, '{{$1}}');
122
+ // Code blocks
123
+ out = out.replace(/```(\w+)?\n([\s\S]*?)```/g, (_m, lang, code) => lang ? `{code:language=${lang}}\n${code.trimEnd()}\n{code}` : `{code}\n${code.trimEnd()}\n{code}`);
124
+ // Links
125
+ out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '[$1|$2]');
126
+ // Images
127
+ out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '!$2!');
128
+ // Lists (same as JIRA)
129
+ out = out.replace(/^(\s*)[-+]\s+/gm, (_m, indent) => {
130
+ const level = Math.floor(indent.length / 2) + 1;
131
+ return '*'.repeat(level) + ' ';
132
+ });
133
+ out = out.replace(/^(\s*)\d+\.\s+/gm, (_m, indent) => {
134
+ const level = Math.floor(indent.length / 2) + 1;
135
+ return '#'.repeat(level) + ' ';
136
+ });
137
+ // Blockquote
138
+ out = out.replace(/^>\s+(.+)$/gm, '{quote}$1{quote}');
139
+ // Horizontal rules
140
+ out = out.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '----');
141
+ // Info/note panels from HTML comments
142
+ out = out.replace(/<!--\s*note:\s*([\s\S]*?)-->/gi, '{note}$1{note}');
143
+ out = out.replace(/<!--\s*info:\s*([\s\S]*?)-->/gi, '{info}$1{info}');
144
+ return out;
145
+ }
146
+ // ── AsciiDoc ─────────────────────────────────────────────────────────
147
+ export function markdownToAsciiDoc(md) {
148
+ let out = md;
149
+ // Headers: # → =, ## → ==, etc.
150
+ out = out.replace(/^######\s+(.+)$/gm, '====== $1');
151
+ out = out.replace(/^#####\s+(.+)$/gm, '===== $1');
152
+ out = out.replace(/^####\s+(.+)$/gm, '==== $1');
153
+ out = out.replace(/^###\s+(.+)$/gm, '=== $1');
154
+ out = out.replace(/^##\s+(.+)$/gm, '== $1');
155
+ out = out.replace(/^#\s+(.+)$/gm, '= $1');
156
+ // Bold: **text** → *text*
157
+ out = out.replace(/\*\*([^*]+)\*\*/g, '*$1*');
158
+ // Italic: *text* → _text_
159
+ out = out.replace(/(?<!\*)\*(?!\*)([^*]+)(?<!\*)\*(?!\*)/g, '_$1_');
160
+ // Inline code stays as backtick (AsciiDoc uses + but backtick also works)
161
+ out = out.replace(/`([^`]+)`/g, '`$1`');
162
+ // Code blocks: ```lang → [source,lang]\n----\n...\n----
163
+ out = out.replace(/```(\w+)?\n([\s\S]*?)```/g, (_m, lang, code) => {
164
+ const attr = lang ? `[source,${lang}]\n` : '';
165
+ return `${attr}----\n${code.trimEnd()}\n----`;
166
+ });
167
+ // Links: [text](url) → url[text]
168
+ out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$2[$1]');
169
+ // Images: ![alt](url) → image::url[alt]
170
+ out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, 'image::$2[$1]');
171
+ // Blockquote: > text → [quote]\ntext\n
172
+ const lines = out.split('\n');
173
+ const result = [];
174
+ let inQuote = false;
175
+ for (const line of lines) {
176
+ if (line.startsWith('> ')) {
177
+ if (!inQuote) {
178
+ result.push('[quote]');
179
+ result.push('____');
180
+ inQuote = true;
181
+ }
182
+ result.push(line.slice(2));
183
+ }
184
+ else {
185
+ if (inQuote) {
186
+ result.push('____');
187
+ inQuote = false;
188
+ }
189
+ result.push(line);
190
+ }
191
+ }
192
+ if (inQuote)
193
+ result.push('____');
194
+ out = result.join('\n');
195
+ // Horizontal rules
196
+ out = out.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, "'''");
197
+ // Unordered list marker stays as *
198
+ out = out.replace(/^(\s*)[-+]\s+/gm, '* ');
199
+ // Ordered list: 1. → .
200
+ out = out.replace(/^\d+\.\s+/gm, '. ');
201
+ return out;
202
+ }
203
+ // ── reStructuredText ─────────────────────────────────────────────────
204
+ export function markdownToRST(md) {
205
+ let out = md;
206
+ // Code blocks: ```lang → .. code-block:: lang
207
+ out = out.replace(/```(\w+)?\n([\s\S]*?)```/g, (_m, lang, code) => {
208
+ const directive = lang ? `.. code-block:: ${lang}` : '.. code-block::';
209
+ const indented = code.split('\n').map((l) => ' ' + l).join('\n').trimEnd();
210
+ return `${directive}\n\n${indented}`;
211
+ });
212
+ // Bold: **text** → **text** (same in RST)
213
+ // Italic: *text* → *text* (same in RST)
214
+ // Inline code: `code` → ``code``
215
+ out = out.replace(/(?<!`)(`[^`]+`)(?!`)/g, '`$1`');
216
+ // Links: [text](url) → `text <url>`_
217
+ out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '`$1 <$2>`_');
218
+ // Images: ![alt](url) → .. image:: url\n :alt: alt
219
+ out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '.. image:: $2\n :alt: $1');
220
+ // Headers: use underline characters
221
+ const lines = out.split('\n');
222
+ const result = [];
223
+ for (const line of lines) {
224
+ const h1 = line.match(/^#\s+(.+)$/);
225
+ const h2 = line.match(/^##\s+(.+)$/);
226
+ const h3 = line.match(/^###\s+(.+)$/);
227
+ const h4 = line.match(/^####\s+(.+)$/);
228
+ if (h1) {
229
+ result.push(h1[1], '='.repeat(h1[1].length));
230
+ }
231
+ else if (h2) {
232
+ result.push(h2[1], '-'.repeat(h2[1].length));
233
+ }
234
+ else if (h3) {
235
+ result.push(h3[1], '~'.repeat(h3[1].length));
236
+ }
237
+ else if (h4) {
238
+ result.push(h4[1], '^'.repeat(h4[1].length));
239
+ }
240
+ else {
241
+ result.push(line);
242
+ }
243
+ }
244
+ out = result.join('\n');
245
+ // Blockquote: > text → indented text
246
+ out = out.replace(/^>\s+(.+)$/gm, ' $1');
247
+ // Horizontal rules
248
+ out = out.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '----------');
249
+ // Ordered list: keep as-is (RST uses #.)
250
+ out = out.replace(/^(\d+)\.\s+/gm, '#. ');
251
+ return out;
252
+ }
253
+ // ── MediaWiki markup ─────────────────────────────────────────────────
254
+ export function markdownToMediaWiki(md) {
255
+ let out = md;
256
+ // Headers: # text → == text ==
257
+ out = out.replace(/^######\s+(.+)$/gm, '======= $1 =======');
258
+ out = out.replace(/^#####\s+(.+)$/gm, '====== $1 ======');
259
+ out = out.replace(/^####\s+(.+)$/gm, '===== $1 =====');
260
+ out = out.replace(/^###\s+(.+)$/gm, '==== $1 ====');
261
+ out = out.replace(/^##\s+(.+)$/gm, '=== $1 ===');
262
+ out = out.replace(/^#\s+(.+)$/gm, '== $1 ==');
263
+ // Bold: **text** → '''text'''
264
+ out = out.replace(/\*\*([^*]+)\*\*/g, "'''$1'''");
265
+ // Italic: *text* → ''text''
266
+ out = out.replace(/(?<!\*)\*(?!\*)([^*]+)(?<!\*)\*(?!\*)/g, "''$1''");
267
+ // Inline code: `code` → <code>code</code>
268
+ out = out.replace(/`([^`]+)`/g, '<code>$1</code>');
269
+ // Code blocks
270
+ out = out.replace(/```(\w+)?\n([\s\S]*?)```/g, (_m, lang, code) => lang ? `<syntaxhighlight lang="${lang}">\n${code.trimEnd()}\n</syntaxhighlight>` :
271
+ `<pre>\n${code.trimEnd()}\n</pre>`);
272
+ // Links: [text](url) → [url text]
273
+ out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '[$2 $1]');
274
+ // Images: ![alt](url) → [[File:url|alt]]
275
+ out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '[[File:$2|$1]]');
276
+ // Unordered list: - text → * text
277
+ out = out.replace(/^(\s*)[-+]\s+/gm, (_m, indent) => {
278
+ const level = Math.floor(indent.length / 2) + 1;
279
+ return '*'.repeat(level) + ' ';
280
+ });
281
+ // Ordered list
282
+ out = out.replace(/^(\s*)\d+\.\s+/gm, (_m, indent) => {
283
+ const level = Math.floor(indent.length / 2) + 1;
284
+ return '#'.repeat(level) + ' ';
285
+ });
286
+ // Blockquote
287
+ out = out.replace(/^>\s+(.+)$/gm, '<blockquote>$1</blockquote>');
288
+ // Horizontal rules
289
+ out = out.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '----');
290
+ // Tables
291
+ const lines = out.split('\n');
292
+ const result = [];
293
+ let inTable = false;
294
+ for (let i = 0; i < lines.length; i++) {
295
+ const line = lines[i].trim();
296
+ if (line.startsWith('|') && line.endsWith('|')) {
297
+ if (/^\|[\s-:]+\|/.test(line))
298
+ continue; // skip separator
299
+ const cells = line.split('|').filter(c => c.trim()).map(c => c.trim());
300
+ if (!inTable) {
301
+ result.push('{| class="wikitable"');
302
+ inTable = true;
303
+ // First row as header
304
+ result.push('|-');
305
+ cells.forEach(c => result.push('! ' + c));
306
+ }
307
+ else {
308
+ result.push('|-');
309
+ cells.forEach(c => result.push('| ' + c));
310
+ }
311
+ }
312
+ else {
313
+ if (inTable) {
314
+ result.push('|}');
315
+ inTable = false;
316
+ }
317
+ result.push(lines[i]);
318
+ }
319
+ }
320
+ if (inTable)
321
+ result.push('|}');
322
+ return result.join('\n');
323
+ }
324
+ // ── BBCode ───────────────────────────────────────────────────────────
325
+ export function markdownToBBCode(md) {
326
+ let out = md;
327
+ // Headers
328
+ out = out.replace(/^#{1,6}\s+(.+)$/gm, '[b][size=5]$1[/size][/b]');
329
+ // Bold
330
+ out = out.replace(/\*\*([^*]+)\*\*/g, '[b]$1[/b]');
331
+ // Italic
332
+ out = out.replace(/(?<!\*)\*(?!\*)([^*]+)(?<!\*)\*(?!\*)/g, '[i]$1[/i]');
333
+ // Strikethrough
334
+ out = out.replace(/~~([^~]+)~~/g, '[s]$1[/s]');
335
+ // Inline code
336
+ out = out.replace(/`([^`]+)`/g, '[code]$1[/code]');
337
+ // Code blocks
338
+ out = out.replace(/```(\w+)?\n([\s\S]*?)```/g, (_m, _lang, code) => `[code]\n${code.trimEnd()}\n[/code]`);
339
+ // Links
340
+ out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '[url=$2]$1[/url]');
341
+ // Images
342
+ out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '[img]$2[/img]');
343
+ // Blockquote
344
+ out = out.replace(/^>\s+(.+)$/gm, '[quote]$1[/quote]');
345
+ // Unordered list
346
+ out = out.replace(/^[-*+]\s+(.+)$/gm, '[*]$1');
347
+ // Horizontal rules
348
+ out = out.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '[hr]');
349
+ return out;
350
+ }
351
+ // ── Textile ──────────────────────────────────────────────────────────
352
+ export function markdownToTextile(md) {
353
+ let out = md;
354
+ // Headers: # → h1., ## → h2.
355
+ out = out.replace(/^######\s+(.+)$/gm, 'h6. $1');
356
+ out = out.replace(/^#####\s+(.+)$/gm, 'h5. $1');
357
+ out = out.replace(/^####\s+(.+)$/gm, 'h4. $1');
358
+ out = out.replace(/^###\s+(.+)$/gm, 'h3. $1');
359
+ out = out.replace(/^##\s+(.+)$/gm, 'h2. $1');
360
+ out = out.replace(/^#\s+(.+)$/gm, 'h1. $1');
361
+ // Bold: **text** → *text*
362
+ out = out.replace(/\*\*([^*]+)\*\*/g, '*$1*');
363
+ // Italic: *text* → _text_
364
+ out = out.replace(/(?<!\*)\*(?!\*)([^*]+)(?<!\*)\*(?!\*)/g, '_$1_');
365
+ // Strikethrough: ~~text~~ → -text-
366
+ out = out.replace(/~~([^~]+)~~/g, '-$1-');
367
+ // Inline code
368
+ out = out.replace(/`([^`]+)`/g, '@$1@');
369
+ // Code blocks
370
+ out = out.replace(/```(\w+)?\n([\s\S]*?)```/g, (_m, _lang, code) => `bc. ${code.trimEnd()}`);
371
+ // Links: [text](url) → "text":url
372
+ out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '"$1":$2');
373
+ // Images: ![alt](url) → !url(alt)!
374
+ out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '!$2($1)!');
375
+ // Blockquote
376
+ out = out.replace(/^>\s+(.+)$/gm, 'bq. $1');
377
+ // Unordered list
378
+ out = out.replace(/^[-+]\s+/gm, '* ');
379
+ // Ordered list
380
+ out = out.replace(/^\d+\.\s+/gm, '# ');
381
+ // Horizontal rules
382
+ out = out.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '---');
383
+ return out;
384
+ }
385
+ // ── Org Mode ─────────────────────────────────────────────────────────
386
+ export function markdownToOrgMode(md) {
387
+ let out = md;
388
+ // Headers: # → *, ## → **, etc.
389
+ out = out.replace(/^######\s+(.+)$/gm, '****** $1');
390
+ out = out.replace(/^#####\s+(.+)$/gm, '***** $1');
391
+ out = out.replace(/^####\s+(.+)$/gm, '**** $1');
392
+ out = out.replace(/^###\s+(.+)$/gm, '*** $1');
393
+ out = out.replace(/^##\s+(.+)$/gm, '** $1');
394
+ out = out.replace(/^#\s+(.+)$/gm, '* $1');
395
+ // Bold: **text** → *text*
396
+ out = out.replace(/\*\*([^*]+)\*\*/g, '*$1*');
397
+ // Italic: *text* → /text/
398
+ out = out.replace(/(?<!\*)\*(?!\*)([^*]+)(?<!\*)\*(?!\*)/g, '/$1/');
399
+ // Strikethrough: ~~text~~ → +text+
400
+ out = out.replace(/~~([^~]+)~~/g, '+$1+');
401
+ // Inline code: `code` → ~code~
402
+ out = out.replace(/`([^`]+)`/g, '~$1~');
403
+ // Code blocks: ```lang → #+BEGIN_SRC lang
404
+ out = out.replace(/```(\w+)?\n([\s\S]*?)```/g, (_m, lang, code) => lang ? `#+BEGIN_SRC ${lang}\n${code.trimEnd()}\n#+END_SRC` :
405
+ `#+BEGIN_SRC\n${code.trimEnd()}\n#+END_SRC`);
406
+ // Links: [text](url) → [[url][text]]
407
+ out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '[[$2][$1]]');
408
+ // Images: ![alt](url) → [[url]]
409
+ out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '[[$2]]');
410
+ // Blockquote
411
+ out = out.replace(/^>\s+(.+)$/gm, '#+BEGIN_QUOTE\n$1\n#+END_QUOTE');
412
+ // Unordered list: - → - (Org uses -)
413
+ // Ordered list stays similar
414
+ // Horizontal rules
415
+ out = out.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '-----');
416
+ // Task lists: - [ ] → - [ ], - [x] → - [X]
417
+ out = out.replace(/^- \[x\]/gm, '- [X]');
418
+ return out;
419
+ }