package brain import ( "bufio" "fmt" "os" "path/filepath" "sort" "strings" "time" ) // noteEntry is one row in a Wing _index.md. type noteEntry struct { Hall string Slug string Title string Created string } // BuildWingIndex regenerates brain/wiki//_index.md as a Map of // Content listing every note in that wing with its Hall and creation // date. Returns nil if the wing directory does not exist. func BuildWingIndex(brainDir, wing string) error { w := Sanitise(wing) if w == "" { return fmt.Errorf("invalid wing %q", wing) } wingDir := filepath.Join(brainDir, "wiki", w) if _, err := os.Stat(wingDir); os.IsNotExist(err) { return nil } else if err != nil { return fmt.Errorf("stat wing: %w", err) } entries, err := collectWingEntries(wingDir) if err != nil { return err } sort.Slice(entries, func(i, j int) bool { if entries[i].Hall != entries[j].Hall { return entries[i].Hall < entries[j].Hall } return entries[i].Slug < entries[j].Slug }) var b strings.Builder fmt.Fprintf(&b, "# %s\n\n", w) b.WriteString("| Hall | Note | Created |\n") b.WriteString("|------|------|---------|\n") for _, e := range entries { fmt.Fprintf(&b, "| %s | [%s](%s/%s.md) | %s |\n", e.Hall, e.Title, e.Hall, e.Slug, e.Created) } dest := filepath.Join(wingDir, "_index.md") return os.WriteFile(dest, []byte(b.String()), 0o644) } // BuildAllWingIndexes regenerates _index.md for every wing under brain/wiki/. func BuildAllWingIndexes(brainDir string) error { wikiDir := filepath.Join(brainDir, "wiki") ents, err := os.ReadDir(wikiDir) if os.IsNotExist(err) { return nil } if err != nil { return fmt.Errorf("read wiki: %w", err) } for _, e := range ents { if !e.IsDir() { continue } if err := BuildWingIndex(brainDir, e.Name()); err != nil { return fmt.Errorf("index %s: %w", e.Name(), err) } } return nil } func collectWingEntries(wingDir string) ([]noteEntry, error) { var out []noteEntry ents, err := os.ReadDir(wingDir) if err != nil { return nil, fmt.Errorf("read wing: %w", err) } for _, hallEnt := range ents { if !hallEnt.IsDir() { continue } hall := hallEnt.Name() if !IsValidHall(hall) { continue } hallDir := filepath.Join(wingDir, hall) notes, err := os.ReadDir(hallDir) if err != nil { return nil, fmt.Errorf("read hall %s: %w", hall, err) } for _, n := range notes { if n.IsDir() || !strings.HasSuffix(n.Name(), ".md") || n.Name() == "_index.md" { continue } slug := strings.TrimSuffix(n.Name(), ".md") full := filepath.Join(hallDir, n.Name()) title, created := readTitleAndCreated(full, slug) out = append(out, noteEntry{Hall: hall, Slug: slug, Title: title, Created: created}) } } return out, nil } // readTitleAndCreated reads YAML frontmatter for title + created_at; falls // back to slug and file mtime when absent. func readTitleAndCreated(path, slug string) (string, string) { f, err := os.Open(path) if err != nil { return slug, "" } defer func() { _ = f.Close() }() title, created := "", "" scanner := bufio.NewScanner(f) inFrontmatter := false for scanner.Scan() { line := scanner.Text() if strings.TrimSpace(line) == "---" { if !inFrontmatter { inFrontmatter = true continue } break } if !inFrontmatter { continue } key, val, ok := strings.Cut(line, ":") if !ok { continue } v := strings.Trim(strings.TrimSpace(val), `"'`) switch strings.TrimSpace(key) { case "title": title = v case "created_at": if t, err := time.Parse(time.RFC3339, v); err == nil { created = t.UTC().Format("2006-01-02") } else { created = v } } } if title == "" { title = strings.ReplaceAll(slug, "-", " ") } if created == "" { if info, err := os.Stat(path); err == nil { created = info.ModTime().UTC().Format("2006-01-02") } } return title, created }