@jagadeesh52423/atlassian-mcp-server 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -36
- package/dist/formatters/adf-formatter.d.ts +30 -2
- package/dist/formatters/adf-formatter.d.ts.map +1 -1
- package/dist/formatters/adf-formatter.js +423 -107
- package/dist/formatters/adf-formatter.js.map +1 -1
- package/dist/formatters/index.d.ts +1 -1
- package/dist/formatters/index.d.ts.map +1 -1
- package/dist/formatters/index.js +1 -1
- package/dist/formatters/index.js.map +1 -1
- package/dist/handlers/base-handler.d.ts +5 -0
- package/dist/handlers/base-handler.d.ts.map +1 -0
- package/dist/handlers/base-handler.js +7 -0
- package/dist/handlers/base-handler.js.map +1 -0
- package/dist/handlers/bitbucket-handlers.d.ts +2 -1
- package/dist/handlers/bitbucket-handlers.d.ts.map +1 -1
- package/dist/handlers/bitbucket-handlers.js +31 -191
- package/dist/handlers/bitbucket-handlers.js.map +1 -1
- package/dist/handlers/confluence-handlers.d.ts +2 -1
- package/dist/handlers/confluence-handlers.d.ts.map +1 -1
- package/dist/handlers/confluence-handlers.js +46 -243
- package/dist/handlers/confluence-handlers.js.map +1 -1
- package/dist/handlers/jira-handlers.d.ts +9 -1
- package/dist/handlers/jira-handlers.d.ts.map +1 -1
- package/dist/handlers/jira-handlers.js +144 -575
- package/dist/handlers/jira-handlers.js.map +1 -1
- package/dist/index.js +111 -189
- package/dist/index.js.map +1 -1
- package/dist/registry/tool-registry.d.ts +11 -0
- package/dist/registry/tool-registry.d.ts.map +1 -0
- package/dist/registry/tool-registry.js +24 -0
- package/dist/registry/tool-registry.js.map +1 -0
- package/dist/services/bitbucket.d.ts +2 -0
- package/dist/services/bitbucket.d.ts.map +1 -1
- package/dist/services/bitbucket.js +22 -74
- package/dist/services/bitbucket.js.map +1 -1
- package/dist/services/confluence.d.ts.map +1 -1
- package/dist/services/confluence.js +5 -4
- package/dist/services/confluence.js.map +1 -1
- package/dist/services/jira/agile.d.ts +33 -0
- package/dist/services/jira/agile.d.ts.map +1 -0
- package/dist/services/jira/agile.js +118 -0
- package/dist/services/jira/agile.js.map +1 -0
- package/dist/services/jira/attachments.d.ts +7 -0
- package/dist/services/jira/attachments.d.ts.map +1 -0
- package/dist/services/jira/attachments.js +26 -0
- package/dist/services/jira/attachments.js.map +1 -0
- package/dist/services/jira/base.d.ts +9 -0
- package/dist/services/jira/base.d.ts.map +1 -0
- package/dist/services/jira/base.js +11 -0
- package/dist/services/jira/base.js.map +1 -0
- package/dist/services/jira/comments.d.ts +8 -0
- package/dist/services/jira/comments.d.ts.map +1 -0
- package/dist/services/jira/comments.js +33 -0
- package/dist/services/jira/comments.js.map +1 -0
- package/dist/services/jira/issues.d.ts +32 -0
- package/dist/services/jira/issues.d.ts.map +1 -0
- package/dist/services/jira/issues.js +177 -0
- package/dist/services/jira/issues.js.map +1 -0
- package/dist/services/jira/labels.d.ts +7 -0
- package/dist/services/jira/labels.d.ts.map +1 -0
- package/dist/services/jira/labels.js +21 -0
- package/dist/services/jira/labels.js.map +1 -0
- package/dist/services/jira/links.d.ts +9 -0
- package/dist/services/jira/links.d.ts.map +1 -0
- package/dist/services/jira/links.js +55 -0
- package/dist/services/jira/links.js.map +1 -0
- package/dist/services/jira/metadata.d.ts +8 -0
- package/dist/services/jira/metadata.d.ts.map +1 -0
- package/dist/services/jira/metadata.js +32 -0
- package/dist/services/jira/metadata.js.map +1 -0
- package/dist/services/jira/subtasks.d.ts +9 -0
- package/dist/services/jira/subtasks.d.ts.map +1 -0
- package/dist/services/jira/subtasks.js +32 -0
- package/dist/services/jira/subtasks.js.map +1 -0
- package/dist/services/jira/transitions.d.ts +16 -0
- package/dist/services/jira/transitions.d.ts.map +1 -0
- package/dist/services/jira/transitions.js +203 -0
- package/dist/services/jira/transitions.js.map +1 -0
- package/dist/services/jira/versions.d.ts +19 -0
- package/dist/services/jira/versions.d.ts.map +1 -0
- package/dist/services/jira/versions.js +62 -0
- package/dist/services/jira/versions.js.map +1 -0
- package/dist/services/jira/watchers.d.ts +7 -0
- package/dist/services/jira/watchers.d.ts.map +1 -0
- package/dist/services/jira/watchers.js +23 -0
- package/dist/services/jira/watchers.js.map +1 -0
- package/dist/services/jira/worklogs.d.ts +8 -0
- package/dist/services/jira/worklogs.d.ts.map +1 -0
- package/dist/services/jira/worklogs.js +37 -0
- package/dist/services/jira/worklogs.js.map +1 -0
- package/dist/services/jira.d.ts +58 -103
- package/dist/services/jira.d.ts.map +1 -1
- package/dist/services/jira.js +100 -1481
- package/dist/services/jira.js.map +1 -1
- package/dist/tools/jira-tools.d.ts.map +1 -1
- package/dist/tools/jira-tools.js +35 -3
- package/dist/tools/jira-tools.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,135 +3,245 @@
|
|
|
3
3
|
* Handles conversion between plain text/markdown and ADF format used by Jira API v3
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Jira/Confluence ADF supported languages mapped from common LLM output aliases
|
|
7
|
+
*/
|
|
8
|
+
const LANGUAGE_ALIASES = {
|
|
9
|
+
tsx: 'javascript',
|
|
10
|
+
jsx: 'javascript',
|
|
11
|
+
sh: 'bash',
|
|
12
|
+
shell: 'bash',
|
|
13
|
+
zsh: 'bash',
|
|
14
|
+
dockerfile: 'bash',
|
|
15
|
+
tf: 'plaintext',
|
|
16
|
+
hcl: 'plaintext',
|
|
17
|
+
mdx: 'markdown',
|
|
18
|
+
yml: 'yaml',
|
|
19
|
+
};
|
|
20
|
+
function normalizeCodeLanguage(lang) {
|
|
21
|
+
if (!lang)
|
|
22
|
+
return '';
|
|
23
|
+
const lower = lang.toLowerCase();
|
|
24
|
+
return LANGUAGE_ALIASES[lower] ?? lower;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Recursively converts a single ADF node to plain text.
|
|
28
|
+
* Mirrors sooperset's adf_to_text recursive approach:
|
|
29
|
+
* - Content arrays are joined with '\n'
|
|
30
|
+
* - Marks (bold, italic, links) are stripped — plain text only
|
|
31
|
+
*/
|
|
32
|
+
function adfNodeToText(node) {
|
|
33
|
+
if (!node)
|
|
34
|
+
return '';
|
|
35
|
+
// Array: map each item and join with newline (mirrors sooperset's "\n".join)
|
|
36
|
+
if (Array.isArray(node)) {
|
|
37
|
+
return node.map(adfNodeToText).filter(Boolean).join('\n');
|
|
38
|
+
}
|
|
39
|
+
if (typeof node !== 'object')
|
|
40
|
+
return String(node);
|
|
41
|
+
switch (node.type) {
|
|
42
|
+
case 'text':
|
|
43
|
+
// Marks (bold, italic, link, etc.) are stripped — plain text only
|
|
44
|
+
return node.text ?? '';
|
|
45
|
+
case 'hardBreak':
|
|
46
|
+
return '\n';
|
|
47
|
+
case 'mention': {
|
|
48
|
+
const attrs = node.attrs ?? {};
|
|
49
|
+
return attrs.text ?? `@${attrs.id ?? 'unknown'}`;
|
|
50
|
+
}
|
|
51
|
+
case 'emoji': {
|
|
52
|
+
const attrs = node.attrs ?? {};
|
|
53
|
+
return attrs.text ?? attrs.shortName ?? '';
|
|
54
|
+
}
|
|
55
|
+
case 'date': {
|
|
56
|
+
const timestamp = node.attrs?.timestamp;
|
|
57
|
+
if (!timestamp)
|
|
58
|
+
return '';
|
|
59
|
+
try {
|
|
60
|
+
return new Date(Number(timestamp)).toISOString().slice(0, 10);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return String(timestamp);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
case 'status':
|
|
67
|
+
return `[${node.attrs?.text ?? ''}]`;
|
|
68
|
+
case 'inlineCard': {
|
|
69
|
+
const attrs = node.attrs ?? {};
|
|
70
|
+
return attrs.url ?? attrs.data?.url ?? attrs.data?.name ?? '';
|
|
71
|
+
}
|
|
72
|
+
case 'heading': {
|
|
73
|
+
const level = node.attrs?.level ?? 1;
|
|
74
|
+
return `${'#'.repeat(level)} ${adfNodeToText(node.content ?? [])}`;
|
|
75
|
+
}
|
|
76
|
+
case 'listItem':
|
|
77
|
+
return `- ${adfNodeToText(node.content ?? [])}`;
|
|
78
|
+
case 'codeBlock': {
|
|
79
|
+
const lang = node.attrs?.language ? normalizeCodeLanguage(node.attrs.language) : '';
|
|
80
|
+
return `\`\`\`${lang}\n${adfNodeToText(node.content ?? [])}\n\`\`\``;
|
|
81
|
+
}
|
|
82
|
+
case 'table': {
|
|
83
|
+
if (!node.content)
|
|
84
|
+
return '';
|
|
85
|
+
const rows = node.content
|
|
86
|
+
.filter((row) => row.type === 'tableRow' && row.content)
|
|
87
|
+
.map((row) => row.content
|
|
88
|
+
.filter((cell) => cell.type === 'tableCell' || cell.type === 'tableHeader')
|
|
89
|
+
.map((cell) => adfNodeToText(cell.content ?? []).replace(/\n/g, ' ').trim()))
|
|
90
|
+
.filter((cells) => cells.length > 0);
|
|
91
|
+
if (rows.length === 0)
|
|
92
|
+
return '';
|
|
93
|
+
const [header, ...data] = rows;
|
|
94
|
+
return [
|
|
95
|
+
'| ' + header.join(' | ') + ' |',
|
|
96
|
+
'|' + header.map(() => '--------').join('|') + '|',
|
|
97
|
+
...data.map(row => '| ' + row.join(' | ') + ' |'),
|
|
98
|
+
].join('\n');
|
|
99
|
+
}
|
|
100
|
+
default:
|
|
101
|
+
if (node.content)
|
|
102
|
+
return adfNodeToText(node.content);
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Extracts plain text from ADF (Atlassian Document Format) body.
|
|
108
|
+
* Follows sooperset's recursive '\n'-joining approach.
|
|
7
109
|
*/
|
|
8
110
|
export function extractTextFromADF(body) {
|
|
9
111
|
if (!body)
|
|
10
112
|
return '';
|
|
11
|
-
|
|
12
|
-
if (typeof body === 'string') {
|
|
113
|
+
if (typeof body === 'string')
|
|
13
114
|
return formatWikiMarkup(body);
|
|
14
|
-
|
|
15
|
-
// If body doesn't have content property, it might be wiki markup in another format
|
|
16
|
-
if (!body.content) {
|
|
115
|
+
if (!body.content)
|
|
17
116
|
return formatWikiMarkup(String(body));
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
else if (item.type === 'bulletList' || item.type === 'orderedList') {
|
|
42
|
-
if (item.content) {
|
|
43
|
-
extractFromContent(item.content);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
else if (item.type === 'listItem') {
|
|
47
|
-
text += '- ';
|
|
48
|
-
if (item.content) {
|
|
49
|
-
extractFromContent(item.content);
|
|
117
|
+
return formatWikiMarkup(adfNodeToText(body.content).trim());
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Converts wiki-style tables (||header||col||) to markdown tables.
|
|
121
|
+
* Only converts blocks that start with || (header row), leaving existing
|
|
122
|
+
* markdown tables untouched.
|
|
123
|
+
*/
|
|
124
|
+
function convertWikiTablesToMarkdown(text) {
|
|
125
|
+
const lines = text.split('\n');
|
|
126
|
+
const result = [];
|
|
127
|
+
let i = 0;
|
|
128
|
+
while (i < lines.length) {
|
|
129
|
+
const line = lines[i].trim();
|
|
130
|
+
// Only enter table mode when we see a wiki header row (||…)
|
|
131
|
+
if (line.startsWith('||')) {
|
|
132
|
+
const tableLines = [];
|
|
133
|
+
while (i < lines.length) {
|
|
134
|
+
const tl = lines[i].trim();
|
|
135
|
+
if (tl.startsWith('||') || tl.startsWith('|')) {
|
|
136
|
+
tableLines.push(tl);
|
|
137
|
+
i++;
|
|
50
138
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
text += '```';
|
|
54
|
-
if (item.content) {
|
|
55
|
-
extractFromContent(item.content);
|
|
139
|
+
else {
|
|
140
|
+
break;
|
|
56
141
|
}
|
|
57
|
-
text += '```';
|
|
58
142
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
let cellText = '';
|
|
69
|
-
const extractCellText = (cellContent) => {
|
|
70
|
-
cellContent.forEach((cellItem) => {
|
|
71
|
-
if (cellItem.type === 'paragraph' && cellItem.content) {
|
|
72
|
-
cellItem.content.forEach((textItem) => {
|
|
73
|
-
if (textItem.type === 'text' && textItem.text) {
|
|
74
|
-
cellText += textItem.text;
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
};
|
|
80
|
-
extractCellText(cell.content);
|
|
81
|
-
cells.push(cellText.trim());
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
if (cells.length > 0) {
|
|
85
|
-
rows.push(cells);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
// Format as markdown table
|
|
90
|
-
if (rows.length > 0) {
|
|
91
|
-
const [headerRow, ...dataRows] = rows;
|
|
92
|
-
// Header row
|
|
93
|
-
text += '\n| ' + headerRow.join(' | ') + ' |\n';
|
|
94
|
-
// Separator row
|
|
95
|
-
text += '|' + headerRow.map(() => '--------').join('|') + '|\n';
|
|
96
|
-
// Data rows
|
|
97
|
-
dataRows.forEach(row => {
|
|
98
|
-
text += '| ' + row.join(' | ') + ' |\n';
|
|
99
|
-
});
|
|
143
|
+
const markdownRows = [];
|
|
144
|
+
let headerDone = false;
|
|
145
|
+
for (const tl of tableLines) {
|
|
146
|
+
if (tl.startsWith('||')) {
|
|
147
|
+
const cells = tl.split('||').map(c => c.trim()).filter(c => c !== '');
|
|
148
|
+
markdownRows.push('| ' + cells.join(' | ') + ' |');
|
|
149
|
+
if (!headerDone) {
|
|
150
|
+
markdownRows.push('| ' + cells.map(() => '---').join(' | ') + ' |');
|
|
151
|
+
headerDone = true;
|
|
100
152
|
}
|
|
101
153
|
}
|
|
154
|
+
else {
|
|
155
|
+
const cells = tl.split('|').map(c => c.trim()).filter(c => c !== '');
|
|
156
|
+
if (cells.length > 0)
|
|
157
|
+
markdownRows.push('| ' + cells.join(' | ') + ' |');
|
|
158
|
+
}
|
|
102
159
|
}
|
|
103
|
-
|
|
104
|
-
extractFromContent(item.content);
|
|
105
|
-
}
|
|
160
|
+
result.push(...markdownRows);
|
|
106
161
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
162
|
+
else {
|
|
163
|
+
result.push(lines[i]);
|
|
164
|
+
i++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return result.join('\n');
|
|
110
168
|
}
|
|
111
169
|
/**
|
|
112
|
-
* Formats wiki markup to cleaner text
|
|
170
|
+
* Formats wiki markup to cleaner markdown text.
|
|
171
|
+
*
|
|
172
|
+
* Handles (in order):
|
|
173
|
+
* - {code} / {noformat} blocks → fenced code (extracted as placeholders first
|
|
174
|
+
* so their contents are never touched by later transforms)
|
|
175
|
+
* - Wiki tables ||h|| → markdown tables (pre-pass)
|
|
176
|
+
* - {quote} → > blockquote
|
|
177
|
+
* - {note}/{warning}/{info}/{tip}→ labelled blockquote
|
|
178
|
+
* - [text|url] / [~user] / [url] → markdown links / @mention / bare URL
|
|
179
|
+
* - \\ → newline
|
|
180
|
+
* - # item / ## item → 1. / nested ordered list (wiki list syntax)
|
|
181
|
+
* - h1. … h6. → # … ###### (must come AFTER list conversion)
|
|
182
|
+
* - ---- / {panel} / {color} / checkboxes (existing)
|
|
113
183
|
*/
|
|
114
184
|
export function formatWikiMarkup(text) {
|
|
115
185
|
if (!text)
|
|
116
186
|
return '';
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
187
|
+
// --- Phase 1: extract code/noformat blocks into placeholders so their
|
|
188
|
+
// contents survive all subsequent regex transforms safely ---
|
|
189
|
+
const codeStore = [];
|
|
190
|
+
text = text.replace(/\{code(?::(?:language=)?(\w+))?\}([\s\S]*?)\{code\}/g, (_full, lang, code) => {
|
|
191
|
+
const langStr = lang ? lang.toLowerCase() : '';
|
|
192
|
+
const idx = codeStore.length;
|
|
193
|
+
codeStore.push(`\`\`\`${langStr}\n${code.replace(/^\n/, '').replace(/\n$/, '')}\n\`\`\``);
|
|
194
|
+
return `\x00CODE_${idx}\x00`;
|
|
195
|
+
});
|
|
196
|
+
text = text.replace(/\{noformat(?:[^}]*)?\}([\s\S]*?)\{noformat\}/g, (_full, code) => {
|
|
197
|
+
const idx = codeStore.length;
|
|
198
|
+
codeStore.push(`\`\`\`\n${code.replace(/^\n/, '').replace(/\n$/, '')}\n\`\`\``);
|
|
199
|
+
return `\x00CODE_${idx}\x00`;
|
|
200
|
+
});
|
|
201
|
+
// --- Phase 2: pre-process wiki tables ---
|
|
202
|
+
text = convertWikiTablesToMarkdown(text);
|
|
203
|
+
// --- Phase 3: remaining transforms ---
|
|
204
|
+
text = text
|
|
205
|
+
// {quote} blocks → blockquotes
|
|
206
|
+
.replace(/\{quote\}([\s\S]*?)\{quote\}/g, (_full, content) => content.trim().split('\n').map((l) => `> ${l}`).join('\n'))
|
|
207
|
+
// Notification panels {note:…}…{note} etc.
|
|
208
|
+
.replace(/\{(note|info|tip|warning)(?::[^}]*)?\}([\s\S]*?)\{\1\}/g, (_full, type, content) => {
|
|
209
|
+
const label = type.charAt(0).toUpperCase() + type.slice(1);
|
|
210
|
+
const ls = content.trim().split('\n');
|
|
211
|
+
return `> **[${label}]** ${ls[0]}` +
|
|
212
|
+
(ls.length > 1 ? '\n' + ls.slice(1).map((l) => `> ${l}`).join('\n') : '');
|
|
213
|
+
})
|
|
214
|
+
// Wiki links [text|url] → markdown
|
|
215
|
+
.replace(/\[([^\]|]+)\|([^\]]+)\]/g, '[$1]($2)')
|
|
216
|
+
// User mentions [~username] → @username
|
|
217
|
+
.replace(/\[~([^\]]+)\]/g, '@$1')
|
|
218
|
+
// Bare URL in brackets [https://…] → plain URL
|
|
219
|
+
.replace(/\[([a-z]+:\/\/[^\]]+)\]/g, '$1')
|
|
220
|
+
// Wiki line break \\ → newline
|
|
221
|
+
.replace(/\\\\/g, '\n')
|
|
222
|
+
// Wiki ordered lists (# item, ## nested) — MUST precede header conversion
|
|
223
|
+
// because in wiki markup headings are h1./h2. (never bare #)
|
|
224
|
+
.replace(/^(#+) /gm, (_, hashes) => ' '.repeat(hashes.length - 1) + '1. ')
|
|
225
|
+
// Confluence headers h1.–h6. → markdown #–######
|
|
226
|
+
.replace(/^h([1-6])\.\s*/gm, (_match, level) => '#'.repeat(parseInt(level)) + ' ')
|
|
227
|
+
// Horizontal rules
|
|
121
228
|
.replace(/^----+$/gm, '---')
|
|
122
|
-
//
|
|
229
|
+
// Panels → blockquotes
|
|
123
230
|
.replace(/\{panel:title=([^|]*)[^}]*\}/g, '\n> **$1**\n>')
|
|
124
231
|
.replace(/\{panel\}/g, '\n')
|
|
125
|
-
//
|
|
232
|
+
// Colored text — strip styling, keep content
|
|
126
233
|
.replace(/\{color:[^}]*\}([^{]*)\{color\}/g, '$1')
|
|
127
|
-
//
|
|
234
|
+
// Checkboxes
|
|
128
235
|
.replace(/☐/g, '- [ ]')
|
|
129
236
|
.replace(/☑/g, '- [x]')
|
|
130
|
-
//
|
|
237
|
+
// Tidy up
|
|
131
238
|
.replace(/\n{3,}/g, '\n\n')
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
239
|
+
.replace(/\n(#{1,6}\s)/g, '\n\n$1');
|
|
240
|
+
// --- Phase 4: restore code blocks ---
|
|
241
|
+
codeStore.forEach((code, idx) => {
|
|
242
|
+
text = text.replace(`\x00CODE_${idx}\x00`, code);
|
|
243
|
+
});
|
|
244
|
+
return text.trim();
|
|
135
245
|
}
|
|
136
246
|
/**
|
|
137
247
|
* Parses inline formatting like **bold**, *italic*, `code`, {{code}}
|
|
@@ -143,6 +253,7 @@ export function parseInlineFormatting(text) {
|
|
|
143
253
|
// **bold** should be processed before *italic* to handle **text** correctly
|
|
144
254
|
const patterns = [
|
|
145
255
|
{ regex: /\*\*(.*?)\*\*/g, type: 'strong' }, // **bold** (process first)
|
|
256
|
+
{ regex: /~~(.*?)~~/g, type: 'strike' }, // ~~strikethrough~~
|
|
146
257
|
{ regex: /`([^`]+?)`/g, type: 'code' }, // `code` (markdown style)
|
|
147
258
|
{ regex: /\{\{(.+?)\}\}/g, type: 'code' }, // {{code}} (Confluence/Jira wiki style)
|
|
148
259
|
];
|
|
@@ -238,7 +349,8 @@ export function parseInlineFormatting(text) {
|
|
|
238
349
|
* Enhanced description parser that converts text to proper ADF format
|
|
239
350
|
*/
|
|
240
351
|
export function parseDescriptionToADF(description) {
|
|
241
|
-
|
|
352
|
+
// Normalise wiki-style tables (||header||) to markdown before line processing
|
|
353
|
+
const lines = convertWikiTablesToMarkdown(description).split('\n');
|
|
242
354
|
const content = [];
|
|
243
355
|
for (let i = 0; i < lines.length; i++) {
|
|
244
356
|
const line = lines[i].trim();
|
|
@@ -393,19 +505,23 @@ export function parseDescriptionToADF(description) {
|
|
|
393
505
|
}
|
|
394
506
|
// Handle code blocks (```)
|
|
395
507
|
else if (line.startsWith('```')) {
|
|
508
|
+
const rawLang = line.substring(3).trim();
|
|
509
|
+
const lang = normalizeCodeLanguage(rawLang);
|
|
396
510
|
const codeLines = [];
|
|
397
511
|
i++; // Skip the opening ```
|
|
398
512
|
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
|
399
513
|
codeLines.push(lines[i]);
|
|
400
514
|
i++;
|
|
401
515
|
}
|
|
402
|
-
|
|
516
|
+
const codeBlock = {
|
|
403
517
|
type: 'codeBlock',
|
|
518
|
+
attrs: lang ? { language: lang } : {},
|
|
404
519
|
content: [{
|
|
405
520
|
type: 'text',
|
|
406
521
|
text: codeLines.join('\n')
|
|
407
522
|
}]
|
|
408
|
-
}
|
|
523
|
+
};
|
|
524
|
+
content.push(codeBlock);
|
|
409
525
|
}
|
|
410
526
|
// Handle markdown tables - convert to proper ADF table structure
|
|
411
527
|
else if (line.includes('|') && line.trim() !== '|') {
|
|
@@ -525,14 +641,43 @@ export function parseDescriptionToADF(description) {
|
|
|
525
641
|
* Convert description to wiki markup text format
|
|
526
642
|
*/
|
|
527
643
|
export function parseDescriptionToWikiMarkup(description) {
|
|
528
|
-
|
|
644
|
+
// Extract code blocks before any line-by-line processing so their
|
|
645
|
+
// contents are never transformed by heading/table/list logic.
|
|
646
|
+
const codeBlockStore = [];
|
|
647
|
+
const protected_ = description.replace(/```(\w*)\n?([\s\S]*?)```/g, (_full, lang, code) => {
|
|
648
|
+
const idx = codeBlockStore.length;
|
|
649
|
+
const normalizedLang = normalizeCodeLanguage(lang);
|
|
650
|
+
const langSuffix = normalizedLang ? `:language=${normalizedLang}` : '';
|
|
651
|
+
codeBlockStore.push(`{code${langSuffix}}\n${code.replace(/^\n/, '').replace(/\n$/, '')}\n{code}`);
|
|
652
|
+
return `\x00CODEBLOCK_${idx}\x00`;
|
|
653
|
+
});
|
|
654
|
+
const lines = protected_.split('\n');
|
|
529
655
|
const result = [];
|
|
530
656
|
for (let i = 0; i < lines.length; i++) {
|
|
531
657
|
const line = lines[i].trim();
|
|
658
|
+
// Restore code blocks
|
|
659
|
+
if (line.match(/^\x00CODEBLOCK_(\d+)\x00$/)) {
|
|
660
|
+
const idx = parseInt(line.match(/\x00CODEBLOCK_(\d+)\x00/)[1]);
|
|
661
|
+
result.push(codeBlockStore[idx]);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
532
664
|
if (line === '') {
|
|
533
665
|
result.push('');
|
|
534
666
|
continue;
|
|
535
667
|
}
|
|
668
|
+
// Handle blockquotes (> text) — collect consecutive lines and wrap in {quote}
|
|
669
|
+
if (line.startsWith('> ')) {
|
|
670
|
+
const quoteLines = [];
|
|
671
|
+
while (i < lines.length && lines[i].trim().startsWith('> ')) {
|
|
672
|
+
quoteLines.push(lines[i].trim().substring(2).trim());
|
|
673
|
+
i++;
|
|
674
|
+
}
|
|
675
|
+
i--; // back up — outer loop will increment
|
|
676
|
+
result.push('{quote}');
|
|
677
|
+
result.push(...quoteLines);
|
|
678
|
+
result.push('{quote}');
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
536
681
|
// Handle markdown headers
|
|
537
682
|
if (line.startsWith('###')) {
|
|
538
683
|
result.push('h3. ' + line.substring(3).trim());
|
|
@@ -606,4 +751,175 @@ export function parseDescriptionToWikiMarkup(description) {
|
|
|
606
751
|
}
|
|
607
752
|
return result.join('\n');
|
|
608
753
|
}
|
|
754
|
+
// ---------------------------------------------------------------------------
|
|
755
|
+
// Confluence Storage Format Converter
|
|
756
|
+
// ---------------------------------------------------------------------------
|
|
757
|
+
/**
|
|
758
|
+
* Escapes special HTML characters for use inside storage-format text nodes.
|
|
759
|
+
*/
|
|
760
|
+
function escapeStorageHtml(text) {
|
|
761
|
+
return text
|
|
762
|
+
.replace(/&/g, '&')
|
|
763
|
+
.replace(/</g, '<')
|
|
764
|
+
.replace(/>/g, '>');
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Applies inline markdown formatting and converts it to Confluence storage
|
|
768
|
+
* format HTML (strong, em, strike, code, links).
|
|
769
|
+
* Code spans are protected from further transformation.
|
|
770
|
+
*/
|
|
771
|
+
function applyInlineStorageFormat(text) {
|
|
772
|
+
// Protect inline code spans first
|
|
773
|
+
const spans = [];
|
|
774
|
+
let out = text.replace(/`([^`]+)`/g, (_full, code) => {
|
|
775
|
+
const idx = spans.length;
|
|
776
|
+
spans.push(`<code>${escapeStorageHtml(code)}</code>`);
|
|
777
|
+
return `\x00SPAN_${idx}\x00`;
|
|
778
|
+
});
|
|
779
|
+
out = out
|
|
780
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
781
|
+
.replace(/~~(.*?)~~/g, '<s>$1</s>')
|
|
782
|
+
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
783
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
784
|
+
// Restore code spans
|
|
785
|
+
spans.forEach((span, idx) => {
|
|
786
|
+
out = out.replace(`\x00SPAN_${idx}\x00`, span);
|
|
787
|
+
});
|
|
788
|
+
return out;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Returns true when the string already contains Confluence storage-format
|
|
792
|
+
* markup so that it can be passed through as-is.
|
|
793
|
+
*/
|
|
794
|
+
function isAlreadyStorageFormat(content) {
|
|
795
|
+
const trimmed = content.trim();
|
|
796
|
+
return (trimmed.startsWith('<') &&
|
|
797
|
+
(trimmed.includes('<ac:') ||
|
|
798
|
+
trimmed.includes('<ri:') ||
|
|
799
|
+
/^<(p|h[1-6]|ul|ol|table|blockquote|hr)\b/i.test(trimmed)));
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Converts LLM-produced markdown to Confluence storage format (XHTML).
|
|
803
|
+
* If the input already looks like storage format it is returned unchanged.
|
|
804
|
+
*
|
|
805
|
+
* Supported elements:
|
|
806
|
+
* - ATX headings (#–######)
|
|
807
|
+
* - Fenced code blocks (``` lang … ```) → ac:structured-macro "code"
|
|
808
|
+
* - Blockquotes (> …)
|
|
809
|
+
* - Unordered lists (- / * / +)
|
|
810
|
+
* - Ordered lists (1. …)
|
|
811
|
+
* - Markdown tables
|
|
812
|
+
* - Horizontal rules (---, ***, ___)
|
|
813
|
+
* - Inline: **bold**, *italic*, ~~strike~~, `code`, [text](url)
|
|
814
|
+
*/
|
|
815
|
+
export function markdownToConfluenceStorage(markdown) {
|
|
816
|
+
if (isAlreadyStorageFormat(markdown)) {
|
|
817
|
+
return markdown;
|
|
818
|
+
}
|
|
819
|
+
// --- Phase 1: extract fenced code blocks into placeholders ---
|
|
820
|
+
const codeBlocks = [];
|
|
821
|
+
const withoutCode = markdown.replace(/```(\w*)\n?([\s\S]*?)```/g, (_full, rawLang, code) => {
|
|
822
|
+
const idx = codeBlocks.length;
|
|
823
|
+
const lang = normalizeCodeLanguage(rawLang);
|
|
824
|
+
const langParam = lang
|
|
825
|
+
? `<ac:parameter ac:name="language">${lang}</ac:parameter>`
|
|
826
|
+
: '';
|
|
827
|
+
const body = code.replace(/^\n/, '').replace(/\n$/, '');
|
|
828
|
+
codeBlocks.push(`<ac:structured-macro ac:name="code">${langParam}` +
|
|
829
|
+
`<ac:plain-text-body><![CDATA[${body}]]></ac:plain-text-body>` +
|
|
830
|
+
`</ac:structured-macro>`);
|
|
831
|
+
return `\x00CODE_${idx}\x00`;
|
|
832
|
+
});
|
|
833
|
+
// --- Phase 2: line-by-line conversion ---
|
|
834
|
+
const lines = withoutCode.split('\n');
|
|
835
|
+
const result = [];
|
|
836
|
+
let i = 0;
|
|
837
|
+
while (i < lines.length) {
|
|
838
|
+
const raw = lines[i];
|
|
839
|
+
const line = raw.trim();
|
|
840
|
+
// Code block placeholder
|
|
841
|
+
const codeMatch = line.match(/^\x00CODE_(\d+)\x00$/);
|
|
842
|
+
if (codeMatch) {
|
|
843
|
+
result.push(codeBlocks[parseInt(codeMatch[1])]);
|
|
844
|
+
i++;
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
// Heading
|
|
848
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
849
|
+
if (headingMatch) {
|
|
850
|
+
const level = headingMatch[1].length;
|
|
851
|
+
result.push(`<h${level}>${applyInlineStorageFormat(headingMatch[2])}</h${level}>`);
|
|
852
|
+
i++;
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
// Horizontal rule
|
|
856
|
+
if (/^([-*_])\1{2,}$/.test(line)) {
|
|
857
|
+
result.push('<hr/>');
|
|
858
|
+
i++;
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
// Blockquote
|
|
862
|
+
if (line.startsWith('> ')) {
|
|
863
|
+
result.push(`<blockquote><p>${applyInlineStorageFormat(line.substring(2))}</p></blockquote>`);
|
|
864
|
+
i++;
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
// Table — collect all consecutive table lines
|
|
868
|
+
if (line.startsWith('|')) {
|
|
869
|
+
const tableRows = [];
|
|
870
|
+
while (i < lines.length && lines[i].trim().startsWith('|')) {
|
|
871
|
+
const row = lines[i].trim();
|
|
872
|
+
// Skip separator lines (e.g. |---|---|)
|
|
873
|
+
if (!/^\|[\s\-:|]+\|?$/.test(row)) {
|
|
874
|
+
const cells = row.split('|').map(c => c.trim()).filter(c => c !== '');
|
|
875
|
+
if (cells.length > 0)
|
|
876
|
+
tableRows.push(cells);
|
|
877
|
+
}
|
|
878
|
+
i++;
|
|
879
|
+
}
|
|
880
|
+
if (tableRows.length > 0) {
|
|
881
|
+
let html = '<table><tbody>';
|
|
882
|
+
html += '<tr>' + tableRows[0].map(c => `<th>${applyInlineStorageFormat(c)}</th>`).join('') + '</tr>';
|
|
883
|
+
for (let r = 1; r < tableRows.length; r++) {
|
|
884
|
+
html += '<tr>' + tableRows[r].map(c => `<td>${applyInlineStorageFormat(c)}</td>`).join('') + '</tr>';
|
|
885
|
+
}
|
|
886
|
+
html += '</tbody></table>';
|
|
887
|
+
result.push(html);
|
|
888
|
+
}
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
// Unordered list — collect consecutive items
|
|
892
|
+
if (/^[-*+]\s/.test(line)) {
|
|
893
|
+
let html = '<ul>';
|
|
894
|
+
while (i < lines.length && /^[-*+]\s/.test(lines[i].trim())) {
|
|
895
|
+
html += `<li>${applyInlineStorageFormat(lines[i].trim().substring(2).trim())}</li>`;
|
|
896
|
+
i++;
|
|
897
|
+
}
|
|
898
|
+
html += '</ul>';
|
|
899
|
+
result.push(html);
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
// Ordered list — collect consecutive items
|
|
903
|
+
if (/^\d+\.\s/.test(line)) {
|
|
904
|
+
let html = '<ol>';
|
|
905
|
+
while (i < lines.length && /^\d+\.\s/.test(lines[i].trim())) {
|
|
906
|
+
const text = lines[i].trim().replace(/^\d+\.\s/, '');
|
|
907
|
+
html += `<li>${applyInlineStorageFormat(text)}</li>`;
|
|
908
|
+
i++;
|
|
909
|
+
}
|
|
910
|
+
html += '</ol>';
|
|
911
|
+
result.push(html);
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
// Empty line — skip (Confluence doesn't need explicit paragraph breaks between block elements)
|
|
915
|
+
if (line === '') {
|
|
916
|
+
i++;
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
// Regular paragraph
|
|
920
|
+
result.push(`<p>${applyInlineStorageFormat(line)}</p>`);
|
|
921
|
+
i++;
|
|
922
|
+
}
|
|
923
|
+
return result.join('\n');
|
|
924
|
+
}
|
|
609
925
|
//# sourceMappingURL=adf-formatter.js.map
|