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}