confluence-cli 1.31.1 → 1.32.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.
@@ -0,0 +1,150 @@
1
+ const NAMED_ENTITIES = {
2
+ aring: 'å', auml: 'ä', ouml: 'ö',
3
+ eacute: 'é', egrave: 'è', ecirc: 'ê', euml: 'ë',
4
+ aacute: 'á', agrave: 'à', acirc: 'â', atilde: 'ã',
5
+ oacute: 'ó', ograve: 'ò', ocirc: 'ô', otilde: 'õ',
6
+ uacute: 'ú', ugrave: 'ù', ucirc: 'û', uuml: 'ü',
7
+ iacute: 'í', igrave: 'ì', icirc: 'î', iuml: 'ï',
8
+ ntilde: 'ñ', ccedil: 'ç', szlig: 'ß', yuml: 'ÿ',
9
+ eth: 'ð', thorn: 'þ',
10
+ Aring: 'Å', Auml: 'Ä', Ouml: 'Ö',
11
+ Eacute: 'É', Egrave: 'È', Ecirc: 'Ê', Euml: 'Ë',
12
+ Aacute: 'Á', Agrave: 'À', Acirc: 'Â', Atilde: 'Ã',
13
+ Oacute: 'Ó', Ograve: 'Ò', Ocirc: 'Ô', Otilde: 'Õ',
14
+ Uacute: 'Ú', Ugrave: 'Ù', Ucirc: 'Û', Uuml: 'Ü',
15
+ Iacute: 'Í', Igrave: 'Ì', Icirc: 'Î', Iuml: 'Ï',
16
+ Ntilde: 'Ñ', Ccedil: 'Ç', Szlig: 'SS', Yuml: 'Ÿ',
17
+ Eth: 'Ð', Thorn: 'Þ'
18
+ };
19
+
20
+ function htmlToMarkdown(html) {
21
+ let markdown = html;
22
+
23
+ markdown = markdown.replace(/<time\s+datetime="([^"]+)"[^>]*(?:\/>|>\s*<\/time>)/g, '$1');
24
+
25
+ markdown = markdown.replace(/<strong[^>]*>(.*?)<\/strong>/g, '**$1**');
26
+
27
+ markdown = markdown.replace(/<em[^>]*>(.*?)<\/em>/g, '*$1*');
28
+
29
+ markdown = markdown.replace(/<code[^>]*>(.*?)<\/code>/g, '`$1`');
30
+
31
+ markdown = markdown.replace(/<(\w+)[^>]*>/g, '<$1>');
32
+ markdown = markdown.replace(/<\/(\w+)[^>]*>/g, '</$1>');
33
+
34
+ markdown = markdown.replace(/<h([1-6])>(.*?)<\/h[1-6]>/g, (_, level, text) => {
35
+ return '\n' + '#'.repeat(parseInt(level)) + ' ' + text.trim() + '\n';
36
+ });
37
+
38
+ markdown = markdown.replace(/<table>(.*?)<\/table>/gs, (_, content) => {
39
+ const rows = [];
40
+ let isHeader = true;
41
+
42
+ const rowMatches = content.match(/<tr>(.*?)<\/tr>/gs);
43
+ if (rowMatches) {
44
+ rowMatches.forEach(rowMatch => {
45
+ const cells = [];
46
+ const cellContent = rowMatch.replace(/<tr>(.*?)<\/tr>/s, '$1');
47
+
48
+ const cellMatches = cellContent.match(/<t[hd]>(.*?)<\/t[hd]>/gs);
49
+ if (cellMatches) {
50
+ cellMatches.forEach(cellMatch => {
51
+ let cellText = cellMatch.replace(/<t[hd]>(.*?)<\/t[hd]>/s, '$1');
52
+ cellText = cellText.replace(/<p>/g, '').replace(/<\/p>/g, ' ');
53
+ cellText = cellText.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
54
+ cells.push(cellText || ' ');
55
+ });
56
+ }
57
+
58
+ if (cells.length > 0) {
59
+ rows.push('| ' + cells.join(' | ') + ' |');
60
+
61
+ if (isHeader) {
62
+ rows.push('| ' + cells.map(() => '---').join(' | ') + ' |');
63
+ isHeader = false;
64
+ }
65
+ }
66
+ });
67
+ }
68
+
69
+ return rows.length > 0 ? '\n' + rows.join('\n') + '\n' : '';
70
+ });
71
+
72
+ markdown = markdown.replace(/<ul>(.*?)<\/ul>/gs, (_, content) => {
73
+ let listItems = '';
74
+ const itemMatches = content.match(/<li>(.*?)<\/li>/gs);
75
+ if (itemMatches) {
76
+ itemMatches.forEach(itemMatch => {
77
+ let itemText = itemMatch.replace(/<li>(.*?)<\/li>/s, '$1');
78
+ itemText = itemText.replace(/<p>/g, '').replace(/<\/p>/g, ' ');
79
+ itemText = itemText.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
80
+ if (itemText) {
81
+ listItems += '- ' + itemText + '\n';
82
+ }
83
+ });
84
+ }
85
+ return '\n' + listItems;
86
+ });
87
+
88
+ markdown = markdown.replace(/<ol>(.*?)<\/ol>/gs, (_, content) => {
89
+ let listItems = '';
90
+ let counter = 1;
91
+ const itemMatches = content.match(/<li>(.*?)<\/li>/gs);
92
+ if (itemMatches) {
93
+ itemMatches.forEach(itemMatch => {
94
+ let itemText = itemMatch.replace(/<li>(.*?)<\/li>/s, '$1');
95
+ itemText = itemText.replace(/<p>/g, '').replace(/<\/p>/g, ' ');
96
+ itemText = itemText.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
97
+ if (itemText) {
98
+ listItems += `${counter++}. ${itemText}\n`;
99
+ }
100
+ });
101
+ }
102
+ return '\n' + listItems;
103
+ });
104
+
105
+ markdown = markdown.replace(/<p>(.*?)<\/p>/gs, (_, content) => {
106
+ return '\n' + content.trim() + '\n';
107
+ });
108
+
109
+ markdown = markdown.replace(/<br\s*\/?>/g, '\n');
110
+
111
+ markdown = markdown.replace(/<hr\s*\/?>/g, '\n---\n');
112
+
113
+ markdown = markdown.replace(/<(?!\/?(details|summary)\b)[^>]+>/g, ' ');
114
+
115
+ markdown = markdown.replace(/&nbsp;/g, ' ');
116
+ markdown = markdown.replace(/&lt;/g, '<');
117
+ markdown = markdown.replace(/&gt;/g, '>');
118
+ markdown = markdown.replace(/&amp;/g, '&');
119
+ markdown = markdown.replace(/&quot;/g, '"');
120
+ markdown = markdown.replace(/&apos;/g, '\'');
121
+ markdown = markdown.replace(/&ldquo;/g, '"');
122
+ markdown = markdown.replace(/&rdquo;/g, '"');
123
+ markdown = markdown.replace(/&lsquo;/g, '\'');
124
+ markdown = markdown.replace(/&rsquo;/g, '\'');
125
+ markdown = markdown.replace(/&mdash;/g, '—');
126
+ markdown = markdown.replace(/&ndash;/g, '–');
127
+ markdown = markdown.replace(/&hellip;/g, '...');
128
+ markdown = markdown.replace(/&bull;/g, '•');
129
+ markdown = markdown.replace(/&copy;/g, '©');
130
+ markdown = markdown.replace(/&reg;/g, '®');
131
+ markdown = markdown.replace(/&trade;/g, '™');
132
+ markdown = markdown.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code, 10)));
133
+ markdown = markdown.replace(/&#x([0-9a-fA-F]+);/g, (_, code) => String.fromCharCode(parseInt(code, 16)));
134
+
135
+ markdown = markdown.replace(/&([a-zA-Z]+);/g, (match, name) => NAMED_ENTITIES[name] || match);
136
+
137
+ markdown = markdown.replace(/[ \t]+$/gm, '');
138
+ markdown = markdown.replace(/^[ \t]+(?!([`>]|[*+-] |\d+[.)] ))/gm, '');
139
+ markdown = markdown.replace(/^(#{1,6}[^\n]+)\n(?!\n)/gm, '$1\n\n');
140
+ markdown = markdown.replace(/\n\s*\n\s*\n+/g, '\n\n');
141
+ markdown = markdown.replace(/[ \t]+/g, ' ');
142
+ markdown = markdown.trim();
143
+
144
+ return markdown;
145
+ }
146
+
147
+ module.exports = {
148
+ htmlToMarkdown,
149
+ NAMED_ENTITIES
150
+ };
@@ -0,0 +1,298 @@
1
+ const MarkdownIt = require('markdown-it');
2
+ const { htmlToMarkdown } = require('./html-to-markdown');
3
+
4
+ class MacroConverter {
5
+ constructor({ isCloud = false, webUrlPrefix = '', buildUrl = null } = {}) {
6
+ this._isCloud = isCloud;
7
+ this.webUrlPrefix = webUrlPrefix;
8
+ this.buildUrl = buildUrl || ((pathOrUrl) => pathOrUrl);
9
+ this.markdown = new MarkdownIt();
10
+ this.setupConfluenceMarkdownExtensions();
11
+ }
12
+
13
+ isCloud() {
14
+ return this._isCloud;
15
+ }
16
+
17
+ setupConfluenceMarkdownExtensions() {
18
+ this.markdown.enable(['table', 'strikethrough', 'linkify']);
19
+
20
+ this.markdown.core.ruler.before('normalize', 'confluence_macros', (state) => {
21
+ const src = state.src;
22
+
23
+ state.src = src.replace(/\[!info\]\s*([\s\S]*?)(?=\n\s*\n|\n\s*\[!|$)/g, (_, content) => {
24
+ return `> **INFO**\n> ${content.trim().replace(/\n/g, '\n> ')}`;
25
+ });
26
+
27
+ state.src = state.src.replace(/\[!warning\]\s*([\s\S]*?)(?=\n\s*\n|\n\s*\[!|$)/g, (_, content) => {
28
+ return `> **WARNING**\n> ${content.trim().replace(/\n/g, '\n> ')}`;
29
+ });
30
+
31
+ state.src = state.src.replace(/\[!note\]\s*([\s\S]*?)(?=\n\s*\n|\n\s*\[!|$)/g, (_, content) => {
32
+ return `> **NOTE**\n> ${content.trim().replace(/\n/g, '\n> ')}`;
33
+ });
34
+
35
+ state.src = state.src.replace(/^(\s*)- \[([ x])\] (.+)$/gm, (_, indent, checked, text) => {
36
+ return `${indent}- [${checked}] ${text}`;
37
+ });
38
+ });
39
+ }
40
+
41
+ markdownToStorage(markdown) {
42
+ const html = this.markdown.render(markdown);
43
+ return this.htmlToConfluenceStorage(html);
44
+ }
45
+
46
+ markdownToNativeStorage(markdown) {
47
+ const html = this.markdown.render(markdown);
48
+ return this.htmlToConfluenceStorage(html);
49
+ }
50
+
51
+ htmlToConfluenceStorage(html) {
52
+ let storage = html;
53
+
54
+ storage = storage.replace(/<h([1-6])>(.*?)<\/h[1-6]>/g, '<h$1>$2</h$1>');
55
+
56
+ storage = storage.replace(/<p>(.*?)<\/p>/g, '<p>$1</p>');
57
+
58
+ storage = storage.replace(/<strong>(.*?)<\/strong>/g, '<strong>$1</strong>');
59
+
60
+ storage = storage.replace(/<em>(.*?)<\/em>/g, '<em>$1</em>');
61
+
62
+ storage = storage.replace(/<ul>(.*?)<\/ul>/gs, '<ul>$1</ul>');
63
+ storage = storage.replace(/<li>(.*?)<\/li>/g, '<li><p>$1</p></li>');
64
+
65
+ storage = storage.replace(/<ol>(.*?)<\/ol>/gs, '<ol>$1</ol>');
66
+
67
+ storage = storage.replace(/<pre><code(?:\s+class="language-(\w+)")?>(.*?)<\/code><\/pre>/gs, (_, lang, code) => {
68
+ const language = lang || 'text';
69
+ const decodedCode = code.replace(/\n$/, '')
70
+ .replace(/&quot;/g, '"')
71
+ .replace(/&lt;/g, '<')
72
+ .replace(/&gt;/g, '>')
73
+ .replace(/&amp;/g, '&');
74
+ const safeCode = decodedCode.replace(/]]>/g, ']]]]><![CDATA[>');
75
+ return `<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">${language}</ac:parameter><ac:plain-text-body><![CDATA[${safeCode}]]></ac:plain-text-body></ac:structured-macro>`;
76
+ });
77
+
78
+ storage = storage.replace(/<code>(.*?)<\/code>/g, '<code>$1</code>');
79
+
80
+ storage = storage.replace(/<blockquote>(.*?)<\/blockquote>/gs, (_, content) => {
81
+ if (content.includes('<strong>INFO</strong>')) {
82
+ const cleanContent = content.replace(/<p><strong>INFO<\/strong><\/p>\s*/, '');
83
+ return `<ac:structured-macro ac:name="info">
84
+ <ac:rich-text-body>${cleanContent}</ac:rich-text-body>
85
+ </ac:structured-macro>`;
86
+ } else if (content.includes('<strong>WARNING</strong>')) {
87
+ const cleanContent = content.replace(/<p><strong>WARNING<\/strong><\/p>\s*/, '');
88
+ return `<ac:structured-macro ac:name="warning">
89
+ <ac:rich-text-body>${cleanContent}</ac:rich-text-body>
90
+ </ac:structured-macro>`;
91
+ } else if (content.includes('<strong>NOTE</strong>')) {
92
+ const cleanContent = content.replace(/<p><strong>NOTE<\/strong><\/p>\s*/, '');
93
+ return `<ac:structured-macro ac:name="note">
94
+ <ac:rich-text-body>${cleanContent}</ac:rich-text-body>
95
+ </ac:structured-macro>`;
96
+ } else {
97
+ return `<ac:structured-macro ac:name="info">
98
+ <ac:rich-text-body>${content}</ac:rich-text-body>
99
+ </ac:structured-macro>`;
100
+ }
101
+ });
102
+
103
+ storage = storage.replace(/<table>(.*?)<\/table>/gs, '<table>$1</table>');
104
+ storage = storage.replace(/<thead>(.*?)<\/thead>/gs, '<thead>$1</thead>');
105
+ storage = storage.replace(/<tbody>(.*?)<\/tbody>/gs, '<tbody>$1</tbody>');
106
+ storage = storage.replace(/<tr>(.*?)<\/tr>/gs, '<tr>$1</tr>');
107
+ storage = storage.replace(/<th>(.*?)<\/th>/g, '<th><p>$1</p></th>');
108
+ storage = storage.replace(/<td>(.*?)<\/td>/g, '<td><p>$1</p></td>');
109
+
110
+ if (this.isCloud()) {
111
+ storage = storage.replace(/<a href="(.*?)">(.*?)<\/a>/g, '<a href="$1" data-card-appearance="inline">$2</a>');
112
+ } else {
113
+ storage = storage.replace(/<a href="(.*?)">(.*?)<\/a>/g, '<ac:link><ri:url ri:value="$1" /><ac:plain-text-link-body><![CDATA[$2]]></ac:plain-text-link-body></ac:link>');
114
+ }
115
+
116
+ storage = storage.replace(/<hr\s*\/?>/g, '<hr />');
117
+
118
+ return storage;
119
+ }
120
+
121
+ detectLanguageLabels(text) {
122
+ const labels = {
123
+ includePage: 'Include Page',
124
+ sharedBlock: 'Shared Block',
125
+ includeSharedBlock: 'Include Shared Block',
126
+ fromPage: 'from page',
127
+ expandDetails: 'Expand Details'
128
+ };
129
+
130
+ if (/[\u4e00-\u9fa5]/.test(text)) {
131
+ labels.includePage = '包含页面';
132
+ labels.sharedBlock = '共享块';
133
+ labels.includeSharedBlock = '包含共享块';
134
+ labels.fromPage = '来自页面';
135
+ labels.expandDetails = '展开详情';
136
+ } else if (/[\u3040-\u309f\u30a0-\u30ff]/.test(text)) {
137
+ labels.includePage = 'ページを含む';
138
+ labels.sharedBlock = '共有ブロック';
139
+ labels.includeSharedBlock = '共有ブロックを含む';
140
+ labels.fromPage = 'ページから';
141
+ labels.expandDetails = '詳細を表示';
142
+ } else if (/[\uac00-\ud7af]/.test(text)) {
143
+ labels.includePage = '페이지 포함';
144
+ labels.sharedBlock = '공유 블록';
145
+ labels.includeSharedBlock = '공유 블록 포함';
146
+ labels.fromPage = '페이지에서';
147
+ labels.expandDetails = '상세 보기';
148
+ } else if (/[\u0400-\u04ff]/.test(text)) {
149
+ labels.includePage = 'Включить страницу';
150
+ labels.sharedBlock = 'Общий блок';
151
+ labels.includeSharedBlock = 'Включить общий блок';
152
+ labels.fromPage = 'со страницы';
153
+ labels.expandDetails = 'Подробнее';
154
+ } else if ((text.match(/[àâäéèêëïîôùûüÿœæç]/gi) || []).length >= 2) {
155
+ labels.includePage = 'Inclure la page';
156
+ labels.sharedBlock = 'Bloc partagé';
157
+ labels.includeSharedBlock = 'Inclure le bloc partagé';
158
+ labels.fromPage = 'de la page';
159
+ labels.expandDetails = 'Détails';
160
+ } else if ((text.match(/[äöüß]/gi) || []).length >= 2) {
161
+ labels.includePage = 'Seite einbinden';
162
+ labels.sharedBlock = 'Gemeinsamer Block';
163
+ labels.includeSharedBlock = 'Gemeinsamen Block einbinden';
164
+ labels.fromPage = 'von Seite';
165
+ labels.expandDetails = 'Details';
166
+ } else if ((text.match(/[áéíóúñ¿¡]/gi) || []).length >= 2) {
167
+ labels.includePage = 'Incluir página';
168
+ labels.sharedBlock = 'Bloque compartido';
169
+ labels.includeSharedBlock = 'Incluir bloque compartido';
170
+ labels.fromPage = 'de la página';
171
+ labels.expandDetails = 'Detalles';
172
+ }
173
+
174
+ return labels;
175
+ }
176
+
177
+ storageToMarkdown(storage, options = {}) {
178
+ const attachmentsDir = options.attachmentsDir || 'attachments';
179
+ let markdown = storage;
180
+
181
+ const labels = this.detectLanguageLabels(markdown);
182
+
183
+ markdown = markdown.replace(/<ac:structured-macro ac:name="toc"[^>]*\s*\/>/g, '');
184
+ markdown = markdown.replace(/<ac:structured-macro ac:name="toc"[^>]*>[\s\S]*?<\/ac:structured-macro>/g, '');
185
+
186
+ markdown = markdown.replace(/<ac:structured-macro ac:name="floatmenu"[^>]*>[\s\S]*?<\/ac:structured-macro>/g, '');
187
+
188
+ markdown = markdown.replace(/<ac:image[^>]*>\s*<ri:attachment\s+ri:filename="([^"]+)"[^>]*\s*\/>\s*<\/ac:image>/g, (_, filename) => {
189
+ return `![${filename}](${attachmentsDir}/${filename})`;
190
+ });
191
+
192
+ markdown = markdown.replace(/<ac:image[^>]*><ri:attachment\s+ri:filename="([^"]+)"[^>]*><\/ri:attachment><\/ac:image>/g, (_, filename) => {
193
+ return `![${filename}](${attachmentsDir}/${filename})`;
194
+ });
195
+
196
+ markdown = markdown.replace(/<ac:structured-macro ac:name="mermaid-macro"[^>]*>[\s\S]*?<ac:plain-text-body><!\[CDATA\[([\s\S]*?)\]\]><\/ac:plain-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, code) => {
197
+ return `\n\`\`\`mermaid\n${code.trim()}\n\`\`\`\n`;
198
+ });
199
+
200
+ markdown = markdown.replace(/<ac:structured-macro ac:name="expand"[^>]*>[\s\S]*?<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, content) => {
201
+ return `\n<details>\n<summary>${labels.expandDetails}</summary>\n\n${content}\n\n</details>\n`;
202
+ });
203
+
204
+ markdown = markdown.replace(/<ac:structured-macro ac:name="code"[^>]*>[\s\S]*?<ac:parameter ac:name="language">([^<]*)<\/ac:parameter>[\s\S]*?<ac:plain-text-body><!\[CDATA\[([\s\S]*?)\]\]><\/ac:plain-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, lang, code) => {
205
+ return `\n\`\`\`${lang}\n${code}\n\`\`\`\n`;
206
+ });
207
+
208
+ markdown = markdown.replace(/<ac:structured-macro ac:name="code"[^>]*>[\s\S]*?<ac:plain-text-body><!\[CDATA\[([\s\S]*?)\]\]><\/ac:plain-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, code) => {
209
+ return `\n\`\`\`\n${code}\n\`\`\`\n`;
210
+ });
211
+
212
+ markdown = markdown.replace(/<ac:structured-macro ac:name="info"[^>]*>[\s\S]*?<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, content) => {
213
+ const cleanContent = htmlToMarkdown(content);
214
+ return `[!info]\n${cleanContent}`;
215
+ });
216
+
217
+ markdown = markdown.replace(/<ac:structured-macro ac:name="warning"[^>]*>[\s\S]*?<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, content) => {
218
+ const cleanContent = htmlToMarkdown(content);
219
+ return `[!warning]\n${cleanContent}`;
220
+ });
221
+
222
+ markdown = markdown.replace(/<ac:structured-macro ac:name="note"[^>]*>[\s\S]*?<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, content) => {
223
+ const cleanContent = htmlToMarkdown(content);
224
+ return `[!note]\n${cleanContent}`;
225
+ });
226
+
227
+ markdown = markdown.replace(/<ac:task-list>([\s\S]*?)<\/ac:task-list>/g, (_, content) => {
228
+ const tasks = [];
229
+ const taskRegex = /<ac:task>[\s\S]*?<ac:task-status>([^<]*)<\/ac:task-status>[\s\S]*?<ac:task-body>([\s\S]*?)<\/ac:task-body>[\s\S]*?<\/ac:task>/g;
230
+ let match;
231
+ while ((match = taskRegex.exec(content)) !== null) {
232
+ const status = match[1];
233
+ let taskBody = match[2];
234
+ taskBody = taskBody.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
235
+ const checkbox = status === 'complete' ? '[x]' : '[ ]';
236
+ if (taskBody) {
237
+ tasks.push(`- ${checkbox} ${taskBody}`);
238
+ }
239
+ }
240
+ return tasks.length > 0 ? '\n' + tasks.join('\n') + '\n' : '';
241
+ });
242
+
243
+ markdown = markdown.replace(/<ac:structured-macro ac:name="panel"[^>]*>[\s\S]*?<ac:parameter ac:name="title">([^<]*)<\/ac:parameter>[\s\S]*?<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, title, content) => {
244
+ const cleanContent = htmlToMarkdown(content);
245
+ return `\n> **${title}**\n>\n${cleanContent.split('\n').map(line => line ? `> ${line}` : '>').join('\n')}\n`;
246
+ });
247
+
248
+ markdown = markdown.replace(/<ac:structured-macro ac:name="include"[^>]*>[\s\S]*?<ac:parameter ac:name="">[\s\S]*?<ac:link>[\s\S]*?<ri:page\s+ri:space-key="([^"]+)"\s+ri:content-title="([^"]+)"[^>]*\/>[\s\S]*?<\/ac:link>[\s\S]*?<\/ac:parameter>[\s\S]*?<\/ac:structured-macro>/g, (_, spaceKey, title) => {
249
+ if (spaceKey.startsWith('~')) {
250
+ const spacePath = `display/${spaceKey}/${encodeURIComponent(title)}`;
251
+ return `\n> 📄 **${labels.includePage}**: [${title}](${this.buildUrl(`${this.webUrlPrefix}/${spacePath}`)})\n`;
252
+ } else {
253
+ return `\n> 📄 **${labels.includePage}**: [${title}](${this.buildUrl(`${this.webUrlPrefix}/spaces/${spaceKey}/pages/[PAGE_ID_HERE]`)}) _(manual link correction required)_\n`;
254
+ }
255
+ });
256
+
257
+ markdown = markdown.replace(/<ac:structured-macro ac:name="(shared-block|include-shared-block)"[^>]*>[\s\S]*?<ac:parameter ac:name="shared-block-key">([^<]*)<\/ac:parameter>[\s\S]*?<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, macroType, blockKey, content) => {
258
+ const cleanContent = htmlToMarkdown(content);
259
+ return `\n> **${labels.sharedBlock}: ${blockKey}**\n>\n${cleanContent.split('\n').map(line => line ? `> ${line}` : '>').join('\n')}\n`;
260
+ });
261
+
262
+ markdown = markdown.replace(/<ac:structured-macro ac:name="include-shared-block"[^>]*>[\s\S]*?<ac:parameter ac:name="shared-block-key">([^<]*)<\/ac:parameter>[\s\S]*?<ac:parameter ac:name="page">[\s\S]*?<ac:link>[\s\S]*?<ri:page\s+ri:space-key="([^"]+)"\s+ri:content-title="([^"]+)"[^>]*\/>[\s\S]*?<\/ac:link>[\s\S]*?<\/ac:parameter>[\s\S]*?<\/ac:structured-macro>/g, (_, blockKey, spaceKey, pageTitle) => {
263
+ return `\n> 📄 **${labels.includeSharedBlock}**: ${blockKey} (${labels.fromPage}: ${pageTitle} [link needs manual correction])\n`;
264
+ });
265
+
266
+ markdown = markdown.replace(/<ac:structured-macro ac:name="view-file"[^>]*>[\s\S]*?<ac:parameter ac:name="name">[\s\S]*?<ri:attachment\s+ri:filename="([^"]+)"[^>]*\/>[\s\S]*?<\/ac:parameter>[\s\S]*?<\/ac:structured-macro>/g, (_, filename) => {
267
+ return `\n📎 [${filename}](${attachmentsDir}/${filename})\n`;
268
+ });
269
+
270
+ markdown = markdown.replace(/<ac:structured-macro ac:name="view-file"[^>]*>[\s\S]*?<ac:parameter ac:name="name">[\s\S]*?<ri:attachment\s+ri:filename="([^"]+)"[^>]*\/>[\s\S]*?<\/ac:parameter>[\s\S]*?<ac:parameter ac:name="height">([^<]*)<\/ac:parameter>[\s\S]*?<\/ac:structured-macro>/g, (_, filename, _height) => {
271
+ return `\n📎 [${filename}](${attachmentsDir}/${filename})\n`;
272
+ });
273
+
274
+ markdown = markdown.replace(/<ac:layout>/g, '');
275
+ markdown = markdown.replace(/<\/ac:layout>/g, '');
276
+ markdown = markdown.replace(/<ac:layout-section[^>]*>/g, '');
277
+ markdown = markdown.replace(/<\/ac:layout-section>/g, '');
278
+ markdown = markdown.replace(/<ac:layout-cell[^>]*>/g, '');
279
+ markdown = markdown.replace(/<\/ac:layout-cell>/g, '');
280
+
281
+ markdown = markdown.replace(/<ac:structured-macro[^>]*>[\s\S]*?<\/ac:structured-macro>/g, '');
282
+
283
+ markdown = markdown.replace(/<ac:link><ri:url ri:value="([^"]*)" \/><ac:plain-text-link-body><!\[CDATA\[([^\]]*)\]\]><\/ac:plain-text-link-body><\/ac:link>/g, '[$2]($1)');
284
+
285
+ markdown = markdown.replace(/<ac:link>\s*<ri:page[^>]*ri:content-title="([^"]*)"[^>]*\/>\s*<\/ac:link>/g, '[$1]');
286
+ markdown = markdown.replace(/<ac:link>\s*<ri:page[^>]*ri:content-title="([^"]*)"[^>]*>\s*<\/ri:page>\s*<\/ac:link>/g, '[$1]');
287
+
288
+ markdown = markdown.replace(/<ac:link[^>]*>[\s\S]*?<ac:link-body>([\s\S]*?)<\/ac:link-body>[\s\S]*?<\/ac:link>/g, '$1');
289
+
290
+ markdown = markdown.replace(/<ac:link[^>]*>[\s\S]*?<\/ac:link>/g, '');
291
+
292
+ markdown = htmlToMarkdown(markdown);
293
+
294
+ return markdown;
295
+ }
296
+ }
297
+
298
+ module.exports = MacroConverter;