diff options
| -rw-r--r-- | internal/bible/bible.go | 11 | ||||
| -rw-r--r-- | internal/bible/registry.go | 110 | ||||
| -rw-r--r-- | internal/bible/registry_test.go | 35 | ||||
| -rw-r--r-- | internal/config/config.go | 11 |
4 files changed, 165 insertions, 2 deletions
diff --git a/internal/bible/bible.go b/internal/bible/bible.go index 1bc8983..ac746e1 100644 --- a/internal/bible/bible.go +++ b/internal/bible/bible.go @@ -2,6 +2,7 @@ package bible import ( "embed" + "os" "sort" "strconv" "strings" @@ -32,8 +33,14 @@ func load(version string) *corpus { if c, ok := corpora[version]; ok { return c } - data, err := corporaFS.ReadFile("corpora/" + version + ".tsv") - if err != nil { + var data []byte + if p := userTSVPath(version); p != "" { + data, _ = os.ReadFile(p) + } + if data == nil { + data, _ = corporaFS.ReadFile("corpora/" + version + ".tsv") + } + if data == nil { corpora[version] = &corpus{books: map[string]map[int][]Verse{}} return corpora[version] } diff --git a/internal/bible/registry.go b/internal/bible/registry.go new file mode 100644 index 0000000..2ab721b --- /dev/null +++ b/internal/bible/registry.go @@ -0,0 +1,110 @@ +package bible + +import ( + "os" + "path/filepath" + "sort" + "strings" + "sync" +) + +var ( + regMu sync.Mutex + userDir string + regBuilt bool + regMeta map[string]CorpusMeta // code -> meta (user overrides embed) + regUserTSV map[string]string // code -> user .tsv path (override source) +) + +// SetUserCorporaDir sets the drop-in dir and invalidates the registry cache. +// Empty string disables user corpora (used by tests). +func SetUserCorporaDir(dir string) { + regMu.Lock() + userDir = dir + regBuilt = false + regMu.Unlock() + + corporaMu.Lock() + corpora = map[string]*corpus{} // drop cached loads so overrides take effect + corporaMu.Unlock() +} + +func buildRegistry() { + if regBuilt { + return + } + regMeta = map[string]CorpusMeta{} + regUserTSV = map[string]string{} + // embedded first + ents, _ := corporaFS.ReadDir("corpora") + for _, e := range ents { + if code, ok := strings.CutSuffix(e.Name(), ".ini"); ok { + if m, ok := embeddedCorpusMeta(code); ok { + regMeta[code] = m + } + } + } + // user dir overrides + if userDir != "" { + ents, _ := os.ReadDir(userDir) + for _, e := range ents { + name := e.Name() + if code, ok := strings.CutSuffix(name, ".tsv"); ok { + regUserTSV[code] = filepath.Join(userDir, name) + if _, have := regMeta[code]; !have { + regMeta[code] = CorpusMeta{Code: code, PsalmSystem: "vulgate"} + } + } + } + for _, e := range ents { + if code, ok := strings.CutSuffix(e.Name(), ".ini"); ok { + data, err := os.ReadFile(filepath.Join(userDir, e.Name())) + if err == nil { + regMeta[code] = parseCorpusMeta(code, data) + } + } + } + } + regBuilt = true +} + +// Corpora returns all known corpora, sorted by code. +func Corpora() []CorpusMeta { + regMu.Lock() + defer regMu.Unlock() + buildRegistry() + out := make([]CorpusMeta, 0, len(regMeta)) + for _, m := range regMeta { + out = append(out, m) + } + sort.Slice(out, func(i, j int) bool { return out[i].Code < out[j].Code }) + return out +} + +// Meta returns a corpus's metadata. +func Meta(code string) (CorpusMeta, bool) { + regMu.Lock() + defer regMu.Unlock() + buildRegistry() + m, ok := regMeta[code] + return m, ok +} + +// CorporaForLang returns corpora whose lang == lang, sorted by code. +func CorporaForLang(lang string) []CorpusMeta { + var out []CorpusMeta + for _, m := range Corpora() { + if m.Lang == lang { + out = append(out, m) + } + } + return out +} + +// userTSVPath returns the override .tsv path for code, or "". +func userTSVPath(code string) string { + regMu.Lock() + defer regMu.Unlock() + buildRegistry() + return regUserTSV[code] +} diff --git a/internal/bible/registry_test.go b/internal/bible/registry_test.go new file mode 100644 index 0000000..0e9aab2 --- /dev/null +++ b/internal/bible/registry_test.go @@ -0,0 +1,35 @@ +package bible + +import ( + "os" + "path/filepath" + "testing" +) + +func TestUserCorpusOverride(t *testing.T) { + dir := t.TempDir() + os.WriteFile(filepath.Join(dir, "drb.tsv"), []byte("Genesis\tGn\t1\t1\t1\tUSER TEXT\n"), 0o644) + os.WriteFile(filepath.Join(dir, "drb.ini"), []byte("lang = en\nname = User DRB\npsalm_system = drb\n"), 0o644) + SetUserCorporaDir(dir) + t.Cleanup(func() { SetUserCorporaDir("") }) + + if got := Verses("drb", "Genesis", 1); len(got) == 0 || got[0].Text != "USER TEXT" { + t.Fatalf("user drb.tsv did not override embed: %+v", got) + } + m, _ := Meta("drb") + if m.Name != "User DRB" { + t.Fatalf("user sidecar not used: %+v", m) + } +} + +func TestUserCorpusNewCode(t *testing.T) { + dir := t.TempDir() + os.WriteFile(filepath.Join(dir, "fr-x.tsv"), []byte("Genesis\tGn\t1\t1\t1\tAu commencement\n"), 0o644) + os.WriteFile(filepath.Join(dir, "fr-x.ini"), []byte("lang = fr\nname = Test FR\npsalm_system = vulgate\n"), 0o644) + SetUserCorporaDir(dir) + t.Cleanup(func() { SetUserCorporaDir("") }) + + if l := CorporaForLang("fr"); len(l) != 1 || l[0].Code != "fr-x" { + t.Fatalf("new fr corpus not registered: %+v", l) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 0f99a3a..55abd72 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -274,6 +274,17 @@ func CalendarsDir() (string, error) { return filepath.Join(filepath.Dir(p), "calendars"), nil } +// CorporaDir is the user directory for drop-in bible corpora (<code>.tsv + +// <code>.ini), alongside calendars/ under the lectio config dir. Mirrors +// CalendarsDir exactly (verified: it uses configPath() then filepath.Dir). +func CorporaDir() (string, error) { + p, _, err := configPath() + if err != nil { + return "", err + } + return filepath.Join(filepath.Dir(p), "corpora"), nil +} + // BooksPath returns the path to the optional user books.ini (in the same // directory as the config file). There is no embedded seed on disk -- absence // means "use the built-in defaults". |
