From 008fa3e10e1c1c555c28c1777e9abd3c18d9c78a Mon Sep 17 00:00:00 2001 From: WOLIKIMCHENG <35391914+WOLIKIMCHENG@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:10:10 +0800 Subject: [PATCH] fix(parser): preserve loose body text in HTMLParser (#17290) --- internal/parser/parser/html_parser.go | 51 ++++++---- .../parser/parser/parse_with_result_test.go | 95 ++++++++++++++++++- 2 files changed, 124 insertions(+), 22 deletions(-) diff --git a/internal/parser/parser/html_parser.go b/internal/parser/parser/html_parser.go index a6c831bdc2..f613528a70 100644 --- a/internal/parser/parser/html_parser.go +++ b/internal/parser/parser/html_parser.go @@ -103,11 +103,17 @@ func (p *HTMLParser) ParseWithResult(ctx context.Context, filename string, data // walkHTMLBlocks emits one normalized item per block-level // descendant of root. Inline elements (b, i, a, span, …) are -// collapsed into the parent's text via leafText. + + +Tail text +`) + res := p.ParseWithResult(ctx, "doc.html", src) + if res.Err != nil { + t.Fatalf("ParseWithResult: %v", res.Err) + } + want := []struct { + text string + ckType string + }{ + {"Intro text", "text"}, + {"Title", "heading"}, + {"Between blocks", "text"}, + {"Body inline.", "paragraph"}, + {"Tail text", "text"}, + } + if len(res.JSON) != len(want) { + t.Fatalf("JSON len = %d, want %d: %#v", len(res.JSON), len(want), res.JSON) + } + for i, w := range want { + if got := res.JSON[i]["text"]; got != w.text { + t.Errorf("JSON[%d].text = %v, want %v", i, got, w.text) + } + if got := res.JSON[i]["doc_type_kwd"]; got != "text" { + t.Errorf("JSON[%d].doc_type_kwd = %v, want text", i, got) + } + if got := res.JSON[i]["ck_type"]; got != w.ckType { + t.Errorf("JSON[%d].ck_type = %v, want %v", i, got, w.ckType) + } + } +} + +func TestHTMLParser_ParseWithResult_PreservesLooseTextWithoutExplicitBody(t *testing.T) { + ctx := t.Context() + p := NewHTMLParser() + src := []byte(` +Intro text +

Title

+Tail text`) + res := p.ParseWithResult(ctx, "doc.html", src) + if res.Err != nil { + t.Fatalf("ParseWithResult: %v", res.Err) + } + want := []string{"Intro text", "Title", "Tail text"} + if len(res.JSON) != len(want) { + t.Fatalf("JSON len = %d, want %d: %#v", len(res.JSON), len(want), res.JSON) + } + for i, text := range want { + if got := res.JSON[i]["text"]; got != text { + t.Errorf("JSON[%d].text = %v, want %v", i, got, text) + } + } +} + +func TestWalkHTMLBlocks_SkipsHeadLooseText(t *testing.T) { + head := &html.Node{Type: html.ElementNode, Data: "head"} + head.AppendChild(&html.Node{Type: html.TextNode, Data: "Head metadata"}) + + var items []map[string]any + walkHTMLBlocks(head, &items) + if len(items) != 0 { + t.Fatalf("JSON len = %d, want 0: %#v", len(items), items) + } +} + // TestHTMLParser_ParseWithResult_SkipsScriptAndStyle pins the -// rule that +

Also visible.

Also visible.

`) res := p.ParseWithResult(ctx, "doc.html", src) if res.Err != nil { t.Fatalf("ParseWithResult: %v", res.Err) } - if len(res.JSON) != 2 { - t.Errorf("JSON len = %d, want 2 (script+style skipped)", len(res.JSON)) + if len(res.JSON) != 3 { + t.Errorf("JSON len = %d, want 3 (script+style+noscript skipped)", len(res.JSON)) } for _, it := range res.JSON { - if txt, _ := it["text"].(string); strings.Contains(txt, "alert") || strings.Contains(txt, "color") { + if txt, _ := it["text"].(string); strings.Contains(txt, "alert") || + strings.Contains(txt, "color") || + strings.Contains(txt, "inline alert") || + strings.Contains(txt, "blue") || + strings.Contains(txt, "fallback") { t.Errorf("item text leaks script/style content: %q", txt) } }