Monorepo for Tangled
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}