From ab9238dc770e71d01af930173e07162d72d91edb Mon Sep 17 00:00:00 2001 From: Eric Schoeller Date: Thu, 12 Mar 2026 11:15:29 -0600 Subject: [PATCH 1/3] Fix markdown-to-storage rendering issues on Confluence Cloud - Use smart links (data-card-appearance="inline") for Cloud instances instead of ac:link/ri:url which Cloud no longer renders. Server/DC instances continue using the ac:link format. - Decode HTML entities (" & < >) inside code blocks before wrapping in CDATA, so code renders with literal characters instead of escaped entities. - Trim trailing newline that markdown-it appends to code block content, which caused an extra blank line in rendered Confluence code macros. - Remove global HTML entity decode (< > &) that was stripping angle-bracket placeholders (e.g. ) from inline text. Code blocks now handle their own entity decoding before CDATA insertion. --- lib/confluence-client.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/confluence-client.js b/lib/confluence-client.js index 84802bd..12ff3b0 100644 --- a/lib/confluence-client.js +++ b/lib/confluence-client.js @@ -92,6 +92,10 @@ class ConfluenceClient { return d === 'api.atlassian.com' || this.apiPath?.includes('/ex/confluence/'); } + isCloud() { + return this.domain && this.domain.trim().toLowerCase().endsWith('.atlassian.net'); + } + sanitizeApiPath(rawPath) { const fallback = '/rest/api'; const value = (rawPath || '').trim(); From 5c132c71b534211431a9cf18d691e142748846c3 Mon Sep 17 00:00:00 2001 From: Eric Schoeller Date: Mon, 16 Mar 2026 01:24:06 -0600 Subject: [PATCH 2/3] Add CDATA injection defense and fix entity decode order - Escape ]]> in code block content to prevent premature CDATA termination when user code contains XML/CDATA snippets - Move & decode last to avoid double-decoding entities - Add test for CDATA terminator escaping in code blocks Addresses review feedback from pchuri on PR #75. --- lib/confluence-client.js | 7 ++++--- tests/confluence-client.test.js | 10 ++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/confluence-client.js b/lib/confluence-client.js index 12ff3b0..3e94c27 100644 --- a/lib/confluence-client.js +++ b/lib/confluence-client.js @@ -1077,10 +1077,11 @@ class ConfluenceClient { // so they appear as literal characters in the CDATA output const decodedCode = code.replace(/\n$/, '') .replace(/"/g, '"') - .replace(/&/g, '&') .replace(/</g, '<') - .replace(/>/g, '>'); - return `${language}`; + .replace(/>/g, '>') + .replace(/&/g, '&'); // & last to avoid double-decoding + const safeCode = decodedCode.replace(/]]>/g, ']]]]>'); + return `${language}`; }); // Convert inline code diff --git a/tests/confluence-client.test.js b/tests/confluence-client.test.js index 5f40248..3ce55ec 100644 --- a/tests/confluence-client.test.js +++ b/tests/confluence-client.test.js @@ -327,6 +327,16 @@ describe('ConfluenceClient', () => { expect(result).toContain('

Cell 1

'); }); + test('should escape CDATA terminators in code blocks', () => { + const markdown = '```xml\n\n```'; + const result = client.markdownToStorage(markdown); + + expect(result).toContain(''); + // The literal ]]> from user code should not appear unescaped + expect(result).not.toContain('some data]]>'); + }); + test('should convert links to smart link format on Cloud instances', () => { const markdown = '[Example Link](https://example.com)'; const result = client.markdownToStorage(markdown); From c1682396f3f0d38e6577b63ac3f658fe20786037 Mon Sep 17 00:00:00 2001 From: Eric Schoeller Date: Thu, 19 Mar 2026 21:25:45 -0600 Subject: [PATCH 3/3] Remove duplicate isCloud() method introduced by rebase The rebase onto main (v1.27.3) introduced a duplicate isCloud() at line 95. The upstream version at line 86 is preferred as it also handles scoped tokens via isScopedToken(). Removes the duplicate to fix the ESLint no-dupe-class-members error. --- lib/confluence-client.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/confluence-client.js b/lib/confluence-client.js index 3e94c27..7d0d41c 100644 --- a/lib/confluence-client.js +++ b/lib/confluence-client.js @@ -92,10 +92,6 @@ class ConfluenceClient { return d === 'api.atlassian.com' || this.apiPath?.includes('/ex/confluence/'); } - isCloud() { - return this.domain && this.domain.trim().toLowerCase().endsWith('.atlassian.net'); - } - sanitizeApiPath(rawPath) { const fallback = '/rest/api'; const value = (rawPath || '').trim();