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}