@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.
- package/.env.example +6 -1
- package/README.md +26 -14
- package/dist/config.d.ts +2 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +0 -4
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +0 -5
- package/dist/constants.js.map +1 -1
- package/dist/formatters/adf-formatter.d.ts +35 -4
- package/dist/formatters/adf-formatter.d.ts.map +1 -1
- package/dist/formatters/adf-formatter.js +691 -156
- package/dist/formatters/adf-formatter.js.map +1 -1
- package/dist/formatters/adf-to-text.d.ts +9 -0
- package/dist/formatters/adf-to-text.d.ts.map +1 -0
- package/dist/formatters/adf-to-text.js +25 -0
- package/dist/formatters/adf-to-text.js.map +1 -0
- package/dist/formatters/index.d.ts +7 -1
- package/dist/formatters/index.d.ts.map +1 -1
- package/dist/formatters/index.js +7 -1
- package/dist/formatters/index.js.map +1 -1
- package/dist/formatters/markdown-to-adf.d.ts +6 -0
- package/dist/formatters/markdown-to-adf.d.ts.map +1 -0
- package/dist/formatters/markdown-to-adf.js +27 -0
- package/dist/formatters/markdown-to-adf.js.map +1 -0
- 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 +9 -1
- package/dist/handlers/bitbucket-handlers.d.ts.map +1 -1
- package/dist/handlers/bitbucket-handlers.js +51 -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 +123 -194
- 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 +7 -0
- package/dist/services/bitbucket.d.ts.map +1 -1
- package/dist/services/bitbucket.js +36 -74
- package/dist/services/bitbucket.js.map +1 -1
- package/dist/services/confluence.d.ts +52 -10
- package/dist/services/confluence.d.ts.map +1 -1
- package/dist/services/confluence.js +247 -311
- 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 +8 -0
- package/dist/services/jira/base.d.ts.map +1 -0
- package/dist/services/jira/base.js +10 -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/snapshot/decorators/bitbucket-snapshots.d.ts +4 -0
- package/dist/snapshot/decorators/bitbucket-snapshots.d.ts.map +1 -0
- package/dist/snapshot/decorators/bitbucket-snapshots.js +65 -0
- package/dist/snapshot/decorators/bitbucket-snapshots.js.map +1 -0
- package/dist/snapshot/decorators/confluence-snapshots.d.ts +4 -0
- package/dist/snapshot/decorators/confluence-snapshots.d.ts.map +1 -0
- package/dist/snapshot/decorators/confluence-snapshots.js +71 -0
- package/dist/snapshot/decorators/confluence-snapshots.js.map +1 -0
- package/dist/snapshot/decorators/jira-snapshots.d.ts +4 -0
- package/dist/snapshot/decorators/jira-snapshots.d.ts.map +1 -0
- package/dist/snapshot/decorators/jira-snapshots.js +171 -0
- package/dist/snapshot/decorators/jira-snapshots.js.map +1 -0
- package/dist/snapshot/index.d.ts +7 -0
- package/dist/snapshot/index.d.ts.map +1 -0
- package/dist/snapshot/index.js +7 -0
- package/dist/snapshot/index.js.map +1 -0
- package/dist/snapshot/snapshot-decorator.d.ts +4 -0
- package/dist/snapshot/snapshot-decorator.d.ts.map +1 -0
- package/dist/snapshot/snapshot-decorator.js +74 -0
- package/dist/snapshot/snapshot-decorator.js.map +1 -0
- package/dist/snapshot/snapshot-manager.d.ts +9 -0
- package/dist/snapshot/snapshot-manager.d.ts.map +1 -0
- package/dist/snapshot/snapshot-manager.js +56 -0
- package/dist/snapshot/snapshot-manager.js.map +1 -0
- package/dist/snapshot/snapshot-types.d.ts +26 -0
- package/dist/snapshot/snapshot-types.d.ts.map +1 -0
- package/dist/snapshot/snapshot-types.js +2 -0
- package/dist/snapshot/snapshot-types.js.map +1 -0
- package/dist/tools/bitbucket-tools.d.ts.map +1 -1
- package/dist/tools/bitbucket-tools.js +30 -0
- package/dist/tools/bitbucket-tools.js.map +1 -1
- package/dist/tools/confluence-tools.js +12 -12
- package/dist/tools/confluence-tools.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/dist/types.d.ts +17 -2
- package/dist/types.d.ts.map +1 -1
- 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
|
-
*
|
|
6
|
+
* Jira/Confluence ADF supported languages mapped from common LLM output aliases
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
extractFromContent(item.content);
|
|
44
|
-
}
|
|
128
|
+
catch {
|
|
129
|
+
return String(timestamp);
|
|
45
130
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
text += '```';
|
|
54
|
-
if (item.content) {
|
|
55
|
-
extractFromContent(item.content);
|
|
205
|
+
else {
|
|
206
|
+
break;
|
|
56
207
|
}
|
|
57
|
-
text += '```';
|
|
58
208
|
}
|
|
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
|
-
});
|
|
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
|
-
|
|
104
|
-
extractFromContent(item.content);
|
|
105
|
-
}
|
|
226
|
+
result.push(...markdownRows);
|
|
106
227
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
//
|
|
295
|
+
// Panels → blockquotes
|
|
123
296
|
.replace(/\{panel:title=([^|]*)[^}]*\}/g, '\n> **$1**\n>')
|
|
124
297
|
.replace(/\{panel\}/g, '\n')
|
|
125
|
-
//
|
|
298
|
+
// Colored text — strip styling, keep content
|
|
126
299
|
.replace(/\{color:[^}]*\}([^{]*)\{color\}/g, '$1')
|
|
127
|
-
//
|
|
300
|
+
// Checkboxes
|
|
128
301
|
.replace(/☐/g, '- [ ]')
|
|
129
302
|
.replace(/☑/g, '- [x]')
|
|
130
|
-
//
|
|
303
|
+
// Tidy up
|
|
131
304
|
.replace(/\n{3,}/g, '\n\n')
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
//
|
|
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.
|
|
156
|
-
start
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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:
|
|
178
|
-
end:
|
|
179
|
-
text:
|
|
180
|
-
type: 'em',
|
|
181
|
-
fullMatch:
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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, '&')
|
|
982
|
+
.replace(/</g, '<')
|
|
983
|
+
.replace(/>/g, '>');
|
|
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
|