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}