feat(help): add LoadContentDir for directory-based catalog loading
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
0c474a82d0
commit
a0b81319dd
2 changed files with 80 additions and 0 deletions
|
|
@ -2,9 +2,13 @@ package help
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"iter"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Catalog manages help topics.
|
||||
|
|
@ -90,6 +94,44 @@ func (c *Catalog) SearchResults(query string) iter.Seq[*SearchResult] {
|
|||
return slices.Values(c.Search(query))
|
||||
}
|
||||
|
||||
// LoadContentDir recursively loads all .md files from a directory into a Catalog.
|
||||
func LoadContentDir(dir string) (*Catalog, error) {
|
||||
c := &Catalog{
|
||||
topics: make(map[string]*Topic),
|
||||
index: newSearchIndex(),
|
||||
}
|
||||
|
||||
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(strings.ToLower(d.Name()), ".md") {
|
||||
return nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading %s: %w", path, err)
|
||||
}
|
||||
|
||||
topic, err := ParseTopic(path, content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing %s: %w", path, err)
|
||||
}
|
||||
|
||||
c.Add(topic)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("walking directory %s: %w", dir, err)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Get returns a topic by ID.
|
||||
func (c *Catalog) Get(id string) (*Topic, error) {
|
||||
t, ok := c.topics[id]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package help
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -176,6 +178,42 @@ func TestCatalog_Search_Good_ScoreTiebreaking(t *testing.T) {
|
|||
"scores should be equal for tie-breaking to apply")
|
||||
}
|
||||
|
||||
func TestCatalog_LoadContentDir_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
os.MkdirAll(filepath.Join(dir, "cli"), 0o755)
|
||||
os.WriteFile(filepath.Join(dir, "cli", "dev-work.md"), []byte("---\ntitle: Dev Work\ntags: [cli, dev]\n---\n\n## Usage\n\ncore dev work syncs your workspace.\n"), 0o644)
|
||||
os.WriteFile(filepath.Join(dir, "cli", "setup.md"), []byte("---\ntitle: Setup\ntags: [cli]\n---\n\n## Installation\n\nRun core setup to get started.\n"), 0o644)
|
||||
|
||||
catalog, err := LoadContentDir(dir)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, catalog.List(), 2)
|
||||
|
||||
devWork, err := catalog.Get("dev-work")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Dev Work", devWork.Title)
|
||||
assert.Contains(t, devWork.Tags, "cli")
|
||||
|
||||
results := catalog.Search("workspace")
|
||||
assert.NotEmpty(t, results)
|
||||
}
|
||||
|
||||
func TestCatalog_LoadContentDir_Good_Empty(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
catalog, err := LoadContentDir(dir)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, catalog.List())
|
||||
}
|
||||
|
||||
func TestCatalog_LoadContentDir_Good_SkipsNonMd(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
os.WriteFile(filepath.Join(dir, "readme.txt"), []byte("not markdown"), 0o644)
|
||||
os.WriteFile(filepath.Join(dir, "topic.md"), []byte("---\ntitle: Topic\n---\n\nContent here.\n"), 0o644)
|
||||
|
||||
catalog, err := LoadContentDir(dir)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, catalog.List(), 1)
|
||||
}
|
||||
|
||||
func BenchmarkSearch(b *testing.B) {
|
||||
// Build a catalog with 100+ topics for benchmarking.
|
||||
c := &Catalog{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue