1// based on HUGO markup/codeblocks/render.go -- Pet
  2
  3// Copyright 2024 The Hugo Authors. All rights reserved.
  4//
  5// Licensed under the Apache License, Version 2.0 (the "License");
  6// you may not use this file except in compliance with the License.
  7// You may obtain a copy of the License at
  8// http://www.apache.org/licenses/LICENSE-2.0
  9//
 10// Unless required by applicable law or agreed to in writing, software
 11// distributed under the License is distributed on an "AS IS" BASIS,
 12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13// See the License for the specific language governing permissions and
 14// limitations under the License.
 15
 16package goldmark_extensions
 17
 18import (
 19    "bytes"
 20    "fmt"
 21    "html"
 22    "io"
 23    "strings"
 24
 25    "github.com/yuin/goldmark"
 26    "github.com/yuin/goldmark/ast"
 27    "github.com/yuin/goldmark/renderer"
 28    "github.com/yuin/goldmark/util"
 29
 30    html_formatter "github.com/alecthomas/chroma/v2/formatters/html"
 31    "github.com/alecthomas/chroma/v2/lexers"
 32    "github.com/alecthomas/chroma/v2/styles"
 33)
 34
 35const
 36    ChromaStyle string = "colorful"
 37
 38func Highlight(lang, code string, w io.Writer, with_line_numbers bool) {
 39/*
 40 * code highlighter
 41 * lang can be either language or file name
 42 */
 43    if lang == "" {
 44        fmt.Fprintf(w, "<pre><code>%s</code></pre>", html.EscapeString(code))
 45        return
 46    }
 47
 48    lexer := lexers.Get(lang)
 49    if lexer == nil {
 50        lexer = lexers.Match(lang)
 51        if lexer == nil {
 52            lexer = lexers.Fallback
 53        }
 54    }
 55
 56    formatter := html_formatter.New(html_formatter.WithClasses(true), html_formatter.WithLineNumbers(with_line_numbers))
 57
 58    iterator, err := lexer.Tokenise(nil, code)
 59    if err != nil {
 60        panic(err)
 61    }
 62    style := styles.Get(ChromaStyle)
 63    if style == nil {
 64        style = styles.Fallback
 65    }
 66    err = formatter.Format(w, style, iterator)
 67    if err != nil {
 68        panic(err)
 69    }
 70}
 71
 72func Chomp(s string) string {
 73    /*
 74     * remove trailing newline characters from s.
 75     */
 76    return strings.TrimRightFunc(s, func(r rune) bool {
 77        return r == '\n' || r == '\r'
 78    })
 79}
 80
 81type (
 82    codeBlocksExtension struct{}
 83    htmlRenderer        struct{}
 84)
 85
 86var CodeBlocksExtension = &codeBlocksExtension{}
 87
 88func (e *codeBlocksExtension) Extend(m goldmark.Markdown) {
 89    m.Renderer().AddOptions(renderer.WithNodeRenderers(
 90        util.Prioritized(newHTMLRenderer(), 100),
 91    ))
 92}
 93
 94func newHTMLRenderer() renderer.NodeRenderer {
 95    r := &htmlRenderer{}
 96    return r
 97}
 98
 99func (r *htmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
100    reg.Register(ast.KindFencedCodeBlock, r.renderCodeBlock)
101}
102
103func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
104
105    if entering {
106        return ast.WalkContinue, nil
107    }
108
109    n := node.(*ast.FencedCodeBlock)
110
111    lang := getLang(n, src)
112
113    var buff bytes.Buffer
114
115    l := n.Lines().Len()
116    for i := range l {
117        line := n.Lines().At(i)
118        buff.Write(line.Value(src))
119    }
120    codeblock := Chomp(buff.String())
121    Highlight(lang, codeblock, w, false)
122    return ast.WalkContinue, nil
123}
124
125func getLang(node *ast.FencedCodeBlock, src []byte) string {
126    langWithAttributes := string(node.Language(src))
127    lang, _, _ := strings.Cut(langWithAttributes, "{")
128    return lang
129}