Monorepo for Tangled
at add-code-block-copy-button 134 lines 3.5 kB view raw
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}