// SPDX-FileCopyrightText: 2025 Romain Maneschi // // SPDX-License-Identifier: EUPL-1.2 package main import ( "bytes" "fmt" "io" "strings" "github.com/alecthomas/chroma" codeHtml "github.com/alecthomas/chroma/formatters/html" "github.com/alecthomas/chroma/lexers" "github.com/alecthomas/chroma/styles" "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/ast" "github.com/gomarkdown/markdown/html" "github.com/gomarkdown/markdown/parser" ) func mdToHTML(md []byte, readIncludeFn parser.ReadIncludeFunc) []byte { metadata := []byte("") if bytes.HasPrefix(md, []byte("---")) { metadatas := bytes.Split(bytes.TrimPrefix(md, []byte("---")), []byte("---")) metadata = metadatas[0] md = bytes.Join(metadatas[1:], []byte("---")) } // create markdown parser with extensions extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock | parser.Includes | parser.Mmark p := parser.NewWithExtensions(extensions) p.Opts.ReadIncludeFn = readIncludeFn doc := modifyAst(p.Parse(md), metadata) // create HTML renderer with extensions htmlFlags := html.CommonFlags | html.HrefTargetBlank opts := html.RendererOptions{Flags: htmlFlags, RenderNodeHook: persoRenderer} renderer := html.NewRenderer(opts) return markdown.Render(doc, renderer) } type Break struct { ast.Container } type Metadata struct { ast.Container metadatas map[string]string } func modifyAst(doc ast.Node, metadata []byte) ast.Node { if len(metadata) > 0 { meta := &Metadata{metadatas: make(map[string]string)} for _, d := range bytes.Split(metadata, []byte("\n")) { if len(bytes.TrimSpace(d)) > 0 { dd := strings.Split(string(d), ":") if len(dd) > 1 { meta.metadatas[strings.TrimSpace(dd[0])] = strings.TrimSpace(dd[1]) } else { meta.metadatas[string(bytes.TrimSpace(d))] = "" } } } metadataAsNode := []ast.Node{meta} doc.SetChildren(append(metadataAsNode, doc.GetChildren()...)) } isExternalURI := func(uri string) bool { return (strings.HasPrefix(uri, "https://") || strings.HasPrefix(uri, "http://")) } ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus { if link, ok := node.(*ast.Link); ok && entering { if isExternalURI(string(link.Destination)) { link.AdditionalAttributes = append(link.AdditionalAttributes, `target="_blank"`) } cleanDest := strings.Split(string(link.Destination), "#") if strings.HasSuffix(cleanDest[0], ".md") { link.Destination = []byte(fmt.Sprintf("%s.html", strings.TrimSuffix(cleanDest[0], ".md"))) if len(cleanDest) > 1 { link.Destination = bytes.Join([][]byte{link.Destination, []byte(cleanDest[1])}, []byte("#")) } } } return ast.GoToNext }) return doc } func persoRenderer(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) { if n, ok := node.(*Metadata); ok { if !entering { return ast.SkipChildren, true } return renderMetadata(w, n) } if _, ok := node.(*Break); ok { if !entering { return ast.SkipChildren, true } return renderBreak(w) } if code, ok := node.(*ast.CodeBlock); ok { renderCode(w, code, entering) return ast.GoToNext, true } return ast.GoToNext, false } func renderMetadata(w io.Writer, n *Metadata) (ast.WalkStatus, bool) { tmp := bytes.Buffer{} tmp.WriteString("
") for key, value := range n.metadatas { tmp.WriteString("
") tmp.WriteString(key) tmp.WriteString("
") tmp.WriteString("
") tmp.WriteString(value) tmp.WriteString("
") } tmp.WriteString("
\n\n") w.Write(tmp.Bytes()) return ast.SkipChildren, true } func renderBreak(w io.Writer) (ast.WalkStatus, bool) { w.Write([]byte("
\n\n")) return ast.SkipChildren, true } func renderCode(w io.Writer, codeBlock *ast.CodeBlock, entering bool) { defaultLang := "" lang := string(codeBlock.Info) htmlHighlight(w, string(codeBlock.Literal), lang, defaultLang) } func htmlHighlight(w io.Writer, source, lang, defaultLang string) error { if lang == "" { lang = defaultLang } l := lexers.Get(lang) if l == nil { l = lexers.Analyse(source) } if l == nil { l = lexers.Fallback } l = chroma.Coalesce(l) it, err := l.Tokenise(nil, source) if err != nil { return err } highlightStyle := styles.Get("dracula") if highlightStyle == nil { return fmt.Errorf("didn't find style '%s'", "dracula") } return codeHtml.New(codeHtml.WithLineNumbers(true)).Format(w, highlightStyle, it) }