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