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 "path/filepath"
13 "strings"
14
15 "github.com/gomarkdown/markdown"
16 "github.com/gomarkdown/markdown/ast"
17 "github.com/gomarkdown/markdown/html"
18 "github.com/gomarkdown/markdown/parser"
19)
20
21func (plug *Plugin) mdToHTML(fp string, md []byte) (string, map[string]string) {
22 metadata := []byte("")
23 if bytes.HasPrefix(md, []byte("---")) {
24 metadatas := bytes.Split(bytes.TrimPrefix(md, []byte("---")), []byte("---"))
25 metadata = metadatas[0]
26 md = bytes.Join(metadatas[1:], []byte("---"))
27 }
28
29 // create markdown parser with extensions
30 extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock | parser.Includes | parser.Mmark
31 p := parser.NewWithExtensions(extensions)
32 p.Opts.ReadIncludeFn = plug.readInclude
33 doc, metadatas := plug.modifyAst(p.Parse(md), metadata, fp)
34
35 // create HTML renderer with extensions
36 htmlFlags := html.CommonFlags
37 opts := html.RendererOptions{Flags: htmlFlags, RenderNodeHook: plug.persoRenderer}
38 renderer := html.NewRenderer(opts)
39
40 return string(markdown.Render(doc, renderer)), metadatas
41}
42
43func (p *Plugin) readInclude(from, path string, address []byte) []byte {
44 content, _ := fs.ReadFile(p.server.Worktree(), path)
45 return content
46}
47
48type Break struct {
49 ast.Container
50}
51
52type Metadata struct {
53 ast.Container
54
55 metadatas map[string]string
56}
57
58func (plug *Plugin) modifyAst(doc ast.Node, metadata []byte, fp string) (ast.Node, map[string]string) {
59 metadatas := make(map[string]string)
60 if len(metadata) > 0 {
61 meta := &Metadata{metadatas: metadatas}
62 renderMetadatas := false
63 for _, d := range bytes.Split(metadata, []byte("\n")) {
64 dString := string(bytes.TrimSpace(d))
65 if i := strings.LastIndex(dString, ":"); i > 0 {
66 prefix := dString[:i]
67 suffix := dString[i+1:]
68 key := strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(prefix), "\""), "\"")
69 val := strings.TrimSpace(suffix)
70 if key == "apexRenderMetadatas" && val == "true" {
71 renderMetadatas = true
72 } else {
73 meta.metadatas[key] = val
74 }
75 } else if len(dString) > 0 {
76 meta.metadatas[dString] = ""
77 }
78 }
79 if renderMetadatas {
80 metadataAsNode := []ast.Node{meta}
81 doc.SetChildren(append(metadataAsNode, doc.GetChildren()...))
82 }
83 }
84
85 isExternalURI := func(uri string) bool {
86 external := strings.HasPrefix(uri, "https://") || strings.HasPrefix(uri, "http://")
87 if external {
88 plug.server.Log(fmt.Sprintf("isExternal %s", uri))
89 }
90 return external
91 }
92 ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus {
93 if link, ok := node.(*ast.Link); ok && entering {
94 if isExternalURI(string(link.Destination)) {
95 link.AdditionalAttributes = append(link.AdditionalAttributes, `target="_blank"`)
96 } else {
97 cleanDest := strings.Split(string(link.Destination), "#")
98 file := cleanDest[0]
99 dir, fileFp := filepath.Split(fp)
100 if file == "" {
101 file = fileFp
102 }
103 if strings.HasSuffix(file, ".md") {
104 link.Destination = []byte(fmt.Sprintf("%s.html", strings.TrimSuffix(file, ".md")))
105 if len(cleanDest) > 1 {
106 link.Destination = bytes.Join([][]byte{link.Destination, []byte(cleanDest[1])}, []byte("#"))
107 }
108 }
109 link.Destination = []byte(filepath.Clean(filepath.Join(dir, string(link.Destination))))
110 }
111 }
112
113 return ast.GoToNext
114 })
115
116 return doc, metadatas
117}
118
119func (plug *Plugin) persoRenderer(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
120 if n, ok := node.(*Metadata); ok {
121 if !entering {
122 return ast.SkipChildren, true
123 }
124 return renderMetadata(w, n)
125 }
126 if _, ok := node.(*Break); ok {
127 if !entering {
128 return ast.SkipChildren, true
129 }
130 return renderBreak(w)
131 }
132 if code, ok := node.(*ast.CodeBlock); ok {
133 plug.renderCode(w, code)
134 return ast.GoToNext, true
135 }
136 return ast.GoToNext, false
137}
138
139func renderMetadata(w io.Writer, n *Metadata) (ast.WalkStatus, bool) {
140 tmp := bytes.Buffer{}
141 tmp.WriteString("<dl>")
142 for key, value := range n.metadatas {
143 tmp.WriteString("<dt>")
144 tmp.WriteString(key)
145 tmp.WriteString("</dt>")
146 tmp.WriteString("<dd>")
147 tmp.WriteString(value)
148 tmp.WriteString("</dd>")
149 }
150 tmp.WriteString("</dl>\n\n")
151 w.Write(tmp.Bytes())
152 return ast.SkipChildren, true
153}
154
155func renderBreak(w io.Writer) (ast.WalkStatus, bool) {
156 w.Write([]byte("<hr />\n\n"))
157 return ast.SkipChildren, true
158}
159
160func (p *Plugin) renderCode(w io.Writer, codeBlock *ast.CodeBlock) {
161 code := string(codeBlock.Literal)
162 lang := string(codeBlock.Info)
163 htmlCode, err := p.server.CallFunc(CODE_PLUGIN, "renderCode", map[string]string{"code": code, "lang": lang})
164 if err != nil {
165 p.server.LogError("renderCode call renderCode fail", err)
166 return
167 }
168 fmt.Fprint(w, htmlCode["html"])
169}