confluence-cli 1.11.0 ā 1.12.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.
- package/CHANGELOG.md +14 -0
- package/README.md +13 -0
- package/bin/confluence.js +57 -3
- package/lib/confluence-client.js +229 -40
- package/package.json +1 -1
- package/tests/confluence-client.test.js +40 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.12.0](https://github.com/pchuri/confluence-cli/compare/v1.11.1...v1.12.0) (2025-12-31)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add page delete command ([#25](https://github.com/pchuri/confluence-cli/issues/25)) ([bc3e412](https://github.com/pchuri/confluence-cli/commit/bc3e412a6ccd0774d62ab0816a6c2735cbd470a4))
|
|
7
|
+
|
|
8
|
+
## [1.11.1](https://github.com/pchuri/confluence-cli/compare/v1.11.0...v1.11.1) (2025-12-17)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* 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))
|
|
14
|
+
|
|
1
15
|
# [1.11.0](https://github.com/pchuri/confluence-cli/compare/v1.10.1...v1.11.0) (2025-12-12)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ A powerful command-line interface for Atlassian Confluence that allows you to re
|
|
|
10
10
|
- š **List spaces** - View all available Confluence spaces
|
|
11
11
|
- āļø **Create pages** - Create new pages with support for Markdown, HTML, or Storage format
|
|
12
12
|
- š **Update pages** - Update existing page content and titles
|
|
13
|
+
- šļø **Delete pages** - Delete (or move to trash) pages by ID or URL
|
|
13
14
|
- š **Attachments** - List or download page attachments
|
|
14
15
|
- š¦ **Export** - Save a page and its attachments to a local folder
|
|
15
16
|
- š ļø **Edit workflow** - Export page content for editing and re-import
|
|
@@ -209,6 +210,18 @@ confluence update 123456789 --file ./updated-content.md --format markdown
|
|
|
209
210
|
confluence update 123456789 --title "New Title" --content "And new content"
|
|
210
211
|
```
|
|
211
212
|
|
|
213
|
+
### Delete a Page
|
|
214
|
+
```bash
|
|
215
|
+
# Delete by page ID (prompts for confirmation)
|
|
216
|
+
confluence delete 123456789
|
|
217
|
+
|
|
218
|
+
# Delete by URL
|
|
219
|
+
confluence delete "https://your-domain.atlassian.net/wiki/viewpage.action?pageId=123456789"
|
|
220
|
+
|
|
221
|
+
# Skip confirmation (useful for scripts)
|
|
222
|
+
confluence delete 123456789 --yes
|
|
223
|
+
```
|
|
224
|
+
|
|
212
225
|
### Edit Workflow
|
|
213
226
|
The `edit` and `update` commands work together to create a seamless editing workflow.
|
|
214
227
|
```bash
|
package/bin/confluence.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { program } = require('commander');
|
|
4
4
|
const chalk = require('chalk');
|
|
5
|
+
const inquirer = require('inquirer');
|
|
5
6
|
const ConfluenceClient = require('../lib/confluence-client');
|
|
6
7
|
const { getConfig, initConfig } = require('../lib/config');
|
|
7
8
|
const Analytics = require('../lib/analytics');
|
|
@@ -274,6 +275,49 @@ program
|
|
|
274
275
|
}
|
|
275
276
|
});
|
|
276
277
|
|
|
278
|
+
// Delete command
|
|
279
|
+
program
|
|
280
|
+
.command('delete <pageIdOrUrl>')
|
|
281
|
+
.description('Delete a Confluence page by ID or URL')
|
|
282
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
283
|
+
.action(async (pageIdOrUrl, options) => {
|
|
284
|
+
const analytics = new Analytics();
|
|
285
|
+
try {
|
|
286
|
+
const config = getConfig();
|
|
287
|
+
const client = new ConfluenceClient(config);
|
|
288
|
+
const pageInfo = await client.getPageInfo(pageIdOrUrl);
|
|
289
|
+
|
|
290
|
+
if (!options.yes) {
|
|
291
|
+
const spaceLabel = pageInfo.space?.key ? ` (${pageInfo.space.key})` : '';
|
|
292
|
+
const { confirmed } = await inquirer.prompt([
|
|
293
|
+
{
|
|
294
|
+
type: 'confirm',
|
|
295
|
+
name: 'confirmed',
|
|
296
|
+
default: false,
|
|
297
|
+
message: `Delete "${pageInfo.title}" (ID: ${pageInfo.id})${spaceLabel}?`
|
|
298
|
+
}
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
if (!confirmed) {
|
|
302
|
+
console.log(chalk.yellow('Cancelled.'));
|
|
303
|
+
analytics.track('delete_cancel', true);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const result = await client.deletePage(pageInfo.id);
|
|
309
|
+
|
|
310
|
+
console.log(chalk.green('ā
Page deleted successfully!'));
|
|
311
|
+
console.log(`Title: ${chalk.blue(pageInfo.title)}`);
|
|
312
|
+
console.log(`ID: ${chalk.blue(result.id)}`);
|
|
313
|
+
analytics.track('delete', true);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
analytics.track('delete', false);
|
|
316
|
+
console.error(chalk.red('Error:'), error.message);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
277
321
|
// Edit command - opens page content for editing
|
|
278
322
|
program
|
|
279
323
|
.command('edit <pageId>')
|
|
@@ -445,7 +489,9 @@ program
|
|
|
445
489
|
const contentExt = formatExt[format] || 'txt';
|
|
446
490
|
|
|
447
491
|
const pageInfo = await client.getPageInfo(pageId);
|
|
448
|
-
|
|
492
|
+
// Read page with attachment extraction enabled
|
|
493
|
+
const content = await client.readPage(pageId, format, { extractReferencedAttachments: true });
|
|
494
|
+
const referencedAttachments = client._referencedAttachments || new Set();
|
|
449
495
|
|
|
450
496
|
const baseDir = path.resolve(options.dest || '.');
|
|
451
497
|
const folderName = sanitizeTitle(pageInfo.title || 'page');
|
|
@@ -462,8 +508,16 @@ program
|
|
|
462
508
|
|
|
463
509
|
if (!options.skipAttachments) {
|
|
464
510
|
const pattern = options.pattern ? options.pattern.trim() : null;
|
|
465
|
-
const
|
|
466
|
-
|
|
511
|
+
const allAttachments = await client.getAllAttachments(pageId);
|
|
512
|
+
|
|
513
|
+
// Filter: only referenced attachments (unless pattern is specified, then use pattern)
|
|
514
|
+
let filtered;
|
|
515
|
+
if (pattern) {
|
|
516
|
+
filtered = allAttachments.filter(att => client.matchesPattern(att.title, pattern));
|
|
517
|
+
} else {
|
|
518
|
+
// Only download attachments that are referenced in the page content
|
|
519
|
+
filtered = allAttachments.filter(att => referencedAttachments.has(att.title));
|
|
520
|
+
}
|
|
467
521
|
|
|
468
522
|
if (filtered.length === 0) {
|
|
469
523
|
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
|
|
|
@@ -1029,6 +1208,16 @@ class ConfluenceClient {
|
|
|
1029
1208
|
};
|
|
1030
1209
|
}
|
|
1031
1210
|
|
|
1211
|
+
/**
|
|
1212
|
+
* Delete a Confluence page
|
|
1213
|
+
* Note: Confluence may move the page to trash depending on instance settings.
|
|
1214
|
+
*/
|
|
1215
|
+
async deletePage(pageIdOrUrl) {
|
|
1216
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
1217
|
+
await this.client.delete(`/content/${pageId}`);
|
|
1218
|
+
return { id: String(pageId) };
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1032
1221
|
/**
|
|
1033
1222
|
* Search for a page by title and space
|
|
1034
1223
|
*/
|
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>';
|
|
@@ -250,6 +267,29 @@ describe('ConfluenceClient', () => {
|
|
|
250
267
|
expect(typeof client.getPageForEdit).toBe('function');
|
|
251
268
|
expect(typeof client.createChildPage).toBe('function');
|
|
252
269
|
expect(typeof client.findPageByTitle).toBe('function');
|
|
270
|
+
expect(typeof client.deletePage).toBe('function');
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('deletePage', () => {
|
|
275
|
+
test('should delete a page by ID', async () => {
|
|
276
|
+
const mock = new MockAdapter(client.client);
|
|
277
|
+
mock.onDelete('/content/123456789').reply(204);
|
|
278
|
+
|
|
279
|
+
await expect(client.deletePage('123456789')).resolves.toEqual({ id: '123456789' });
|
|
280
|
+
|
|
281
|
+
mock.restore();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('should delete a page by URL', async () => {
|
|
285
|
+
const mock = new MockAdapter(client.client);
|
|
286
|
+
mock.onDelete('/content/987654321').reply(204);
|
|
287
|
+
|
|
288
|
+
await expect(
|
|
289
|
+
client.deletePage('https://test.atlassian.net/wiki/viewpage.action?pageId=987654321')
|
|
290
|
+
).resolves.toEqual({ id: '987654321' });
|
|
291
|
+
|
|
292
|
+
mock.restore();
|
|
253
293
|
});
|
|
254
294
|
});
|
|
255
295
|
|