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