@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.
- package/README.md +1 -0
- package/dist/document-analysis.js +223 -0
- package/dist/email-html.js +94 -0
- package/dist/html-import.js +158 -0
- package/dist/index.js +1067 -98
- package/dist/markdown-repair.js +263 -0
- package/dist/platform-converters.js +419 -0
- package/icon.svg +15 -0
- package/package.json +4 -4
|
@@ -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:  → <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:  → !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:  → 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:  → .. 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:  → [[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:  → !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:  → [[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
|
+
}
|