From 4078cf898c4bf54f3da56ad74393cd2d163657e9 Mon Sep 17 00:00:00 2001 From: Oliver Steele Date: Fri, 27 Feb 2026 02:04:38 +0800 Subject: [PATCH] fix: handle HTML void elements in markdown attribute processing (#66)
,
, , and other HTML void elements inside markdown="1" blocks caused an "unexpected EOF" error because the HTML tokenizer depth tracker incremented on their start tags but never saw matching end tags. Skip depth increment for void elements. --- renderers/markdown_attrs.go | 31 +++++++++++++++++++++++++++---- renderers/markdown_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/renderers/markdown_attrs.go b/renderers/markdown_attrs.go index 705aeb4..9514a93 100644 --- a/renderers/markdown_attrs.go +++ b/renderers/markdown_attrs.go @@ -4,11 +4,34 @@ import ( "bytes" "io" "regexp" + "strings" "github.com/osteele/gojekyll/utils" "golang.org/x/net/html" ) +// HTML void elements that are self-closing and never have an end tag. +// See https://html.spec.whatwg.org/multipage/syntax.html#void-elements +var htmlVoidElements = map[string]bool{ + "area": true, "base": true, "br": true, "col": true, + "embed": true, "hr": true, "img": true, "input": true, + "link": true, "meta": true, "param": true, "source": true, + "track": true, "wbr": true, +} + +// isVoidElement returns true if the raw token bytes represent a void element. +func isVoidElement(raw []byte) bool { + s := strings.TrimLeft(string(raw), "< ") + // Extract tag name (up to space, /, or >) + for i, c := range s { + if c == ' ' || c == '/' || c == '>' || c == '\t' || c == '\n' { + s = s[:i] + break + } + } + return htmlVoidElements[strings.ToLower(s)] +} + var markdownAttrRE = regexp.MustCompile(`\s*markdown\s*=[^\s>]*\s*`) // Used inside markdown=1. @@ -107,12 +130,12 @@ loop: if err == io.EOF { return utils.WrapError(err, "unexpected EOF while processing markdown attribute. "+ - "Common causes: unclosed HTML tags (use
instead of
), "+ + "Common causes: unclosed HTML tags, "+ "or mismatched opening/closing tags") } return err case html.StartTagToken: - if !notATagRE.Match(z.Raw()) { + if !notATagRE.Match(z.Raw()) && !isVoidElement(z.Raw()) { depth++ } case html.EndTagToken: @@ -179,12 +202,12 @@ loop: if err == io.EOF { return utils.WrapError(err, "unexpected EOF while processing markdown=\"0\" attribute. "+ - "Common causes: unclosed HTML tags (use
instead of
), "+ + "Common causes: unclosed HTML tags, "+ "or mismatched opening/closing tags") } return err case html.StartTagToken: - if !notATagRE.Match(z.Raw()) { + if !notATagRE.Match(z.Raw()) && !isVoidElement(z.Raw()) { depth++ } case html.EndTagToken: diff --git a/renderers/markdown_test.go b/renderers/markdown_test.go index 33aafc4..6082570 100644 --- a/renderers/markdown_test.go +++ b/renderers/markdown_test.go @@ -102,6 +102,30 @@ func TestDeIndentHTMLBlocks(t *testing.T) { } } +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 + // tracker must not increment for them. + + // These should not panic or error (the main fix for #66) + result := mustMarkdownString("\n
\n
\n
\n
\n") + require.Contains(t, result, "\n
\n\n") + require.Contains(t, result, "\n\n\n") + require.Contains(t, result, "img") + + // Self-closing variants should also work + result = mustMarkdownString("\n
\n
\n
\n") + require.Contains(t, result, "\n
\n
\ntext\n\n") + require.Contains(t, result, "text") +} + func mustMarkdownString(md string) string { s, err := renderMarkdown([]byte(md)) if err != nil {