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}