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}