@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.
Files changed (98) hide show
  1. package/README.md +165 -36
  2. package/dist/formatters/adf-formatter.d.ts +30 -2
  3. package/dist/formatters/adf-formatter.d.ts.map +1 -1
  4. package/dist/formatters/adf-formatter.js +423 -107
  5. package/dist/formatters/adf-formatter.js.map +1 -1
  6. package/dist/formatters/index.d.ts +1 -1
  7. package/dist/formatters/index.d.ts.map +1 -1
  8. package/dist/formatters/index.js +1 -1
  9. package/dist/formatters/index.js.map +1 -1
  10. package/dist/handlers/base-handler.d.ts +5 -0
  11. package/dist/handlers/base-handler.d.ts.map +1 -0
  12. package/dist/handlers/base-handler.js +7 -0
  13. package/dist/handlers/base-handler.js.map +1 -0
  14. package/dist/handlers/bitbucket-handlers.d.ts +2 -1
  15. package/dist/handlers/bitbucket-handlers.d.ts.map +1 -1
  16. package/dist/handlers/bitbucket-handlers.js +31 -191
  17. package/dist/handlers/bitbucket-handlers.js.map +1 -1
  18. package/dist/handlers/confluence-handlers.d.ts +2 -1
  19. package/dist/handlers/confluence-handlers.d.ts.map +1 -1
  20. package/dist/handlers/confluence-handlers.js +46 -243
  21. package/dist/handlers/confluence-handlers.js.map +1 -1
  22. package/dist/handlers/jira-handlers.d.ts +9 -1
  23. package/dist/handlers/jira-handlers.d.ts.map +1 -1
  24. package/dist/handlers/jira-handlers.js +144 -575
  25. package/dist/handlers/jira-handlers.js.map +1 -1
  26. package/dist/index.js +111 -189
  27. package/dist/index.js.map +1 -1
  28. package/dist/registry/tool-registry.d.ts +11 -0
  29. package/dist/registry/tool-registry.d.ts.map +1 -0
  30. package/dist/registry/tool-registry.js +24 -0
  31. package/dist/registry/tool-registry.js.map +1 -0
  32. package/dist/services/bitbucket.d.ts +2 -0
  33. package/dist/services/bitbucket.d.ts.map +1 -1
  34. package/dist/services/bitbucket.js +22 -74
  35. package/dist/services/bitbucket.js.map +1 -1
  36. package/dist/services/confluence.d.ts.map +1 -1
  37. package/dist/services/confluence.js +5 -4
  38. package/dist/services/confluence.js.map +1 -1
  39. package/dist/services/jira/agile.d.ts +33 -0
  40. package/dist/services/jira/agile.d.ts.map +1 -0
  41. package/dist/services/jira/agile.js +118 -0
  42. package/dist/services/jira/agile.js.map +1 -0
  43. package/dist/services/jira/attachments.d.ts +7 -0
  44. package/dist/services/jira/attachments.d.ts.map +1 -0
  45. package/dist/services/jira/attachments.js +26 -0
  46. package/dist/services/jira/attachments.js.map +1 -0
  47. package/dist/services/jira/base.d.ts +9 -0
  48. package/dist/services/jira/base.d.ts.map +1 -0
  49. package/dist/services/jira/base.js +11 -0
  50. package/dist/services/jira/base.js.map +1 -0
  51. package/dist/services/jira/comments.d.ts +8 -0
  52. package/dist/services/jira/comments.d.ts.map +1 -0
  53. package/dist/services/jira/comments.js +33 -0
  54. package/dist/services/jira/comments.js.map +1 -0
  55. package/dist/services/jira/issues.d.ts +32 -0
  56. package/dist/services/jira/issues.d.ts.map +1 -0
  57. package/dist/services/jira/issues.js +177 -0
  58. package/dist/services/jira/issues.js.map +1 -0
  59. package/dist/services/jira/labels.d.ts +7 -0
  60. package/dist/services/jira/labels.d.ts.map +1 -0
  61. package/dist/services/jira/labels.js +21 -0
  62. package/dist/services/jira/labels.js.map +1 -0
  63. package/dist/services/jira/links.d.ts +9 -0
  64. package/dist/services/jira/links.d.ts.map +1 -0
  65. package/dist/services/jira/links.js +55 -0
  66. package/dist/services/jira/links.js.map +1 -0
  67. package/dist/services/jira/metadata.d.ts +8 -0
  68. package/dist/services/jira/metadata.d.ts.map +1 -0
  69. package/dist/services/jira/metadata.js +32 -0
  70. package/dist/services/jira/metadata.js.map +1 -0
  71. package/dist/services/jira/subtasks.d.ts +9 -0
  72. package/dist/services/jira/subtasks.d.ts.map +1 -0
  73. package/dist/services/jira/subtasks.js +32 -0
  74. package/dist/services/jira/subtasks.js.map +1 -0
  75. package/dist/services/jira/transitions.d.ts +16 -0
  76. package/dist/services/jira/transitions.d.ts.map +1 -0
  77. package/dist/services/jira/transitions.js +203 -0
  78. package/dist/services/jira/transitions.js.map +1 -0
  79. package/dist/services/jira/versions.d.ts +19 -0
  80. package/dist/services/jira/versions.d.ts.map +1 -0
  81. package/dist/services/jira/versions.js +62 -0
  82. package/dist/services/jira/versions.js.map +1 -0
  83. package/dist/services/jira/watchers.d.ts +7 -0
  84. package/dist/services/jira/watchers.d.ts.map +1 -0
  85. package/dist/services/jira/watchers.js +23 -0
  86. package/dist/services/jira/watchers.js.map +1 -0
  87. package/dist/services/jira/worklogs.d.ts +8 -0
  88. package/dist/services/jira/worklogs.d.ts.map +1 -0
  89. package/dist/services/jira/worklogs.js +37 -0
  90. package/dist/services/jira/worklogs.js.map +1 -0
  91. package/dist/services/jira.d.ts +58 -103
  92. package/dist/services/jira.d.ts.map +1 -1
  93. package/dist/services/jira.js +100 -1481
  94. package/dist/services/jira.js.map +1 -1
  95. package/dist/tools/jira-tools.d.ts.map +1 -1
  96. package/dist/tools/jira-tools.js +35 -3
  97. package/dist/tools/jira-tools.js.map +1 -1
  98. 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
- * Extracts plain text from ADF (Atlassian Document Format) body
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
- // If body is already a string (wiki markup), return it directly
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
- let text = '';
20
- const extractFromContent = (content) => {
21
- for (const item of content) {
22
- if (item.type === 'text' && item.text) {
23
- text += item.text;
24
- }
25
- else if (item.type === 'hardBreak') {
26
- text += '\n';
27
- }
28
- else if (item.type === 'paragraph') {
29
- if (item.content) {
30
- extractFromContent(item.content);
31
- }
32
- }
33
- else if (item.type === 'heading') {
34
- const level = item.attrs?.level || 1;
35
- const prefix = '#'.repeat(level);
36
- text += `${prefix} `;
37
- if (item.content) {
38
- extractFromContent(item.content);
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
- else if (item.type === 'codeBlock') {
53
- text += '```';
54
- if (item.content) {
55
- extractFromContent(item.content);
139
+ else {
140
+ break;
56
141
  }
57
- text += '```';
58
142
  }
59
- else if (item.type === 'table') {
60
- // Extract table as markdown
61
- if (item.content) {
62
- const rows = [];
63
- item.content.forEach((row) => {
64
- if (row.type === 'tableRow' && row.content) {
65
- const cells = [];
66
- row.content.forEach((cell) => {
67
- if ((cell.type === 'tableCell' || cell.type === 'tableHeader') && cell.content) {
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
- else if (item.content) {
104
- extractFromContent(item.content);
105
- }
160
+ result.push(...markdownRows);
106
161
  }
107
- };
108
- extractFromContent(body.content);
109
- return formatWikiMarkup(text.trim());
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
- return text
118
- // Convert Confluence headers to markdown headers
119
- .replace(/^h([1-6])\.\s*/gm, (match, level) => '#'.repeat(parseInt(level)) + ' ')
120
- // Convert horizontal rules
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
- // Handle panels - convert to blockquotes for better readability
229
+ // Panels → blockquotes
123
230
  .replace(/\{panel:title=([^|]*)[^}]*\}/g, '\n> **$1**\n>')
124
231
  .replace(/\{panel\}/g, '\n')
125
- // Handle colored text/backgrounds - keep the text but remove styling
232
+ // Colored text — strip styling, keep content
126
233
  .replace(/\{color:[^}]*\}([^{]*)\{color\}/g, '$1')
127
- // Convert checkboxes
234
+ // Checkboxes
128
235
  .replace(/☐/g, '- [ ]')
129
236
  .replace(/☑/g, '- [x]')
130
- // Clean up multiple newlines
237
+ // Tidy up
131
238
  .replace(/\n{3,}/g, '\n\n')
132
- // Ensure proper spacing around headers
133
- .replace(/\n(#{1,6}\s)/g, '\n\n$1')
134
- .trim();
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
- const lines = description.split('\n');
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
- content.push({
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
- const lines = description.split('\n');
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, '&amp;')
763
+ .replace(/</g, '&lt;')
764
+ .replace(/>/g, '&gt;');
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