confluence-cli 1.3.2 → 1.4.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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [1.4.1](https://github.com/pchuri/confluence-cli/compare/v1.4.0...v1.4.1) (2025-06-30)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * correct version display in CLI ([#6](https://github.com/pchuri/confluence-cli/issues/6)) ([36f8419](https://github.com/pchuri/confluence-cli/commit/36f8419b309ae1ff99fa94c12ace9a527ee3f162))
7
+
8
+ # [1.4.0](https://github.com/pchuri/confluence-cli/compare/v1.3.2...v1.4.0) (2025-06-30)
9
+
10
+
11
+ ### Features
12
+
13
+ * Enhanced Markdown Support with Bidirectional Conversion ([#5](https://github.com/pchuri/confluence-cli/issues/5)) ([d17771b](https://github.com/pchuri/confluence-cli/commit/d17771b40d8d60ed68c0ac0a3594fed6b9a4e771))
14
+
1
15
  ## [1.3.2](https://github.com/pchuri/confluence-cli/compare/v1.3.1...v1.3.2) (2025-06-27)
2
16
 
3
17
 
package/CONTRIBUTING.md CHANGED
@@ -126,6 +126,32 @@ node bin/confluence.js read 123456789
126
126
  - Add comments for complex logic
127
127
  - Keep functions small and focused
128
128
 
129
+ ### Markdown Support
130
+
131
+ The CLI includes enhanced markdown support with:
132
+
133
+ - **Native Confluence Storage Format**: Converts markdown to native Confluence elements instead of HTML macros
134
+ - **Confluence Extensions**: Support for admonitions (`[!info]`, `[!warning]`, `[!note]`)
135
+ - **Bidirectional Conversion**: Convert from markdown to storage format and back
136
+ - **Rich Elements**: Tables, code blocks, lists, links, and formatting
137
+
138
+ Example markdown with Confluence extensions:
139
+ ```markdown
140
+ # My Page
141
+
142
+ [!info]
143
+ This is an info admonition that will render as a Confluence info macro.
144
+
145
+ ```javascript
146
+ console.log("Code blocks preserve syntax highlighting");
147
+ ```
148
+
149
+ | Feature | Status |
150
+ |---------|--------|
151
+ | Tables | ✅ |
152
+ | Lists | ✅ |
153
+ ```
154
+
129
155
  ### File Structure
130
156
 
131
157
  ```
package/bin/confluence.js CHANGED
@@ -5,11 +5,12 @@ const chalk = require('chalk');
5
5
  const ConfluenceClient = require('../lib/confluence-client');
6
6
  const { getConfig, initConfig } = require('../lib/config');
7
7
  const Analytics = require('../lib/analytics');
8
+ const pkg = require('../package.json');
8
9
 
9
10
  program
10
11
  .name('confluence')
11
12
  .description('CLI tool for Atlassian Confluence')
12
- .version('1.0.0');
13
+ .version(pkg.version);
13
14
 
14
15
  // Init command
15
16
  program
@@ -23,7 +24,7 @@ program
23
24
  program
24
25
  .command('read <pageId>')
25
26
  .description('Read a Confluence page by ID or URL')
26
- .option('-f, --format <format>', 'Output format (html, text)', 'text')
27
+ .option('-f, --format <format>', 'Output format (html, text, markdown)', 'text')
27
28
  .action(async (pageId, options) => {
28
29
  const analytics = new Analytics();
29
30
  try {
@@ -8,6 +8,7 @@ class ConfluenceClient {
8
8
  this.token = config.token;
9
9
  this.domain = config.domain;
10
10
  this.markdown = new MarkdownIt();
11
+ this.setupConfluenceMarkdownExtensions();
11
12
 
12
13
  this.client = axios.create({
13
14
  baseURL: this.baseURL,
@@ -62,6 +63,10 @@ class ConfluenceClient {
62
63
  return htmlContent;
63
64
  }
64
65
 
66
+ if (format === 'markdown') {
67
+ return this.storageToMarkdown(htmlContent);
68
+ }
69
+
65
70
  // Convert HTML to text
66
71
  return convert(htmlContent, {
67
72
  wordwrap: 80,
@@ -139,11 +144,94 @@ class ConfluenceClient {
139
144
  * Convert markdown to Confluence storage format
140
145
  */
141
146
  markdownToStorage(markdown) {
142
- // Use Confluence's markdown macro instead of HTML
143
- return `<ac:structured-macro ac:name="markdown">
144
- <ac:parameter ac:name="atlassian-macro-output-type">BLOCK</ac:parameter>
145
- <ac:plain-text-body><![CDATA[${markdown}]]></ac:plain-text-body>
146
- </ac:structured-macro>`;
147
+ // Convert markdown to HTML first
148
+ const html = this.markdown.render(markdown);
149
+
150
+ // Convert HTML to native Confluence storage format elements
151
+ return this.htmlToConfluenceStorage(html);
152
+ }
153
+
154
+ /**
155
+ * Convert HTML to native Confluence storage format
156
+ */
157
+ htmlToConfluenceStorage(html) {
158
+ let storage = html;
159
+
160
+ // Convert headings to native Confluence format
161
+ storage = storage.replace(/<h([1-6])>(.*?)<\/h[1-6]>/g, '<h$1>$2</h$1>');
162
+
163
+ // Convert paragraphs
164
+ storage = storage.replace(/<p>(.*?)<\/p>/g, '<p>$1</p>');
165
+
166
+ // Convert strong/bold text
167
+ storage = storage.replace(/<strong>(.*?)<\/strong>/g, '<strong>$1</strong>');
168
+
169
+ // Convert emphasis/italic text
170
+ storage = storage.replace(/<em>(.*?)<\/em>/g, '<em>$1</em>');
171
+
172
+ // Convert unordered lists
173
+ storage = storage.replace(/<ul>(.*?)<\/ul>/gs, '<ul>$1</ul>');
174
+ storage = storage.replace(/<li>(.*?)<\/li>/g, '<li><p>$1</p></li>');
175
+
176
+ // Convert ordered lists
177
+ storage = storage.replace(/<ol>(.*?)<\/ol>/gs, '<ol>$1</ol>');
178
+
179
+ // Convert code blocks to Confluence code macro
180
+ storage = storage.replace(/<pre><code(?:\s+class="language-(\w+)")?>(.*?)<\/code><\/pre>/gs, (_, lang, code) => {
181
+ const language = lang || 'text';
182
+ return `<ac:structured-macro ac:name="code">
183
+ <ac:parameter ac:name="language">${language}</ac:parameter>
184
+ <ac:plain-text-body><![CDATA[${code}]]></ac:plain-text-body>
185
+ </ac:structured-macro>`;
186
+ });
187
+
188
+ // Convert inline code
189
+ storage = storage.replace(/<code>(.*?)<\/code>/g, '<code>$1</code>');
190
+
191
+ // Convert blockquotes to appropriate macros based on content
192
+ storage = storage.replace(/<blockquote>(.*?)<\/blockquote>/gs, (_, content) => {
193
+ // Check for admonition patterns
194
+ if (content.includes('<strong>INFO</strong>')) {
195
+ const cleanContent = content.replace(/<p><strong>INFO<\/strong><\/p>\s*/, '');
196
+ return `<ac:structured-macro ac:name="info">
197
+ <ac:rich-text-body>${cleanContent}</ac:rich-text-body>
198
+ </ac:structured-macro>`;
199
+ } else if (content.includes('<strong>WARNING</strong>')) {
200
+ const cleanContent = content.replace(/<p><strong>WARNING<\/strong><\/p>\s*/, '');
201
+ return `<ac:structured-macro ac:name="warning">
202
+ <ac:rich-text-body>${cleanContent}</ac:rich-text-body>
203
+ </ac:structured-macro>`;
204
+ } else if (content.includes('<strong>NOTE</strong>')) {
205
+ const cleanContent = content.replace(/<p><strong>NOTE<\/strong><\/p>\s*/, '');
206
+ return `<ac:structured-macro ac:name="note">
207
+ <ac:rich-text-body>${cleanContent}</ac:rich-text-body>
208
+ </ac:structured-macro>`;
209
+ } else {
210
+ // Default to info macro for regular blockquotes
211
+ return `<ac:structured-macro ac:name="info">
212
+ <ac:rich-text-body>${content}</ac:rich-text-body>
213
+ </ac:structured-macro>`;
214
+ }
215
+ });
216
+
217
+ // Convert tables
218
+ storage = storage.replace(/<table>(.*?)<\/table>/gs, '<table>$1</table>');
219
+ storage = storage.replace(/<thead>(.*?)<\/thead>/gs, '<thead>$1</thead>');
220
+ storage = storage.replace(/<tbody>(.*?)<\/tbody>/gs, '<tbody>$1</tbody>');
221
+ storage = storage.replace(/<tr>(.*?)<\/tr>/gs, '<tr>$1</tr>');
222
+ storage = storage.replace(/<th>(.*?)<\/th>/g, '<th><p>$1</p></th>');
223
+ storage = storage.replace(/<td>(.*?)<\/td>/g, '<td><p>$1</p></td>');
224
+
225
+ // Convert links
226
+ 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>');
227
+
228
+ // Convert horizontal rules
229
+ storage = storage.replace(/<hr\s*\/?>/g, '<hr />');
230
+
231
+ // Clean up any remaining HTML entities and normalize whitespace
232
+ storage = storage.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
233
+
234
+ return storage;
147
235
  }
148
236
 
149
237
  /**
@@ -182,6 +270,217 @@ class ConfluenceClient {
182
270
  return storage;
183
271
  }
184
272
 
273
+ /**
274
+ * Setup Confluence-specific markdown extensions
275
+ */
276
+ setupConfluenceMarkdownExtensions() {
277
+ // Enable additional markdown-it features
278
+ this.markdown.enable(['table', 'strikethrough', 'linkify']);
279
+
280
+ // Add custom rule for Confluence macros in markdown
281
+ this.markdown.core.ruler.before('normalize', 'confluence_macros', (state) => {
282
+ const src = state.src;
283
+
284
+ // Convert [!info] admonitions to info macro
285
+ state.src = src.replace(/\[!info\]\s*([\s\S]*?)(?=\n\s*\n|\n\s*\[!|$)/g, (_, content) => {
286
+ return `> **INFO**\n> ${content.trim().replace(/\n/g, '\n> ')}`;
287
+ });
288
+
289
+ // Convert [!warning] admonitions to warning macro
290
+ state.src = state.src.replace(/\[!warning\]\s*([\s\S]*?)(?=\n\s*\n|\n\s*\[!|$)/g, (_, content) => {
291
+ return `> **WARNING**\n> ${content.trim().replace(/\n/g, '\n> ')}`;
292
+ });
293
+
294
+ // Convert [!note] admonitions to note macro
295
+ state.src = state.src.replace(/\[!note\]\s*([\s\S]*?)(?=\n\s*\n|\n\s*\[!|$)/g, (_, content) => {
296
+ return `> **NOTE**\n> ${content.trim().replace(/\n/g, '\n> ')}`;
297
+ });
298
+
299
+ // Convert task lists to proper format
300
+ state.src = state.src.replace(/^(\s*)- \[([ x])\] (.+)$/gm, (_, indent, checked, text) => {
301
+ return `${indent}- [${checked}] ${text}`;
302
+ });
303
+ });
304
+ }
305
+
306
+ /**
307
+ * Convert Confluence storage format to markdown
308
+ */
309
+ storageToMarkdown(storage) {
310
+ let markdown = storage;
311
+
312
+ // Remove table of contents macro
313
+ markdown = markdown.replace(/<ac:structured-macro ac:name="toc"[^>]*\s*\/>/g, '');
314
+ markdown = markdown.replace(/<ac:structured-macro ac:name="toc"[^>]*>[\s\S]*?<\/ac:structured-macro>/g, '');
315
+
316
+ // Convert Confluence code macros to markdown
317
+ 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) => {
318
+ return `\`\`\`${lang}\n${code}\n\`\`\``;
319
+ });
320
+
321
+ // Convert code macros without language parameter
322
+ 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) => {
323
+ return `\`\`\`\n${code}\n\`\`\``;
324
+ });
325
+
326
+ // Convert info macro to admonition
327
+ 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) => {
328
+ const cleanContent = this.htmlToMarkdown(content);
329
+ return `[!info]\n${cleanContent}`;
330
+ });
331
+
332
+ // Convert warning macro to admonition
333
+ 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) => {
334
+ const cleanContent = this.htmlToMarkdown(content);
335
+ return `[!warning]\n${cleanContent}`;
336
+ });
337
+
338
+ // Convert note macro to admonition
339
+ 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) => {
340
+ const cleanContent = this.htmlToMarkdown(content);
341
+ return `[!note]\n${cleanContent}`;
342
+ });
343
+
344
+ // Remove other unhandled macros (replace with empty string for now)
345
+ markdown = markdown.replace(/<ac:structured-macro[^>]*>[\s\S]*?<\/ac:structured-macro>/g, '');
346
+
347
+ // Convert links
348
+ 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)');
349
+
350
+ // Convert remaining HTML to markdown
351
+ markdown = this.htmlToMarkdown(markdown);
352
+
353
+ return markdown;
354
+ }
355
+
356
+ /**
357
+ * Convert basic HTML to markdown
358
+ */
359
+ htmlToMarkdown(html) {
360
+ let markdown = html;
361
+
362
+ // Convert strong/bold BEFORE removing HTML attributes
363
+ markdown = markdown.replace(/<strong[^>]*>(.*?)<\/strong>/g, '**$1**');
364
+
365
+ // Convert emphasis/italic BEFORE removing HTML attributes
366
+ markdown = markdown.replace(/<em[^>]*>(.*?)<\/em>/g, '*$1*');
367
+
368
+ // Convert code BEFORE removing HTML attributes
369
+ markdown = markdown.replace(/<code[^>]*>(.*?)<\/code>/g, '`$1`');
370
+
371
+ // Remove HTML attributes from tags (but preserve content formatting)
372
+ markdown = markdown.replace(/<(\w+)[^>]*>/g, '<$1>');
373
+ markdown = markdown.replace(/<\/(\w+)[^>]*>/g, '</$1>');
374
+
375
+ // Convert headings first (they don't contain other elements typically)
376
+ markdown = markdown.replace(/<h([1-6])>(.*?)<\/h[1-6]>/g, (_, level, text) => {
377
+ return '\n' + '#'.repeat(parseInt(level)) + ' ' + text.trim() + '\n';
378
+ });
379
+
380
+ // Convert tables BEFORE paragraphs
381
+ markdown = markdown.replace(/<table>(.*?)<\/table>/gs, (_, content) => {
382
+ const rows = [];
383
+ let isHeader = true;
384
+
385
+ // Extract table rows
386
+ const rowMatches = content.match(/<tr>(.*?)<\/tr>/gs);
387
+ if (rowMatches) {
388
+ rowMatches.forEach(rowMatch => {
389
+ const cells = [];
390
+ const cellContent = rowMatch.replace(/<tr>(.*?)<\/tr>/s, '$1');
391
+
392
+ // Extract cells (th or td)
393
+ const cellMatches = cellContent.match(/<t[hd]>(.*?)<\/t[hd]>/gs);
394
+ if (cellMatches) {
395
+ cellMatches.forEach(cellMatch => {
396
+ let cellText = cellMatch.replace(/<t[hd]>(.*?)<\/t[hd]>/s, '$1');
397
+ // Clean up cell content - remove nested HTML but preserve text and some formatting
398
+ cellText = cellText.replace(/<p>/g, '').replace(/<\/p>/g, ' ');
399
+ cellText = cellText.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
400
+ cells.push(cellText || ' ');
401
+ });
402
+ }
403
+
404
+ if (cells.length > 0) {
405
+ rows.push('| ' + cells.join(' | ') + ' |');
406
+
407
+ if (isHeader) {
408
+ rows.push('| ' + cells.map(() => '---').join(' | ') + ' |');
409
+ isHeader = false;
410
+ }
411
+ }
412
+ });
413
+ }
414
+
415
+ return rows.length > 0 ? '\n' + rows.join('\n') + '\n' : '';
416
+ });
417
+
418
+ // Convert unordered lists BEFORE paragraphs
419
+ markdown = markdown.replace(/<ul>(.*?)<\/ul>/gs, (_, content) => {
420
+ let listItems = '';
421
+ const itemMatches = content.match(/<li>(.*?)<\/li>/gs);
422
+ if (itemMatches) {
423
+ itemMatches.forEach(itemMatch => {
424
+ let itemText = itemMatch.replace(/<li>(.*?)<\/li>/s, '$1');
425
+ // Clean up nested HTML but preserve some formatting
426
+ itemText = itemText.replace(/<p>/g, '').replace(/<\/p>/g, ' ');
427
+ itemText = itemText.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
428
+ if (itemText) {
429
+ listItems += '- ' + itemText + '\n';
430
+ }
431
+ });
432
+ }
433
+ return '\n' + listItems;
434
+ });
435
+
436
+ // Convert ordered lists BEFORE paragraphs
437
+ markdown = markdown.replace(/<ol>(.*?)<\/ol>/gs, (_, content) => {
438
+ let listItems = '';
439
+ let counter = 1;
440
+ const itemMatches = content.match(/<li>(.*?)<\/li>/gs);
441
+ if (itemMatches) {
442
+ itemMatches.forEach(itemMatch => {
443
+ let itemText = itemMatch.replace(/<li>(.*?)<\/li>/s, '$1');
444
+ // Clean up nested HTML but preserve some formatting
445
+ itemText = itemText.replace(/<p>/g, '').replace(/<\/p>/g, ' ');
446
+ itemText = itemText.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
447
+ if (itemText) {
448
+ listItems += `${counter++}. ${itemText}\n`;
449
+ }
450
+ });
451
+ }
452
+ return '\n' + listItems;
453
+ });
454
+
455
+ // Convert paragraphs (after lists and tables)
456
+ markdown = markdown.replace(/<p>(.*?)<\/p>/g, (_, content) => {
457
+ return content.trim() + '\n';
458
+ });
459
+
460
+ // Convert line breaks
461
+ markdown = markdown.replace(/<br\s*\/?>/g, '\n');
462
+
463
+ // Convert horizontal rules
464
+ markdown = markdown.replace(/<hr\s*\/?>/g, '\n---\n');
465
+
466
+ // Remove any remaining HTML tags
467
+ markdown = markdown.replace(/<[^>]+>/g, ' ');
468
+
469
+ // Clean up whitespace and HTML entities
470
+ markdown = markdown.replace(/&nbsp;/g, ' ');
471
+ markdown = markdown.replace(/&lt;/g, '<');
472
+ markdown = markdown.replace(/&gt;/g, '>');
473
+ markdown = markdown.replace(/&amp;/g, '&');
474
+ markdown = markdown.replace(/&quot;/g, '"');
475
+
476
+ // Clean up extra whitespace
477
+ markdown = markdown.replace(/\n\s*\n\s*\n+/g, '\n\n');
478
+ markdown = markdown.replace(/[ \t]+/g, ' ');
479
+ markdown = markdown.trim();
480
+
481
+ return markdown;
482
+ }
483
+
185
484
  /**
186
485
  * Create a new Confluence page
187
486
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "1.3.2",
3
+ "version": "1.4.1",
4
4
  "description": "A command-line interface for Atlassian Confluence with page creation and editing capabilities",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -19,7 +19,7 @@
19
19
  "wiki",
20
20
  "documentation"
21
21
  ],
22
- "author": "Your Name",
22
+ "author": "pchuri",
23
23
  "license": "MIT",
24
24
  "dependencies": {
25
25
  "axios": "^1.6.2",
@@ -28,14 +28,121 @@ describe('ConfluenceClient', () => {
28
28
  });
29
29
 
30
30
  describe('markdownToStorage', () => {
31
- test('should convert markdown to Confluence storage format', () => {
32
- const markdown = '# Hello World\n\nThis is a **test** page.';
31
+ test('should convert basic markdown to native Confluence storage format', () => {
32
+ const markdown = '# Hello World\n\nThis is a **test** page with *italic* text.';
33
33
  const result = client.markdownToStorage(markdown);
34
34
 
35
- expect(result).toContain('<ac:structured-macro ac:name="markdown">');
36
- expect(result).toContain('Hello World');
35
+ expect(result).toContain('<h1>Hello World</h1>');
36
+ expect(result).toContain('<p>This is a <strong>test</strong> page with <em>italic</em> text.</p>');
37
+ expect(result).not.toContain('<ac:structured-macro ac:name="html">');
38
+ });
39
+
40
+ test('should convert code blocks to Confluence code macro', () => {
41
+ const markdown = '```javascript\nconsole.log("Hello World");\n```';
42
+ const result = client.markdownToStorage(markdown);
43
+
44
+ expect(result).toContain('<ac:structured-macro ac:name="code">');
45
+ expect(result).toContain('<ac:parameter ac:name="language">javascript</ac:parameter>');
46
+ expect(result).toContain('console.log(&quot;Hello World&quot;);');
47
+ });
48
+
49
+ test('should convert lists to native Confluence format', () => {
50
+ const markdown = '- Item 1\n- Item 2\n\n1. First\n2. Second';
51
+ const result = client.markdownToStorage(markdown);
52
+
53
+ expect(result).toContain('<ul>');
54
+ expect(result).toContain('<li><p>Item 1</p></li>');
55
+ expect(result).toContain('<ol>');
56
+ expect(result).toContain('<li><p>First</p></li>');
57
+ });
58
+
59
+ test('should convert Confluence admonitions', () => {
60
+ const markdown = '[!info]\nThis is an info message';
61
+ const result = client.markdownToStorage(markdown);
62
+
63
+ expect(result).toContain('<ac:structured-macro ac:name="info">');
64
+ expect(result).toContain('This is an info message');
65
+ });
66
+
67
+ test('should convert tables to native Confluence format', () => {
68
+ const markdown = '| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |';
69
+ const result = client.markdownToStorage(markdown);
70
+
71
+ expect(result).toContain('<table>');
72
+ expect(result).toContain('<th><p>Header 1</p></th>');
73
+ expect(result).toContain('<td><p>Cell 1</p></td>');
74
+ });
75
+
76
+ test('should convert links to Confluence link format', () => {
77
+ const markdown = '[Example Link](https://example.com)';
78
+ const result = client.markdownToStorage(markdown);
79
+
80
+ expect(result).toContain('<ac:link>');
81
+ expect(result).toContain('ri:value="https://example.com"');
82
+ expect(result).toContain('Example Link');
83
+ });
84
+ });
85
+
86
+ describe('storageToMarkdown', () => {
87
+ test('should convert Confluence storage format to markdown', () => {
88
+ const storage = '<h1>Hello World</h1><p>This is a <strong>test</strong> page.</p>';
89
+ const result = client.storageToMarkdown(storage);
90
+
91
+ expect(result).toContain('# Hello World');
37
92
  expect(result).toContain('**test**');
38
93
  });
94
+
95
+ test('should convert Confluence code macro to markdown', () => {
96
+ const storage = '<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">javascript</ac:parameter><ac:plain-text-body><![CDATA[console.log("Hello");]]></ac:plain-text-body></ac:structured-macro>';
97
+ const result = client.storageToMarkdown(storage);
98
+
99
+ expect(result).toContain('```javascript');
100
+ expect(result).toContain('console.log("Hello");');
101
+ expect(result).toContain('```');
102
+ });
103
+
104
+ test('should convert Confluence macros to admonitions', () => {
105
+ const storage = '<ac:structured-macro ac:name="info"><ac:rich-text-body><p>This is info</p></ac:rich-text-body></ac:structured-macro>';
106
+ const result = client.storageToMarkdown(storage);
107
+
108
+ expect(result).toContain('[!info]');
109
+ expect(result).toContain('This is info');
110
+ });
111
+
112
+ test('should convert Confluence links to markdown', () => {
113
+ const storage = '<ac:link><ri:url ri:value="https://example.com" /><ac:plain-text-link-body><![CDATA[Example]]></ac:plain-text-link-body></ac:link>';
114
+ const result = client.storageToMarkdown(storage);
115
+
116
+ expect(result).toContain('[Example](https://example.com)');
117
+ });
118
+ });
119
+
120
+ describe('htmlToMarkdown', () => {
121
+ test('should convert basic HTML to markdown', () => {
122
+ const html = '<h2>Title</h2><p>Some <strong>bold</strong> and <em>italic</em> text.</p>';
123
+ const result = client.htmlToMarkdown(html);
124
+
125
+ expect(result).toContain('## Title');
126
+ expect(result).toContain('**bold**');
127
+ expect(result).toContain('*italic*');
128
+ });
129
+
130
+ test('should convert HTML lists to markdown', () => {
131
+ const html = '<ul><li><p>Item 1</p></li><li><p>Item 2</p></li></ul>';
132
+ const result = client.htmlToMarkdown(html);
133
+
134
+ expect(result).toContain('- Item 1');
135
+ expect(result).toContain('- Item 2');
136
+ });
137
+
138
+ test('should convert HTML tables to markdown', () => {
139
+ const html = '<table><tr><th><p>Header</p></th></tr><tr><td><p>Cell</p></td></tr></table>';
140
+ const result = client.htmlToMarkdown(html);
141
+
142
+ expect(result).toContain('| Header |');
143
+ expect(result).toContain('| --- |');
144
+ expect(result).toContain('| Cell |');
145
+ });
39
146
  });
40
147
 
41
148
  describe('page creation and updates', () => {