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}