Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions lib/confluence-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -1293,12 +1293,12 @@ class ConfluenceClient {

// Convert Confluence code macros to markdown
markdown = markdown.replace(/<ac:structured-macro ac:name="code"[^>]*>[\s\S]*?<ac:parameter ac:name="language">([^<]*)<\/ac:parameter>[\s\S]*?<ac:plain-text-body><!\[CDATA\[([\s\S]*?)\]\]><\/ac:plain-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, lang, code) => {
return `\`\`\`${lang}\n${code}\n\`\`\``;
return `\n\`\`\`${lang}\n${code}\n\`\`\`\n`;
});

// Convert code macros without language parameter
markdown = markdown.replace(/<ac:structured-macro ac:name="code"[^>]*>[\s\S]*?<ac:plain-text-body><!\[CDATA\[([\s\S]*?)\]\]><\/ac:plain-text-body>[\s\S]*?<\/ac:structured-macro>/g, (_, code) => {
return `\`\`\`\n${code}\n\`\`\``;
return `\n\`\`\`\n${code}\n\`\`\`\n`;
});
Comment on lines 1294 to 1302
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think this is out of the change's scope.


// Convert info macro to admonition
Expand Down Expand Up @@ -1515,8 +1515,8 @@ class ConfluenceClient {
});

// Convert paragraphs (after lists and tables)
markdown = markdown.replace(/<p>(.*?)<\/p>/g, (_, content) => {
return content.trim() + '\n';
markdown = markdown.replace(/<p>(.*?)<\/p>/gs, (_, content) => {
return '\n' + content.trim() + '\n';
});

// Convert line breaks
Expand Down
107 changes: 106 additions & 1 deletion tests/confluence-client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -379,12 +379,59 @@ describe('ConfluenceClient', () => {
test('should convert Confluence code macro to markdown', () => {
const storage = '<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">javascript</ac:parameter><ac:plain-text-body><![CDATA[console.log("Hello");]]></ac:plain-text-body></ac:structured-macro>';
const result = client.storageToMarkdown(storage);

expect(result).toContain('```javascript');
expect(result).toContain('console.log("Hello");');
expect(result).toContain('```');
});

test('should separate code block (with language) from surrounding content with blank lines', () => {
const storage = '<p>Intro</p><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">python</ac:parameter><ac:plain-text-body><![CDATA[print("hi")]]></ac:plain-text-body></ac:structured-macro><p>Outro</p>';
const result = client.storageToMarkdown(storage);
expect(result).toMatch(/Intro\n\n/);
expect(result).toMatch(/\n\n```python\n/);
expect(result).toMatch(/\n```\n\n/);
expect(result).toMatch(/\n\nOutro/);
});

test('should separate code block (no language) from surrounding content with blank lines', () => {
const storage = '<p>Before</p><ac:structured-macro ac:name="code"><ac:plain-text-body><![CDATA[raw code]]></ac:plain-text-body></ac:structured-macro><p>After</p>';
const result = client.storageToMarkdown(storage);
expect(result).toMatch(/Before\n\n/);
expect(result).toMatch(/\n\n```\n/);
expect(result).toMatch(/\n```\n\n/);
expect(result).toMatch(/\n\nAfter/);
});

test('should separate mermaid macro from surrounding content with blank lines', () => {
const storage = '<p>Diagram:</p><ac:structured-macro ac:name="mermaid-macro"><ac:plain-text-body><![CDATA[graph TD; A-->B]]></ac:plain-text-body></ac:structured-macro><p>End</p>';
const result = client.storageToMarkdown(storage);
expect(result).toMatch(/Diagram:\n\n/);
expect(result).toMatch(/\n\n```mermaid\n/);
expect(result).toMatch(/\n```\n\n/);
expect(result).toMatch(/\n\nEnd/);
});

test('complex page: heading, multi-line paragraph, code block, ordered list', () => {
const storage = [
'<h1>Deployment Guide</h1>',
'<p>Deploy using the following steps.\nEnsure prerequisites are met.</p>',
'<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">bash</ac:parameter><ac:plain-text-body><![CDATA[git pull origin main\nnpm run build]]></ac:plain-text-body></ac:structured-macro>',
'<p>Then verify:</p>',
'<ol><li>Check logs</li><li>Run smoke tests</li></ol>',
'<p>Deployment complete.</p>'
].join('');
const result = client.storageToMarkdown(storage);
expect(result).toBe(
'# Deployment Guide\n\n' +
'Deploy using the following steps.\nEnsure prerequisites are met.\n\n' +
'```bash\ngit pull origin main\nnpm run build\n```\n\n' +
'Then verify:\n\n' +
'1. Check logs\n2. Run smoke tests\n\n' +
'Deployment complete.'
);
});

test('should convert Confluence macros to admonitions', () => {
const storage = '<ac:structured-macro ac:name="info"><ac:rich-text-body><p>This is info</p></ac:rich-text-body></ac:structured-macro>';
const result = client.storageToMarkdown(storage);
Expand Down Expand Up @@ -428,6 +475,64 @@ describe('ConfluenceClient', () => {
expect(result).toContain('| Cell |');
});

test('should preserve content of multi-line paragraphs', () => {
// Without the dotAll flag on the <p> regex, content with embedded newlines is silently dropped
const html = '<p>First line\nSecond line</p>';
const result = client.htmlToMarkdown(html);
expect(result).toContain('First line');
expect(result).toContain('Second line');
});

test('should separate consecutive paragraphs with a blank line', () => {
const html = '<p>Alpha</p><p>Beta</p>';
const result = client.htmlToMarkdown(html);
expect(result).toMatch(/Alpha\n\nBeta/);
});

test('should separate lists from surrounding content with blank lines', () => {
const html = '<p>Intro</p><ul><li>Item A</li><li>Item B</li></ul><p>Outro</p>';
const result = client.htmlToMarkdown(html);
expect(result).toMatch(/Intro\n\n/);
expect(result).toMatch(/\n\n- Item A\n- Item B\n\n/);
expect(result).toMatch(/\n\nOutro/);
});

test('should separate ordered lists from surrounding content with blank lines', () => {
const html = '<p>Steps:</p><ol><li>First</li><li>Second</li></ol><p>Done</p>';
const result = client.htmlToMarkdown(html);
expect(result).toMatch(/Steps:\n\n/);
expect(result).toMatch(/\n\n1\. First\n2\. Second\n\n/);
expect(result).toMatch(/\n\nDone/);
});

test('should separate tables from surrounding content with blank lines', () => {
const html = '<p>See table:</p><table><tr><th>Col</th></tr><tr><td>Val</td></tr></table><p>End</p>';
const result = client.htmlToMarkdown(html);
expect(result).toMatch(/See table:\n\n/);
expect(result).toMatch(/\| Col \|/);
expect(result).toMatch(/\n\nEnd/);
});

test('complex page: heading, multi-line paragraph, table, list', () => {
const html = [
'<h2>API Reference</h2>',
'<p>The following endpoints are available.\nAll requests require authentication.</p>',
'<table><tr><th>Method</th><th>Path</th></tr><tr><td>GET</td><td>/users</td></tr><tr><td>POST</td><td>/users</td></tr></table>',
'<p>Authentication options:</p>',
'<ul><li>Bearer token</li><li>API key</li></ul>',
'<p>See docs for details.</p>'
].join('');
const result = client.htmlToMarkdown(html);
expect(result).toBe(
'## API Reference\n\n' +
'The following endpoints are available.\nAll requests require authentication.\n\n' +
'| Method | Path |\n| --- | --- |\n| GET | /users |\n| POST | /users |\n\n' +
'Authentication options:\n\n' +
'- Bearer token\n- API key\n\n' +
'See docs for details.'
);
});

test('should convert named characters correctly', () => {
const NAMED_ENTITIES = ConfluenceClient.NAMED_ENTITIES;

Expand Down
Loading