@@ -10,6 +10,7 @@ import {
1010 addBlockquotePipe ,
1111 addCodeBlockBox ,
1212 addIndent ,
13+ collapseNestedListBlanks ,
1314 fixCheckboxSpacing ,
1415 fixListInlineTokens ,
1516 replaceMermaidBlocks ,
@@ -68,6 +69,31 @@ function renderMarkdownWithImageStyle(md: string): string {
6869 return instance . parse ( md ) as string
6970}
7071
72+ function renderMarkdownWithNestedListFix ( md : string ) : string {
73+ const instance = new Marked ( )
74+ const extension = markedTerminal ( { width : 80 , tab : 2 } )
75+ collapseNestedListBlanks ( extension )
76+ instance . use ( extension )
77+ return instance . parse ( md ) as string
78+ }
79+
80+ function countBlankLines ( output : string ) : number {
81+ const lines = output . split ( '\n' )
82+ return lines . filter ( l => ! l . replace ( / \x1b \[ [ 0 - 9 ; ] * m / g, '' ) . trim ( ) ) . length
83+ }
84+
85+ function countBlankLinesBetween ( output : string , textA : string , textB : string ) : number {
86+ const lines = output . split ( '\n' )
87+ const idxA = lines . findIndex ( l => stripAnsi ( l ) . includes ( textA ) )
88+ const idxB = lines . findIndex ( l => stripAnsi ( l ) . includes ( textB ) )
89+ if ( idxA === - 1 || idxB === - 1 ) return - 1
90+ let blanks = 0
91+ for ( let i = idxA + 1 ; i < idxB ; i ++ ) {
92+ if ( ! lines [ i ] ! . replace ( / \x1b \[ [ 0 - 9 ; ] * m / g, '' ) . trim ( ) ) blanks ++
93+ }
94+ return blanks
95+ }
96+
7197describe ( 'replaceMermaidBlocks' , ( ) => {
7298 test ( 'replaces a single mermaid block with ASCII art' , ( ) => {
7399 const input = '```mermaid\ngraph LR\n A --> B\n```'
@@ -410,3 +436,69 @@ describe('styleImage', () => {
410436 expect ( plain ) . toContain ( 'https://example.com/badge.svg' )
411437 } )
412438} )
439+
440+ describe ( 'collapseNestedListBlanks' , ( ) => {
441+ test ( 'no blank lines between parent and sub-items' , ( ) => {
442+ const output = renderMarkdownWithNestedListFix ( '- Parent\n - Child one\n - Child two' )
443+ const blanks = countBlankLinesBetween ( output , 'Parent' , 'Child one' )
444+
445+ expect ( blanks ) . toBe ( 0 )
446+ } )
447+
448+ test ( 'no blank lines between sibling sub-items' , ( ) => {
449+ const output = renderMarkdownWithNestedListFix ( '- Parent\n - Child one\n - Child two' )
450+ const blanks = countBlankLinesBetween ( output , 'Child one' , 'Child two' )
451+
452+ expect ( blanks ) . toBe ( 0 )
453+ } )
454+
455+ test ( 'no blank lines with deeply nested lists' , ( ) => {
456+ const md = '- A\n - B\n - C'
457+ const output = renderMarkdownWithNestedListFix ( md )
458+
459+ expect ( countBlankLinesBetween ( output , 'A' , 'B' ) ) . toBe ( 0 )
460+ expect ( countBlankLinesBetween ( output , 'B' , 'C' ) ) . toBe ( 0 )
461+ } )
462+
463+ test ( 'fewer blank lines than unfixed output' , ( ) => {
464+ const md = '- Parent\n - Child one\n - Child two\n- Another\n - Sub A'
465+ const instance = new Marked ( )
466+ const ext = markedTerminal ( { width : 80 , tab : 2 } )
467+ instance . use ( ext )
468+ const unfixed = instance . parse ( md ) as string
469+ const fixed = renderMarkdownWithNestedListFix ( md )
470+
471+ expect ( countBlankLines ( fixed ) ) . toBeLessThan ( countBlankLines ( unfixed ) )
472+ } )
473+
474+ test ( 'flat lists are unaffected' , ( ) => {
475+ const md = '- One\n- Two\n- Three'
476+ const fixed = renderMarkdownWithNestedListFix ( md )
477+ const plain = stripAnsi ( fixed )
478+
479+ expect ( plain ) . toContain ( 'One' )
480+ expect ( plain ) . toContain ( 'Two' )
481+ expect ( plain ) . toContain ( 'Three' )
482+ expect ( countBlankLinesBetween ( fixed , 'One' , 'Two' ) ) . toBe ( 0 )
483+ } )
484+
485+ test ( 'preserves trailing newlines for inter-block spacing' , ( ) => {
486+ const md = '- Item one\n- Item two'
487+ const output = renderMarkdownWithNestedListFix ( md )
488+
489+ // List output should end with \n\n for block separation
490+ expect ( output ) . toMatch ( / \n \n $ / )
491+ } )
492+
493+ test ( 'all list content is preserved' , ( ) => {
494+ const md = '- Parent\n - Child A\n - Child B\n - Deep\n- Sibling'
495+ const output = renderMarkdownWithNestedListFix ( md )
496+ const plain = stripAnsi ( output )
497+
498+ expect ( plain ) . toContain ( 'Parent' )
499+ expect ( plain ) . toContain ( 'Child A' )
500+ expect ( plain ) . toContain ( 'Child B' )
501+ expect ( plain ) . toContain ( 'Deep' )
502+ expect ( plain ) . toContain ( 'Sibling' )
503+ } )
504+ } )
0 commit comments