Monorepo for Tangled
at 9259beb17e5eb25f45d05c4d541415f1b6639829 149 lines 3.4 kB view raw
1package extension 2 3import ( 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// KindTangledLink is a NodeKind of the TangledLink node. 16var KindTangledLink = ast.NewNodeKind("TangledLink") 17 18type TangledLinkNode struct { 19 ast.BaseInline 20 Destination string 21 Commit *TangledCommitLink 22 // TODO: add more Tangled-link types 23} 24 25type TangledCommitLink struct { 26 Sha string 27} 28 29var _ ast.Node = new(TangledLinkNode) 30 31// Dump implements [ast.Node]. 32func (n *TangledLinkNode) Dump(source []byte, level int) { 33 ast.DumpHelper(n, source, level, nil, nil) 34} 35 36// Kind implements [ast.Node]. 37func (n *TangledLinkNode) Kind() ast.NodeKind { 38 return KindTangledLink 39} 40 41type tangledLinkTransformer struct { 42 host string 43} 44 45var _ parser.ASTTransformer = new(tangledLinkTransformer) 46 47// Transform implements [parser.ASTTransformer]. 48func (t *tangledLinkTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { 49 ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 50 if !entering { 51 return ast.WalkContinue, nil 52 } 53 54 var dest string 55 56 switch n := n.(type) { 57 case *ast.AutoLink: 58 dest = string(n.URL(reader.Source())) 59 case *ast.Link: 60 // maybe..? not sure 61 default: 62 return ast.WalkContinue, nil 63 } 64 65 if sha := t.parseLinkCommitSha(dest); sha != "" { 66 newLink := &TangledLinkNode{ 67 Destination: dest, 68 Commit: &TangledCommitLink{ 69 Sha: sha, 70 }, 71 } 72 n.Parent().ReplaceChild(n.Parent(), n, newLink) 73 } 74 75 return ast.WalkContinue, nil 76 }) 77} 78 79func (t *tangledLinkTransformer) parseLinkCommitSha(raw string) string { 80 u, err := url.Parse(raw) 81 if err != nil || u.Host != t.host { 82 return "" 83 } 84 85 // /{owner}/{repo}/commit/<sha> 86 parts := strings.Split(strings.Trim(u.Path, "/"), "/") 87 if len(parts) != 4 || parts[2] != "commit" { 88 return "" 89 } 90 91 sha := parts[3] 92 93 // basic sha validation 94 if len(sha) < 7 { 95 return "" 96 } 97 for _, c := range sha { 98 if !strings.ContainsRune("0123456789abcdef", c) { 99 return "" 100 } 101 } 102 103 return sha[:8] 104} 105 106type tangledLinkRenderer struct{} 107 108var _ renderer.NodeRenderer = new(tangledLinkRenderer) 109 110// RegisterFuncs implements [renderer.NodeRenderer]. 111func (r *tangledLinkRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 112 reg.Register(KindTangledLink, r.renderTangledLink) 113} 114 115func (r *tangledLinkRenderer) renderTangledLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 116 link := node.(*TangledLinkNode) 117 118 if link.Commit != nil { 119 if entering { 120 w.WriteString(`<a href="`) 121 w.WriteString(link.Destination) 122 w.WriteString(`"><code>`) 123 w.WriteString(link.Commit.Sha) 124 } else { 125 w.WriteString(`</code></a>`) 126 } 127 } 128 129 return ast.WalkContinue, nil 130} 131 132type tangledLinkExt struct { 133 host string 134} 135 136var _ goldmark.Extender = new(tangledLinkExt) 137 138func (e *tangledLinkExt) Extend(m goldmark.Markdown) { 139 m.Parser().AddOptions(parser.WithASTTransformers( 140 util.Prioritized(&tangledLinkTransformer{host: e.host}, 500), 141 )) 142 m.Renderer().AddOptions(renderer.WithNodeRenderers( 143 util.Prioritized(&tangledLinkRenderer{}, 500), 144 )) 145} 146 147func NewTangledLinkExt(host string) goldmark.Extender { 148 return &tangledLinkExt{host} 149}