confluence-cli 1.11.0 → 1.11.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 +7 -0
- package/bin/confluence.js +13 -3
- package/lib/confluence-client.js +219 -40
- package/package.json +1 -1
- package/tests/confluence-client.test.js +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [1.11.1](https://github.com/pchuri/confluence-cli/compare/v1.11.0...v1.11.1) (2025-12-17)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* support children macro, improve macro handling, and filter attachments ([#23](https://github.com/pchuri/confluence-cli/issues/23)) ([15b721a](https://github.com/pchuri/confluence-cli/commit/15b721acdc296e72470ee438c9fe3470e09ae52e))
|
|
7
|
+
|
|
1
8
|
# [1.11.0](https://github.com/pchuri/confluence-cli/compare/v1.10.1...v1.11.0) (2025-12-12)
|
|
2
9
|
|
|
3
10
|
|
package/bin/confluence.js
CHANGED
|
@@ -445,7 +445,9 @@ program
|
|
|
445
445
|
const contentExt = formatExt[format] || 'txt';
|
|
446
446
|
|
|
447
447
|
const pageInfo = await client.getPageInfo(pageId);
|
|
448
|
-
|
|
448
|
+
// Read page with attachment extraction enabled
|
|
449
|
+
const content = await client.readPage(pageId, format, { extractReferencedAttachments: true });
|
|
450
|
+
const referencedAttachments = client._referencedAttachments || new Set();
|
|
449
451
|
|
|
450
452
|
const baseDir = path.resolve(options.dest || '.');
|
|
451
453
|
const folderName = sanitizeTitle(pageInfo.title || 'page');
|
|
@@ -462,8 +464,16 @@ program
|
|
|
462
464
|
|
|
463
465
|
if (!options.skipAttachments) {
|
|
464
466
|
const pattern = options.pattern ? options.pattern.trim() : null;
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
+
const allAttachments = await client.getAllAttachments(pageId);
|
|
468
|
+
|
|
469
|
+
// Filter: only referenced attachments (unless pattern is specified, then use pattern)
|
|
470
|
+
let filtered;
|
|
471
|
+
if (pattern) {
|
|
472
|
+
filtered = allAttachments.filter(att => client.matchesPattern(att.title, pattern));
|
|
473
|
+
} else {
|
|
474
|
+
// Only download attachments that are referenced in the page content
|
|
475
|
+
filtered = allAttachments.filter(att => referencedAttachments.has(att.title));
|
|
476
|
+
}
|
|
467
477
|
|
|
468
478
|
if (filtered.length === 0) {
|
|
469
479
|
console.log(chalk.yellow('No attachments to download.'));
|
package/lib/confluence-client.js
CHANGED
|
@@ -99,12 +99,43 @@ class ConfluenceClient {
|
|
|
99
99
|
return pageIdOrUrl;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Extract referenced attachment filenames from HTML content
|
|
104
|
+
* @param {string} htmlContent - HTML content in storage format
|
|
105
|
+
* @returns {Set<string>} Set of referenced attachment filenames
|
|
106
|
+
*/
|
|
107
|
+
extractReferencedAttachments(htmlContent) {
|
|
108
|
+
const referenced = new Set();
|
|
109
|
+
|
|
110
|
+
// Extract from ac:image with ri:attachment
|
|
111
|
+
const imageRegex = /<ac:image[^>]*>[\s\S]*?<ri:attachment\s+ri:filename="([^"]+)"[^>]*\/?>[\s\S]*?<\/ac:image>/g;
|
|
112
|
+
let match;
|
|
113
|
+
while ((match = imageRegex.exec(htmlContent)) !== null) {
|
|
114
|
+
referenced.add(match[1]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Extract from view-file macro
|
|
118
|
+
const viewFileRegex = /<ac:structured-macro ac:name="view-file"[^>]*>[\s\S]*?<ri:attachment\s+ri:filename="([^"]+)"[^>]*\/?>[\s\S]*?<\/ac:structured-macro>/g;
|
|
119
|
+
while ((match = viewFileRegex.exec(htmlContent)) !== null) {
|
|
120
|
+
referenced.add(match[1]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Extract from any ri:attachment references
|
|
124
|
+
const attachmentRegex = /<ri:attachment\s+ri:filename="([^"]+)"[^>]*\/?>/g;
|
|
125
|
+
while ((match = attachmentRegex.exec(htmlContent)) !== null) {
|
|
126
|
+
referenced.add(match[1]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return referenced;
|
|
130
|
+
}
|
|
131
|
+
|
|
102
132
|
/**
|
|
103
133
|
* Read a Confluence page content
|
|
104
134
|
* @param {string} pageIdOrUrl - Page ID or URL
|
|
105
135
|
* @param {string} format - Output format: 'text', 'html', or 'markdown'
|
|
106
136
|
* @param {object} options - Additional options
|
|
107
137
|
* @param {boolean} options.resolveUsers - Whether to resolve userkeys to display names (default: true for markdown)
|
|
138
|
+
* @param {boolean} options.extractReferencedAttachments - Whether to extract referenced attachments (default: false)
|
|
108
139
|
*/
|
|
109
140
|
async readPage(pageIdOrUrl, format = 'text', options = {}) {
|
|
110
141
|
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
@@ -117,6 +148,11 @@ class ConfluenceClient {
|
|
|
117
148
|
|
|
118
149
|
let htmlContent = response.data.body.storage.value;
|
|
119
150
|
|
|
151
|
+
// Extract referenced attachments if requested
|
|
152
|
+
if (options.extractReferencedAttachments) {
|
|
153
|
+
this._referencedAttachments = this.extractReferencedAttachments(htmlContent);
|
|
154
|
+
}
|
|
155
|
+
|
|
120
156
|
if (format === 'html') {
|
|
121
157
|
return htmlContent;
|
|
122
158
|
}
|
|
@@ -135,6 +171,9 @@ class ConfluenceClient {
|
|
|
135
171
|
htmlContent = await this.resolvePageLinksInHtml(htmlContent);
|
|
136
172
|
}
|
|
137
173
|
|
|
174
|
+
// Resolve children macro to child pages list
|
|
175
|
+
htmlContent = await this.resolveChildrenMacro(htmlContent, pageId);
|
|
176
|
+
|
|
138
177
|
return this.storageToMarkdown(htmlContent);
|
|
139
178
|
}
|
|
140
179
|
|
|
@@ -358,6 +397,58 @@ class ConfluenceClient {
|
|
|
358
397
|
return resolvedHtml;
|
|
359
398
|
}
|
|
360
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Resolve children macro to child pages list
|
|
402
|
+
* @param {string} html - HTML content with children macro
|
|
403
|
+
* @param {string} pageId - Page ID to get children from
|
|
404
|
+
* @returns {Promise<string>} - HTML with children macro replaced by markdown list
|
|
405
|
+
*/
|
|
406
|
+
async resolveChildrenMacro(html, pageId) {
|
|
407
|
+
// Check if there's a children macro (self-closing or with closing tag)
|
|
408
|
+
const childrenMacroRegex = /<ac:structured-macro\s+ac:name="children"[^>]*(?:\/>|>[\s\S]*?<\/ac:structured-macro>)/g;
|
|
409
|
+
const hasChildrenMacro = childrenMacroRegex.test(html);
|
|
410
|
+
|
|
411
|
+
if (!hasChildrenMacro) {
|
|
412
|
+
return html;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
// Get child pages with full info including _links
|
|
417
|
+
const response = await this.client.get(`/content/${pageId}/child/page`, {
|
|
418
|
+
params: {
|
|
419
|
+
limit: 500,
|
|
420
|
+
expand: 'space,version'
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const childPages = response.data.results || [];
|
|
425
|
+
|
|
426
|
+
if (childPages.length === 0) {
|
|
427
|
+
// No children, remove the macro
|
|
428
|
+
return html.replace(childrenMacroRegex, '');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Convert child pages to markdown list
|
|
432
|
+
// Format: - [Page Title](URL)
|
|
433
|
+
const childPagesList = childPages.map(page => {
|
|
434
|
+
const webui = page._links?.webui || '';
|
|
435
|
+
const url = webui ? `https://${this.domain}/wiki${webui}` : '';
|
|
436
|
+
if (url) {
|
|
437
|
+
return `- [${page.title}](${url})`;
|
|
438
|
+
} else {
|
|
439
|
+
return `- ${page.title}`;
|
|
440
|
+
}
|
|
441
|
+
}).join('\n');
|
|
442
|
+
|
|
443
|
+
// Replace children macro with markdown list
|
|
444
|
+
return html.replace(childrenMacroRegex, `\n${childPagesList}\n`);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
// If error getting children, just remove the macro
|
|
447
|
+
console.error(`Error resolving children macro: ${error.message}`);
|
|
448
|
+
return html.replace(childrenMacroRegex, '');
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
361
452
|
/**
|
|
362
453
|
* List attachments for a page with pagination support
|
|
363
454
|
*/
|
|
@@ -556,33 +647,8 @@ class ConfluenceClient {
|
|
|
556
647
|
// Convert markdown to HTML first
|
|
557
648
|
const html = this.markdown.render(markdown);
|
|
558
649
|
|
|
559
|
-
//
|
|
560
|
-
|
|
561
|
-
let storage = html
|
|
562
|
-
.replace(/<h1>/g, '<h1>')
|
|
563
|
-
.replace(/<\/h1>/g, '</h1>')
|
|
564
|
-
.replace(/<h2>/g, '<h2>')
|
|
565
|
-
.replace(/<\/h2>/g, '</h2>')
|
|
566
|
-
.replace(/<h3>/g, '<h3>')
|
|
567
|
-
.replace(/<\/h3>/g, '</h3>')
|
|
568
|
-
.replace(/<p>/g, '<p>')
|
|
569
|
-
.replace(/<\/p>/g, '</p>')
|
|
570
|
-
.replace(/<strong>/g, '<strong>')
|
|
571
|
-
.replace(/<\/strong>/g, '</strong>')
|
|
572
|
-
.replace(/<em>/g, '<em>')
|
|
573
|
-
.replace(/<\/em>/g, '</em>')
|
|
574
|
-
.replace(/<ul>/g, '<ul>')
|
|
575
|
-
.replace(/<\/ul>/g, '</ul>')
|
|
576
|
-
.replace(/<ol>/g, '<ol>')
|
|
577
|
-
.replace(/<\/ol>/g, '</ol>')
|
|
578
|
-
.replace(/<li>/g, '<li>')
|
|
579
|
-
.replace(/<\/li>/g, '</li>')
|
|
580
|
-
.replace(/<code>/g, '<code>')
|
|
581
|
-
.replace(/<\/code>/g, '</code>')
|
|
582
|
-
.replace(/<pre><code>/g, '<ac:structured-macro ac:name="code"><ac:plain-text-body><![CDATA[')
|
|
583
|
-
.replace(/<\/code><\/pre>/g, ']]></ac:plain-text-body></ac:structured-macro>');
|
|
584
|
-
|
|
585
|
-
return storage;
|
|
650
|
+
// Delegate to htmlToConfluenceStorage for proper conversion including code blocks
|
|
651
|
+
return this.htmlToConfluenceStorage(html);
|
|
586
652
|
}
|
|
587
653
|
|
|
588
654
|
/**
|
|
@@ -618,6 +684,76 @@ class ConfluenceClient {
|
|
|
618
684
|
});
|
|
619
685
|
}
|
|
620
686
|
|
|
687
|
+
/**
|
|
688
|
+
* Detect language from text content and return appropriate labels
|
|
689
|
+
* @param {string} text - Text content to analyze
|
|
690
|
+
* @returns {object} Object with language-specific labels
|
|
691
|
+
*/
|
|
692
|
+
detectLanguageLabels(text) {
|
|
693
|
+
const labels = {
|
|
694
|
+
includePage: 'Include Page',
|
|
695
|
+
sharedBlock: 'Shared Block',
|
|
696
|
+
includeSharedBlock: 'Include Shared Block',
|
|
697
|
+
fromPage: 'from page',
|
|
698
|
+
expandDetails: 'Expand Details'
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
if (/[\u4e00-\u9fa5]/.test(text)) {
|
|
702
|
+
// Chinese
|
|
703
|
+
labels.includePage = '包含页面';
|
|
704
|
+
labels.sharedBlock = '共享块';
|
|
705
|
+
labels.includeSharedBlock = '包含共享块';
|
|
706
|
+
labels.fromPage = '来自页面';
|
|
707
|
+
labels.expandDetails = '展开详情';
|
|
708
|
+
} else if (/[\u3040-\u309f\u30a0-\u30ff]/.test(text)) {
|
|
709
|
+
// Japanese
|
|
710
|
+
labels.includePage = 'ページを含む';
|
|
711
|
+
labels.sharedBlock = '共有ブロック';
|
|
712
|
+
labels.includeSharedBlock = '共有ブロックを含む';
|
|
713
|
+
labels.fromPage = 'ページから';
|
|
714
|
+
labels.expandDetails = '詳細を表示';
|
|
715
|
+
} else if (/[\uac00-\ud7af]/.test(text)) {
|
|
716
|
+
// Korean
|
|
717
|
+
labels.includePage = '페이지 포함';
|
|
718
|
+
labels.sharedBlock = '공유 블록';
|
|
719
|
+
labels.includeSharedBlock = '공유 블록 포함';
|
|
720
|
+
labels.fromPage = '페이지에서';
|
|
721
|
+
labels.expandDetails = '상세 보기';
|
|
722
|
+
} else if (/[\u0400-\u04ff]/.test(text)) {
|
|
723
|
+
// Russian/Cyrillic
|
|
724
|
+
labels.includePage = 'Включить страницу';
|
|
725
|
+
labels.sharedBlock = 'Общий блок';
|
|
726
|
+
labels.includeSharedBlock = 'Включить общий блок';
|
|
727
|
+
labels.fromPage = 'со страницы';
|
|
728
|
+
labels.expandDetails = 'Подробнее';
|
|
729
|
+
} else if ((text.match(/[àâäéèêëïîôùûüÿœæç]/gi) || []).length >= 2) {
|
|
730
|
+
// French (requires at least 2 French-specific characters to avoid false positives)
|
|
731
|
+
labels.includePage = 'Inclure la page';
|
|
732
|
+
labels.sharedBlock = 'Bloc partagé';
|
|
733
|
+
labels.includeSharedBlock = 'Inclure le bloc partagé';
|
|
734
|
+
labels.fromPage = 'de la page';
|
|
735
|
+
labels.expandDetails = 'Détails';
|
|
736
|
+
} else if ((text.match(/[äöüß]/gi) || []).length >= 2) {
|
|
737
|
+
// German (requires at least 2 German-specific characters)
|
|
738
|
+
// Note: French is checked before German because French regex includes more characters
|
|
739
|
+
// that overlap with German (ä, ü). The threshold helps distinguish between them.
|
|
740
|
+
labels.includePage = 'Seite einbinden';
|
|
741
|
+
labels.sharedBlock = 'Gemeinsamer Block';
|
|
742
|
+
labels.includeSharedBlock = 'Gemeinsamen Block einbinden';
|
|
743
|
+
labels.fromPage = 'von Seite';
|
|
744
|
+
labels.expandDetails = 'Details';
|
|
745
|
+
} else if ((text.match(/[áéíóúñ¿¡]/gi) || []).length >= 2) {
|
|
746
|
+
// Spanish (requires at least 2 Spanish-specific characters)
|
|
747
|
+
labels.includePage = 'Incluir página';
|
|
748
|
+
labels.sharedBlock = 'Bloque compartido';
|
|
749
|
+
labels.includeSharedBlock = 'Incluir bloque compartido';
|
|
750
|
+
labels.fromPage = 'de la página';
|
|
751
|
+
labels.expandDetails = 'Detalles';
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return labels;
|
|
755
|
+
}
|
|
756
|
+
|
|
621
757
|
/**
|
|
622
758
|
* Convert Confluence storage format to markdown
|
|
623
759
|
* @param {string} storage - Confluence storage format HTML
|
|
@@ -628,6 +764,9 @@ class ConfluenceClient {
|
|
|
628
764
|
const attachmentsDir = options.attachmentsDir || 'attachments';
|
|
629
765
|
let markdown = storage;
|
|
630
766
|
|
|
767
|
+
// Detect language from content
|
|
768
|
+
const labels = this.detectLanguageLabels(markdown);
|
|
769
|
+
|
|
631
770
|
// Remove table of contents macro
|
|
632
771
|
markdown = markdown.replace(/<ac:structured-macro ac:name="toc"[^>]*\s*\/>/g, '');
|
|
633
772
|
markdown = markdown.replace(/<ac:structured-macro ac:name="toc"[^>]*>[\s\S]*?<\/ac:structured-macro>/g, '');
|
|
@@ -652,20 +791,8 @@ class ConfluenceClient {
|
|
|
652
791
|
});
|
|
653
792
|
|
|
654
793
|
// Convert expand macro - extract content from rich-text-body
|
|
655
|
-
// Detect language based on content for the expand summary text
|
|
656
|
-
const detectExpandSummary = (text) => {
|
|
657
|
-
if (/[\u4e00-\u9fa5]/.test(text)) return '展开详情'; // Chinese
|
|
658
|
-
if (/[\u3040-\u309f\u30a0-\u30ff]/.test(text)) return '詳細を表示'; // Japanese
|
|
659
|
-
if (/[\uac00-\ud7af]/.test(text)) return '상세 보기'; // Korean
|
|
660
|
-
if (/[\u0400-\u04ff]/.test(text)) return 'Подробнее'; // Russian/Cyrillic
|
|
661
|
-
if (/[àâäéèêëïîôùûüÿœæç]/i.test(text)) return 'Détails'; // French
|
|
662
|
-
if (/[äöüß]/i.test(text)) return 'Details'; // German
|
|
663
|
-
if (/[áéíóúñ¿¡]/i.test(text)) return 'Detalles'; // Spanish
|
|
664
|
-
return 'Expand Details'; // Default: English
|
|
665
|
-
};
|
|
666
|
-
const expandSummary = detectExpandSummary(markdown);
|
|
667
794
|
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) => {
|
|
668
|
-
return `\n<details>\n<summary>${
|
|
795
|
+
return `\n<details>\n<summary>${labels.expandDetails}</summary>\n\n${content}\n\n</details>\n`;
|
|
669
796
|
});
|
|
670
797
|
|
|
671
798
|
// Convert Confluence code macros to markdown
|
|
@@ -716,6 +843,58 @@ class ConfluenceClient {
|
|
|
716
843
|
return tasks.length > 0 ? '\n' + tasks.join('\n') + '\n' : '';
|
|
717
844
|
});
|
|
718
845
|
|
|
846
|
+
// Convert panel macro to markdown blockquote with title
|
|
847
|
+
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) => {
|
|
848
|
+
const cleanContent = this.htmlToMarkdown(content);
|
|
849
|
+
return `\n> **${title}**\n>\n${cleanContent.split('\n').map(line => line ? `> ${line}` : '>').join('\n')}\n`;
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// Convert include macro - extract page link and convert to markdown link
|
|
853
|
+
// Handle both with and without parameter name
|
|
854
|
+
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) => {
|
|
855
|
+
// Try to build a proper URL - if spaceKey starts with ~, it's a user space
|
|
856
|
+
if (spaceKey.startsWith('~')) {
|
|
857
|
+
const spacePath = `display/${spaceKey}/${encodeURIComponent(title)}`;
|
|
858
|
+
return `\n> 📄 **${labels.includePage}**: [${title}](https://${this.domain}/wiki/${spacePath})\n`;
|
|
859
|
+
} else {
|
|
860
|
+
// For non-user spaces, we cannot construct a valid link without the page ID.
|
|
861
|
+
// Document that manual correction is required.
|
|
862
|
+
return `\n> 📄 **${labels.includePage}**: [${title}](https://${this.domain}/wiki/spaces/${spaceKey}/pages/[PAGE_ID_HERE]) _(manual link correction required)_\n`;
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
// Convert shared-block and include-shared-block macros - extract content
|
|
867
|
+
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) => {
|
|
868
|
+
const cleanContent = this.htmlToMarkdown(content);
|
|
869
|
+
return `\n> **${labels.sharedBlock}: ${blockKey}**\n>\n${cleanContent.split('\n').map(line => line ? `> ${line}` : '>').join('\n')}\n`;
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
// Convert include-shared-block with page parameter
|
|
873
|
+
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) => {
|
|
874
|
+
// The page ID is not available, so we cannot generate a valid link.
|
|
875
|
+
// Instead, document that the link needs manual correction.
|
|
876
|
+
return `\n> 📄 **${labels.includeSharedBlock}**: ${blockKey} (${labels.fromPage}: ${pageTitle} [link needs manual correction])\n`;
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
// Convert view-file macro to file link
|
|
880
|
+
// Handle both orders: name first or height first
|
|
881
|
+
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) => {
|
|
882
|
+
return `\n📎 [${filename}](${attachmentsDir}/${filename})\n`;
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
// Also handle view-file with height parameter (which might appear after name)
|
|
886
|
+
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) => {
|
|
887
|
+
return `\n📎 [${filename}](${attachmentsDir}/${filename})\n`;
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
// Remove layout macros but preserve content
|
|
891
|
+
markdown = markdown.replace(/<ac:layout>/g, '');
|
|
892
|
+
markdown = markdown.replace(/<\/ac:layout>/g, '');
|
|
893
|
+
markdown = markdown.replace(/<ac:layout-section[^>]*>/g, '');
|
|
894
|
+
markdown = markdown.replace(/<\/ac:layout-section>/g, '');
|
|
895
|
+
markdown = markdown.replace(/<ac:layout-cell[^>]*>/g, '');
|
|
896
|
+
markdown = markdown.replace(/<\/ac:layout-cell>/g, '');
|
|
897
|
+
|
|
719
898
|
// Remove other unhandled macros (replace with empty string for now)
|
|
720
899
|
markdown = markdown.replace(/<ac:structured-macro[^>]*>[\s\S]*?<\/ac:structured-macro>/g, '');
|
|
721
900
|
|
package/package.json
CHANGED
|
@@ -181,6 +181,23 @@ describe('ConfluenceClient', () => {
|
|
|
181
181
|
});
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
+
describe('markdownToNativeStorage', () => {
|
|
185
|
+
test('should act as an alias to htmlToConfluenceStorage via markdown render', () => {
|
|
186
|
+
const markdown = '# Native Storage Test';
|
|
187
|
+
const result = client.markdownToNativeStorage(markdown);
|
|
188
|
+
|
|
189
|
+
expect(result).toContain('<h1>Native Storage Test</h1>');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('should handle code blocks correctly', () => {
|
|
193
|
+
const markdown = '```javascript\nconst a = 1;\n```';
|
|
194
|
+
const result = client.markdownToNativeStorage(markdown);
|
|
195
|
+
|
|
196
|
+
expect(result).toContain('<ac:structured-macro ac:name="code">');
|
|
197
|
+
expect(result).toContain('const a = 1;');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
184
201
|
describe('storageToMarkdown', () => {
|
|
185
202
|
test('should convert Confluence storage format to markdown', () => {
|
|
186
203
|
const storage = '<h1>Hello World</h1><p>This is a <strong>test</strong> page.</p>';
|