@jagadeesh52423/atlassian-mcp-server 1.1.1 → 1.2.1

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 (151) hide show
  1. package/.env.example +6 -1
  2. package/README.md +26 -14
  3. package/dist/config.d.ts +2 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +7 -0
  6. package/dist/config.js.map +1 -1
  7. package/dist/constants.d.ts +0 -4
  8. package/dist/constants.d.ts.map +1 -1
  9. package/dist/constants.js +0 -5
  10. package/dist/constants.js.map +1 -1
  11. package/dist/formatters/adf-formatter.d.ts +35 -4
  12. package/dist/formatters/adf-formatter.d.ts.map +1 -1
  13. package/dist/formatters/adf-formatter.js +691 -156
  14. package/dist/formatters/adf-formatter.js.map +1 -1
  15. package/dist/formatters/adf-to-text.d.ts +9 -0
  16. package/dist/formatters/adf-to-text.d.ts.map +1 -0
  17. package/dist/formatters/adf-to-text.js +25 -0
  18. package/dist/formatters/adf-to-text.js.map +1 -0
  19. package/dist/formatters/index.d.ts +7 -1
  20. package/dist/formatters/index.d.ts.map +1 -1
  21. package/dist/formatters/index.js +7 -1
  22. package/dist/formatters/index.js.map +1 -1
  23. package/dist/formatters/markdown-to-adf.d.ts +6 -0
  24. package/dist/formatters/markdown-to-adf.d.ts.map +1 -0
  25. package/dist/formatters/markdown-to-adf.js +27 -0
  26. package/dist/formatters/markdown-to-adf.js.map +1 -0
  27. package/dist/handlers/base-handler.d.ts +5 -0
  28. package/dist/handlers/base-handler.d.ts.map +1 -0
  29. package/dist/handlers/base-handler.js +7 -0
  30. package/dist/handlers/base-handler.js.map +1 -0
  31. package/dist/handlers/bitbucket-handlers.d.ts +9 -1
  32. package/dist/handlers/bitbucket-handlers.d.ts.map +1 -1
  33. package/dist/handlers/bitbucket-handlers.js +51 -191
  34. package/dist/handlers/bitbucket-handlers.js.map +1 -1
  35. package/dist/handlers/confluence-handlers.d.ts +2 -1
  36. package/dist/handlers/confluence-handlers.d.ts.map +1 -1
  37. package/dist/handlers/confluence-handlers.js +46 -243
  38. package/dist/handlers/confluence-handlers.js.map +1 -1
  39. package/dist/handlers/jira-handlers.d.ts +9 -1
  40. package/dist/handlers/jira-handlers.d.ts.map +1 -1
  41. package/dist/handlers/jira-handlers.js +144 -575
  42. package/dist/handlers/jira-handlers.js.map +1 -1
  43. package/dist/index.js +123 -194
  44. package/dist/index.js.map +1 -1
  45. package/dist/registry/tool-registry.d.ts +11 -0
  46. package/dist/registry/tool-registry.d.ts.map +1 -0
  47. package/dist/registry/tool-registry.js +24 -0
  48. package/dist/registry/tool-registry.js.map +1 -0
  49. package/dist/services/bitbucket.d.ts +7 -0
  50. package/dist/services/bitbucket.d.ts.map +1 -1
  51. package/dist/services/bitbucket.js +36 -74
  52. package/dist/services/bitbucket.js.map +1 -1
  53. package/dist/services/confluence.d.ts +52 -10
  54. package/dist/services/confluence.d.ts.map +1 -1
  55. package/dist/services/confluence.js +247 -311
  56. package/dist/services/confluence.js.map +1 -1
  57. package/dist/services/jira/agile.d.ts +33 -0
  58. package/dist/services/jira/agile.d.ts.map +1 -0
  59. package/dist/services/jira/agile.js +118 -0
  60. package/dist/services/jira/agile.js.map +1 -0
  61. package/dist/services/jira/attachments.d.ts +7 -0
  62. package/dist/services/jira/attachments.d.ts.map +1 -0
  63. package/dist/services/jira/attachments.js +26 -0
  64. package/dist/services/jira/attachments.js.map +1 -0
  65. package/dist/services/jira/base.d.ts +8 -0
  66. package/dist/services/jira/base.d.ts.map +1 -0
  67. package/dist/services/jira/base.js +10 -0
  68. package/dist/services/jira/base.js.map +1 -0
  69. package/dist/services/jira/comments.d.ts +8 -0
  70. package/dist/services/jira/comments.d.ts.map +1 -0
  71. package/dist/services/jira/comments.js +33 -0
  72. package/dist/services/jira/comments.js.map +1 -0
  73. package/dist/services/jira/issues.d.ts +32 -0
  74. package/dist/services/jira/issues.d.ts.map +1 -0
  75. package/dist/services/jira/issues.js +177 -0
  76. package/dist/services/jira/issues.js.map +1 -0
  77. package/dist/services/jira/labels.d.ts +7 -0
  78. package/dist/services/jira/labels.d.ts.map +1 -0
  79. package/dist/services/jira/labels.js +21 -0
  80. package/dist/services/jira/labels.js.map +1 -0
  81. package/dist/services/jira/links.d.ts +9 -0
  82. package/dist/services/jira/links.d.ts.map +1 -0
  83. package/dist/services/jira/links.js +55 -0
  84. package/dist/services/jira/links.js.map +1 -0
  85. package/dist/services/jira/metadata.d.ts +8 -0
  86. package/dist/services/jira/metadata.d.ts.map +1 -0
  87. package/dist/services/jira/metadata.js +32 -0
  88. package/dist/services/jira/metadata.js.map +1 -0
  89. package/dist/services/jira/subtasks.d.ts +9 -0
  90. package/dist/services/jira/subtasks.d.ts.map +1 -0
  91. package/dist/services/jira/subtasks.js +32 -0
  92. package/dist/services/jira/subtasks.js.map +1 -0
  93. package/dist/services/jira/transitions.d.ts +16 -0
  94. package/dist/services/jira/transitions.d.ts.map +1 -0
  95. package/dist/services/jira/transitions.js +203 -0
  96. package/dist/services/jira/transitions.js.map +1 -0
  97. package/dist/services/jira/versions.d.ts +19 -0
  98. package/dist/services/jira/versions.d.ts.map +1 -0
  99. package/dist/services/jira/versions.js +62 -0
  100. package/dist/services/jira/versions.js.map +1 -0
  101. package/dist/services/jira/watchers.d.ts +7 -0
  102. package/dist/services/jira/watchers.d.ts.map +1 -0
  103. package/dist/services/jira/watchers.js +23 -0
  104. package/dist/services/jira/watchers.js.map +1 -0
  105. package/dist/services/jira/worklogs.d.ts +8 -0
  106. package/dist/services/jira/worklogs.d.ts.map +1 -0
  107. package/dist/services/jira/worklogs.js +37 -0
  108. package/dist/services/jira/worklogs.js.map +1 -0
  109. package/dist/services/jira.d.ts +58 -103
  110. package/dist/services/jira.d.ts.map +1 -1
  111. package/dist/services/jira.js +100 -1481
  112. package/dist/services/jira.js.map +1 -1
  113. package/dist/snapshot/decorators/bitbucket-snapshots.d.ts +4 -0
  114. package/dist/snapshot/decorators/bitbucket-snapshots.d.ts.map +1 -0
  115. package/dist/snapshot/decorators/bitbucket-snapshots.js +65 -0
  116. package/dist/snapshot/decorators/bitbucket-snapshots.js.map +1 -0
  117. package/dist/snapshot/decorators/confluence-snapshots.d.ts +4 -0
  118. package/dist/snapshot/decorators/confluence-snapshots.d.ts.map +1 -0
  119. package/dist/snapshot/decorators/confluence-snapshots.js +71 -0
  120. package/dist/snapshot/decorators/confluence-snapshots.js.map +1 -0
  121. package/dist/snapshot/decorators/jira-snapshots.d.ts +4 -0
  122. package/dist/snapshot/decorators/jira-snapshots.d.ts.map +1 -0
  123. package/dist/snapshot/decorators/jira-snapshots.js +171 -0
  124. package/dist/snapshot/decorators/jira-snapshots.js.map +1 -0
  125. package/dist/snapshot/index.d.ts +7 -0
  126. package/dist/snapshot/index.d.ts.map +1 -0
  127. package/dist/snapshot/index.js +7 -0
  128. package/dist/snapshot/index.js.map +1 -0
  129. package/dist/snapshot/snapshot-decorator.d.ts +4 -0
  130. package/dist/snapshot/snapshot-decorator.d.ts.map +1 -0
  131. package/dist/snapshot/snapshot-decorator.js +74 -0
  132. package/dist/snapshot/snapshot-decorator.js.map +1 -0
  133. package/dist/snapshot/snapshot-manager.d.ts +9 -0
  134. package/dist/snapshot/snapshot-manager.d.ts.map +1 -0
  135. package/dist/snapshot/snapshot-manager.js +56 -0
  136. package/dist/snapshot/snapshot-manager.js.map +1 -0
  137. package/dist/snapshot/snapshot-types.d.ts +26 -0
  138. package/dist/snapshot/snapshot-types.d.ts.map +1 -0
  139. package/dist/snapshot/snapshot-types.js +2 -0
  140. package/dist/snapshot/snapshot-types.js.map +1 -0
  141. package/dist/tools/bitbucket-tools.d.ts.map +1 -1
  142. package/dist/tools/bitbucket-tools.js +30 -0
  143. package/dist/tools/bitbucket-tools.js.map +1 -1
  144. package/dist/tools/confluence-tools.js +12 -12
  145. package/dist/tools/confluence-tools.js.map +1 -1
  146. package/dist/tools/jira-tools.d.ts.map +1 -1
  147. package/dist/tools/jira-tools.js +35 -3
  148. package/dist/tools/jira-tools.js.map +1 -1
  149. package/dist/types.d.ts +17 -2
  150. package/dist/types.d.ts.map +1 -1
  151. package/package.json +6 -2
@@ -3,162 +3,400 @@
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
7
  */
8
- export function extractTextFromADF(body) {
9
- if (!body)
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)
10
22
  return '';
11
- // If body is already a string (wiki markup), return it directly
12
- if (typeof body === 'string') {
13
- return formatWikiMarkup(body);
23
+ const lower = lang.toLowerCase();
24
+ return LANGUAGE_ALIASES[lower] ?? lower;
25
+ }
26
+ const TABLE_TOTAL_WIDTH_PX = 760;
27
+ const TABLE_MIN_COL_WIDTH_PX = 60;
28
+ const TABLE_MAX_COL_WIDTH_PX = 400;
29
+ function extractCellText(cell) {
30
+ const para = cell?.content?.[0];
31
+ if (!para?.content)
32
+ return '';
33
+ return para.content.map((n) => n.text ?? '').join('');
34
+ }
35
+ /**
36
+ * Sets attrs.colwidth on every cell so Confluence renders columns proportional
37
+ * to their max content length, clamped to [TABLE_MIN_COL_WIDTH_PX, TABLE_MAX_COL_WIDTH_PX].
38
+ */
39
+ function applyColWidths(tableRows) {
40
+ if (!tableRows.length)
41
+ return;
42
+ const numCols = Math.max(...tableRows.map(r => r.content?.length ?? 0));
43
+ if (numCols === 0)
44
+ return;
45
+ const maxLens = new Array(numCols).fill(1);
46
+ for (const row of tableRows) {
47
+ row.content?.forEach((cell, idx) => {
48
+ const len = extractCellText(cell).length || 1;
49
+ if (len > maxLens[idx])
50
+ maxLens[idx] = len;
51
+ });
14
52
  }
15
- // If body doesn't have content property, it might be wiki markup in another format
16
- if (!body.content) {
17
- return formatWikiMarkup(String(body));
53
+ const totalLen = maxLens.reduce((a, b) => a + b, 0) || 1;
54
+ const widths = maxLens.map((len) => {
55
+ const proportional = (len / totalLen) * TABLE_TOTAL_WIDTH_PX;
56
+ return Math.round(Math.min(TABLE_MAX_COL_WIDTH_PX, Math.max(TABLE_MIN_COL_WIDTH_PX, proportional)));
57
+ });
58
+ for (const row of tableRows) {
59
+ row.content?.forEach((cell, idx) => {
60
+ cell.attrs = { ...(cell.attrs ?? {}), colwidth: [widths[idx]] };
61
+ });
18
62
  }
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
- }
63
+ }
64
+ const STORAGE_TABLE_MIN_PCT = 5;
65
+ /**
66
+ * Builds a Confluence storage-format <colgroup> sized proportionally to the
67
+ * max content length per column. Returns '' for empty/single-column input.
68
+ */
69
+ function buildColGroupHtml(tableRows) {
70
+ if (!tableRows.length)
71
+ return '';
72
+ const numCols = Math.max(...tableRows.map(r => r.length));
73
+ if (numCols < 2)
74
+ return '';
75
+ const maxLens = new Array(numCols).fill(1);
76
+ for (const row of tableRows) {
77
+ row.forEach((cell, idx) => {
78
+ const len = (cell ?? '').length || 1;
79
+ if (len > maxLens[idx])
80
+ maxLens[idx] = len;
81
+ });
82
+ }
83
+ const totalLen = maxLens.reduce((a, b) => a + b, 0) || 1;
84
+ const raw = maxLens.map((len) => Math.max(STORAGE_TABLE_MIN_PCT, (len / totalLen) * 100));
85
+ const sum = raw.reduce((a, b) => a + b, 0);
86
+ const normalized = raw.map((p) => (p / sum) * 100);
87
+ const cols = normalized
88
+ .map((p) => `<col style="width: ${p.toFixed(1)}%;" />`)
89
+ .join('');
90
+ return `<colgroup>${cols}</colgroup>`;
91
+ }
92
+ /**
93
+ * Recursively converts a single ADF node to plain text.
94
+ * Mirrors sooperset's adf_to_text recursive approach:
95
+ * - Content arrays are joined with '\n'
96
+ * - Marks (bold, italic, links) are stripped — plain text only
97
+ */
98
+ function adfNodeToText(node) {
99
+ if (!node)
100
+ return '';
101
+ // Array: map each item and join with newline (mirrors sooperset's "\n".join)
102
+ if (Array.isArray(node)) {
103
+ return node.map(adfNodeToText).filter(Boolean).join('\n');
104
+ }
105
+ if (typeof node !== 'object')
106
+ return String(node);
107
+ switch (node.type) {
108
+ case 'text':
109
+ // Marks (bold, italic, link, etc.) are stripped — plain text only
110
+ return node.text ?? '';
111
+ case 'hardBreak':
112
+ return '\n';
113
+ case 'mention': {
114
+ const attrs = node.attrs ?? {};
115
+ return attrs.text ?? `@${attrs.id ?? 'unknown'}`;
116
+ }
117
+ case 'emoji': {
118
+ const attrs = node.attrs ?? {};
119
+ return attrs.text ?? attrs.shortName ?? '';
120
+ }
121
+ case 'date': {
122
+ const timestamp = node.attrs?.timestamp;
123
+ if (!timestamp)
124
+ return '';
125
+ try {
126
+ return new Date(Number(timestamp)).toISOString().slice(0, 10);
40
127
  }
41
- else if (item.type === 'bulletList' || item.type === 'orderedList') {
42
- if (item.content) {
43
- extractFromContent(item.content);
44
- }
128
+ catch {
129
+ return String(timestamp);
45
130
  }
46
- else if (item.type === 'listItem') {
47
- text += '- ';
48
- if (item.content) {
49
- extractFromContent(item.content);
131
+ }
132
+ case 'status':
133
+ return `[${node.attrs?.text ?? ''}]`;
134
+ case 'inlineCard': {
135
+ const attrs = node.attrs ?? {};
136
+ return attrs.url ?? attrs.data?.url ?? attrs.data?.name ?? '';
137
+ }
138
+ case 'heading': {
139
+ const level = node.attrs?.level ?? 1;
140
+ return `${'#'.repeat(level)} ${adfNodeToText(node.content ?? [])}`;
141
+ }
142
+ case 'listItem':
143
+ return `- ${adfNodeToText(node.content ?? [])}`;
144
+ case 'codeBlock': {
145
+ const lang = node.attrs?.language ? normalizeCodeLanguage(node.attrs.language) : '';
146
+ return `\`\`\`${lang}\n${adfNodeToText(node.content ?? [])}\n\`\`\``;
147
+ }
148
+ case 'table': {
149
+ if (!node.content)
150
+ return '';
151
+ const rows = node.content
152
+ .filter((row) => row.type === 'tableRow' && row.content)
153
+ .map((row) => row.content
154
+ .filter((cell) => cell.type === 'tableCell' || cell.type === 'tableHeader')
155
+ .map((cell) => adfNodeToText(cell.content ?? []).replace(/\n/g, ' ').trim()))
156
+ .filter((cells) => cells.length > 0);
157
+ if (rows.length === 0)
158
+ return '';
159
+ const [header, ...data] = rows;
160
+ return [
161
+ '| ' + header.join(' | ') + ' |',
162
+ '|' + header.map(() => '--------').join('|') + '|',
163
+ ...data.map(row => '| ' + row.join(' | ') + ' |'),
164
+ ].join('\n');
165
+ }
166
+ default:
167
+ if (node.content)
168
+ return adfNodeToText(node.content);
169
+ return '';
170
+ }
171
+ }
172
+ /**
173
+ * Extracts plain text from ADF (Atlassian Document Format) body.
174
+ * Follows sooperset's recursive '\n'-joining approach.
175
+ */
176
+ export function extractTextFromADF(body) {
177
+ if (!body)
178
+ return '';
179
+ if (typeof body === 'string')
180
+ return formatWikiMarkup(body);
181
+ if (!body.content)
182
+ return formatWikiMarkup(String(body));
183
+ return formatWikiMarkup(adfNodeToText(body.content).trim());
184
+ }
185
+ /**
186
+ * Converts wiki-style tables (||header||col||) to markdown tables.
187
+ * Only converts blocks that start with || (header row), leaving existing
188
+ * markdown tables untouched.
189
+ */
190
+ function convertWikiTablesToMarkdown(text) {
191
+ const lines = text.split('\n');
192
+ const result = [];
193
+ let i = 0;
194
+ while (i < lines.length) {
195
+ const line = lines[i].trim();
196
+ // Only enter table mode when we see a wiki header row (||…)
197
+ if (line.startsWith('||')) {
198
+ const tableLines = [];
199
+ while (i < lines.length) {
200
+ const tl = lines[i].trim();
201
+ if (tl.startsWith('||') || tl.startsWith('|')) {
202
+ tableLines.push(tl);
203
+ i++;
50
204
  }
51
- }
52
- else if (item.type === 'codeBlock') {
53
- text += '```';
54
- if (item.content) {
55
- extractFromContent(item.content);
205
+ else {
206
+ break;
56
207
  }
57
- text += '```';
58
208
  }
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
- });
209
+ const markdownRows = [];
210
+ let headerDone = false;
211
+ for (const tl of tableLines) {
212
+ if (tl.startsWith('||')) {
213
+ const cells = tl.split('||').map(c => c.trim()).filter(c => c !== '');
214
+ markdownRows.push('| ' + cells.join(' | ') + ' |');
215
+ if (!headerDone) {
216
+ markdownRows.push('| ' + cells.map(() => '---').join(' | ') + ' |');
217
+ headerDone = true;
100
218
  }
101
219
  }
220
+ else {
221
+ const cells = tl.split('|').map(c => c.trim()).filter(c => c !== '');
222
+ if (cells.length > 0)
223
+ markdownRows.push('| ' + cells.join(' | ') + ' |');
224
+ }
102
225
  }
103
- else if (item.content) {
104
- extractFromContent(item.content);
105
- }
226
+ result.push(...markdownRows);
106
227
  }
107
- };
108
- extractFromContent(body.content);
109
- return formatWikiMarkup(text.trim());
228
+ else {
229
+ result.push(lines[i]);
230
+ i++;
231
+ }
232
+ }
233
+ return result.join('\n');
110
234
  }
111
235
  /**
112
- * Formats wiki markup to cleaner text
236
+ * Formats wiki markup to cleaner markdown text.
237
+ *
238
+ * Handles (in order):
239
+ * - {code} / {noformat} blocks → fenced code (extracted as placeholders first
240
+ * so their contents are never touched by later transforms)
241
+ * - Wiki tables ||h|| → markdown tables (pre-pass)
242
+ * - {quote} → > blockquote
243
+ * - {note}/{warning}/{info}/{tip}→ labelled blockquote
244
+ * - [text|url] / [~user] / [url] → markdown links / @mention / bare URL
245
+ * - \\ → newline
246
+ * - # item / ## item → 1. / nested ordered list (wiki list syntax)
247
+ * - h1. … h6. → # … ###### (must come AFTER list conversion)
248
+ * - ---- / {panel} / {color} / checkboxes (existing)
113
249
  */
114
250
  export function formatWikiMarkup(text) {
115
251
  if (!text)
116
252
  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
253
+ // --- Phase 1: extract code/noformat blocks into placeholders so their
254
+ // contents survive all subsequent regex transforms safely ---
255
+ const codeStore = [];
256
+ text = text.replace(/\{code(?::(?:language=)?(\w+))?\}([\s\S]*?)\{code\}/g, (_full, lang, code) => {
257
+ const langStr = lang ? lang.toLowerCase() : '';
258
+ const idx = codeStore.length;
259
+ codeStore.push(`\`\`\`${langStr}\n${code.replace(/^\n/, '').replace(/\n$/, '')}\n\`\`\``);
260
+ return `\x00CODE_${idx}\x00`;
261
+ });
262
+ text = text.replace(/\{noformat(?:[^}]*)?\}([\s\S]*?)\{noformat\}/g, (_full, code) => {
263
+ const idx = codeStore.length;
264
+ codeStore.push(`\`\`\`\n${code.replace(/^\n/, '').replace(/\n$/, '')}\n\`\`\``);
265
+ return `\x00CODE_${idx}\x00`;
266
+ });
267
+ // --- Phase 2: pre-process wiki tables ---
268
+ text = convertWikiTablesToMarkdown(text);
269
+ // --- Phase 3: remaining transforms ---
270
+ text = text
271
+ // {quote} blocks → blockquotes
272
+ .replace(/\{quote\}([\s\S]*?)\{quote\}/g, (_full, content) => content.trim().split('\n').map((l) => `> ${l}`).join('\n'))
273
+ // Notification panels {note:…}…{note} etc.
274
+ .replace(/\{(note|info|tip|warning)(?::[^}]*)?\}([\s\S]*?)\{\1\}/g, (_full, type, content) => {
275
+ const label = type.charAt(0).toUpperCase() + type.slice(1);
276
+ const ls = content.trim().split('\n');
277
+ return `> **[${label}]** ${ls[0]}` +
278
+ (ls.length > 1 ? '\n' + ls.slice(1).map((l) => `> ${l}`).join('\n') : '');
279
+ })
280
+ // Wiki links [text|url] → markdown
281
+ .replace(/\[([^\]|]+)\|([^\]]+)\]/g, '[$1]($2)')
282
+ // User mentions [~username] → @username
283
+ .replace(/\[~([^\]]+)\]/g, '@$1')
284
+ // Bare URL in brackets [https://…] → plain URL
285
+ .replace(/\[([a-z]+:\/\/[^\]]+)\]/g, '$1')
286
+ // Wiki line break \\ → newline
287
+ .replace(/\\\\/g, '\n')
288
+ // Wiki ordered lists (# item, ## nested) — MUST precede header conversion
289
+ // because in wiki markup headings are h1./h2. (never bare #)
290
+ .replace(/^(#+) /gm, (_, hashes) => ' '.repeat(hashes.length - 1) + '1. ')
291
+ // Confluence headers h1.–h6. → markdown #–######
292
+ .replace(/^h([1-6])\.\s*/gm, (_match, level) => '#'.repeat(parseInt(level)) + ' ')
293
+ // Horizontal rules
121
294
  .replace(/^----+$/gm, '---')
122
- // Handle panels - convert to blockquotes for better readability
295
+ // Panels → blockquotes
123
296
  .replace(/\{panel:title=([^|]*)[^}]*\}/g, '\n> **$1**\n>')
124
297
  .replace(/\{panel\}/g, '\n')
125
- // Handle colored text/backgrounds - keep the text but remove styling
298
+ // Colored text — strip styling, keep content
126
299
  .replace(/\{color:[^}]*\}([^{]*)\{color\}/g, '$1')
127
- // Convert checkboxes
300
+ // Checkboxes
128
301
  .replace(/☐/g, '- [ ]')
129
302
  .replace(/☑/g, '- [x]')
130
- // Clean up multiple newlines
303
+ // Tidy up
131
304
  .replace(/\n{3,}/g, '\n\n')
132
- // Ensure proper spacing around headers
133
- .replace(/\n(#{1,6}\s)/g, '\n\n$1')
134
- .trim();
305
+ .replace(/\n(#{1,6}\s)/g, '\n\n$1');
306
+ // --- Phase 4: restore code blocks ---
307
+ codeStore.forEach((code, idx) => {
308
+ text = text.replace(`\x00CODE_${idx}\x00`, code);
309
+ });
310
+ return text.trim();
311
+ }
312
+ /**
313
+ * Detects whether the given text uses Jira/Confluence wiki markup rather than markdown.
314
+ * Returns true if any wiki-specific patterns are found.
315
+ */
316
+ function isWikiMarkup(text) {
317
+ // h1. through h6. headings at line start
318
+ if (/^h[1-6]\.\s/m.test(text))
319
+ return true;
320
+ // {code} or {code:lang} blocks
321
+ if (/\{code[}:]/.test(text))
322
+ return true;
323
+ // [text|url] wiki links — pipe inside brackets is unambiguous wiki syntax
324
+ if (/\[[^\]\|]+\|[^\]]+\]/.test(text))
325
+ return true;
326
+ // ||header|| wiki table headers at line start
327
+ if (/^\|\|/m.test(text))
328
+ return true;
329
+ // {panel blocks
330
+ if (/\{panel/.test(text))
331
+ return true;
332
+ return false;
135
333
  }
136
334
  /**
137
335
  * Parses inline formatting like **bold**, *italic*, `code`, {{code}}
336
+ * When wikiMode is true, single *text* is treated as bold (wiki convention).
337
+ * Also handles [text|url] wiki links and [text](url) markdown links.
138
338
  */
139
- export function parseInlineFormatting(text) {
339
+ export function parseInlineFormatting(text, wikiMode = false) {
140
340
  const result = [];
141
341
  let currentPos = 0;
142
342
  // Process patterns in order of precedence to avoid conflicts
143
343
  // **bold** should be processed before *italic* to handle **text** correctly
144
344
  const patterns = [
145
345
  { regex: /\*\*(.*?)\*\*/g, type: 'strong' }, // **bold** (process first)
346
+ { regex: /~~(.*?)~~/g, type: 'strike' }, // ~~strikethrough~~
146
347
  { regex: /`([^`]+?)`/g, type: 'code' }, // `code` (markdown style)
147
348
  { regex: /\{\{(.+?)\}\}/g, type: 'code' }, // {{code}} (Confluence/Jira wiki style)
148
349
  ];
149
350
  let matches = [];
150
- // Find all formatting matches
351
+ // Process links FIRST to avoid conflicts with brackets and formatting markers
352
+ // Wiki links: [text|url]
353
+ const wikiLinkRegex = /\[([^\]\|]+)\|([^\]]+)\]/g;
354
+ let wikiLinkMatch;
355
+ while ((wikiLinkMatch = wikiLinkRegex.exec(text)) !== null) {
356
+ matches.push({
357
+ start: wikiLinkMatch.index,
358
+ end: wikiLinkMatch.index + wikiLinkMatch[0].length,
359
+ text: wikiLinkMatch[1],
360
+ type: 'link',
361
+ href: wikiLinkMatch[2],
362
+ fullMatch: wikiLinkMatch[0]
363
+ });
364
+ }
365
+ // Markdown links: [text](url)
366
+ const mdLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
367
+ let mdLinkMatch;
368
+ while ((mdLinkMatch = mdLinkRegex.exec(text)) !== null) {
369
+ // Only add if not overlapping with an already-found wiki link
370
+ const overlaps = matches.some(existing => (mdLinkMatch.index >= existing.start && mdLinkMatch.index < existing.end) ||
371
+ (mdLinkMatch.index + mdLinkMatch[0].length > existing.start && mdLinkMatch.index + mdLinkMatch[0].length <= existing.end));
372
+ if (!overlaps) {
373
+ matches.push({
374
+ start: mdLinkMatch.index,
375
+ end: mdLinkMatch.index + mdLinkMatch[0].length,
376
+ text: mdLinkMatch[1],
377
+ type: 'link',
378
+ href: mdLinkMatch[2],
379
+ fullMatch: mdLinkMatch[0]
380
+ });
381
+ }
382
+ }
383
+ // Find all formatting matches (skip regions already claimed by links)
151
384
  patterns.forEach(pattern => {
152
385
  let match;
153
386
  const regex = new RegExp(pattern.regex.source, 'g');
154
387
  while ((match = regex.exec(text)) !== null) {
155
- matches.push({
156
- start: match.index,
157
- end: match.index + match[0].length,
158
- text: match[1],
159
- type: pattern.type,
160
- fullMatch: match[0]
161
- });
388
+ const overlapsLink = matches.some(existing => (match.index >= existing.start && match.index < existing.end) ||
389
+ (match.index + match[0].length > existing.start && match.index + match[0].length <= existing.end) ||
390
+ (match.index <= existing.start && match.index + match[0].length >= existing.end));
391
+ if (!overlapsLink) {
392
+ matches.push({
393
+ start: match.index,
394
+ end: match.index + match[0].length,
395
+ text: match[1],
396
+ type: pattern.type,
397
+ fullMatch: match[0]
398
+ });
399
+ }
162
400
  }
163
401
  });
164
402
  // Sort matches by position and remove overlapping matches
@@ -169,16 +407,17 @@ export function parseInlineFormatting(text) {
169
407
  matches.forEach(match => {
170
408
  tempText = tempText.substring(0, match.start) + ' '.repeat(match.end - match.start) + tempText.substring(match.end);
171
409
  });
172
- // Now find italic patterns in the remaining text
173
- const italicRegex = /\*([^*]+?)\*/g;
174
- let italicMatch;
175
- while ((italicMatch = italicRegex.exec(tempText)) !== null) {
410
+ // Now find single *text* patterns in the remaining text
411
+ // In wiki mode: *text* = bold (strong). In markdown mode: *text* = italic (em).
412
+ const singleStarRegex = /\*([^*]+?)\*/g;
413
+ let singleStarMatch;
414
+ while ((singleStarMatch = singleStarRegex.exec(tempText)) !== null) {
176
415
  matches.push({
177
- start: italicMatch.index,
178
- end: italicMatch.index + italicMatch[0].length,
179
- text: italicMatch[1],
180
- type: 'em',
181
- fullMatch: italicMatch[0]
416
+ start: singleStarMatch.index,
417
+ end: singleStarMatch.index + singleStarMatch[0].length,
418
+ text: singleStarMatch[1],
419
+ type: wikiMode ? 'strong' : 'em',
420
+ fullMatch: singleStarMatch[0]
182
421
  });
183
422
  }
184
423
  // Sort matches by position and remove overlapping matches
@@ -208,11 +447,20 @@ export function parseInlineFormatting(text) {
208
447
  }
209
448
  }
210
449
  // Add formatted text
211
- result.push({
212
- type: 'text',
213
- text: match.text,
214
- marks: [{ type: match.type }]
215
- });
450
+ if (match.type === 'link') {
451
+ result.push({
452
+ type: 'text',
453
+ text: match.text,
454
+ marks: [{ type: 'link', attrs: { href: match.href } }]
455
+ });
456
+ }
457
+ else {
458
+ result.push({
459
+ type: 'text',
460
+ text: match.text,
461
+ marks: [{ type: match.type }]
462
+ });
463
+ }
216
464
  currentPos = match.end;
217
465
  });
218
466
  // Add remaining text
@@ -235,10 +483,28 @@ export function parseInlineFormatting(text) {
235
483
  return result;
236
484
  }
237
485
  /**
238
- * Enhanced description parser that converts text to proper ADF format
486
+ * Enhanced description parser that converts text to proper ADF format.
487
+ * Auto-detects wiki markup vs markdown and adjusts parsing accordingly.
239
488
  */
240
489
  export function parseDescriptionToADF(description) {
241
- const lines = description.split('\n');
490
+ const wikiMode = isWikiMarkup(description);
491
+ // Extract {code}...{code} blocks before line-by-line processing (wiki mode only)
492
+ const codeBlockStore = [];
493
+ let processedDesc = description;
494
+ if (wikiMode) {
495
+ processedDesc = description.replace(/\{code(?::([^}]*))?\}([\s\S]*?)\{code\}/g, (_full, params, code) => {
496
+ const idx = codeBlockStore.length;
497
+ let lang = '';
498
+ if (params) {
499
+ const langMatch = params.match(/(?:language=)?(\w+)/);
500
+ if (langMatch)
501
+ lang = normalizeCodeLanguage(langMatch[1]);
502
+ }
503
+ codeBlockStore.push({ lang, code: code.replace(/^\n/, '').replace(/\n$/, '') });
504
+ return `\x00WIKICODE_${idx}\x00`;
505
+ });
506
+ }
507
+ const lines = processedDesc.split('\n');
242
508
  const content = [];
243
509
  for (let i = 0; i < lines.length; i++) {
244
510
  const line = lines[i].trim();
@@ -246,6 +512,17 @@ export function parseDescriptionToADF(description) {
246
512
  // Skip empty lines - let Claude handle spacing naturally
247
513
  continue;
248
514
  }
515
+ // Restore wiki {code} block placeholders
516
+ const wikiCodeMatch = line.match(/^\x00WIKICODE_(\d+)\x00$/);
517
+ if (wikiCodeMatch) {
518
+ const { lang, code } = codeBlockStore[parseInt(wikiCodeMatch[1])];
519
+ content.push({
520
+ type: 'codeBlock',
521
+ attrs: lang ? { language: lang } : {},
522
+ content: [{ type: 'text', text: code }]
523
+ });
524
+ continue;
525
+ }
249
526
  // Handle Confluence wiki markup headers (h1., h2., h3., etc.)
250
527
  const confluenceHeaderMatch = line.match(/^h([1-6])\.\s*(.*)$/);
251
528
  if (confluenceHeaderMatch) {
@@ -254,42 +531,51 @@ export function parseDescriptionToADF(description) {
254
531
  content.push({
255
532
  type: 'heading',
256
533
  attrs: { level },
257
- content: [{
258
- type: 'text',
259
- text: headerText
260
- }]
534
+ content: parseInlineFormatting(headerText, wikiMode)
261
535
  });
262
536
  continue;
263
537
  }
538
+ // In wiki mode, # at line start = ordered list (not heading)
539
+ if (wikiMode && /^#+\s/.test(line)) {
540
+ const listText = line.replace(/^#+\s*/, '');
541
+ const lastItem = content[content.length - 1];
542
+ if (lastItem && lastItem.type === 'orderedList') {
543
+ lastItem.content.push({
544
+ type: 'listItem',
545
+ content: [{ type: 'paragraph', content: parseInlineFormatting(listText, wikiMode) }]
546
+ });
547
+ }
548
+ else {
549
+ content.push({
550
+ type: 'orderedList',
551
+ content: [{
552
+ type: 'listItem',
553
+ content: [{ type: 'paragraph', content: parseInlineFormatting(listText, wikiMode) }]
554
+ }]
555
+ });
556
+ }
557
+ continue;
558
+ }
264
559
  // Handle markdown headers (## Header, ### Header)
265
560
  if (line.startsWith('###')) {
266
561
  content.push({
267
562
  type: 'heading',
268
563
  attrs: { level: 3 },
269
- content: [{
270
- type: 'text',
271
- text: line.substring(3).trim()
272
- }]
564
+ content: parseInlineFormatting(line.substring(3).trim(), wikiMode)
273
565
  });
274
566
  }
275
567
  else if (line.startsWith('##')) {
276
568
  content.push({
277
569
  type: 'heading',
278
570
  attrs: { level: 2 },
279
- content: [{
280
- type: 'text',
281
- text: line.substring(2).trim()
282
- }]
571
+ content: parseInlineFormatting(line.substring(2).trim(), wikiMode)
283
572
  });
284
573
  }
285
574
  else if (line.startsWith('#')) {
286
575
  content.push({
287
576
  type: 'heading',
288
577
  attrs: { level: 1 },
289
- content: [{
290
- type: 'text',
291
- text: line.substring(1).trim()
292
- }]
578
+ content: parseInlineFormatting(line.substring(1).trim(), wikiMode)
293
579
  });
294
580
  }
295
581
  // Handle horizontal rules (---- or ----)
@@ -312,7 +598,7 @@ export function parseDescriptionToADF(description) {
312
598
  if (panelLine) {
313
599
  panelContent.push({
314
600
  type: 'paragraph',
315
- content: parseInlineFormatting(panelLine)
601
+ content: parseInlineFormatting(panelLine, wikiMode)
316
602
  });
317
603
  }
318
604
  i++;
@@ -345,7 +631,7 @@ export function parseDescriptionToADF(description) {
345
631
  type: 'listItem',
346
632
  content: [{
347
633
  type: 'paragraph',
348
- content: parseInlineFormatting(bulletText)
634
+ content: parseInlineFormatting(bulletText, wikiMode)
349
635
  }]
350
636
  });
351
637
  }
@@ -356,7 +642,7 @@ export function parseDescriptionToADF(description) {
356
642
  type: 'listItem',
357
643
  content: [{
358
644
  type: 'paragraph',
359
- content: parseInlineFormatting(bulletText)
645
+ content: parseInlineFormatting(bulletText, wikiMode)
360
646
  }]
361
647
  }]
362
648
  });
@@ -373,7 +659,7 @@ export function parseDescriptionToADF(description) {
373
659
  type: 'listItem',
374
660
  content: [{
375
661
  type: 'paragraph',
376
- content: parseInlineFormatting(listText)
662
+ content: parseInlineFormatting(listText, wikiMode)
377
663
  }]
378
664
  });
379
665
  }
@@ -384,7 +670,7 @@ export function parseDescriptionToADF(description) {
384
670
  type: 'listItem',
385
671
  content: [{
386
672
  type: 'paragraph',
387
- content: parseInlineFormatting(listText)
673
+ content: parseInlineFormatting(listText, wikiMode)
388
674
  }]
389
675
  }]
390
676
  });
@@ -393,19 +679,67 @@ export function parseDescriptionToADF(description) {
393
679
  }
394
680
  // Handle code blocks (```)
395
681
  else if (line.startsWith('```')) {
682
+ const rawLang = line.substring(3).trim();
683
+ const lang = normalizeCodeLanguage(rawLang);
396
684
  const codeLines = [];
397
685
  i++; // Skip the opening ```
398
686
  while (i < lines.length && !lines[i].trim().startsWith('```')) {
399
687
  codeLines.push(lines[i]);
400
688
  i++;
401
689
  }
402
- content.push({
690
+ const codeBlock = {
403
691
  type: 'codeBlock',
692
+ attrs: lang ? { language: lang } : {},
404
693
  content: [{
405
694
  type: 'text',
406
695
  text: codeLines.join('\n')
407
696
  }]
408
- });
697
+ };
698
+ content.push(codeBlock);
699
+ }
700
+ // Handle wiki tables (||header|| for headers, |cell| for data rows)
701
+ else if (wikiMode && line.startsWith('||')) {
702
+ const tableRows = [];
703
+ let tableIdx = i;
704
+ while (tableIdx < lines.length) {
705
+ const tableLine = lines[tableIdx].trim();
706
+ if (!tableLine || (!tableLine.startsWith('||') && !tableLine.startsWith('|')))
707
+ break;
708
+ if (tableLine.startsWith('||')) {
709
+ // Header row - split on || and filter empties
710
+ const cells = tableLine.split('||').filter(c => c !== '');
711
+ const headerCells = cells.map(cellText => ({
712
+ type: 'tableHeader',
713
+ attrs: {},
714
+ content: [{ type: 'paragraph', content: parseInlineFormatting(cellText.trim(), wikiMode) }]
715
+ }));
716
+ tableRows.push({ type: 'tableRow', content: headerCells });
717
+ }
718
+ else if (tableLine.startsWith('|')) {
719
+ // Data row - split on | and filter empties
720
+ const cells = tableLine.split('|').filter(c => c !== '');
721
+ // Skip separator rows like |-|-|
722
+ if (!tableLine.match(/^\|[\s\-:|]+\|?$/)) {
723
+ const dataCells = cells.map(cellText => ({
724
+ type: 'tableCell',
725
+ attrs: {},
726
+ content: [{ type: 'paragraph', content: parseInlineFormatting(cellText.trim(), wikiMode) }]
727
+ }));
728
+ tableRows.push({ type: 'tableRow', content: dataCells });
729
+ }
730
+ }
731
+ tableIdx++;
732
+ }
733
+ if (tableRows.length > 0) {
734
+ applyColWidths(tableRows);
735
+ content.push({
736
+ type: 'table',
737
+ attrs: { isNumberColumnEnabled: false, layout: 'default' },
738
+ content: tableRows
739
+ });
740
+ i = tableIdx - 1; // -1 because the for loop will increment
741
+ continue;
742
+ }
409
743
  }
410
744
  // Handle markdown tables - convert to proper ADF table structure
411
745
  else if (line.includes('|') && line.trim() !== '|') {
@@ -454,7 +788,7 @@ export function parseDescriptionToADF(description) {
454
788
  attrs: {},
455
789
  content: [{
456
790
  type: 'paragraph',
457
- content: parseInlineFormatting(cellText)
791
+ content: parseInlineFormatting(cellText, wikiMode)
458
792
  }]
459
793
  }));
460
794
  tableRows.push({
@@ -469,7 +803,7 @@ export function parseDescriptionToADF(description) {
469
803
  attrs: {},
470
804
  content: [{
471
805
  type: 'paragraph',
472
- content: parseInlineFormatting(cellText)
806
+ content: parseInlineFormatting(cellText, wikiMode)
473
807
  }]
474
808
  }));
475
809
  tableRows.push({
@@ -477,6 +811,7 @@ export function parseDescriptionToADF(description) {
477
811
  content: dataCells
478
812
  });
479
813
  }
814
+ applyColWidths(tableRows);
480
815
  // Create the complete table structure
481
816
  content.push({
482
817
  type: 'table',
@@ -494,14 +829,14 @@ export function parseDescriptionToADF(description) {
494
829
  // If we reach here, it's not a valid table, treat as regular paragraph
495
830
  content.push({
496
831
  type: 'paragraph',
497
- content: parseInlineFormatting(line)
832
+ content: parseInlineFormatting(line, wikiMode)
498
833
  });
499
834
  }
500
835
  // Regular paragraph
501
836
  else {
502
837
  content.push({
503
838
  type: 'paragraph',
504
- content: parseInlineFormatting(line)
839
+ content: parseInlineFormatting(line, wikiMode)
505
840
  });
506
841
  }
507
842
  }
@@ -525,14 +860,43 @@ export function parseDescriptionToADF(description) {
525
860
  * Convert description to wiki markup text format
526
861
  */
527
862
  export function parseDescriptionToWikiMarkup(description) {
528
- const lines = description.split('\n');
863
+ // Extract code blocks before any line-by-line processing so their
864
+ // contents are never transformed by heading/table/list logic.
865
+ const codeBlockStore = [];
866
+ const protected_ = description.replace(/```(\w*)\n?([\s\S]*?)```/g, (_full, lang, code) => {
867
+ const idx = codeBlockStore.length;
868
+ const normalizedLang = normalizeCodeLanguage(lang);
869
+ const langSuffix = normalizedLang ? `:language=${normalizedLang}` : '';
870
+ codeBlockStore.push(`{code${langSuffix}}\n${code.replace(/^\n/, '').replace(/\n$/, '')}\n{code}`);
871
+ return `\x00CODEBLOCK_${idx}\x00`;
872
+ });
873
+ const lines = protected_.split('\n');
529
874
  const result = [];
530
875
  for (let i = 0; i < lines.length; i++) {
531
876
  const line = lines[i].trim();
877
+ // Restore code blocks
878
+ if (line.match(/^\x00CODEBLOCK_(\d+)\x00$/)) {
879
+ const idx = parseInt(line.match(/\x00CODEBLOCK_(\d+)\x00/)[1]);
880
+ result.push(codeBlockStore[idx]);
881
+ continue;
882
+ }
532
883
  if (line === '') {
533
884
  result.push('');
534
885
  continue;
535
886
  }
887
+ // Handle blockquotes (> text) — collect consecutive lines and wrap in {quote}
888
+ if (line.startsWith('> ')) {
889
+ const quoteLines = [];
890
+ while (i < lines.length && lines[i].trim().startsWith('> ')) {
891
+ quoteLines.push(lines[i].trim().substring(2).trim());
892
+ i++;
893
+ }
894
+ i--; // back up — outer loop will increment
895
+ result.push('{quote}');
896
+ result.push(...quoteLines);
897
+ result.push('{quote}');
898
+ continue;
899
+ }
536
900
  // Handle markdown headers
537
901
  if (line.startsWith('###')) {
538
902
  result.push('h3. ' + line.substring(3).trim());
@@ -606,4 +970,175 @@ export function parseDescriptionToWikiMarkup(description) {
606
970
  }
607
971
  return result.join('\n');
608
972
  }
973
+ // ---------------------------------------------------------------------------
974
+ // Confluence Storage Format Converter
975
+ // ---------------------------------------------------------------------------
976
+ /**
977
+ * Escapes special HTML characters for use inside storage-format text nodes.
978
+ */
979
+ function escapeStorageHtml(text) {
980
+ return text
981
+ .replace(/&/g, '&amp;')
982
+ .replace(/</g, '&lt;')
983
+ .replace(/>/g, '&gt;');
984
+ }
985
+ /**
986
+ * Applies inline markdown formatting and converts it to Confluence storage
987
+ * format HTML (strong, em, strike, code, links).
988
+ * Code spans are protected from further transformation.
989
+ */
990
+ function applyInlineStorageFormat(text) {
991
+ // Protect inline code spans first
992
+ const spans = [];
993
+ let out = text.replace(/`([^`]+)`/g, (_full, code) => {
994
+ const idx = spans.length;
995
+ spans.push(`<code>${escapeStorageHtml(code)}</code>`);
996
+ return `\x00SPAN_${idx}\x00`;
997
+ });
998
+ out = out
999
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
1000
+ .replace(/~~(.*?)~~/g, '<s>$1</s>')
1001
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
1002
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
1003
+ // Restore code spans
1004
+ spans.forEach((span, idx) => {
1005
+ out = out.replace(`\x00SPAN_${idx}\x00`, span);
1006
+ });
1007
+ return out;
1008
+ }
1009
+ /**
1010
+ * Returns true when the string already contains Confluence storage-format
1011
+ * markup so that it can be passed through as-is.
1012
+ */
1013
+ function isAlreadyStorageFormat(content) {
1014
+ const trimmed = content.trim();
1015
+ return (trimmed.startsWith('<') &&
1016
+ (trimmed.includes('<ac:') ||
1017
+ trimmed.includes('<ri:') ||
1018
+ /^<(p|h[1-6]|ul|ol|table|blockquote|hr)\b/i.test(trimmed)));
1019
+ }
1020
+ /**
1021
+ * Converts LLM-produced markdown to Confluence storage format (XHTML).
1022
+ * If the input already looks like storage format it is returned unchanged.
1023
+ *
1024
+ * Supported elements:
1025
+ * - ATX headings (#–######)
1026
+ * - Fenced code blocks (``` lang … ```) → ac:structured-macro "code"
1027
+ * - Blockquotes (> …)
1028
+ * - Unordered lists (- / * / +)
1029
+ * - Ordered lists (1. …)
1030
+ * - Markdown tables
1031
+ * - Horizontal rules (---, ***, ___)
1032
+ * - Inline: **bold**, *italic*, ~~strike~~, `code`, [text](url)
1033
+ */
1034
+ export function markdownToConfluenceStorage(markdown) {
1035
+ if (isAlreadyStorageFormat(markdown)) {
1036
+ return markdown;
1037
+ }
1038
+ // --- Phase 1: extract fenced code blocks into placeholders ---
1039
+ const codeBlocks = [];
1040
+ const withoutCode = markdown.replace(/```(\w*)\n?([\s\S]*?)```/g, (_full, rawLang, code) => {
1041
+ const idx = codeBlocks.length;
1042
+ const lang = normalizeCodeLanguage(rawLang);
1043
+ const langParam = lang
1044
+ ? `<ac:parameter ac:name="language">${lang}</ac:parameter>`
1045
+ : '';
1046
+ const body = code.replace(/^\n/, '').replace(/\n$/, '');
1047
+ codeBlocks.push(`<ac:structured-macro ac:name="code">${langParam}` +
1048
+ `<ac:plain-text-body><![CDATA[${body}]]></ac:plain-text-body>` +
1049
+ `</ac:structured-macro>`);
1050
+ return `\x00CODE_${idx}\x00`;
1051
+ });
1052
+ // --- Phase 2: line-by-line conversion ---
1053
+ const lines = withoutCode.split('\n');
1054
+ const result = [];
1055
+ let i = 0;
1056
+ while (i < lines.length) {
1057
+ const raw = lines[i];
1058
+ const line = raw.trim();
1059
+ // Code block placeholder
1060
+ const codeMatch = line.match(/^\x00CODE_(\d+)\x00$/);
1061
+ if (codeMatch) {
1062
+ result.push(codeBlocks[parseInt(codeMatch[1])]);
1063
+ i++;
1064
+ continue;
1065
+ }
1066
+ // Heading
1067
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
1068
+ if (headingMatch) {
1069
+ const level = headingMatch[1].length;
1070
+ result.push(`<h${level}>${applyInlineStorageFormat(headingMatch[2])}</h${level}>`);
1071
+ i++;
1072
+ continue;
1073
+ }
1074
+ // Horizontal rule
1075
+ if (/^([-*_])\1{2,}$/.test(line)) {
1076
+ result.push('<hr/>');
1077
+ i++;
1078
+ continue;
1079
+ }
1080
+ // Blockquote
1081
+ if (line.startsWith('> ')) {
1082
+ result.push(`<blockquote><p>${applyInlineStorageFormat(line.substring(2))}</p></blockquote>`);
1083
+ i++;
1084
+ continue;
1085
+ }
1086
+ // Table — collect all consecutive table lines
1087
+ if (line.startsWith('|')) {
1088
+ const tableRows = [];
1089
+ while (i < lines.length && lines[i].trim().startsWith('|')) {
1090
+ const row = lines[i].trim();
1091
+ // Skip separator lines (e.g. |---|---|)
1092
+ if (!/^\|[\s\-:|]+\|?$/.test(row)) {
1093
+ const cells = row.split('|').map(c => c.trim()).filter(c => c !== '');
1094
+ if (cells.length > 0)
1095
+ tableRows.push(cells);
1096
+ }
1097
+ i++;
1098
+ }
1099
+ if (tableRows.length > 0) {
1100
+ let html = '<table data-layout="default" data-table-width="760">' + buildColGroupHtml(tableRows) + '<tbody>';
1101
+ html += '<tr>' + tableRows[0].map(c => `<th>${applyInlineStorageFormat(c)}</th>`).join('') + '</tr>';
1102
+ for (let r = 1; r < tableRows.length; r++) {
1103
+ html += '<tr>' + tableRows[r].map(c => `<td>${applyInlineStorageFormat(c)}</td>`).join('') + '</tr>';
1104
+ }
1105
+ html += '</tbody></table>';
1106
+ result.push(html);
1107
+ }
1108
+ continue;
1109
+ }
1110
+ // Unordered list — collect consecutive items
1111
+ if (/^[-*+]\s/.test(line)) {
1112
+ let html = '<ul>';
1113
+ while (i < lines.length && /^[-*+]\s/.test(lines[i].trim())) {
1114
+ html += `<li>${applyInlineStorageFormat(lines[i].trim().substring(2).trim())}</li>`;
1115
+ i++;
1116
+ }
1117
+ html += '</ul>';
1118
+ result.push(html);
1119
+ continue;
1120
+ }
1121
+ // Ordered list — collect consecutive items
1122
+ if (/^\d+\.\s/.test(line)) {
1123
+ let html = '<ol>';
1124
+ while (i < lines.length && /^\d+\.\s/.test(lines[i].trim())) {
1125
+ const text = lines[i].trim().replace(/^\d+\.\s/, '');
1126
+ html += `<li>${applyInlineStorageFormat(text)}</li>`;
1127
+ i++;
1128
+ }
1129
+ html += '</ol>';
1130
+ result.push(html);
1131
+ continue;
1132
+ }
1133
+ // Empty line — skip (Confluence doesn't need explicit paragraph breaks between block elements)
1134
+ if (line === '') {
1135
+ i++;
1136
+ continue;
1137
+ }
1138
+ // Regular paragraph
1139
+ result.push(`<p>${applyInlineStorageFormat(line)}</p>`);
1140
+ i++;
1141
+ }
1142
+ return result.join('\n');
1143
+ }
609
1144
  //# sourceMappingURL=adf-formatter.js.map