confluence-cli 1.31.0 → 1.32.0

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;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "1.31.0",
3
+ "version": "1.32.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "confluence-cli",
9
- "version": "1.31.0",
9
+ "version": "1.32.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "axios": "^1.15.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "1.31.0",
3
+ "version": "1.32.0",
4
4
  "description": "A command-line interface for Atlassian Confluence with page creation and editing capabilities",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -149,15 +149,16 @@ confluence --profile staging init --domain "staging.example.com" --auth-type bea
149
149
  Read page content. Outputs to stdout.
150
150
 
151
151
  ```sh
152
- confluence read <pageId> [--format html|text|markdown]
152
+ confluence read <pageId> [--format html|text|storage|markdown]
153
153
  ```
154
154
 
155
155
  | Option | Default | Description |
156
156
  |---|---|---|
157
- | `--format` | `text` | Output format: `html`, `text`, or `markdown` |
157
+ | `--format` | `text` | Output format: `html`, `text`, `storage`, or `markdown` |
158
158
 
159
159
  ```sh
160
160
  confluence read 123456789
161
+ confluence read 123456789 --format storage
161
162
  confluence read 123456789 --format markdown
162
163
  ```
163
164
 
@@ -165,14 +166,15 @@ confluence read 123456789 --format markdown
165
166
 
166
167
  ### `info <pageId>`
167
168
 
168
- Get page metadata (title, ID, type, status, space).
169
+ Get page metadata. Use `--format json` for machine-readable output.
169
170
 
170
171
  ```sh
171
- confluence info <pageId>
172
+ confluence info <pageId> [--format text|json]
172
173
  ```
173
174
 
174
175
  ```sh
175
176
  confluence info 123456789
177
+ confluence info 123456789 --format json
176
178
  ```
177
179
 
178
180
  ---