package generator import ( "fmt" "os" "path/filepath" "sort" "strings" "mokhan.ca/xlgmokha/gitmal/internal/git" "mokhan.ca/xlgmokha/gitmal/internal/links" "mokhan.ca/xlgmokha/gitmal/internal/pool" "mokhan.ca/xlgmokha/gitmal/internal/templates" ) type dirInfo struct { subdirs map[string]struct{} files []git.Blob } func buildDirTree(files []git.Blob) map[string]*dirInfo { dirs := make(map[string]*dirInfo) dirs[""] = &dirInfo{subdirs: make(map[string]struct{})} for _, b := range files { parts := strings.Split(b.Path, "/") cur := "" for i := 0; i < len(parts)-1; i++ { if dirs[cur] == nil { dirs[cur] = &dirInfo{subdirs: make(map[string]struct{})} } dirs[cur].subdirs[parts[i]] = struct{}{} if cur == "" { cur = parts[i] } else { cur += "/" + parts[i] } } if dirs[cur] == nil { dirs[cur] = &dirInfo{subdirs: make(map[string]struct{})} } dirs[cur].files = append(dirs[cur].files, b) } return dirs } func GenerateIndex(files []git.Blob, params Params) error { dirs := buildDirTree(files) dirsSet := links.BuildDirSet(files) filesSet := links.BuildFileSet(files) di := dirs[""] outDir := params.OutputDir if err := os.MkdirAll(outDir, 0o755); err != nil { return err } dirNames := make([]string, 0, len(di.subdirs)) for name := range di.subdirs { dirNames = append(dirNames, name) } sort.Strings(dirNames) sort.Slice(di.files, func(i, j int) bool { return di.files[i].FileName < di.files[j].FileName }) subdirEntries := make([]templates.ListEntry, 0, len(dirNames)) for _, name := range dirNames { subdirEntries = append(subdirEntries, templates.ListEntry{ Name: name + "/", Href: "blob/" + params.Ref.DirName() + "/" + name + "/index.html", IsDir: true, }) } fileEntries := make([]templates.ListEntry, 0, len(di.files)) for _, b := range di.files { fileEntries = append(fileEntries, templates.ListEntry{ Name: b.FileName, Href: "blob/" + params.Ref.DirName() + "/" + b.FileName + ".html", Mode: b.Mode, Size: humanizeSize(b.Size), }) } title := params.Name f, err := os.Create(filepath.Join(outDir, "index.html")) if err != nil { return err } rootHref := "./" readmeHTML := readme(di.files, dirsSet, filesSet, params, rootHref) hasReadme := readmeHTML != "" err = templates.ListTemplate.ExecuteTemplate(f, "layout.gohtml", templates.ListParams{ LayoutParams: templates.LayoutParams{ Title: title, Name: params.Name, RootHref: rootHref, CurrentRefDir: params.Ref.DirName(), Selected: "code", NeedsMarkdownCSS: hasReadme, NeedsSyntaxCSS: hasReadme, }, HeaderParams: templates.HeaderParams{ Ref: params.Ref, Breadcrumbs: breadcrumbs(params.Name, "", false), }, Ref: params.Ref, Dirs: subdirEntries, Files: fileEntries, Readme: readmeHTML, }) if err != nil { _ = f.Close() return err } return f.Close() } func GenerateLists(files []git.Blob, params Params) error { dirs := buildDirTree(files) dirsSet := links.BuildDirSet(files) filesSet := links.BuildFileSet(files) type job struct { dirPath string di *dirInfo } jobsSlice := make([]job, 0, len(dirs)) for dp, di := range dirs { jobsSlice = append(jobsSlice, job{dirPath: dp, di: di}) } sort.Slice(jobsSlice, func(i, j int) bool { return jobsSlice[i].dirPath < jobsSlice[j].dirPath }) return pool.Run(jobsSlice, func(jb job) error { dirPath := jb.dirPath di := jb.di outDir := filepath.Join(params.OutputDir, "blob", params.Ref.DirName()) if dirPath != "" { outDir = filepath.Join(outDir, filepath.FromSlash(dirPath)) } if err := os.MkdirAll(outDir, 0o755); err != nil { return err } dirNames := make([]string, 0, len(di.subdirs)) for name := range di.subdirs { dirNames = append(dirNames, name) } sort.Strings(dirNames) sort.Slice(di.files, func(i, j int) bool { return di.files[i].FileName < di.files[j].FileName }) subdirEntries := make([]templates.ListEntry, 0, len(dirNames)) for _, name := range dirNames { subdirEntries = append(subdirEntries, templates.ListEntry{ Name: name + "/", Href: name + "/index.html", IsDir: true, }) } fileEntries := make([]templates.ListEntry, 0, len(di.files)) for _, b := range di.files { fileEntries = append(fileEntries, templates.ListEntry{ Name: b.FileName, Href: b.FileName + ".html", Mode: b.Mode, Size: humanizeSize(b.Size), }) } title := fmt.Sprintf("%s/%s at %s", params.Name, dirPath, params.Ref) if dirPath == "" { title = fmt.Sprintf("%s at %s", params.Name, params.Ref) } f, err := os.Create(filepath.Join(outDir, "index.html")) if err != nil { return err } defer f.Close() parent := "../index.html" if dirPath == "" { parent = "" } depth := 0 if dirPath != "" { depth = len(strings.Split(dirPath, "/")) } rootHref := strings.Repeat("../", depth+2) readmeHTML := readme(di.files, dirsSet, filesSet, params, rootHref) hasReadme := readmeHTML != "" return templates.ListTemplate.ExecuteTemplate(f, "layout.gohtml", templates.ListParams{ LayoutParams: templates.LayoutParams{ Title: title, Name: params.Name, RootHref: rootHref, CurrentRefDir: params.Ref.DirName(), Selected: "code", NeedsMarkdownCSS: hasReadme, NeedsSyntaxCSS: hasReadme, }, HeaderParams: templates.HeaderParams{ Ref: params.Ref, Breadcrumbs: breadcrumbs(params.Name, dirPath, false), }, Ref: params.Ref, ParentHref: parent, Dirs: subdirEntries, Files: fileEntries, Readme: readmeHTML, }) }) }