markdown.go
1package pgit
2
3import (
4 "bytes"
5 "fmt"
6 "html/template"
7 "io"
8 "path/filepath"
9 "strings"
10
11 "github.com/alecthomas/chroma/v2"
12 "github.com/alecthomas/chroma/v2/formatters"
13 "github.com/alecthomas/chroma/v2/lexers"
14 "github.com/gomarkdown/markdown"
15 "github.com/gomarkdown/markdown/ast"
16 "github.com/gomarkdown/markdown/html"
17 "github.com/gomarkdown/markdown/parser"
18)
19
20func IsMarkdownFile(filename string) bool {
21 ext := strings.ToLower(filepath.Ext(filename))
22 return ext == ".md"
23}
24
25func (c *Config) ParseText(filename string, text string) (string, error) {
26 lexer := lexers.Match(filename)
27 if lexer == nil {
28 lexer = lexers.Analyse(text)
29 }
30 if lexer == nil {
31 lexer = lexers.Get("plaintext")
32 }
33 iterator, err := lexer.Tokenise(nil, text)
34 if err != nil {
35 return text, err
36 }
37 var buf bytes.Buffer
38 err = c.Formatter.Format(&buf, c.Theme, iterator)
39 if err != nil {
40 return text, err
41 }
42 return buf.String(), nil
43}
44
45type chromaMarkdownRenderer struct {
46 defaultRenderer *html.Renderer
47 theme *chroma.Style
48}
49
50func (r *chromaMarkdownRenderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.WalkStatus {
51 if entering {
52 if codeBlock, ok := node.(*ast.CodeBlock); ok {
53 lang := extractLanguage(codeBlock.Info)
54 code := string(codeBlock.Literal)
55 highlighted, err := r.highlightCode(lang, code)
56 if err == nil {
57 w.Write([]byte(highlighted))
58 return ast.SkipChildren
59 }
60 }
61 }
62 return r.defaultRenderer.RenderNode(w, node, entering)
63}
64
65func (r *chromaMarkdownRenderer) RenderHeader(w io.Writer, ast ast.Node) {
66 r.defaultRenderer.RenderHeader(w, ast)
67}
68
69func (r *chromaMarkdownRenderer) RenderFooter(w io.Writer, ast ast.Node) {
70 r.defaultRenderer.RenderFooter(w, ast)
71}
72
73func (r *chromaMarkdownRenderer) highlightCode(lang, code string) (string, error) {
74 var lexer chroma.Lexer
75 if lang != "" {
76 lexer = lexers.Get(lang)
77 }
78 if lexer == nil {
79 lexer = lexers.Analyse(code)
80 }
81 if lexer == nil {
82 lexer = lexers.Get("plaintext")
83 }
84
85 iterator, err := lexer.Tokenise(nil, code)
86 if err != nil {
87 return "", err
88 }
89
90 formatter := formatters.Get("html")
91 if formatter == nil {
92 return "", fmt.Errorf("failed to get HTML formatter")
93 }
94
95 var buf bytes.Buffer
96 err = formatter.Format(&buf, r.theme, iterator)
97 if err != nil {
98 return "", err
99 }
100
101 highlighted := buf.String()
102 highlighted = strings.TrimPrefix(highlighted, "<pre class=\"chroma\">")
103 highlighted = strings.TrimSuffix(highlighted, "</pre>")
104
105 return highlighted, nil
106}
107
108func extractLanguage(info []byte) string {
109 if len(info) == 0 {
110 return ""
111 }
112 parts := strings.Fields(string(info))
113 if len(parts) > 0 {
114 return parts[0]
115 }
116 return ""
117}
118
119func (c *Config) RenderMarkdown(mdText string) template.HTML {
120 extensions := parser.CommonExtensions | parser.FencedCode
121 p := parser.NewWithExtensions(extensions)
122 doc := p.Parse([]byte(mdText))
123
124 htmlFlags := html.CommonFlags
125 opts := html.RendererOptions{Flags: htmlFlags}
126 defaultRenderer := html.NewRenderer(opts)
127
128 customRenderer := &chromaMarkdownRenderer{
129 defaultRenderer: defaultRenderer,
130 theme: c.Theme,
131 }
132
133 htmlBytes := markdown.Render(doc, customRenderer)
134 return template.HTML(htmlBytes)
135}