diff --git a/CHANGELOG.md b/CHANGELOG.md index 331882a..5089c69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **GitHub Metadata Cache** (#43): Reuse successful GitHub repository metadata requests for ten minutes and coalesce concurrent requests +### Fixed + +- **HTML De-indenting in Code Fences**: `deIndentHTMLBlocks` no longer strips indentation from HTML-looking lines inside fenced code blocks, which mangled indented HTML code samples + ## [0.3.1] - 2026-02-27 ### Fixed diff --git a/renderers/markdown.go b/renderers/markdown.go index b9a3aff..63a4f0d 100644 --- a/renderers/markdown.go +++ b/renderers/markdown.go @@ -51,12 +51,67 @@ func createGoldmarkConverter() goldmark.Markdown { // (optionally preceded by up to 3 spaces) and ends at a blank line. var htmlBlockStartRE = regexp.MustCompile(`(?i)^\s{0,3} 0 && suffix[len(suffix)-1] == '\r' { + suffix = suffix[:len(suffix)-1] + } + return len(bytes.Trim(suffix, " \t")) == 0 +} + func deIndentHTMLBlocks(md []byte) []byte { lines := bytes.Split(md, []byte("\n")) result := make([][]byte, 0, len(lines)) inHTMLBlock := false + inCodeFence := false + var fenceMarker byte + fenceLength := 0 for _, line := range lines { + marker, length, suffix, isFence := codeFenceDelimiter(line) + if inCodeFence { + if isFence && marker == fenceMarker && length >= fenceLength && isCodeFenceClosingSuffix(suffix) { + inCodeFence = false + fenceMarker = 0 + fenceLength = 0 + } + result = append(result, line) + continue + } + if isFence { + inCodeFence = true + fenceMarker = marker + fenceLength = length + result = append(result, line) + continue + } if !inHTMLBlock { if htmlBlockStartRE.Match(line) { inHTMLBlock = true diff --git a/renderers/markdown_test.go b/renderers/markdown_test.go index 6082570..a864e02 100644 --- a/renderers/markdown_test.go +++ b/renderers/markdown_test.go @@ -93,6 +93,56 @@ func TestDeIndentHTMLBlocks(t *testing.T) { require.Contains(t, result, " outside") }, }, + { + name: "HTML inside backtick code fence untouched", + input: "```\n\n```\n", + check: func(t *testing.T, result string) { + require.Contains(t, result, "
  • item
  • ") + }, + }, + { + name: "HTML inside tilde code fence untouched", + input: "~~~\n
    \n indented\n
    \n~~~\n", + check: func(t *testing.T, result string) { + require.Contains(t, result, " indented") + }, + }, + { + name: "longer outer fence containing shorter fence lines", + input: "````html\n```\n\n```\n````\n", + check: func(t *testing.T, result string) { + require.Contains(t, result, "
  • item
  • ") + }, + }, + { + name: "backtick fence containing tilde fence lines", + input: "```\n~~~\n\n~~~\n```\n", + check: func(t *testing.T, result string) { + require.Contains(t, result, "
  • item
  • ") + }, + }, + { + name: "fence-like content with trailing text does not close", + input: "```\n```not-a-close\n\n```\n", + check: func(t *testing.T, result string) { + require.Contains(t, result, "
  • item
  • ") + }, + }, + { + name: "closing fence allows whitespace", + input: "```\r\ncode\r\n``` \t\r\n\r\n", + check: func(t *testing.T, result string) { + require.Contains(t, result, "\r\n
  • item
  • ") + }, + }, + { + name: "HTML block after code fence still de-indented", + input: "```\n

    code

    \n```\n\n\n", + check: func(t *testing.T, result string) { + require.Contains(t, result, "\n
  • item
  • ") + require.Contains(t, result, "

    code

    ") + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -102,6 +152,15 @@ func TestDeIndentHTMLBlocks(t *testing.T) { } } +func TestRenderMarkdownCodeFenceIndentation(t *testing.T) { + // HTML code samples inside fenced code blocks must keep their + // indentation; deIndentHTMLBlocks previously stripped it. + input := "```html\n\n```\n" + result := mustMarkdownString(input) + require.Contains(t, result, " <li>item</li>", + "code sample should keep its indentation") +} + func TestRenderMarkdownVoidElements(t *testing.T) { // Issue #66:
    tags inside markdown="1" blocks should not cause EOF errors. // Void elements like
    ,
    , don't have end tags, so the depth