From c8acccf37c7acdf58ee4d35580e22585fec16f9c Mon Sep 17 00:00:00 2001 From: fresh3nough Date: Mon, 23 Feb 2026 10:53:57 -0500 Subject: [PATCH] fix: handle indented code fences in markdown transformation (#1389) --- src/md.ts | 44 ++++++++++++++++++++++--------------------- test/md.test.ts | 50 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 21 deletions(-) diff --git a/src/md.ts b/src/md.ts index 3e88b95d75..ca7b83e3dc 100644 --- a/src/md.ts +++ b/src/md.ts @@ -26,29 +26,31 @@ export function transformMarkdown(buf: Buffer | string): string { for (const line of bufToString(buf).split(/\r?\n/)) { switch (state) { case 'root': + // Strip up to 3 leading spaces before checking for code fences (per CommonMark spec) + const stripped = line.replace(/^ {0,3}/, '') + const { fence, js, bash } = stripped.match(codeBlockRe)?.groups || {} + if (fence) { + codeBlockEnd = fence + if (js) { + state = 'js' + output.push('') + } else if (bash) { + state = 'bash' + output.push('await $`') + } else { + state = 'other' + output.push('') + } + break + } if (tabRe.test(line) && prevLineIsEmpty) { output.push(line) state = 'tab' continue } - const { fence, js, bash } = line.match(codeBlockRe)?.groups || {} - if (!fence) { - prevLineIsEmpty = line === '' - output.push('// ' + line) - continue - } - codeBlockEnd = fence - if (js) { - state = 'js' - output.push('') - } else if (bash) { - state = 'bash' - output.push('await $`') - } else { - state = 'other' - output.push('') - } - break + prevLineIsEmpty = line === '' + output.push('// ' + line) + continue case 'tab': if (line === '') { output.push('') @@ -60,7 +62,7 @@ export function transformMarkdown(buf: Buffer | string): string { } break case 'js': - if (line === codeBlockEnd) { + if (line.replace(/^ {0,3}/, '') === codeBlockEnd) { output.push('') state = 'root' } else { @@ -68,7 +70,7 @@ export function transformMarkdown(buf: Buffer | string): string { } break case 'bash': - if (line === codeBlockEnd) { + if (line.replace(/^ {0,3}/, '') === codeBlockEnd) { output.push('`') state = 'root' } else { @@ -76,7 +78,7 @@ export function transformMarkdown(buf: Buffer | string): string { } break case 'other': - if (line === codeBlockEnd) { + if (line.replace(/^ {0,3}/, '') === codeBlockEnd) { output.push('') state = 'root' } else { diff --git a/test/md.test.ts b/test/md.test.ts index 412940d6c4..e26bc1f1d4 100644 --- a/test/md.test.ts +++ b/test/md.test.ts @@ -69,4 +69,54 @@ echo foo // // `) }) + + test('transformMarkdown() handles indented code fences in list items (#1389)', () => { + // Code fences indented with up to 3 spaces (e.g. inside list items) should + // be recognized as fenced code blocks, not as tab-indented code. + const input = [ + '# h1', + '', + 'paragraph', + '', + '## h2', + '', + '### h3', + '', + '```bash', + 'echo "1"', + '```', + '', + '### h3', + '', + '- item 1', + '', + '', + ' ```bash', + ' echo "2"', + ' ```', + '', + '', + '### h3', + '', + '```bash', + 'echo "4"', + '```', + ].join('\n') + + const result = transformMarkdown(input) + + // The indented code fence must produce a valid $`` invocation, + // not raw backticks that cause "$(...) is not a function". + assert.ok( + !result.includes(' ```bash'), + 'indented fence opener should not appear as raw code' + ) + assert.ok( + result.includes('await $`'), + 'indented bash fence should produce await $`' + ) + // Verify all three bash blocks are converted + const bashBlocks = result.match(/await \$`/g) + assert.equal(bashBlocks?.length, 3, 'all three bash blocks should be converted') + }) })