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}