From 746ef00eba20d9d106a354d5bf4e559b4230920d Mon Sep 17 00:00:00 2001 From: eti Date: Tue, 2 Jun 2026 17:26:34 +0200 Subject: [PATCH] appview/pages: use proper html parsing in splitCodeBlockSegments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit replace hand-rolled string scanning with golang.org/x/net/html ParseFragment, fixing three infinite-loop paths that triggered on malformed/unclosed
 tags and an off-by-one in line counting
(chroma always emits a trailing newline, so the old +1 overcounted
every code block by one line)

also remove the unused "markdown" template func — all prose rendering
goes through markdownSegments, readme, or description

Signed-off-by: eti 
---
 appview/pages/funcmap.go | 113 +++++++++++++++++----------------------
 1 file changed, 48 insertions(+), 65 deletions(-)

diff --git a/appview/pages/funcmap.go b/appview/pages/funcmap.go
index 4997d0bb..58f2560c 100644
--- a/appview/pages/funcmap.go
+++ b/appview/pages/funcmap.go
@@ -29,6 +29,8 @@ import (
 	"github.com/go-enry/go-enry/v2"
 	"github.com/yuin/goldmark"
 	emoji "github.com/yuin/goldmark-emoji"
+	htmlparse "golang.org/x/net/html"
+	"golang.org/x/net/html/atom"
 	"tangled.org/core/appview/cache"
 	"tangled.org/core/appview/db"
 	"tangled.org/core/appview/models"
@@ -305,13 +307,6 @@ func (p *Pages) funcMap() template.FuncMap {
 			}
 			return v.Slice(0, min(n, v.Len())).Interface()
 		},
-		"markdown": func(text string) template.HTML {
-			rctx := p.rctx.Clone()
-			rctx.RendererType = markup.RendererTypeDefault
-			htmlString := rctx.RenderMarkdown(text)
-			sanitized := rctx.SanitizeDefault(htmlString)
-			return template.HTML(sanitized)
-		},
 		"markdownSegments": func(text string) []MarkdownSegment {
 			rctx := p.rctx.Clone()
 			rctx.RendererType = markup.RendererTypeDefault
@@ -638,71 +633,59 @@ func (p *Pages) AvatarUrl(actor, size string) string {
 	return baseUrl
 }
 
-func splitCodeBlockSegments(html string) []MarkdownSegment {
-	var segments []MarkdownSegment
-	remaining := html
-	closeTag := `
` - - for { - idx := strings.Index(remaining, ``) - if openEnd == -1 { - remaining = remaining[idx:] - continue - } - openEnd += idx - openTag := remaining[idx : openEnd+1] +func splitCodeBlockSegments(rawHTML string) []MarkdownSegment { + nodes, err := htmlparse.ParseFragment(strings.NewReader(rawHTML), &htmlparse.Node{ + Type: htmlparse.ElementNode, + Data: "body", + DataAtom: atom.Body, + }) + if err != nil { + return []MarkdownSegment{{HTML: template.HTML(rawHTML)}} + } - if strings.Contains(openTag, `mermaid`) { - closeIdx := strings.Index(remaining[openEnd+1:], closeTag) - if closeIdx == -1 { - remaining = remaining[idx:] - continue + var segments []MarkdownSegment + var buf strings.Builder + for _, n := range nodes { + if n.Type == htmlparse.ElementNode && n.Data == "pre" { + if buf.Len() > 0 { + segments = append(segments, MarkdownSegment{HTML: template.HTML(buf.String())}) + buf.Reset() + } + _ = htmlparse.Render(&buf, n) + isMermaid := false + for _, attr := range n.Attr { + if attr.Key == "class" && strings.Contains(attr.Val, "mermaid") { + isMermaid = true + break + } } - closeIdx += openEnd + 1 - - if idx > 0 { - segments = append(segments, MarkdownSegment{HTML: template.HTML(remaining[:idx])}) + seg := MarkdownSegment{HTML: template.HTML(buf.String())} + if !isMermaid { + seg.IsCode = true + seg.Lines = countTextNewlines(n) } - - preEnd := closeIdx + len(closeTag) - preHTML := remaining[idx:preEnd] - segments = append(segments, MarkdownSegment{ - HTML: template.HTML(preHTML), - }) - remaining = remaining[preEnd:] - continue - } - - closeIdx := strings.Index(remaining[openEnd+1:], closeTag) - if closeIdx == -1 { - remaining = remaining[idx:] - continue + segments = append(segments, seg) + buf.Reset() + } else { + _ = htmlparse.Render(&buf, n) } - closeIdx += openEnd + 1 + } + if buf.Len() > 0 { + segments = append(segments, MarkdownSegment{HTML: template.HTML(buf.String())}) + } + return segments +} - if idx > 0 { - segments = append(segments, MarkdownSegment{HTML: template.HTML(remaining[:idx])}) +func countTextNewlines(n *htmlparse.Node) int { + count := 0 + for c := n.FirstChild; c != nil; c = c.NextSibling { + if c.Type == htmlparse.TextNode { + count += strings.Count(c.Data, "\n") + } else { + count += countTextNewlines(c) } - - preEnd := closeIdx + len(closeTag) - preHTML := remaining[idx:preEnd] - content := remaining[openEnd+1 : closeIdx] - lines := strings.Count(content, "\n") + 1 - segments = append(segments, MarkdownSegment{ - HTML: template.HTML(preHTML), - IsCode: true, - Lines: lines, - }) - remaining = remaining[preEnd:] } - return segments + return count } func (p *Pages) icon(name string, classes []string) (template.HTML, error) { -- 2.51.2