@bestend/confluence-cli 1.15.3 → 1.15.5

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.
@@ -19,11 +19,15 @@ jobs:
19
19
  node-version: '24.x'
20
20
  registry-url: 'https://registry.npmjs.org'
21
21
 
22
- - name: Set package version from tag
22
+ - name: Ensure package version matches tag
23
23
  run: |
24
24
  TAG="${GITHUB_REF_NAME#v}"
25
- echo "Setting package version to ${TAG}"
26
- npm version "$TAG" --no-git-tag-version
25
+ PKG_VERSION=$(node -p "require('./package.json').version")
26
+ echo "Tag: $TAG / package.json: $PKG_VERSION"
27
+ if [ "$TAG" != "$PKG_VERSION" ]; then
28
+ echo "package.json version mismatch: $PKG_VERSION (expected $TAG)" >&2
29
+ exit 1
30
+ fi
27
31
 
28
32
  - name: Install dependencies
29
33
  run: npm ci
@@ -24,6 +24,12 @@ class ConfluenceClient {
24
24
  });
25
25
  }
26
26
 
27
+ sanitizeStorageContent(content) {
28
+ // Escape unescaped ampersands that would break XML (except already-escaped entities)
29
+ // This is a best-effort sanitizer; users should still provide valid XHTML when using --format storage.
30
+ return content.replace(/&(?![a-zA-Z]+;|#\d+;|#x[0-9a-fA-F]+;)/g, '&');
31
+ }
32
+
27
33
  sanitizeApiPath(rawPath) {
28
34
  const fallback = '/rest/api';
29
35
  const value = (rawPath || '').trim();
@@ -61,6 +67,12 @@ class ConfluenceClient {
61
67
  if (pageIdMatch) {
62
68
  return pageIdMatch[1];
63
69
  }
70
+
71
+ // Handle /spaces/SPACEKEY/pages/PAGEID/Title format
72
+ const spacesMatch = pageIdOrUrl.match(/\/spaces\/[^/]+\/pages\/(\d+)/);
73
+ if (spacesMatch) {
74
+ return spacesMatch[1];
75
+ }
64
76
 
65
77
  // Handle display URLs - search by space and title
66
78
  const displayMatch = pageIdOrUrl.match(/\/display\/([^/]+)\/(.+)/);
@@ -842,9 +854,18 @@ class ConfluenceClient {
842
854
  // Convert code blocks to Confluence code macro
843
855
  storage = storage.replace(/<pre><code(?:\s+class="language-(\w+)")?>(.*?)<\/code><\/pre>/gs, (_, lang, code) => {
844
856
  const language = lang || 'text';
857
+ // Decode HTML entities inside code blocks before wrapping in CDATA
858
+ let decoded = code
859
+ .replace(/&amp;/g, '&')
860
+ .replace(/&lt;/g, '<')
861
+ .replace(/&gt;/g, '>')
862
+ .replace(/&quot;/g, '"')
863
+ .replace(/&#39;/g, "'");
864
+ decoded = decoded.replace(/\]\]>/g, ']]]]><![CDATA[>');
865
+
845
866
  return `<ac:structured-macro ac:name="code">
846
867
  <ac:parameter ac:name="language">${language}</ac:parameter>
847
- <ac:plain-text-body><![CDATA[${code}]]></ac:plain-text-body>
868
+ <ac:plain-text-body><![CDATA[${decoded}]]></ac:plain-text-body>
848
869
  </ac:structured-macro>`;
849
870
  });
850
871
 
@@ -885,8 +906,7 @@ class ConfluenceClient {
885
906
  storage = storage.replace(/<th>(.*?)<\/th>/g, '<th><p>$1</p></th>');
886
907
  storage = storage.replace(/<td>(.*?)<\/td>/g, '<td><p>$1</p></td>');
887
908
 
888
- // Convert links
889
- 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>');
909
+ // Links: keep <a href> as-is (compatible with both Cloud and Server/Data Center)
890
910
 
891
911
  // Convert horizontal rules
892
912
  storage = storage.replace(/<hr\s*\/?>/g, '<hr />');
@@ -1342,6 +1362,10 @@ class ConfluenceClient {
1342
1362
  storageContent = content;
1343
1363
  }
1344
1364
 
1365
+ if (format === 'storage') {
1366
+ storageContent = this.sanitizeStorageContent(storageContent);
1367
+ }
1368
+
1345
1369
  const pageData = {
1346
1370
  type: 'page',
1347
1371
  title: title,
@@ -1373,6 +1397,10 @@ class ConfluenceClient {
1373
1397
  storageContent = content;
1374
1398
  }
1375
1399
 
1400
+ if (format === 'storage') {
1401
+ storageContent = this.sanitizeStorageContent(storageContent);
1402
+ }
1403
+
1376
1404
  const pageData = {
1377
1405
  type: 'page',
1378
1406
  title: title,
@@ -1419,6 +1447,10 @@ class ConfluenceClient {
1419
1447
  } else { // 'storage' format
1420
1448
  storageContent = content;
1421
1449
  }
1450
+
1451
+ if (format === 'storage') {
1452
+ storageContent = this.sanitizeStorageContent(storageContent);
1453
+ }
1422
1454
  } else {
1423
1455
  // If no new content, use the existing content
1424
1456
  storageContent = currentPage.data.body.storage.value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bestend/confluence-cli",
3
- "version": "1.15.3",
3
+ "version": "1.15.5",
4
4
  "description": "A command-line interface for Atlassian Confluence with page creation and editing capabilities",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -141,7 +141,7 @@ describe('ConfluenceClient', () => {
141
141
 
142
142
  expect(result).toContain('<ac:structured-macro ac:name="code">');
143
143
  expect(result).toContain('<ac:parameter ac:name="language">javascript</ac:parameter>');
144
- expect(result).toContain('console.log(&quot;Hello World&quot;);');
144
+ expect(result).toContain('console.log("Hello World");');
145
145
  });
146
146
 
147
147
  test('should convert lists to native Confluence format', () => {
@@ -171,12 +171,11 @@ describe('ConfluenceClient', () => {
171
171
  expect(result).toContain('<td><p>Cell 1</p></td>');
172
172
  });
173
173
 
174
- test('should convert links to Confluence link format', () => {
174
+ test('should keep links as standard HTML anchors', () => {
175
175
  const markdown = '[Example Link](https://example.com)';
176
176
  const result = client.markdownToStorage(markdown);
177
177
 
178
- expect(result).toContain('<ac:link>');
179
- expect(result).toContain('ri:value="https://example.com"');
178
+ expect(result).toContain('<a href="https://example.com">');
180
179
  expect(result).toContain('Example Link');
181
180
  });
182
181
  });