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
7// compile cmd
8// tinygo build -o apex-0.0.1.wasm -scheduler=none --no-debug -target=wasi ./
9
10import (
11 "bytes"
12 _ "embed"
13 "errors"
14 "fmt"
15 "io/fs"
16 "path/filepath"
17 "strings"
18
19 gitroot "gitroot.dev/libs/golang/plugin"
20 "gitroot.dev/libs/golang/plugin/model"
21)
22
23const simple = `<!doctype html>
24<html>
25<head>
26<meta charset="UTF-8">
27<meta name="viewport" content="width=device-width, initial-scale=1.0">
28<title>%s</title>
29<link rel="stylesheet" href="%s">
30</head>
31<body>
32<header>
33 %s
34 <nav>
35 %s
36 </nav>
37</header>
38%s
39<footer>
40 %s
41</footer>
42</body>
43</html>
44`
45
46//go:embed resources/styles/add.css
47var addStyle string
48
49//go:embed resources/styles/pico.min.css
50var picoStyle string
51
52//go:embed resources/styles/simple.min.css
53var simpleStyle string
54
55//go:embed resources/index.md
56var index string
57
58type Plugin struct {
59 server gitroot.Server
60 repoName string
61 config *conf
62 gitWorktree *worktree
63 currentCommit model.Commit
64 branchCommits []*branchCommits
65}
66
67func (p *Plugin) Init(repoName string, confHasChanged bool, serializedConf string) error {
68 p.config = p.NewConf(serializedConf)
69 p.repoName = repoName
70 p.branchCommits = make([]*branchCommits, 0)
71
72 if p.config.generateGitWorktree {
73 p.LoadWorktree()
74 }
75
76 // css style
77 if _, err := fs.Stat(p.server.Webcontent(), "styles/style.css"); errors.Is(err, fs.ErrNotExist) || confHasChanged {
78 style := ""
79 switch p.config.style {
80 case "pico.min.css":
81 style = picoStyle
82 case "simple.min.css":
83 style = simpleStyle
84 default:
85 // TODO download if distant? Copy if local?
86 }
87 p.server.ModifyWebContent(p.config.style, strings.Join([]string{style, addStyle}, "\n"))
88 } else if err != nil {
89 p.server.LogError("can't stats styles", err)
90 }
91
92 // index.html
93 if _, err := fs.Stat(p.server.Webcontent(), "index.html"); errors.Is(err, fs.ErrNotExist) || confHasChanged {
94 if _, err := fs.Stat(p.server.Worktree(), "index.html"); errors.Is(err, fs.ErrNotExist) {
95 if _, err := fs.Stat(p.server.Worktree(), "index.md"); errors.Is(err, fs.ErrNotExist) {
96 p.server.ModifyContent("index.md", index)
97 p.server.CommitAllIfNeeded("init web page")
98 p.AddFile("index.md")
99 } else if err != nil {
100 p.server.LogError("can't stats index.md in wortree", err)
101 }
102 } else if err != nil {
103 p.server.LogError("can't stats index.html in wortree", err)
104 }
105 } else if err != nil {
106 p.server.LogError("can't stats index in webContent", err)
107 }
108
109 // 404.html
110 if _, err := fs.Stat(p.server.Webcontent(), "404.html"); errors.Is(err, fs.ErrNotExist) || confHasChanged {
111 p.buildPage("404.md", []byte("Not found"))
112 }
113 return nil
114}
115
116func (p *Plugin) StartCommit(commit model.Commit) error {
117 p.currentCommit = commit
118 if p.config.branchesDir != "" {
119 p.AddIfNotExist(commit)
120 }
121 return nil
122}
123
124func (p *Plugin) AddFile(fp string) error {
125 if strings.HasSuffix(fp, ".md") {
126 content, err := fs.ReadFile(p.server.Worktree(), fp)
127 if err != nil {
128 p.server.LogError("AddFile ReadFile "+fp, err)
129 return nil
130 }
131 p.buildPage(fp, content)
132 } else {
133 content, err := fs.ReadFile(p.server.Worktree(), fp)
134 if err != nil {
135 p.server.LogError("AddFile ReadFile "+fp, err)
136 return nil
137 }
138 p.server.ModifyWebContent(fp, string(content))
139 }
140 if p.config.generateGitWorktree {
141 p.gitWorktree.addOrModFile(fp, p.currentCommit)
142 }
143 return nil
144}
145
146func (p *Plugin) buildPage(fp string, mdContent []byte) {
147 menu := p.buildMenu(fp)
148 stylePath := relativePath(fp, p.config.style)
149 newContent := fmt.Sprintf(simple, p.repoName, stylePath, p.config.header, menu, string(mdToHTML(mdContent, p.readInclude)), p.config.footer)
150 p.server.ModifyWebContent(fmt.Sprintf("%s.html", strings.TrimSuffix(fp, ".md")), newContent)
151}
152
153func relativePath(fromPath string, toPath string) string {
154 absFromPath, _ := filepath.Abs(fromPath)
155 absToPath, _ := filepath.Abs(toPath)
156
157 dirFromPath, _ := filepath.Split(absFromPath)
158 dirToPath, fileToPath := filepath.Split(absToPath)
159
160 path, err := filepath.Rel(dirFromPath, dirToPath)
161 if err != nil {
162 return toPath
163 }
164 return fmt.Sprintf("%s/%s", path, fileToPath)
165}
166
167func (p *Plugin) buildMenu(fp string) string {
168 menu := bytes.NewBufferString("<ul>")
169 mdOrHtml := filepath.Ext(fp)
170 for _, link := range p.config.menu {
171 targetLink := link.Link
172 if strings.HasSuffix(targetLink, "/") {
173 targetLink = fmt.Sprintf("%sindex%s", targetLink, mdOrHtml)
174 }
175 l := relativePath(fp, link.Link)
176 if fp == targetLink {
177 menu.WriteString(fmt.Sprintf("<li><a aria-current=\"page\">%s</a></li>", link.Display))
178 } else {
179 menu.WriteString(fmt.Sprintf("<li><a href=\"%s\">%s</a></li>", l, link.Display))
180 }
181 }
182 menu.WriteString("</ul>")
183 return menu.String()
184}
185
186func (p *Plugin) readInclude(from, path string, address []byte) []byte {
187 content, _ := fs.ReadFile(p.server.Worktree(), path)
188 return content
189}
190
191func (p *Plugin) DelFile(fp string) error {
192 if p.config.generateGitWorktree {
193 p.gitWorktree.delFile(fp)
194 }
195 return nil
196}
197
198func (p *Plugin) ModFile(fromPath string, toPath string) error {
199 if fromPath != toPath {
200 p.DelFile(fromPath)
201 }
202 return p.AddFile(toPath)
203}
204
205func (p *Plugin) EndCommit(commit model.Commit) error {
206 return nil
207}
208
209func (p *Plugin) Finish() error {
210 if p.config.generateGitWorktree {
211 p.StoreWorktree()
212 p.gitWorktree.renderHtml("", "worktree", func(fp string, htmlContent string) {
213 menu := p.buildMenu(fp)
214 stylePath := relativePath(fp, p.config.style)
215 newContent := fmt.Sprintf(simple, p.repoName, stylePath, p.config.header, menu, htmlContent, p.config.footer)
216 p.server.ModifyWebContent(fp, newContent)
217 })
218 }
219 if p.config.branchesDir != "" {
220 p.RenderBranches()
221 }
222 p.config = nil
223 p.gitWorktree = nil
224 return nil
225}
226
227func Build(server gitroot.Server) gitroot.Plugin {
228 return &Plugin{
229 server: server,
230 }
231}
232
233//go:wasmexport install
234func main() {
235 loadMimeType()
236 loadEmojis()
237 gitroot.Register(defaultRun, Build)
238}