main
  1package generator
  2
  3import (
  4	"fmt"
  5	"os"
  6	"path/filepath"
  7	"sort"
  8	"strings"
  9
 10	"mokhan.ca/xlgmokha/gitmal/internal/git"
 11	"mokhan.ca/xlgmokha/gitmal/internal/links"
 12	"mokhan.ca/xlgmokha/gitmal/internal/pool"
 13	"mokhan.ca/xlgmokha/gitmal/internal/templates"
 14)
 15
 16type dirInfo struct {
 17	subdirs map[string]struct{}
 18	files   []git.Blob
 19}
 20
 21func buildDirTree(files []git.Blob) map[string]*dirInfo {
 22	dirs := make(map[string]*dirInfo)
 23	dirs[""] = &dirInfo{subdirs: make(map[string]struct{})}
 24
 25	for _, b := range files {
 26		parts := strings.Split(b.Path, "/")
 27		cur := ""
 28		for i := 0; i < len(parts)-1; i++ {
 29			if dirs[cur] == nil {
 30				dirs[cur] = &dirInfo{subdirs: make(map[string]struct{})}
 31			}
 32			dirs[cur].subdirs[parts[i]] = struct{}{}
 33			if cur == "" {
 34				cur = parts[i]
 35			} else {
 36				cur += "/" + parts[i]
 37			}
 38		}
 39		if dirs[cur] == nil {
 40			dirs[cur] = &dirInfo{subdirs: make(map[string]struct{})}
 41		}
 42		dirs[cur].files = append(dirs[cur].files, b)
 43	}
 44	return dirs
 45}
 46
 47func GenerateIndex(files []git.Blob, params Params) error {
 48	dirs := buildDirTree(files)
 49	dirsSet := links.BuildDirSet(files)
 50	filesSet := links.BuildFileSet(files)
 51
 52	di := dirs[""]
 53
 54	outDir := params.OutputDir
 55	if err := os.MkdirAll(outDir, 0o755); err != nil {
 56		return err
 57	}
 58
 59	dirNames := make([]string, 0, len(di.subdirs))
 60	for name := range di.subdirs {
 61		dirNames = append(dirNames, name)
 62	}
 63	sort.Strings(dirNames)
 64	sort.Slice(di.files, func(i, j int) bool {
 65		return di.files[i].FileName < di.files[j].FileName
 66	})
 67
 68	subdirEntries := make([]templates.ListEntry, 0, len(dirNames))
 69	for _, name := range dirNames {
 70		subdirEntries = append(subdirEntries, templates.ListEntry{
 71			Name:  name + "/",
 72			Href:  "blob/" + params.Ref.DirName() + "/" + name + "/index.html",
 73			IsDir: true,
 74		})
 75	}
 76
 77	fileEntries := make([]templates.ListEntry, 0, len(di.files))
 78	for _, b := range di.files {
 79		fileEntries = append(fileEntries, templates.ListEntry{
 80			Name: b.FileName,
 81			Href: "blob/" + params.Ref.DirName() + "/" + b.FileName + ".html",
 82			Mode: b.Mode,
 83			Size: humanizeSize(b.Size),
 84		})
 85	}
 86
 87	title := params.Name
 88
 89	f, err := os.Create(filepath.Join(outDir, "index.html"))
 90	if err != nil {
 91		return err
 92	}
 93
 94	rootHref := "./"
 95	readmeHTML := readme(di.files, dirsSet, filesSet, params, rootHref)
 96	hasReadme := readmeHTML != ""
 97
 98	err = templates.ListTemplate.ExecuteTemplate(f, "layout.gohtml", templates.ListParams{
 99		LayoutParams: templates.LayoutParams{
100			Title:            title,
101			Name:             params.Name,
102			RootHref:         rootHref,
103			CurrentRefDir:    params.Ref.DirName(),
104			Selected:         "code",
105			NeedsMarkdownCSS: hasReadme,
106			NeedsSyntaxCSS:   hasReadme,
107			Year:             currentYear(),
108			CloneURL:         params.CloneURL,
109		},
110		HeaderParams: templates.HeaderParams{
111			Ref:         params.Ref,
112			Breadcrumbs: breadcrumbs(params.Name, "", false),
113		},
114		Ref:    params.Ref,
115		Dirs:   subdirEntries,
116		Files:  fileEntries,
117		Readme: readmeHTML,
118	})
119	if err != nil {
120		_ = f.Close()
121		return err
122	}
123	return f.Close()
124}
125
126func GenerateLists(files []git.Blob, params Params) error {
127	dirs := buildDirTree(files)
128	dirsSet := links.BuildDirSet(files)
129	filesSet := links.BuildFileSet(files)
130
131	type job struct {
132		dirPath string
133		di      *dirInfo
134	}
135	jobsSlice := make([]job, 0, len(dirs))
136	for dp, di := range dirs {
137		jobsSlice = append(jobsSlice, job{dirPath: dp, di: di})
138	}
139	sort.Slice(jobsSlice, func(i, j int) bool { return jobsSlice[i].dirPath < jobsSlice[j].dirPath })
140
141	return pool.Run(jobsSlice, func(jb job) error {
142		dirPath := jb.dirPath
143		di := jb.di
144
145		outDir := filepath.Join(params.OutputDir, "blob", params.Ref.DirName())
146		if dirPath != "" {
147			outDir = filepath.Join(outDir, filepath.FromSlash(dirPath))
148		}
149		if err := os.MkdirAll(outDir, 0o755); err != nil {
150			return err
151		}
152
153		dirNames := make([]string, 0, len(di.subdirs))
154		for name := range di.subdirs {
155			dirNames = append(dirNames, name)
156		}
157
158		sort.Strings(dirNames)
159		sort.Slice(di.files, func(i, j int) bool {
160			return di.files[i].FileName < di.files[j].FileName
161		})
162
163		subdirEntries := make([]templates.ListEntry, 0, len(dirNames))
164		for _, name := range dirNames {
165			subdirEntries = append(subdirEntries, templates.ListEntry{
166				Name:  name + "/",
167				Href:  name + "/index.html",
168				IsDir: true,
169			})
170		}
171
172		fileEntries := make([]templates.ListEntry, 0, len(di.files))
173		for _, b := range di.files {
174			fileEntries = append(fileEntries, templates.ListEntry{
175				Name: b.FileName,
176				Href: b.FileName + ".html",
177				Mode: b.Mode,
178				Size: humanizeSize(b.Size),
179			})
180		}
181
182		title := fmt.Sprintf("%s/%s at %s", params.Name, dirPath, params.Ref)
183		if dirPath == "" {
184			title = fmt.Sprintf("%s at %s", params.Name, params.Ref)
185		}
186
187		f, err := os.Create(filepath.Join(outDir, "index.html"))
188		if err != nil {
189			return err
190		}
191		defer f.Close()
192
193		parent := "../index.html"
194		if dirPath == "" {
195			parent = ""
196		}
197
198		depth := 0
199		if dirPath != "" {
200			depth = len(strings.Split(dirPath, "/"))
201		}
202		rootHref := strings.Repeat("../", depth+2)
203
204		readmeHTML := readme(di.files, dirsSet, filesSet, params, rootHref)
205
206		hasReadme := readmeHTML != ""
207		return templates.ListTemplate.ExecuteTemplate(f, "layout.gohtml", templates.ListParams{
208			LayoutParams: templates.LayoutParams{
209				Title:            title,
210				Name:             params.Name,
211				RootHref:         rootHref,
212				CurrentRefDir:    params.Ref.DirName(),
213				Selected:         "code",
214				NeedsMarkdownCSS: hasReadme,
215				NeedsSyntaxCSS:   hasReadme,
216				Year:             currentYear(),
217			},
218			HeaderParams: templates.HeaderParams{
219				Ref:         params.Ref,
220				Breadcrumbs: breadcrumbs(params.Name, dirPath, false),
221			},
222			Ref:        params.Ref,
223			ParentHref: parent,
224			Dirs:       subdirEntries,
225			Files:      fileEntries,
226			Readme:     readmeHTML,
227		})
228	})
229}