GitRoot
Craft your forge, Build your project, Grow your community freely
1// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
2//
3// SPDX-License-Identifier: EUPL-1.2
4
5package main
6
7import (
8 "bytes"
9 "fmt"
10 "io"
11 "io/fs"
12 "maps"
13 "path/filepath"
14 "strings"
15
16 "github.com/gomarkdown/markdown"
17 "github.com/gomarkdown/markdown/ast"
18 "github.com/gomarkdown/markdown/html"
19 "github.com/gomarkdown/markdown/parser"
20)
21
22const (
23 mermaidLang = "mermaid"
24 apexRenderMetadatas = "apexRenderMetadatas"
25 horizontalRuleMetadatas = "horizontalRuleMetadatas"
26 footerSlideMetadatas = "footerSlideMetadatas"
27)
28
29func (plug *Plugin) mdToHTML(fp string, md []byte, extraMedata map[string]string) (string, map[string]string) {
30 metadatas := map[string]string{}
31 if bytes.HasPrefix(md, []byte("---")) {
32 metadata, body, found := bytes.Cut(bytes.TrimPrefix(md, []byte("---")), []byte("---"))
33 if found {
34 md = body
35 for _, d := range bytes.Split(metadata, []byte("\n")) {
36 dString := string(bytes.TrimSpace(d))
37 if i := strings.LastIndex(dString, ": "); i > 0 {
38 prefix := dString[:i]
39 suffix := dString[i+1:]
40 key := strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(prefix), "\""), "\"")
41 val := strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(suffix), "\""), "\"")
42 metadatas[key] = val
43 } else if len(dString) > 0 {
44 metadatas[strings.TrimSuffix(dString, ":")] = ""
45 }
46 }
47 }
48 }
49 allMetaDatas := map[string]string{}
50 maps.Copy(allMetaDatas, extraMedata)
51 maps.Copy(allMetaDatas, metadatas)
52
53 // create markdown parser with extensions
54 extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock | parser.Includes | parser.Mmark
55 p := parser.NewWithExtensions(extensions)
56 p.Opts.ReadIncludeFn = plug.readInclude
57 doc := plug.modifyAst(p.Parse(md), fp)
58
59 // create HTML renderer with extensions
60 htmlFlags := html.CommonFlags
61 opts := html.RendererOptions{Flags: htmlFlags, RenderNodeHook: plug.persoRenderer(allMetaDatas)}
62 renderer := html.NewRenderer(opts)
63
64 res := string(markdown.Render(doc, renderer))
65
66 return res, metadatas
67}
68
69func (p *Plugin) readInclude(from, path string, address []byte) []byte {
70 content, _ := fs.ReadFile(p.server.Worktree(), path)
71 return content
72}
73
74func (plug *Plugin) modifyAst(doc ast.Node, fp string) ast.Node {
75 isExternalURI := func(uri string) bool {
76 external := strings.HasPrefix(uri, "https://") || strings.HasPrefix(uri, "http://")
77 if external {
78 plug.server.Log(fmt.Sprintf("isExternal %s", uri))
79 }
80 return external
81 }
82 ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus {
83 if link, ok := node.(*ast.Link); ok && entering {
84 if isExternalURI(string(link.Destination)) {
85 link.AdditionalAttributes = append(link.AdditionalAttributes, `target="_blank"`)
86 } else {
87 cleanDest := strings.Split(string(link.Destination), "#")
88 file := cleanDest[0]
89 dir, fileFp := filepath.Split(fp)
90 if file == "" {
91 file = fileFp
92 }
93 if strings.HasSuffix(file, ".md") {
94 link.Destination = fmt.Appendf(nil, "%s.html", strings.TrimSuffix(file, ".md"))
95 if len(cleanDest) > 1 {
96 link.Destination = bytes.Join([][]byte{link.Destination, []byte(cleanDest[1])}, []byte("#"))
97 }
98 }
99 link.Destination = []byte(filepath.Clean(filepath.Join(dir, string(link.Destination))))
100 }
101 }
102
103 return ast.GoToNext
104 })
105
106 return doc
107}
108
109func (plug *Plugin) persoRenderer(metadatas map[string]string) func(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
110 apexRenderMetadatasVal, apexRenderMetadatasOk := metadatas[apexRenderMetadatas]
111 horizontalRuleVal, horizontalRuleOk := metadatas[horizontalRuleMetadatas]
112 footerSlideMetadatasVal, footerSlideMetadatasOk := metadatas[footerSlideMetadatas]
113 return func(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
114 if _, ok := node.(*ast.Document); ok {
115 if entering {
116 if apexRenderMetadatasOk && apexRenderMetadatasVal == "true" {
117 renderMetadata(w, metadatas)
118 }
119 if horizontalRuleOk && (horizontalRuleVal == "section" || horizontalRuleVal == "article") {
120 if footerSlideMetadatasOk {
121 fmt.Fprintf(w, "<%s data-footer=\"%s\">", horizontalRuleVal, footerSlideMetadatasVal)
122 } else {
123 fmt.Fprintf(w, "<%s>", horizontalRuleVal)
124 }
125 }
126 } else {
127 if horizontalRuleOk && (horizontalRuleVal == "section" || horizontalRuleVal == "article") {
128 fmt.Fprintf(w, "</%s>", horizontalRuleVal)
129 }
130 }
131 }
132 if _, ok := node.(*ast.HorizontalRule); ok {
133 if !entering {
134 return ast.SkipChildren, true
135 }
136 if horizontalRuleOk && (horizontalRuleVal == "section" || horizontalRuleVal == "article") {
137 if footerSlideMetadatasOk {
138 fmt.Fprintf(w, "\n</%s><%s data-footer=\"%s\">\n", horizontalRuleVal, horizontalRuleVal, footerSlideMetadatasVal)
139 } else {
140 fmt.Fprintf(w, "\n</%s><%s>\n", horizontalRuleVal, horizontalRuleVal)
141 }
142 } else {
143 w.Write([]byte("\n<hr>\n"))
144 }
145 return ast.SkipChildren, true
146 }
147 if code, ok := node.(*ast.CodeBlock); ok {
148 plug.renderCode(w, code)
149 return ast.GoToNext, true
150 }
151 return ast.GoToNext, false
152 }
153}
154
155func renderMetadata(w io.Writer, metadatas map[string]string) (ast.WalkStatus, bool) {
156 tmp := bytes.Buffer{}
157 tmp.WriteString("<dl>")
158 for key, value := range metadatas {
159 if key != apexRenderMetadatas && key != horizontalRuleMetadatas {
160 tmp.WriteString("<dt>")
161 tmp.WriteString(key)
162 tmp.WriteString("</dt>")
163 tmp.WriteString("<dd>")
164 tmp.WriteString(value)
165 tmp.WriteString("</dd>")
166 }
167 }
168 tmp.WriteString("</dl>\n\n")
169 w.Write(tmp.Bytes())
170 return ast.SkipChildren, true
171}
172
173func (p *Plugin) renderCode(w io.Writer, codeBlock *ast.CodeBlock) {
174 code := string(codeBlock.Literal)
175 lang := string(codeBlock.Info)
176 var htmlCode map[string]string
177 var err error
178 if lang == mermaidLang && p.canCallMermaidPlugin {
179 htmlCode, err = p.server.CallFunc(PLUGIN_MERMAID, PLUGIN_MERMAID_FUNC, map[string]string{"code": code})
180 } else if p.canCallCodePlugin {
181 htmlCode, err = p.server.CallFunc(PLUGIN_CODE, PLUGIN_CODE_FUNC, map[string]string{"code": code, "lang": lang})
182 } else {
183 htmlCode = map[string]string{"html": fmt.Sprintf("<pre><code>%s</code></pre>", code)}
184 }
185 if err != nil {
186 p.server.LogError("renderCode call renderCode fail", err)
187 return
188 }
189 fmt.Fprint(w, htmlCode["html"])
190}