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": ""})
 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 := ""
121	if strings.HasSuffix(fp.Path, ".md") {
122		mdContent, err := fs.ReadFile(p.server.Worktree(), fp.Path)
123		if err != nil {
124			p.server.LogError("AddFile ReadFile "+fp.Path, err)
125			return nil
126		}
127		html := ""
128		metas := map[string]string{}
129		if p.canCallMdPlugin {
130			res, err := p.server.CallFunc(PLUGIN_MARKDOWN, PLUGIN_MARKDOWN_FUNC, map[string]string{"fp": fp.Path, "md": string(mdContent)})
131			if err != nil {
132				p.server.LogError("AddFile call renderMd fail", err)
133				return nil
134			}
135			html = res["html"]
136			metasJson := res["metas"]
137			if len(metasJson) > 0 {
138				if err := json.Unmarshal([]byte(metasJson), &metas); err != nil {
139					p.server.LogError("AddFile md metasdata unmarshal fail "+metasJson, err)
140					return nil
141				}
142			}
143		} else {
144			html = fmt.Sprintf("<code>%s</code>", mdContent)
145		}
146		newContent = p.renderer.render(fp.Path, html, metas)
147		path = fmt.Sprintf("%s.html", strings.TrimSuffix(fp.Path, ".md"))
148	} else {
149		content, err := fs.ReadFile(p.server.Worktree(), fp.Path)
150		if err != nil {
151			p.server.LogError("AddFile ReadFile "+fp.Path, err)
152			return nil
153		}
154		newContent = string(content)
155		path = fp.Path
156	}
157	// branch can be "" in init scenario
158	if p.currentCommit.Branch != "" && p.currentCommit.Branch != p.config.defaultBranch {
159		path = filepath.Join(p.config.branchesDir, p.currentCommit.Branch, path)
160		p.reports = append(p.reports, fmt.Sprintf(`- Render [%s](%s%s)`, fp.Path, p.renderer.vars["repo.url"], path))
161	} else {
162		p.server.Log(fmt.Sprintf("no report because branch is %s", p.currentCommit.Branch))
163	}
164	p.server.ModifyWebContent(path, newContent)
165	if p.config.generateGitWorktree && p.currentCommit.Branch == p.config.defaultBranch {
166		p.gitWorktree.addOrModFile(fp.Path, p.currentCommit)
167	}
168	return nil
169}
170
171func (p *Plugin) DelFile(fp model.File) error {
172	if p.config.generateGitWorktree && p.currentCommit.Branch == p.config.defaultBranch {
173		p.gitWorktree.delFile(fp.Path)
174	}
175	return nil
176}
177
178func (p *Plugin) ModFile(fp model.File) error {
179	if fp.OldPath != "" && fp.OldPath != fp.Path {
180		p.DelFile(model.File{Path: fp.OldPath, FileHash: fp.OldFileHash})
181	}
182	return p.AddFile(fp)
183}
184
185func (p *Plugin) EndCommit(commit model.Commit) error {
186	return nil
187}
188
189func (p *Plugin) Finish() error {
190	if p.config.generateGitWorktree && p.currentCommit.Branch == p.config.defaultBranch {
191		p.StoreWorktree()
192		p.gitWorktree.renderHtml("", "worktree", func(fp string, htmlContent string) {
193			p.server.ModifyWebContent(fp, p.renderer.render(fp, htmlContent, map[string]string{"title": fp}))
194		})
195	}
196	if p.config.branchesDir != "" {
197		p.RenderBranches()
198	}
199	if len(p.reports) > 0 {
200		p.server.Report(model.ReportLevelInfo, p.reports)
201		p.reports = []string{}
202	}
203	p.config = nil
204	p.gitWorktree = nil
205	return nil
206}
207
208func Build(server model.Server) model.Plugin {
209	return &Plugin{
210		server: server,
211	}
212}
213
214//go:wasmexport install
215func main() {
216	loadMimeType()
217	loadEmojis()
218	gitroot.Register(defaultRun, Build)
219}