Monorepo for Tangled tangled.org

appview/pages/markup: smart commit autolink renderer

Implemented tangled link extension. This can be extended for other link
types like issue/pull references in future.

The `tangled.org` host is hardcoded right now.

Close: <https://tangled.org/tangled.org/core/issues/382>

Signed-off-by: Seongmin Lee <git@boltless.me>

boltless.me 3fade21a 96413285

verified
+166
+118
appview/pages/markup/extension/tangledlink.go
··· 1 + package extension 2 + 3 + import ( 4 + "net/url" 5 + "strings" 6 + 7 + "github.com/yuin/goldmark" 8 + "github.com/yuin/goldmark/ast" 9 + "github.com/yuin/goldmark/parser" 10 + "github.com/yuin/goldmark/renderer" 11 + "github.com/yuin/goldmark/text" 12 + "github.com/yuin/goldmark/util" 13 + ) 14 + 15 + const ( 16 + tangledCommitLinkAttr = "tangled-commit-link" 17 + ) 18 + 19 + type tangledLinkTransformer struct { 20 + host string 21 + } 22 + 23 + var _ parser.ASTTransformer = new(tangledLinkTransformer) 24 + 25 + // Transform implements [parser.ASTTransformer]. 26 + func (t *tangledLinkTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { 27 + ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 28 + if !entering { 29 + return ast.WalkContinue, nil 30 + } 31 + 32 + link, ok := n.(*ast.AutoLink) 33 + if !ok { 34 + return ast.WalkContinue, nil 35 + } 36 + 37 + dest := string(link.URL(reader.Source())) 38 + if sha := t.parseLinkCommitSha(dest); sha != "" { 39 + link.SetAttributeString(tangledCommitLinkAttr, []byte(sha)) 40 + } 41 + 42 + return ast.WalkContinue, nil 43 + }) 44 + } 45 + 46 + func (t *tangledLinkTransformer) parseLinkCommitSha(raw string) string { 47 + u, err := url.Parse(raw) 48 + if err != nil || u.Host != "tangled.org" { 49 + return "" 50 + } 51 + 52 + // /{owner}/{repo}/commit/<sha> 53 + parts := strings.Split(strings.Trim(u.Path, "/"), "/") 54 + if len(parts) != 4 || parts[2] != "commit" { 55 + return "" 56 + } 57 + 58 + sha := parts[3] 59 + 60 + // basic sha validation 61 + if len(sha) < 7 { 62 + return "" 63 + } 64 + for _, c := range sha { 65 + if !strings.ContainsRune("0123456789abcdef", c) { 66 + return "" 67 + } 68 + } 69 + 70 + return sha[:8] 71 + } 72 + 73 + type tangledLinkRenderer struct{} 74 + 75 + var _ renderer.NodeRenderer = new(tangledLinkRenderer) 76 + 77 + // RegisterFuncs implements [renderer.NodeRenderer]. 78 + func (r *tangledLinkRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 79 + reg.Register(ast.KindAutoLink, r.renderAutoLink) 80 + } 81 + 82 + func (r *tangledLinkRenderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 83 + link := node.(*ast.AutoLink) 84 + sha, ok := link.AttributeString(tangledCommitLinkAttr) 85 + if !ok { 86 + return ast.WalkContinue, nil 87 + } 88 + 89 + if entering { 90 + w.WriteString(`<a href="`) 91 + w.Write(link.URL(source)) 92 + w.WriteString(`"><code>`) 93 + w.Write(sha.([]byte)) 94 + return ast.WalkSkipChildren, nil 95 + } else { 96 + w.WriteString("</code></a>") 97 + } 98 + return ast.WalkContinue, nil 99 + } 100 + 101 + type tangledLinkExt struct { 102 + host string 103 + } 104 + 105 + var _ goldmark.Extender = new(tangledLinkExt) 106 + 107 + func (e *tangledLinkExt) Extend(m goldmark.Markdown) { 108 + m.Parser().AddOptions(parser.WithASTTransformers( 109 + util.Prioritized(&tangledLinkTransformer{host: e.host}, 500), 110 + )) 111 + m.Renderer().AddOptions(renderer.WithNodeRenderers( 112 + util.Prioritized(&tangledLinkRenderer{}, 500), 113 + )) 114 + } 115 + 116 + func NewTangledLinkExt(host string) goldmark.Extender { 117 + return &tangledLinkExt{host} 118 + }
+1
appview/pages/markup/markdown.go
··· 67 67 ), 68 68 callout.CalloutExtention, 69 69 textension.AtExt, 70 + textension.NewTangledLinkExt("tangled.org"), 70 71 emoji.Emoji, 71 72 ), 72 73 goldmark.WithParserOptions(