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}