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}