From 6dbaf7f9de4a9d1f8c9a2b27a20c3e836e84e5c7 Mon Sep 17 00:00:00 2001 From: BrookJeynes Date: Fri, 16 May 2025 12:54:59 +1000 Subject: [PATCH] appview: markdown: introduce post-processor step to transform image links This adds a post-processor step to the markdown parser to parse raw html images `` and resolve their `src` attributes if needed --- appview/pages/markup/camo.go | 15 +++-- appview/pages/markup/markdown.go | 99 ++++++++++++++++++++++++++++---- 2 files changed, 99 insertions(+), 15 deletions(-) diff --git a/appview/pages/markup/camo.go b/appview/pages/markup/camo.go index 3365d3a..9995f72 100644 --- a/appview/pages/markup/camo.go +++ b/appview/pages/markup/camo.go @@ -17,15 +17,20 @@ func generateCamoURL(baseURL, secret, imageURL string) string { return fmt.Sprintf("%s/%s/%s", baseURL, signature, hexURL) } -func (rctx *RenderContext) camoImageLinkTransformer(img *ast.Image) { +func (rctx *RenderContext) camoImageLinkTransformer(dst string) string { // don't camo on dev if rctx.IsDev { - return + return dst } - dst := string(img.Destination) - if rctx.CamoUrl != "" && rctx.CamoSecret != "" { - img.Destination = []byte(generateCamoURL(rctx.CamoUrl, rctx.CamoSecret, dst)) + return generateCamoURL(rctx.CamoUrl, rctx.CamoSecret, dst) } + + return dst +} + +func (rctx *RenderContext) camoImageLinkAstTransformer(img *ast.Image) { + dst := string(img.Destination) + img.Destination = []byte(rctx.camoImageLinkTransformer(dst)) } diff --git a/appview/pages/markup/markdown.go b/appview/pages/markup/markdown.go index 5d3a3a7..1c13791 100644 --- a/appview/pages/markup/markdown.go +++ b/appview/pages/markup/markdown.go @@ -3,8 +3,11 @@ package markup import ( "bytes" + "fmt" + "io" "net/url" "path" + "strings" "github.com/microcosm-cc/bluemonday" "github.com/yuin/goldmark" @@ -14,6 +17,7 @@ import ( "github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" + htmlparse "golang.org/x/net/html" "tangled.sh/tangled.sh/core/appview/pages/repoinfo" ) @@ -61,7 +65,79 @@ func (rctx *RenderContext) RenderMarkdown(source string) string { if err := md.Convert([]byte(source), &buf); err != nil { return source } - return buf.String() + + var processed strings.Builder + if err := postProcess(rctx, strings.NewReader(buf.String()), &processed); err != nil { + return source + } + + return processed.String() +} + +func postProcess(ctx *RenderContext, input io.Reader, output io.Writer) error { + node, err := htmlparse.Parse(io.MultiReader( + strings.NewReader(""), + input, + strings.NewReader(""), + )) + if err != nil { + return fmt.Errorf("failed to parse html: %w", err) + } + + if node.Type == htmlparse.DocumentNode { + node = node.FirstChild + } + + visitNode(ctx, node) + + newNodes := make([]*htmlparse.Node, 0, 5) + + if node.Data == "html" { + node = node.FirstChild + for node != nil && node.Data != "body" { + node = node.NextSibling + } + } + if node != nil { + if node.Data == "body" { + child := node.FirstChild + for child != nil { + newNodes = append(newNodes, child) + child = child.NextSibling + } + } else { + newNodes = append(newNodes, node) + } + } + + for _, node := range newNodes { + if err := htmlparse.Render(output, node); err != nil { + return fmt.Errorf("failed to render processed html: %w", err) + } + } + + return nil +} + +func visitNode(ctx *RenderContext, node *htmlparse.Node) { + switch node.Type { + case htmlparse.ElementNode: + if node.Data == "img" { + for i, attr := range node.Attr { + if attr.Key != "src" { + continue + } + attr.Val = ctx.imageFromKnotTransformer(attr.Val) + attr.Val = ctx.camoImageLinkTransformer(attr.Val) + node.Attr[i] = attr + } + } + + for n := node.FirstChild; n != nil; n = n.NextSibling { + visitNode(ctx, n) + } + default: + } } func (rctx *RenderContext) Sanitize(html string) string { @@ -101,14 +177,14 @@ func (a *MarkdownTransformer) Transform(node *ast.Document, reader text.Reader, case *ast.Link: a.rctx.relativeLinkTransformer(n) case *ast.Image: - a.rctx.imageFromKnotTransformer(n) - a.rctx.camoImageLinkTransformer(n) + a.rctx.imageFromKnotAstTransformer(n) + a.rctx.camoImageLinkAstTransformer(n) } case RendererTypeDefault: switch n := n.(type) { case *ast.Image: - a.rctx.imageFromKnotTransformer(n) - a.rctx.camoImageLinkTransformer(n) + a.rctx.imageFromKnotAstTransformer(n) + a.rctx.camoImageLinkAstTransformer(n) } } @@ -130,11 +206,9 @@ func (rctx *RenderContext) relativeLinkTransformer(link *ast.Link) { link.Destination = []byte(newPath) } -func (rctx *RenderContext) imageFromKnotTransformer(img *ast.Image) { - dst := string(img.Destination) - +func (rctx *RenderContext) imageFromKnotTransformer(dst string) string { if isAbsoluteUrl(dst) { - return + return dst } scheme := "https" @@ -155,7 +229,12 @@ func (rctx *RenderContext) imageFromKnotTransformer(img *ast.Image) { actualPath), } newPath := parsedURL.String() - img.Destination = []byte(newPath) + return newPath +} + +func (rctx *RenderContext) imageFromKnotAstTransformer(img *ast.Image) { + dst := string(img.Destination) + img.Destination = []byte(rctx.imageFromKnotTransformer(dst)) } // actualPath decides when to join the file path with the -- 2.51.2