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	_ "embed"
  9	"encoding/json"
 10	"errors"
 11	"fmt"
 12	"io/fs"
 13	"path/filepath"
 14	"strings"
 15
 16	gitroot "gitroot.dev/libs/golang/plugin"
 17	"gitroot.dev/libs/golang/plugin/model"
 18)
 19
 20const PLUGIN_MARKDOWN = "apex_markdown"
 21const PLUGIN_MARKDOWN_FUNC = "renderMd"
 22const PLUGIN_CODE = "apex_code"
 23const PLUGIN_CODE_FUNC = "renderCode"
 24
 25//go:embed resources/styles/add.css
 26var addStyle string
 27
 28//go:embed resources/styles/pico.min.css
 29var picoStyle string
 30
 31//go:embed resources/styles/simple.min.css
 32var simpleStyle string
 33
 34//go:embed resources/index.md
 35var index string
 36
 37type Plugin struct {
 38	server            model.Server
 39	config            *conf
 40	renderer          *renderer
 41	gitWorktree       *worktree
 42	currentCommit     model.Commit
 43	branchCommits     []*branchCommits
 44	reports           []string
 45	canCallMdPlugin   bool
 46	canCallCodePlugin bool
 47}
 48
 49func (p *Plugin) Init(repoName string, confHasChanged bool, serializedConf string) error {
 50	p.config = p.newConf(serializedConf)
 51	p.reports = []string{}
 52	p.canCallMdPlugin = p.server.CanCallFunc(PLUGIN_MARKDOWN, PLUGIN_MARKDOWN_FUNC, map[string]string{"fp": "", "md": "", "extraMedata": ""})
 53	p.canCallCodePlugin = p.server.CanCallFunc(PLUGIN_CODE, PLUGIN_CODE_FUNC, map[string]string{"code": "", "lang": ""})
 54
 55	forgeConf, err := p.server.ForgeConf()
 56	if err != nil {
 57		p.server.LogError("can't get forge conf", err)
 58	}
 59	p.renderer = p.newRender(repoName, forgeConf)
 60	p.branchCommits = make([]*branchCommits, 0)
 61
 62	if p.config.generateGitWorktree {
 63		p.LoadWorktree()
 64	}
 65
 66	// css style
 67	if _, err := fs.Stat(p.server.Webcontent(), p.config.style); errors.Is(err, fs.ErrNotExist) || confHasChanged {
 68		style := ""
 69		switch p.config.style {
 70		case "pico.min.css":
 71			style = picoStyle
 72		case "simple.min.css":
 73			style = simpleStyle
 74		default:
 75			// TODO download if distant? Copy if local?
 76		}
 77		if style == "" {
 78			style = simpleStyle
 79		}
 80		p.server.ModifyWebContent(p.config.style, strings.Join([]string{style, addStyle}, "\n"))
 81	} else if err != nil {
 82		p.server.LogError("can't stats styles", err)
 83	}
 84
 85	// index.html
 86	if _, err := fs.Stat(p.server.Webcontent(), "index.html"); errors.Is(err, fs.ErrNotExist) || confHasChanged {
 87		if _, err := fs.Stat(p.server.Worktree(), "index.html"); errors.Is(err, fs.ErrNotExist) {
 88			if _, err := fs.Stat(p.server.Worktree(), "index.md"); errors.Is(err, fs.ErrNotExist) {
 89				p.server.ModifyContent("index.md", index)
 90				p.server.CommitAllIfNeeded("init web page")
 91				p.AddFile(model.File{Path: "index.md"})
 92			} else if err != nil {
 93				p.server.LogError("can't stats index.md in wortree", err)
 94			}
 95		} else if err != nil {
 96			p.server.LogError("can't stats index.html in wortree", err)
 97		}
 98	} else if err != nil {
 99		p.server.LogError("can't stats index in webContent", err)
100	}
101
102	// 404.html
103	if _, err := fs.Stat(p.server.Webcontent(), "404.html"); errors.Is(err, fs.ErrNotExist) || confHasChanged {
104		newContent := p.renderer.render("404.html", "<p>Not found</p>", map[string]string{"title": "not found"})
105		p.server.ModifyWebContent("404.html", newContent)
106	}
107	return nil
108}
109
110func (p *Plugin) StartCommit(commit model.Commit) error {
111	p.currentCommit = commit
112	if p.config.branchesDir != "" {
113		p.AddIfNotExist(commit)
114	}
115	return nil
116}
117
118func (p *Plugin) AddFile(fp model.File) error {
119	newContent := ""
120	path := fp.Path
121	if strings.HasSuffix(fp.Path, ".md") {
122		path = fmt.Sprintf("%s.html", strings.TrimSuffix(fp.Path, ".md"))
123		mdContent, err := fs.ReadFile(p.server.Worktree(), fp.Path)
124		if err != nil {
125			p.server.LogError("AddFile ReadFile "+fp.Path, err)
126			return nil
127		}
128		html := ""
129		metas := map[string]string{}
130		if p.canCallMdPlugin {
131			extraMetadata := p.renderer.extraMetadataForMarkdown(fp.Path, path)
132			extraMetadataJson, err := json.Marshal(extraMetadata)
133			if err != nil {
134				p.server.LogError("AddFile can't marshall extraMetadataForMarkdown", err)
135				return nil
136			}
137			res, err := p.server.CallFunc(PLUGIN_MARKDOWN, PLUGIN_MARKDOWN_FUNC, map[string]string{"fp": fp.Path, "md": string(mdContent), "extraMetadata": string(extraMetadataJson)})
138			if err != nil {
139				p.server.LogError(fmt.Sprintf("AddFile call renderMd fail with %s", string(extraMetadataJson)), err)
140				return nil
141			}
142			html = res["html"]
143			metasJson := res["metas"]
144			if len(metasJson) > 0 {
145				if err := json.Unmarshal([]byte(metasJson), &metas); err != nil {
146					p.server.LogError("AddFile md metasdata unmarshal fail "+metasJson, err)
147					return nil
148				}
149			}
150		} else {
151			html = fmt.Sprintf("<code>%s</code>", mdContent)
152		}
153		newContent = p.renderer.render(fp.Path, html, metas)
154	} else {
155		content, err := fs.ReadFile(p.server.Worktree(), fp.Path)
156		if err != nil {
157			p.server.LogError("AddFile ReadFile "+fp.Path, err)
158			return nil
159		}
160		newContent = string(content)
161	}
162	// branch can be "" in init scenario
163	if p.currentCommit.Branch != "" && p.currentCommit.Branch != p.config.defaultBranch {
164		path = filepath.Join(p.config.branchesDir, p.currentCommit.Branch, path)
165		p.reports = append(p.reports, fmt.Sprintf(`- Render [%s](%s%s)`, fp.Path, p.renderer.vars["repo.url"], path))
166	} else {
167		p.server.Log(fmt.Sprintf("no report because branch is %s", p.currentCommit.Branch))
168	}
169	p.server.Webcontent().WriteContent(path, newContent)
170	if p.config.generateGitWorktree && p.currentCommit.Branch == p.config.defaultBranch {
171		p.gitWorktree.addOrModFile(fp.Path, p.currentCommit)
172	}
173	return nil
174}
175
176func (p *Plugin) DelFile(fp model.File) error {
177	if p.config.generateGitWorktree && p.currentCommit.Branch == p.config.defaultBranch {
178		p.gitWorktree.delFile(fp.Path)
179	}
180	return nil
181}
182
183func (p *Plugin) ModFile(fp model.File) error {
184	if fp.OldPath != "" && fp.OldPath != fp.Path {
185		p.DelFile(model.File{Path: fp.OldPath, FileHash: fp.OldFileHash})
186	}
187	return p.AddFile(fp)
188}
189
190func (p *Plugin) EndCommit(commit model.Commit) error {
191	return nil
192}
193
194func (p *Plugin) Finish() error {
195	if p.config.generateGitWorktree && p.currentCommit.Branch == p.config.defaultBranch {
196		p.StoreWorktree()
197		p.gitWorktree.renderHtml("", "worktree", func(fp string, htmlContent string) {
198			p.server.ModifyWebContent(fp, p.renderer.render(fp, htmlContent, map[string]string{"title": fp}))
199		})
200	}
201	if p.config.branchesDir != "" {
202		p.RenderBranches()
203	}
204	if len(p.reports) > 0 {
205		p.server.Report(model.ReportLevelInfo, p.reports)
206		p.reports = []string{}
207	}
208	p.config = nil
209	p.gitWorktree = nil
210	return nil
211}
212
213func Build(server model.Server) model.Plugin {
214	return &Plugin{
215		server: server,
216	}
217}
218
219//go:wasmexport install
220func main() {
221	loadMimeType()
222	loadEmojis()
223	gitroot.Register(defaultRun, Build)
224}