forked from
tangled.org/core
Monorepo for Tangled
1package extension
2
3import (
4 "fmt"
5 "io/fs"
6 "path/filepath"
7 "strings"
8
9 "github.com/yuin/goldmark"
10 "github.com/yuin/goldmark/ast"
11 "github.com/yuin/goldmark/parser"
12 "github.com/yuin/goldmark/renderer"
13 "github.com/yuin/goldmark/text"
14 "github.com/yuin/goldmark/util"
15)
16
17// KindCodeCopyBlock is a NodeKind of the CodeCopyBlock node.
18var KindCodeCopyBlock = ast.NewNodeKind("CodeCopyBlock")
19
20// CodeCopyBlock wraps a FencedCodeBlock to add a copy-to-clipboard button.
21type CodeCopyBlock struct {
22 ast.BaseBlock
23}
24
25var _ ast.Node = (*CodeCopyBlock)(nil)
26
27func (n *CodeCopyBlock) Kind() ast.NodeKind {
28 return KindCodeCopyBlock
29}
30
31func (n *CodeCopyBlock) Dump(source []byte, level int) {
32 ast.DumpHelper(n, source, level, nil, nil)
33}
34
35// codeCopyTransformer wraps FencedCodeBlock nodes in CodeCopyBlock nodes.
36type codeCopyTransformer struct{}
37
38func (t *codeCopyTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
39 var fencedBlocks []*ast.FencedCodeBlock
40
41 _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
42 if !entering {
43 return ast.WalkContinue, nil
44 }
45 if fcb, ok := n.(*ast.FencedCodeBlock); ok {
46 lang := string(fcb.Language(reader.Source()))
47 if lang != "mermaid" {
48 fencedBlocks = append(fencedBlocks, fcb)
49 }
50 }
51 return ast.WalkContinue, nil
52 })
53
54 for _, fcb := range fencedBlocks {
55 wrapper := &CodeCopyBlock{}
56 parent := fcb.Parent()
57 parent.ReplaceChild(parent, fcb, wrapper)
58 wrapper.AppendChild(wrapper, fcb)
59 }
60}
61
62// codeCopyRenderer renders the CodeCopyBlock wrapper with a copy button.
63type codeCopyRenderer struct {
64 copyIconSVG string
65 checkIconSVG string
66}
67
68func (r *codeCopyRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
69 reg.Register(KindCodeCopyBlock, r.renderCodeCopyBlock)
70}
71
72func (r *codeCopyRenderer) renderCodeCopyBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
73 if entering {
74 _, _ = w.WriteString(`<div class="code-copy-wrapper">`)
75 } else {
76 _, _ = w.WriteString(fmt.Sprintf(
77 `<button class="code-copy-btn" type="button" aria-label="Copy code to clipboard">`+
78 `<span class="copy-icon">%s</span>`+
79 `<span class="check-icon">%s</span>`+
80 `</button></div>`,
81 r.copyIconSVG, r.checkIconSVG,
82 ))
83 }
84 return ast.WalkContinue, nil
85}
86
87// CodeCopyExt is a goldmark extension that adds copy-to-clipboard buttons to fenced code blocks.
88type CodeCopyExt struct {
89 files fs.FS
90}
91
92func NewCodeCopyExt(files fs.FS) *CodeCopyExt {
93 return &CodeCopyExt{files: files}
94}
95
96func (e *CodeCopyExt) Extend(md goldmark.Markdown) {
97 copyIcon := readIcon(e.files, "copy", "size-4")
98 checkIcon := readIcon(e.files, "check", "size-4")
99
100 md.Parser().AddOptions(
101 parser.WithASTTransformers(
102 util.Prioritized(&codeCopyTransformer{}, 500),
103 ),
104 )
105 md.Renderer().AddOptions(
106 renderer.WithNodeRenderers(
107 util.Prioritized(&codeCopyRenderer{
108 copyIconSVG: copyIcon,
109 checkIconSVG: checkIcon,
110 }, 500),
111 ),
112 )
113}
114
115func readIcon(files fs.FS, name string, classes ...string) string {
116 if files == nil {
117 return ""
118 }
119
120 iconPath := filepath.Join("static", "icons", name+".svg")
121 data, err := fs.ReadFile(files, iconPath)
122 if err != nil {
123 return ""
124 }
125
126 svgStr := string(data)
127 svgTagEnd := strings.Index(svgStr, ">")
128 if svgTagEnd == -1 {
129 return svgStr
130 }
131
132 classTag := ` class="` + strings.Join(classes, " ") + `"`
133 return svgStr[:svgTagEnd] + classTag + svgStr[svgTagEnd:]
134}