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	"maps"
 11	"mime"
 12	"path/filepath"
 13	"regexp"
 14	"strings"
 15
 16	"gitroot.dev/libs/golang/plugin/model"
 17)
 18
 19const simple = `<!doctype html>
 20<html>
 21<head>
 22<meta charset="UTF-8">
 23<meta name="viewport" content="width=device-width, initial-scale=1.0">
 24<link rel="icon" type="{{repo.faviconType}}" href="{{repo.favicon}}">
 25<base href="{{repo.url}}" />
 26{{meta}}
 27<title>{{page.title}}</title>
 28<link rel="stylesheet" href="{{repo.css}}">
 29</head>
 30<body>
 31<header>
 32	{{repo.header}}
 33	<nav>
 34	{{repo.menu}}
 35	</nav>
 36</header>
 37{{content}}
 38<footer>
 39	{{repo.footer}}
 40</footer>
 41</body>
 42</html>
 43`
 44
 45var regexpTitle = regexp.MustCompile(`(?m)<h1[^>]*>([^<]+)`)
 46
 47type renderer struct {
 48	menu   []link
 49	header string
 50	footer string
 51	meta   string
 52	vars   map[string]string
 53
 54	preRendered string
 55}
 56
 57func (p *Plugin) newRender(repoName string, forgeConf model.ForgeConf) *renderer {
 58	_, filename := filepath.Split(p.config.favicon)
 59	faviconTypeMime := strings.Split(mime.TypeByExtension(filepath.Ext(filename)), ";")[0]
 60
 61	metas := strings.Builder{}
 62	for key, val := range p.config.meta {
 63		metas.WriteString(fmt.Sprintf("<meta name=\"%s\" content=\"%s\" />\n", key, val))
 64	}
 65
 66	r := &renderer{
 67		menu:   p.config.menu,
 68		header: p.config.header,
 69		footer: p.config.footer,
 70		meta:   metas.String(),
 71		vars: map[string]string{
 72			"repo.favicon":           p.config.favicon,
 73			"repo.faviconType":       faviconTypeMime,
 74			"repo.css":               p.config.style,
 75			"repo.name":              repoName,
 76			"repo.url":               fmt.Sprintf("%s%s/", forgeConf.ExternalSshAddr, repoName),
 77			"forge.domain":           forgeConf.Domain,
 78			"forge.externalHttpAddr": forgeConf.ExternalHttpAddr,
 79			"forge.externalSshAddr":  forgeConf.ExternalSshAddr,
 80		},
 81	}
 82	r.preRendered = r.render("", "", map[string]string{})
 83	return r
 84}
 85
 86func (r *renderer) render(fp string, htmlContent string, extraMetadata map[string]string) string {
 87	vars := map[string]string{}
 88	maps.Copy(vars, r.vars)
 89	maps.Copy(vars, extraMetadata)
 90
 91	//if extraMetadata.layout; ok && !="" use it
 92
 93	if fp != "" { //don't render meta in preRender
 94		metas := strings.Builder{}
 95		metas.WriteString(r.meta)
 96		for key, val := range extraMetadata {
 97			metas.WriteString(fmt.Sprintf("<meta name=\"%s\" content=\"%s\" />\n", key, val))
 98		}
 99		vars["meta"] = metas.String()
100	}
101
102	if fp != "" {
103		vars["page.title"] = findTitle(fp, []byte(htmlContent), extraMetadata)
104	}
105	vars["repo.header"] = interpolateVars(r.header, vars)
106	vars["repo.footer"] = interpolateVars(r.footer, vars)
107	if fp != "" {
108		vars["repo.menu"] = r.buildMenu(fp)
109	}
110	if htmlContent != "" {
111		vars["content"] = htmlContent
112	}
113
114	if r.preRendered != "" {
115		return interpolateVars(r.preRendered, vars)
116	}
117	return interpolateVars(simple, vars)
118}
119
120func interpolateVars(content string, vars map[string]string) string {
121	lines := strings.Split(content, "\n")
122	res := strings.Builder{}
123	for _, line := range lines {
124		for strings.Contains(line, "{{") && strings.Contains(line, "}}") {
125			foundFirst := strings.Index(line, "{{")
126			foundLast := strings.Index(line, "}}")
127			if foundFirst == -1 || foundLast == -1 {
128				continue
129			}
130			key := line[foundFirst+2 : foundLast]
131			pre := line[:foundFirst]
132			res.WriteString(pre)
133			if value, ok := vars[key]; ok {
134				res.WriteString(value)
135			} else {
136				res.WriteString(line[foundFirst : foundLast+2])
137			}
138			line = line[foundLast+2:]
139		}
140		res.WriteString(line)
141		res.WriteString("\n")
142	}
143	return strings.TrimRight(res.String(), "\n")
144}
145
146func findTitle(path string, content []byte, meta map[string]string) string {
147	if val, ok := meta["title"]; ok {
148		return val
149	}
150	if len(content) > 0 {
151		t := regexpTitle.FindSubmatch(content)
152		if len(t) > 1 {
153			return strings.TrimSpace(string(t[1]))
154		}
155	}
156	return path
157}
158
159func (r *renderer) buildMenu(fp string) string {
160	menu := bytes.NewBufferString("<ul>")
161	currentPage := pathToLink(fp)
162	for _, link := range r.menu {
163		targetLink := pathToLink(link.Link)
164		if currentPage == targetLink {
165			menu.WriteString(fmt.Sprintf("<li><a aria-current=\"page\">%s</a></li>", link.Display))
166		} else {
167			menu.WriteString(fmt.Sprintf("<li><a href=\"./%s\">%s</a></li>", targetLink, link.Display))
168		}
169	}
170	menu.WriteString("</ul>")
171	return menu.String()
172}
173
174func pathToLink(fp string) string {
175	targetLink := fp
176	if strings.HasSuffix(targetLink, "/") {
177		targetLink = fmt.Sprintf("%sindex.html", targetLink)
178	} else {
179		targetLink = fmt.Sprintf("%s.html", strings.TrimSuffix(fp, filepath.Ext(fp)))
180	}
181	targetLink = strings.TrimPrefix(targetLink, "/")
182	return targetLink
183}