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 = "pkg:gitroot/gitroot/apex_markdown"
 21const PLUGIN_MARKDOWN_FUNC = "renderMd"
 22const PLUGIN_CODE = "pkg:gitroot/gitroot/apex_code"
 23const PLUGIN_CODE_FUNC = "renderCode"
 24const PLUGIN_MERMAID = "pkg:gitroot/gitroot/apex_mermaid"
 25const PLUGIN_MERMAID_FUNC = "renderCode"
 26
 27//go:embed resources/styles/add.css
 28var addStyle string
 29
 30//go:embed resources/styles/pico.min.css
 31var picoStyle string
 32
 33//go:embed resources/styles/simple.min.css
 34var simpleStyle string
 35
 36//go:embed resources/index.md
 37var index string
 38
 39type Plugin struct {
 40	server            model.Server
 41	config            *conf
 42	renderer          *renderer
 43	gitWorktree       *worktree
 44	currentCommit     model.Commit
 45	branchCommits     []*branchCommits
 46	reports           []string
 47	canCallMdPlugin   bool
 48	canCallCodePlugin bool
 49}
 50
 51func (p *Plugin) Init(repoName string, initKind model.InitKind, serializedConf string) error {
 52	p.config = p.newConf(serializedConf)
 53	p.reports = []string{}
 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 initKind == model.InitKindFuncCall {
 63		return nil
 64	}
 65
 66	p.canCallMdPlugin = p.server.CanCallFunc(PLUGIN_MARKDOWN, PLUGIN_MARKDOWN_FUNC, map[string]string{"fp": "", "md": "", "extraMedata": ""})
 67	p.canCallCodePlugin = p.server.CanCallFunc(PLUGIN_CODE, PLUGIN_CODE_FUNC, map[string]string{"code": "", "lang": ""})
 68
 69	if p.config.generateGitWorktree {
 70		p.LoadWorktree()
 71	}
 72
 73	// css style
 74	if _, err := fs.Stat(p.server.Webcontent(), p.config.style); errors.Is(err, fs.ErrNotExist) || initKind == model.InitKindConfHasChange {
 75		style := ""
 76		switch p.config.style {
 77		case "pico.min.css":
 78			style = picoStyle
 79		case "simple.min.css":
 80			style = simpleStyle
 81		default:
 82			// TODO download if distant? Copy if local?
 83		}
 84		if style == "" {
 85			style = simpleStyle
 86		}
 87		p.server.Webcontent().WriteContent(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) || initKind == model.InitKindConfHasChange {
 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.Worktree().WriteContent("index.md", index)
 97				p.server.CommitAllIfNeeded("init web page")
 98				p.AddFile(model.File{Path: "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) || initKind == model.InitKindConfHasChange {
111		newContent := p.renderer.render("404.html", "<p>Not found</p>", map[string]string{"title": "not found"})
112		p.server.Webcontent().WriteContent("404.html", newContent)
113	}
114	return nil
115}
116
117func (p *Plugin) StartCommit(commit model.Commit) error {
118	p.currentCommit = commit
119	if p.config.branchesDir != "" {
120		p.AddIfNotExist(commit)
121	}
122	return nil
123}
124
125func (p *Plugin) AddFile(fp model.File) error {
126	newContent := ""
127	path := fp.Path
128	if strings.HasSuffix(fp.Path, ".md") {
129		path = fmt.Sprintf("%s.html", strings.TrimSuffix(fp.Path, ".md"))
130		mdContent, err := fs.ReadFile(p.server.Worktree(), fp.Path)
131		if err != nil {
132			p.server.LogError("AddFile ReadFile "+fp.Path, err)
133			return nil
134		}
135		html := ""
136		metas := map[string]string{}
137		if p.canCallMdPlugin {
138			extraMetadata := p.renderer.extraMetadataForMarkdown(fp.Path, path)
139			extraMetadataJson, err := json.Marshal(extraMetadata)
140			if err != nil {
141				p.server.LogError("AddFile can't marshall extraMetadataForMarkdown", err)
142				return nil
143			}
144			res, err := p.server.CallFunc(PLUGIN_MARKDOWN, PLUGIN_MARKDOWN_FUNC, map[string]string{"fp": fp.Path, "md": string(mdContent), "extraMetadata": string(extraMetadataJson)})
145			if err != nil {
146				p.server.LogError(fmt.Sprintf("AddFile call renderMd fail with %s", string(extraMetadataJson)), err)
147				return nil
148			}
149			html = res["html"]
150			metasJson := res["metas"]
151			if len(metasJson) > 0 {
152				if err := json.Unmarshal([]byte(metasJson), &metas); err != nil {
153					p.server.LogError("AddFile md metasdata unmarshal fail "+metasJson, err)
154					return nil
155				}
156			}
157		} else {
158			html = fmt.Sprintf("<code>%s</code>", mdContent)
159		}
160		newContent = p.renderer.render(fp.Path, html, metas)
161	} else {
162		content, err := fs.ReadFile(p.server.Worktree(), fp.Path)
163		if err != nil {
164			p.server.LogError("AddFile ReadFile "+fp.Path, err)
165			return nil
166		}
167		newContent = string(content)
168	}
169	// branch can be "" in init scenario
170	if p.currentCommit.Branch != "" && p.currentCommit.Branch != p.config.defaultBranch {
171		path = filepath.Join(p.config.branchesDir, p.currentCommit.Branch, path)
172		p.reports = append(p.reports, fmt.Sprintf(`- Render [%s](%s%s)`, fp.Path, p.renderer.vars["repo.url"], path))
173	} else {
174		p.server.Log(fmt.Sprintf("no report because branch is %s", p.currentCommit.Branch))
175	}
176	p.server.Webcontent().WriteContent(path, newContent)
177	if p.config.generateGitWorktree && p.currentCommit.Branch == p.config.defaultBranch {
178		p.gitWorktree.addOrModFile(fp.Path, p.currentCommit)
179	}
180	return nil
181}
182
183func (p *Plugin) DelFile(fp model.File) error {
184	if p.config.generateGitWorktree && p.currentCommit.Branch == p.config.defaultBranch {
185		p.gitWorktree.delFile(fp.Path)
186	}
187	return nil
188}
189
190func (p *Plugin) ModFile(fp model.File) error {
191	if fp.OldPath != "" && fp.OldPath != fp.Path {
192		p.DelFile(model.File{Path: fp.OldPath, FileHash: fp.OldFileHash})
193	}
194	return p.AddFile(fp)
195}
196
197func (p *Plugin) EndCommit(commit model.Commit) error {
198	return nil
199}
200
201func (p *Plugin) Finish() error {
202	if p.config.generateGitWorktree && p.currentCommit.Branch == p.config.defaultBranch {
203		p.StoreWorktree()
204		p.gitWorktree.renderHtml("", "worktree", func(fp string, htmlContent string) {
205			p.server.Webcontent().WriteContent(fp, p.renderer.render(fp, htmlContent, map[string]string{"title": fp}))
206		})
207	}
208	if p.config.branchesDir != "" {
209		p.RenderBranches()
210	}
211	if len(p.reports) > 0 {
212		p.server.Report(model.ReportLevelInfo, p.reports)
213		p.reports = []string{}
214	}
215	p.config = nil
216	p.gitWorktree = nil
217	return nil
218}
219
220func Build(server model.Server) model.Plugin {
221	p := &Plugin{
222		server: server,
223	}
224	server.ExportFunc("renderHtml", func(args map[string]string) (map[string]string, error) {
225		fp := args["fp"]
226		html := args["html"]
227		extraMetadataJson := args["extraMetadata"]
228		extraMetadata := map[string]string{}
229		if extraMetadataJson != "" {
230			err := json.Unmarshal([]byte(extraMetadataJson), &extraMetadata)
231			if err != nil {
232				return nil, err
233			}
234		}
235		finalHtml := p.renderer.render(fp, html, extraMetadata)
236		return map[string]string{
237			"html": finalHtml,
238		}, nil
239	})
240	return p
241}
242
243//go:wasmexport install
244func main() {
245	loadMimeType()
246	loadEmojis()
247	gitroot.Register(defaultRun, Build)
248}