@@ -379,12 +379,59 @@ describe('ConfluenceClient', () => {
379379 test ( 'should convert Confluence code macro to markdown' , ( ) => {
380380 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>' ;
381381 const result = client . storageToMarkdown ( storage ) ;
382-
382+
383383 expect ( result ) . toContain ( '```javascript' ) ;
384384 expect ( result ) . toContain ( 'console.log("Hello");' ) ;
385385 expect ( result ) . toContain ( '```' ) ;
386386 } ) ;
387387
388+ test ( 'should separate code block (with language) from surrounding content with blank lines' , ( ) => {
389+ 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>' ;
390+ const result = client . storageToMarkdown ( storage ) ;
391+ expect ( result ) . toMatch ( / I n t r o \n \n / ) ;
392+ expect ( result ) . toMatch ( / \n \n ` ` ` p y t h o n \n / ) ;
393+ expect ( result ) . toMatch ( / \n ` ` ` \n \n / ) ;
394+ expect ( result ) . toMatch ( / \n \n O u t r o / ) ;
395+ } ) ;
396+
397+ test ( 'should separate code block (no language) from surrounding content with blank lines' , ( ) => {
398+ 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>' ;
399+ const result = client . storageToMarkdown ( storage ) ;
400+ expect ( result ) . toMatch ( / B e f o r e \n \n / ) ;
401+ expect ( result ) . toMatch ( / \n \n ` ` ` \n / ) ;
402+ expect ( result ) . toMatch ( / \n ` ` ` \n \n / ) ;
403+ expect ( result ) . toMatch ( / \n \n A f t e r / ) ;
404+ } ) ;
405+
406+ test ( 'should separate mermaid macro from surrounding content with blank lines' , ( ) => {
407+ 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>' ;
408+ const result = client . storageToMarkdown ( storage ) ;
409+ expect ( result ) . toMatch ( / D i a g r a m : \n \n / ) ;
410+ expect ( result ) . toMatch ( / \n \n ` ` ` m e r m a i d \n / ) ;
411+ expect ( result ) . toMatch ( / \n ` ` ` \n \n / ) ;
412+ expect ( result ) . toMatch ( / \n \n E n d / ) ;
413+ } ) ;
414+
415+ test ( 'complex page: heading, multi-line paragraph, code block, ordered list' , ( ) => {
416+ const storage = [
417+ '<h1>Deployment Guide</h1>' ,
418+ '<p>Deploy using the following steps.\nEnsure prerequisites are met.</p>' ,
419+ '<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>' ,
420+ '<p>Then verify:</p>' ,
421+ '<ol><li>Check logs</li><li>Run smoke tests</li></ol>' ,
422+ '<p>Deployment complete.</p>'
423+ ] . join ( '' ) ;
424+ const result = client . storageToMarkdown ( storage ) ;
425+ expect ( result ) . toBe (
426+ '# Deployment Guide\n\n' +
427+ 'Deploy using the following steps.\nEnsure prerequisites are met.\n\n' +
428+ '```bash\ngit pull origin main\nnpm run build\n```\n\n' +
429+ 'Then verify:\n\n' +
430+ '1. Check logs\n2. Run smoke tests\n\n' +
431+ 'Deployment complete.'
432+ ) ;
433+ } ) ;
434+
388435 test ( 'should convert Confluence macros to admonitions' , ( ) => {
389436 const storage = '<ac:structured-macro ac:name="info"><ac:rich-text-body><p>This is info</p></ac:rich-text-body></ac:structured-macro>' ;
390437 const result = client . storageToMarkdown ( storage ) ;
@@ -428,6 +475,64 @@ describe('ConfluenceClient', () => {
428475 expect ( result ) . toContain ( '| Cell |' ) ;
429476 } ) ;
430477
478+ test ( 'should preserve content of multi-line paragraphs' , ( ) => {
479+ // Without the dotAll flag on the <p> regex, content with embedded newlines is silently dropped
480+ const html = '<p>First line\nSecond line</p>' ;
481+ const result = client . htmlToMarkdown ( html ) ;
482+ expect ( result ) . toContain ( 'First line' ) ;
483+ expect ( result ) . toContain ( 'Second line' ) ;
484+ } ) ;
485+
486+ test ( 'should separate consecutive paragraphs with a blank line' , ( ) => {
487+ const html = '<p>Alpha</p><p>Beta</p>' ;
488+ const result = client . htmlToMarkdown ( html ) ;
489+ expect ( result ) . toMatch ( / A l p h a \n \n B e t a / ) ;
490+ } ) ;
491+
492+ test ( 'should separate lists from surrounding content with blank lines' , ( ) => {
493+ const html = '<p>Intro</p><ul><li>Item A</li><li>Item B</li></ul><p>Outro</p>' ;
494+ const result = client . htmlToMarkdown ( html ) ;
495+ expect ( result ) . toMatch ( / I n t r o \n \n / ) ;
496+ expect ( result ) . toMatch ( / \n \n - I t e m A \n - I t e m B \n \n / ) ;
497+ expect ( result ) . toMatch ( / \n \n O u t r o / ) ;
498+ } ) ;
499+
500+ test ( 'should separate ordered lists from surrounding content with blank lines' , ( ) => {
501+ const html = '<p>Steps:</p><ol><li>First</li><li>Second</li></ol><p>Done</p>' ;
502+ const result = client . htmlToMarkdown ( html ) ;
503+ expect ( result ) . toMatch ( / S t e p s : \n \n / ) ;
504+ expect ( result ) . toMatch ( / \n \n 1 \. F i r s t \n 2 \. S e c o n d \n \n / ) ;
505+ expect ( result ) . toMatch ( / \n \n D o n e / ) ;
506+ } ) ;
507+
508+ test ( 'should separate tables from surrounding content with blank lines' , ( ) => {
509+ const html = '<p>See table:</p><table><tr><th>Col</th></tr><tr><td>Val</td></tr></table><p>End</p>' ;
510+ const result = client . htmlToMarkdown ( html ) ;
511+ expect ( result ) . toMatch ( / S e e t a b l e : \n \n / ) ;
512+ expect ( result ) . toMatch ( / \| C o l \| / ) ;
513+ expect ( result ) . toMatch ( / \n \n E n d / ) ;
514+ } ) ;
515+
516+ test ( 'complex page: heading, multi-line paragraph, table, list' , ( ) => {
517+ const html = [
518+ '<h2>API Reference</h2>' ,
519+ '<p>The following endpoints are available.\nAll requests require authentication.</p>' ,
520+ '<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>' ,
521+ '<p>Authentication options:</p>' ,
522+ '<ul><li>Bearer token</li><li>API key</li></ul>' ,
523+ '<p>See docs for details.</p>'
524+ ] . join ( '' ) ;
525+ const result = client . htmlToMarkdown ( html ) ;
526+ expect ( result ) . toBe (
527+ '## API Reference\n\n' +
528+ 'The following endpoints are available.\nAll requests require authentication.\n\n' +
529+ '| Method | Path |\n| --- | --- |\n| GET | /users |\n| POST | /users |\n\n' +
530+ 'Authentication options:\n\n' +
531+ '- Bearer token\n- API key\n\n' +
532+ 'See docs for details.'
533+ ) ;
534+ } ) ;
535+
431536 test ( 'should convert named characters correctly' , ( ) => {
432537 const NAMED_ENTITIES = ConfluenceClient . NAMED_ENTITIES ;
433538
0 commit comments