package main import ( "encoding/json" "flag" "fmt" "html/template" "log" "os" "path/filepath" "sort" "strings" ) type IndexData struct { Dir string Files []FileEntry RootURL string } type FileEntry struct { Name string `json:"name"` URL string `json:"url"` Date string `json:"date,omitempty"` Source string `json:"source,omitempty"` Topic string `json:"topic,omitempty"` Gradeband *string `json:"gradeband,omitempty"` } func parseFilename(name string) (date, source, topic string, gradeband *string) { // Remove extension nameWithoutExt := strings.TrimSuffix(name, filepath.Ext(name)) // Split by " - " parts := strings.Split(nameWithoutExt, " - ") if len(parts) < 2 { // Can't parse, return empty return } date = parts[0] source = parts[1] if len(parts) == 2 { // No topic or gradeband return } if len(parts) == 3 { // Just topic, no gradeband topic = parts[2] return } // 4+ parts: third part is gradeband, remaining parts are topic gb := parts[2] gradeband = &gb topic = strings.Join(parts[3:], " - ") return } func generateIndex(dir string, rootURL string) error { entries, err := os.ReadDir(dir) if err != nil { return fmt.Errorf("failed to read directory: %w", err) } // Normalize root URL (remove trailing slash) rootURL = strings.TrimSuffix(rootURL, "/") var fileNames []string for _, entry := range entries { name := entry.Name() ext := filepath.Ext(name) // Skip the index files, build files, and hidden files if name != "index.html" && name != "index.json" && ext != ".go" && ext != ".tmpl" && !filepath.HasPrefix(name, ".") { fileNames = append(fileNames, name) } } sort.Strings(fileNames) // Build file entries with absolute URLs var files []FileEntry dirName := filepath.Base(dir) for _, name := range fileNames { url := fmt.Sprintf("%s/%s/%s", rootURL, dirName, name) date, source, topic, gradeband := parseFilename(name) files = append(files, FileEntry{ Name: name, URL: url, Date: date, Source: source, Topic: topic, Gradeband: gradeband, }) } templatePath := "helios/index.html.tmpl" tmpl, err := template.ParseFiles(templatePath) if err != nil { return fmt.Errorf("failed to parse template: %w", err) } indexPath := filepath.Join(dir, "index.html") f, err := os.Create(indexPath) if err != nil { return fmt.Errorf("failed to create index.html: %w", err) } defer f.Close() data := IndexData{ Dir: dirName, Files: files, RootURL: rootURL, } if err := tmpl.Execute(f, data); err != nil { return fmt.Errorf("failed to execute template: %w", err) } log.Println(indexPath) // Generate index.json jsonPath := filepath.Join(dir, "index.json") jsonFile, err := os.Create(jsonPath) if err != nil { return fmt.Errorf("failed to create index.json: %w", err) } defer jsonFile.Close() encoder := json.NewEncoder(jsonFile) encoder.SetIndent("", " ") encoder.SetEscapeHTML(false) if err := encoder.Encode(files); err != nil { return fmt.Errorf("failed to encode JSON: %w", err) } log.Println(jsonPath) return nil } func main() { rootURL := flag.String("root", "https://s.fc.run", "Root domain URL for absolute links") flag.Parse() dirs := flag.Args() if len(dirs) == 0 { fmt.Println("Usage: generate-index [-root ] [...]") fmt.Println(" -root string") fmt.Println(" Root domain URL for absolute links (default \"https://s.fc.run\")") os.Exit(1) } for _, dir := range dirs { if err := generateIndex(dir, *rootURL); err != nil { log.Fatalf("Error processing %s: %v", dir, err) } } }