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	repoUrl := fmt.Sprintf("%s%s/", forgeConf.ExternalHttpAddr, repoName)
 67	if repoName == forgeConf.RootRepositoryName {
 68		repoUrl = forgeConf.ExternalHttpAddr
 69	}
 70
 71	r := &renderer{
 72		menu:   p.config.menu,
 73		header: p.config.header,
 74		footer: p.config.footer,
 75		meta:   metas.String(),
 76		vars: map[string]string{
 77			"repo.favicon":           p.config.favicon,
 78			"repo.faviconType":       faviconTypeMime,
 79			"repo.css":               p.config.style,
 80			"repo.name":              repoName,
 81			"repo.url":               repoUrl,
 82			"repo.cloneUrl":          fmt.Sprintf("%s%s/", forgeConf.ExternalSshAddr, repoName),
 83			"forge.domain":           forgeConf.Domain,
 84			"forge.externalHttpAddr": forgeConf.ExternalHttpAddr,
 85			"forge.externalSshAddr":  forgeConf.ExternalSshAddr,
 86		},
 87	}
 88	r.preRendered = r.render("", "", map[string]string{})
 89	return r
 90}
 91
 92func (r *renderer) render(fp string, htmlContent string, extraMetadata map[string]string) string {
 93	vars := map[string]string{}
 94	maps.Copy(vars, r.vars)
 95	maps.Copy(vars, extraMetadata)
 96
 97	//if extraMetadata.layout; ok && !="" use it
 98
 99	if fp != "" { //don't render meta in preRender
100		metas := strings.Builder{}
101		metas.WriteString(r.meta)
102		for key, val := range extraMetadata {
103			metas.WriteString(fmt.Sprintf("<meta name=\"%s\" content=\"%s\" />\n", key, val))
104		}
105		vars["meta"] = metas.String()
106	}
107
108	if fp != "" {
109		vars["page.title"] = findTitle(fp, []byte(htmlContent), extraMetadata)
110	}
111	vars["repo.header"] = interpolateVars(r.header, vars)
112	vars["repo.footer"] = interpolateVars(r.footer, vars)
113	if fp != "" {
114		vars["repo.menu"] = r.buildMenu(fp)
115	}
116	if htmlContent != "" {
117		vars["content"] = htmlContent
118	}
119
120	if r.preRendered != "" {
121		return interpolateVars(r.preRendered, vars)
122	}
123	return interpolateVars(simple, vars)
124}
125
126func interpolateVars(content string, vars map[string]string) string {
127	lines := strings.Split(content, "\n")
128	res := strings.Builder{}
129	for _, line := range lines {
130		for strings.Contains(line, "{{") && strings.Contains(line, "}}") {
131			foundFirst := strings.Index(line, "{{")
132			foundLast := strings.Index(line, "}}")
133			if foundFirst == -1 || foundLast == -1 {
134				continue
135			}
136			key := line[foundFirst+2 : foundLast]
137			pre := line[:foundFirst]
138			res.WriteString(pre)
139			if value, ok := vars[key]; ok {
140				res.WriteString(value)
141			} else {
142				res.WriteString(line[foundFirst : foundLast+2])
143			}
144			line = line[foundLast+2:]
145		}
146		res.WriteString(line)
147		res.WriteString("\n")
148	}
149	return strings.TrimRight(res.String(), "\n")
150}
151
152func findTitle(path string, content []byte, meta map[string]string) string {
153	if val, ok := meta["title"]; ok {
154		return val
155	}
156	if len(content) > 0 {
157		t := regexpTitle.FindSubmatch(content)
158		if len(t) > 1 {
159			return strings.TrimSpace(string(t[1]))
160		}
161	}
162	return path
163}
164
165func (r *renderer) buildMenu(fp string) string {
166	menu := bytes.NewBufferString("<ul>")
167	currentPage := pathToLink(fp)
168	for _, link := range r.menu {
169		targetLink := pathToLink(link.Link)
170		if currentPage == targetLink {
171			menu.WriteString(fmt.Sprintf("<li><a aria-current=\"page\">%s</a></li>", link.Display))
172		} else {
173			menu.WriteString(fmt.Sprintf("<li><a href=\"./%s\">%s</a></li>", targetLink, link.Display))
174		}
175	}
176	menu.WriteString("</ul>")
177	return menu.String()
178}
179
180func pathToLink(fp string) string {
181	targetLink := fp
182	if strings.HasSuffix(targetLink, "/") {
183		targetLink = fmt.Sprintf("%sindex.html", targetLink)
184	} else {
185		targetLink = fmt.Sprintf("%s.html", strings.TrimSuffix(fp, filepath.Ext(fp)))
186	}
187	targetLink = strings.TrimPrefix(targetLink, "/")
188	return targetLink
189}