Skip to content

Fix markdown-to-storage rendering on Confluence Cloud#75

Merged
pchuri merged 3 commits intopchuri:mainfrom
eschoeller:fix/cloud-markdown-rendering
Mar 22, 2026
Merged

Fix markdown-to-storage rendering on Confluence Cloud#75
pchuri merged 3 commits intopchuri:mainfrom
eschoeller:fix/cloud-markdown-rendering

Conversation

@eschoeller
Copy link
Contributor

Summary

Fixes several issues with markdownToStorage() / htmlToConfluenceStorage() when targeting Confluence Cloud instances:

  • Links not rendering: Cloud no longer renders the ac:link + ri:url storage format for external URLs. This PR adds an isCloud() helper (checks if domain ends with .atlassian.net) and uses smart links (<a href="..." data-card-appearance="inline">) for Cloud instances while preserving ac:link format for Server/Data Center.

  • HTML entities in code blocks: markdown-it HTML-encodes characters inside <code> blocks ("&quot;, &&amp;, etc.). These entities were being passed verbatim into CDATA sections, rendering as literal &quot; in Confluence code macros. Now decoded before CDATA insertion.

  • Trailing newline in code blocks: markdown-it appends \n to code block content during HTML rendering, causing an extra blank line at the end of every Confluence code macro. Now trimmed.

  • Angle-bracket placeholders stripped: A global &lt;/&gt;/&amp; decode at the end of htmlToConfluenceStorage() was converting &lt;placeholder&gt; back to <placeholder>, which Confluence then stripped as an unknown HTML tag. Removed the global decode — code blocks handle their own entity decoding separately.

Test plan

  • Updated existing code block test to expect decoded entities in CDATA
  • Updated link test to expect Cloud smart link format
  • Added new test for Server/Data Center link format (ac:link) to ensure backward compatibility
  • All 140 tests pass

Copy link
Owner

@pchuri pchuri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution, @eschoeller! This is a well-structured PR with clear explanations for each fix. Really appreciate you tackling multiple rendering issues together with proper test coverage.

Overall

The changes look solid — the isCloud() helper, code block entity decoding, trailing newline trim, and the removal of the global entity decode are all sensible fixes. The Cloud vs Server/DC link branching preserves backward compatibility nicely, and the test additions cover both paths.

One issue: entity decoding order

The current decoding chain has a potential double-decoding bug:

const decodedCode = code.replace(/\n$/, '')
  .replace(/&quot;/g, '"')
  .replace(/&amp;/g, '&')   // &amp;lt; → &lt;
  .replace(/&lt;/g, '<')     // &lt; → <  (double-decoded!)
  .replace(/&gt;/g, '>');

If the original source contains a literal &lt; (i.e. the user wrote &lt; in their code), markdown-it would encode the & to produce &amp;lt;. With the current order, &amp; is decoded first → &lt;, then &lt; is decoded again → <. This silently corrupts the content.

Moving &amp; to the end fixes this:

const decodedCode = code.replace(/\n$/, '')
  .replace(/&quot;/g, '"')
  .replace(/&lt;/g, '<')
  .replace(/&gt;/g, '>')
  .replace(/&amp;/g, '&');   // decode & last

Minor note

The isCloud() heuristic (checking for .atlassian.net) is pragmatic and works for the vast majority of cases. Worth noting that custom-domain Cloud instances won't be detected, but that's an edge case we can address later with a config flag if needed.

Could you fix the decoding order? Everything else looks good to merge. 🙏

@pchuri
Copy link
Owner

pchuri commented Mar 13, 2026

One more thing worth considering: CDATA injection defense.

If user code contains a literal ]]>, it will prematurely terminate the CDATA section, potentially breaking the XML or causing unexpected behavior. A common pattern is to split the terminator:

const decodedCode = code.replace(/\n$/, '')
  .replace(/&quot;/g, '"')
  .replace(/&lt;/g, '<')
  .replace(/&gt;/g, '>')
  .replace(/&amp;/g, '&');   // & last to avoid double-decoding

const safeCode = decodedCode.replace(/]]>/g, ']]]]><![CDATA[>');

return `<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">${language}</ac:parameter><ac:plain-text-body><![CDATA[${safeCode}]]></ac:plain-text-body></ac:structured-macro>`;

This is a defensive measure — unlikely to hit in normal usage, but it prevents breakage when someone pastes XML/CDATA snippets into a code block. Not a blocker, but nice to have while we're touching this code.

eschoeller pushed a commit to eschoeller/confluence-cli that referenced this pull request Mar 16, 2026
- Escape ]]> in code block content to prevent premature CDATA
  termination when user code contains XML/CDATA snippets
- Move &amp; decode last to avoid double-decoding entities
- Add test for CDATA terminator escaping in code blocks

Addresses review feedback from pchuri on PR pchuri#75.
@eschoeller
Copy link
Contributor Author

Added in 1ce15ca — CDATA injection defense plus moved &amp; decode last to avoid double-decoding, as you suggested. Also added a test case for the CDATA escaping.

Copy link
Owner

@pchuri pchuri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great — the decoding order fix and CDATA injection defense are exactly what I had in mind. Tests cover all the new paths well. Approving!

One thing: there are merge conflicts on this branch. Could you rebase on main and resolve them when you get a chance? Once that's sorted, this is good to merge.

Eric Schoeller added 2 commits March 16, 2026 21:38
- 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 (&quot; &amp; &lt; &gt;) 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 (&lt; &gt; &amp;) that was stripping
  angle-bracket placeholders (e.g. <tenant>) from inline text. Code
  blocks now handle their own entity decoding before CDATA insertion.
- Escape ]]> in code block content to prevent premature CDATA
  termination when user code contains XML/CDATA snippets
- Move &amp; decode last to avoid double-decoding entities
- Add test for CDATA terminator escaping in code blocks

Addresses review feedback from pchuri on PR pchuri#75.
@eschoeller eschoeller force-pushed the fix/cloud-markdown-rendering branch from 1ce15ca to 5c132c7 Compare March 17, 2026 03:39
@eschoeller
Copy link
Contributor Author

Rebased on main (v1.27.3), no conflicts. All 145 tests passing. Ready to merge.

@pchuri
Copy link
Owner

pchuri commented Mar 18, 2026

@pchuri
Copy link
Owner

pchuri commented Mar 19, 2026

CI is failing due to an ESLint no-dupe-class-members error — the rebase introduced a duplicate isCloud() method.

Line 86 (existing on main):

isCloud() {
  return this.isScopedToken() || (this.domain && this.domain.trim().toLowerCase().endsWith('.atlassian.net'));
}

Line 95 (added by this PR):

isCloud() {
  return this.domain && this.domain.trim().toLowerCase().endsWith('.atlassian.net');
}

The existing one at line 86 already covers the .atlassian.net check and additionally handles scoped tokens via isScopedToken(). Removing the duplicate at line 95 fixes lint and all 145 tests pass.

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.
@eschoeller
Copy link
Contributor Author

eschoeller commented Mar 20, 2026

Good catch — removed the duplicate isCloud() in c168239. The upstream version at line 86 is the right one since it handles scoped tokens too. ESLint clean, all 145 tests passing.

Thanks a lot for accepting this PR and I also really appreciate the work you've done on this cli tool it's been incredibly useful to me!!

@pchuri pchuri merged commit 1c826d4 into pchuri:main Mar 22, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants