Skip to content

Commit 713424c

Browse files
committed
fix(glowm): extra blank lines in nested list output
1 parent bb88b5c commit 713424c

File tree

3 files changed

+113
-0
lines changed

3 files changed

+113
-0
lines changed

tools/glowm/glowm.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
7197
describe('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+
})

tools/glowm/glowm.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
addBlockquotePipe,
99
addCodeBlockBox,
1010
addIndent,
11+
collapseNestedListBlanks,
1112
fixCheckboxSpacing,
1213
fixListInlineTokens,
1314
INDENT,
@@ -25,6 +26,7 @@ export {
2526
addBlockquotePipe,
2627
addCodeBlockBox,
2728
addIndent,
29+
collapseNestedListBlanks,
2830
fixCheckboxSpacing,
2931
fixListInlineTokens,
3032
styleH1,
@@ -49,6 +51,7 @@ async function main(): Promise<void> {
4951
addCodeBlockBox(ext)
5052
fixCheckboxSpacing(ext)
5153
useCheckmark(ext)
54+
collapseNestedListBlanks(ext)
5255
styleImage(ext)
5356
marked.use(ext)
5457

tools/glowm/lib/renderers.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,24 @@ export function fixCheckboxSpacing(ext: TerminalExtension): void {
6565
}
6666
}
6767

68+
/**
69+
* Collapses extra blank lines in nested list output. marked-terminal's
70+
* section() appends \n\n after each block, including nested sub-lists.
71+
* Lines with only whitespace + ANSI codes survive bulletPointLines'
72+
* filter(identity) since they're truthy strings.
73+
*/
74+
export function collapseNestedListBlanks(ext: TerminalExtension): void {
75+
const orig = getRenderer(ext, 'list')
76+
ext.renderer.list = function (token: Tokens.List) {
77+
const result = orig.call(this, token)
78+
if (!result) return result
79+
return result
80+
.split('\n')
81+
.filter(line => !!line.replace(ANSI_REGEX, '').trim())
82+
.join('\n') + '\n\n'
83+
}
84+
}
85+
6886
/** Replaces [X] with [✓] for completed checkboxes. */
6987
export function useCheckmark(ext: TerminalExtension): void {
7088
const orig = getRenderer(ext, 'list')

0 commit comments

Comments
 (0)