From 83af62b6e5e46475e198c65f5f9718d7601d43fa Mon Sep 17 00:00:00 2001 From: Vladimir Urushev Date: Sun, 21 Jun 2026 20:54:09 +0200 Subject: [PATCH] feat: raw Confluence passthrough via confluence-tagged fenced block --- README.md | 16 ++++++++++++++++ src/elements/code.ts | 6 ++++++ test/converter.test.ts | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/README.md b/README.md index f7a6125..fe9cd15 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ The best Markdown to Confluence converter. Direct AST-to-XML conversion with no - **Full GFM support** — Tables, task lists, strikethrough, autolinks - **Mermaid diagrams** — Rendered to PNG with ELK layout engine - **Admonitions** — `[!NOTE]`, `[!WARNING]`, `[!TIP]` → Confluence info panels +- **Raw passthrough** — ` ```confluence ` blocks emit storage XML verbatim (toc, status, …) - **Smart sync** — MD5-based change detection, only uploads when content changes - **Local images** — Automatically uploaded as attachments - **Frontmatter** — Control page ID, title, and labels via YAML @@ -154,6 +155,21 @@ flowchart TD Mermaid diagrams are rendered to PNG with ELK layout engine for better handling of complex flowcharts and subgraphs. +### Raw Confluence Passthrough + +For Confluence macros that have no Markdown equivalent (table of contents, status lozenges, expand/details panels, …), a ` ```confluence ` fenced block is passed through to the page's storage format **verbatim** — not escaped, not wrapped in a code macro. + +````markdown +```confluence + + 2 + 2 + +``` +```` + +This is a block-level escape hatch — the whole block is raw storage XML. Confluence validates it at publish time. + ## License [MIT](LICENSE) © Vladimir Urushev diff --git a/src/elements/code.ts b/src/elements/code.ts index da3a1f2..f7ab1f4 100644 --- a/src/elements/code.ts +++ b/src/elements/code.ts @@ -32,6 +32,12 @@ registerConverter('code', (node, context) => { return `` } + // Raw Confluence storage-format passthrough. Emitted verbatim — no escaping, no code + // macro — as an escape hatch for macros with no markdown form (toc, details, status, …). + if (lang === 'confluence') { + return code + } + const params: Record = {} if (lang) { params.language = mapLanguage(lang) diff --git a/test/converter.test.ts b/test/converter.test.ts index 881df79..132dc91 100644 --- a/test/converter.test.ts +++ b/test/converter.test.ts @@ -74,6 +74,41 @@ describe('Code blocks', () => { }) }) +describe('Confluence passthrough', () => { + it('emits a confluence fence verbatim and unescaped', () => { + const result = md2confluence('```confluence\n\n```') + expect(result).toContain('') + expect(result).not.toContain('<') + expect(result).not.toContain('ac:name="code"') + }) + + it('preserves special characters and nested macros byte-for-byte', () => { + const body = + 'Greenpublic' + const result = md2confluence(`\`\`\`confluence\n

a & b "c" ${body}

\n\`\`\``) + expect(result).toContain(`

a & b "c" ${body}

`) + expect(result).not.toContain('&') + expect(result).not.toContain('"') + }) + + it('preserves newlines in a multi-line fence body', () => { + const result = md2confluence('```confluence\n\n \n
x
\n```') + expect(result).toBe('\n \n
x
') + }) + + it('does not affect real language code blocks', () => { + const result = md2confluence('```js\nconst x = 1\n```') + expect(result).toContain('') + expect(result).toContain('javascript') + }) + + it('matches the language exactly — uppercase falls through to a code block', () => { + const result = md2confluence('```Confluence\n\n```') + expect(result).toContain('') + expect(result).toContain(']]>') + }) +}) + describe('Lists', () => { it('converts unordered lists', () => { const result = md2confluence('- Item 1\n- Item 2')