package brain import ( "bufio" "bytes" "errors" "fmt" "io" "io/fs" "sort" "strings" "time" "github.com/mathiasbq/hyperguild/ingestion/internal/brainfs" ) // 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) } // Listed and read through the brain root (hyperguild#157): a wing or // hall that is a symlink is not followed, and notes are regular files. bfs, err := brainfs.Open(brainDir) if err != nil { return fmt.Errorf("open brain: %w", err) } defer func() { _ = bfs.Close() }() if _, err := bfs.Lstat("wiki/" + w); errors.Is(err, fs.ErrNotExist) { return nil } entries, err := collectWingEntries(bfs, "wiki/"+w) 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) } // Written through the brain root, never through a symlinked _index.md // (hyperguild#158). return bfs.PutRegular("wiki/"+w+"/_index.md", []byte(b.String())) } // BuildAllWingIndexes regenerates _index.md for every wing under brain/wiki/. func BuildAllWingIndexes(brainDir string) error { bfs, err := brainfs.Open(brainDir) if err != nil { return fmt.Errorf("open brain: %w", err) } wings, err := bfs.ReadDirDirs("wiki") _ = bfs.Close() if errors.Is(err, fs.ErrNotExist) { return nil } if err != nil { return fmt.Errorf("read wiki: %w", err) } for _, w := range wings { if err := BuildWingIndex(brainDir, w); err != nil { return fmt.Errorf("index %s: %w", w, err) } } return nil } func collectWingEntries(bfs *brainfs.Brain, wingRel string) ([]noteEntry, error) { var out []noteEntry halls, err := bfs.ReadDirDirs(wingRel) if err != nil { return nil, fmt.Errorf("read wing: %w", err) } for _, hall := range halls { if !IsValidHall(hall) { continue } hallRel := wingRel + "/" + hall names, err := bfs.ReadDirRegular(hallRel) if err != nil { return nil, fmt.Errorf("read hall %s: %w", hall, err) } for _, name := range names { if !strings.HasSuffix(name, ".md") || name == "_index.md" { continue } slug := strings.TrimSuffix(name, ".md") title, created := readTitleAndCreatedIn(bfs, hallRel+"/"+name, slug) out = append(out, noteEntry{Hall: hall, Slug: slug, Title: title, Created: created}) } } return out, nil } // readTitleAndCreatedIn is readTitleAndCreated through the brain root, // regular files only (hyperguild#157). func readTitleAndCreatedIn(bfs *brainfs.Brain, rel, slug string) (string, string) { content, err := bfs.ReadRegular(rel) if err != nil { return slug, "" } title, created := parseTitleAndCreated(bytes.NewReader(content)) if created == "" { if info, err := bfs.Lstat(rel); err == nil { created = info.ModTime().UTC().Format("2006-01-02") } } return titleOrSlug(title, slug), created } func titleOrSlug(title, slug string) string { if title == "" { return strings.ReplaceAll(slug, "-", " ") } return title } func parseTitleAndCreated(r io.Reader) (title, created string) { scanner := bufio.NewScanner(r) 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 } } } return title, created }