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 "fmt"
10 "math"
11 "path/filepath"
12 "slices"
13 "strconv"
14 "strings"
15
16 gitroot "gitroot.dev/libs/golang/plugin"
17 "gitroot.dev/libs/golang/plugin/model"
18)
19
20const CACHE_PATH = "boards"
21const PLUGIN_APEX = "pkg:gitroot/gitroot/apex"
22const PLUGIN_APEX_FUNC = "renderHtml"
23const PLUGIN_MARKDOWN = "pkg:gitroot/gitroot/apex_markdown"
24const PLUGIN_MARKDOWN_FUNC = "renderMd"
25
26type Plugin struct {
27 server model.Server
28 config []Conf
29 commitsHash []string
30 cache []CacheLine
31}
32
33func (p *Plugin) Init(repoName string, initKind model.InitKind, serializedConf string) error {
34 if initKind == model.InitKindConfHasChange {
35 p.server.Log("confHasChanged don't use cache")
36 p.cache = []CacheLine{}
37 } else {
38 p.cache = p.fromCache()
39 }
40 p.commitsHash = []string{}
41 p.config = p.unmarshalConf(serializedConf)
42 return nil
43}
44
45func (p *Plugin) StartCommit(commit model.Commit) error {
46 p.commitsHash = append(p.commitsHash, commit.Hash)
47 return nil
48}
49
50func (p *Plugin) AddFile(fp model.File) error {
51 toAdd, toDel := p.extractCacheLines(fp.Path)
52 for _, line := range toAdd {
53 p.appendInCache(line)
54 }
55 for _, line := range toDel {
56 p.deleteInCacheConf(line)
57 }
58 return nil
59}
60
61func (p *Plugin) DelFile(fp model.File) error {
62 p.deleteInCache(fp.Path)
63 return nil
64}
65
66func (p *Plugin) ModFile(fp model.File) error {
67 if fp.OldPath != fp.Path {
68 p.DelFile(model.File{Path: fp.OldPath, FileHash: fp.OldFileHash})
69 p.AddFile(fp)
70 return nil
71 }
72
73 toMod, toDel := p.extractCacheLines(fp.Path)
74 for _, line := range toMod {
75 p.modifyInCache(fp.OldPath, line)
76 }
77 for _, line := range toDel {
78 p.deleteInCacheConf(line)
79 }
80 return nil
81}
82
83func (p *Plugin) EndCommit(commit model.Commit) error {
84 return nil
85}
86
87type confAndContent struct {
88 conf Conf
89 content string
90}
91
92type confAndLines struct {
93 conf Conf
94 lines []CacheLine
95}
96
97func (p *Plugin) Finish() error {
98 confs := make(map[string]Conf)
99 for _, conf := range p.config {
100 confs[conf.To] = conf
101 }
102 cachedFiles := make(map[string]confAndLines)
103 for _, cachedLine := range p.cache {
104 realPath := cachedLine.toPath
105 if len(cachedLine.selects) > 0 && strings.Contains(realPath, "$select[") {
106 for i, s := range cachedLine.selects {
107 realPath = strings.ReplaceAll(realPath, fmt.Sprintf("$select[%d]", i), s)
108 }
109 }
110 if c, ok := confs[cachedLine.toPath]; ok {
111 if oldContent, ok := cachedFiles[realPath]; !ok {
112 cachedFiles[realPath] = confAndLines{conf: c, lines: []CacheLine{cachedLine}}
113 } else {
114 cachedFiles[realPath] = confAndLines{conf: c, lines: append(oldContent.lines, cachedLine)}
115 }
116 }
117 }
118
119 contents := make(map[string]confAndContent)
120
121 // add nothing where there is conf but not related files
122 for _, c := range confs {
123 found := false
124 for _, confAndLines := range cachedFiles {
125 if c.To == confAndLines.conf.To {
126 found = true
127 break
128 }
129 }
130 if !found {
131 renderer := p.NewRenderer(c)
132 contents[c.To] = confAndContent{conf: c, content: fmt.Sprintf("%s%s", renderer.renderPage([]CacheLine{}), "Nothing here.")}
133 }
134 }
135
136 for confTo, cachedFileAndConf := range cachedFiles {
137 conf := cachedFileAndConf.conf
138 cachedFile := cachedFileAndConf.lines
139 slices.SortFunc(cachedFile, func(a CacheLine, b CacheLine) int {
140 res := 0
141 if numSelect, ok := strings.CutPrefix(conf.Sort, "select["); ok {
142 numSelect = strings.TrimSuffix(numSelect, "]")
143 i, err := strconv.Atoi(numSelect)
144 if err == nil {
145 if len(a.selects) >= i && len(b.selects) >= i {
146 i1, err := strconv.Atoi(a.selects[i])
147 i2, err2 := strconv.Atoi(b.selects[i])
148 if err != nil || err2 != nil {
149 res = strings.Compare(a.selects[i], b.selects[i])
150 } else {
151 res = i1 - i2
152 }
153 }
154 } else {
155 p.server.LogError("atoi fail", err)
156 }
157 }
158 //if same or with a different sort
159 if res == 0 {
160 if conf.Sort == "file" {
161 res = strings.Compare(a.fp, b.fp)
162 } else {
163 res = strings.Compare(a.title, b.title)
164 }
165 }
166
167 if conf.SortOrder == "desc" {
168 return res * -1
169 }
170 return res
171 })
172 currentFile := confTo
173 dir, file := filepath.Split(confTo)
174 confToNoExt, _ := strings.CutSuffix(file, ".md")
175 nbPages := int(math.Ceil(float64(len(cachedFile)) / float64(conf.Paginator)))
176 renderer := p.NewRenderer(conf)
177 chunkSize := len(cachedFile)
178 if conf.Paginator > 0 {
179 chunkSize = int(conf.Paginator)
180 }
181 curPage := 1
182 for cachedLines := range slices.Chunk(cachedFile, chunkSize) {
183 if curPage > 1 {
184 currentFile = fmt.Sprintf("%s%s_%d.md", dir, confToNoExt, curPage)
185 }
186 content := renderer.renderPage(cachedLines)
187 contents[currentFile] = confAndContent{conf: conf, content: content}
188 if conf.Paginator != -1 && nbPages > 1 {
189 buttonPages := ""
190 for i := range nbPages {
191 if i == 0 && curPage > 1 {
192 buttonPages = fmt.Sprintf("[%d](./%s.md)\n", i+1, confToNoExt)
193 } else if i == 0 {
194 buttonPages = fmt.Sprintf("%d\n", i+1)
195 } else if curPage != i+1 {
196 buttonPages = fmt.Sprintf("%s[%d](./%s_%d.md)\n", buttonPages, i+1, confToNoExt, i+1)
197 } else {
198 buttonPages = fmt.Sprintf("%s%d\n", buttonPages, i+1)
199 }
200 }
201 contents[currentFile] = confAndContent{conf: conf, content: fmt.Sprintf("%s\n\n%s", contents[currentFile].content, buttonPages)}
202 }
203 curPage++
204 }
205 }
206 newCache := p.toCache()
207 p.server.Cache().WriteContent(CACHE_PATH, newCache)
208 for pathTo, content := range contents {
209 if content.conf.Fs == "web" {
210 metasJson := fmt.Sprintf("{\"title\":\"%s\"}", content.conf.Title)
211 mdArgs := map[string]string{"fp": pathTo, "md": content.content, "extraMedata": metasJson}
212 if p.server.CanCallFunc(PLUGIN_MARKDOWN, PLUGIN_MARKDOWN_FUNC, mdArgs) {
213 res, err := p.server.CallFunc(PLUGIN_MARKDOWN, PLUGIN_MARKDOWN_FUNC, mdArgs)
214 if err == nil {
215 content.content = res["html"]
216 metasJson = res["metas"]
217 }
218 args := map[string]string{
219 "fp": pathTo,
220 "html": content.content,
221 "extraMetadata": metasJson,
222 }
223 if p.server.CanCallFunc(PLUGIN_APEX, PLUGIN_APEX_FUNC, args) {
224 res, err := p.server.CallFunc(PLUGIN_APEX, PLUGIN_APEX_FUNC, args)
225 if err == nil {
226 content.content = res["html"]
227 }
228 }
229 }
230 p.server.Webcontent().WriteContent(pathTo, content.content)
231 } else {
232 p.server.Worktree().WriteContent(pathTo, content.content)
233 }
234 }
235 p.server.CommitAllIfNeeded(fmt.Sprintf("silo plugin\n\nfor %s", strings.Join(p.commitsHash, ", ")))
236 p.cache = nil
237 p.config = nil
238 return nil
239}
240
241func Build(server model.Server) model.Plugin {
242 return &Plugin{
243 server: server,
244 }
245}
246
247//go:wasmexport install
248func main() {
249 gitroot.Register(defaultRun, Build)
250}