package extension import ( "fmt" "io/fs" "path/filepath" "strings" "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" ) // KindCodeCopyBlock is a NodeKind of the CodeCopyBlock node. var KindCodeCopyBlock = ast.NewNodeKind("CodeCopyBlock") // CodeCopyBlock wraps a FencedCodeBlock to add a copy-to-clipboard button. type CodeCopyBlock struct { ast.BaseBlock } var _ ast.Node = (*CodeCopyBlock)(nil) func (n *CodeCopyBlock) Kind() ast.NodeKind { return KindCodeCopyBlock } func (n *CodeCopyBlock) Dump(source []byte, level int) { ast.DumpHelper(n, source, level, nil, nil) } // codeCopyTransformer wraps FencedCodeBlock nodes in CodeCopyBlock nodes. type codeCopyTransformer struct{} func (t *codeCopyTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { var fencedBlocks []*ast.FencedCodeBlock _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { return ast.WalkContinue, nil } if fcb, ok := n.(*ast.FencedCodeBlock); ok { lang := string(fcb.Language(reader.Source())) if lang != "mermaid" { fencedBlocks = append(fencedBlocks, fcb) } } return ast.WalkContinue, nil }) for _, fcb := range fencedBlocks { wrapper := &CodeCopyBlock{} parent := fcb.Parent() parent.ReplaceChild(parent, fcb, wrapper) wrapper.AppendChild(wrapper, fcb) } } // codeCopyRenderer renders the CodeCopyBlock wrapper with a copy button. type codeCopyRenderer struct { copyIconSVG string checkIconSVG string } func (r *codeCopyRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(KindCodeCopyBlock, r.renderCodeCopyBlock) } func (r *codeCopyRenderer) renderCodeCopyBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { if entering { _, _ = w.WriteString(`
`) } else { _, _ = w.WriteString(fmt.Sprintf( `
`, r.copyIconSVG, r.checkIconSVG, )) } return ast.WalkContinue, nil } // CodeCopyExt is a goldmark extension that adds copy-to-clipboard buttons to fenced code blocks. type CodeCopyExt struct { files fs.FS } func NewCodeCopyExt(files fs.FS) *CodeCopyExt { return &CodeCopyExt{files: files} } func (e *CodeCopyExt) Extend(md goldmark.Markdown) { copyIcon := readIcon(e.files, "copy", "size-4") checkIcon := readIcon(e.files, "check", "size-4") md.Parser().AddOptions( parser.WithASTTransformers( util.Prioritized(&codeCopyTransformer{}, 500), ), ) md.Renderer().AddOptions( renderer.WithNodeRenderers( util.Prioritized(&codeCopyRenderer{ copyIconSVG: copyIcon, checkIconSVG: checkIcon, }, 500), ), ) } func readIcon(files fs.FS, name string, classes ...string) string { if files == nil { return "" } iconPath := filepath.Join("static", "icons", name+".svg") data, err := fs.ReadFile(files, iconPath) if err != nil { return "" } svgStr := string(data) svgTagEnd := strings.Index(svgStr, ">") if svgTagEnd == -1 { return svgStr } classTag := ` class="` + strings.Join(classes, " ") + `"` return svgStr[:svgTagEnd] + classTag + svgStr[svgTagEnd:] }