package bible import ( "embed" "io/fs" "os" "sort" "strconv" "strings" "sync" ) // corporaFS embeds only the public-domain Latin Vulgate (vul), which is the // sole corpus compiled into the default binary. Every other translation lives // in corpora_optional/ and is NOT embedded unless the build carries the // "fullbible" tag (see embed_optional.go), keeping the distributed binary free // of any third-party scripture text -- bible corpora are not covered by // lectio's AGPL licence. Missing corpora resolve gracefully to a "(not in // )" note, and users may drop any .tsv/.ini into their // corpora dir (SetUserCorporaDir) to add a translation without rebuilding. // //go:embed corpora var corporaFS embed.FS // optionalFS holds the extra corpora (corpora_optional/) when the "fullbible" // build tag is set; it is nil in the default build. Set by embed_optional.go. var optionalFS fs.FS // embReadCorpus returns the bytes of an embedded corpus file (e.g. "vul.tsv", // "drb.ini") from the core FS, then the optional FS if present. func embReadCorpus(name string) ([]byte, bool) { if b, err := corporaFS.ReadFile("corpora/" + name); err == nil { return b, true } if optionalFS != nil { if b, err := fs.ReadFile(optionalFS, "corpora_optional/"+name); err == nil { return b, true } } return nil, false } // embCorpusFiles lists the base file names of every embedded corpus file across // the core and optional FSes. func embCorpusFiles() []string { var names []string if ents, err := corporaFS.ReadDir("corpora"); err == nil { for _, e := range ents { names = append(names, e.Name()) } } if optionalFS != nil { if ents, err := fs.ReadDir(optionalFS, "corpora_optional"); err == nil { for _, e := range ents { names = append(names, e.Name()) } } } return names } // Verse is a single verse. type Verse struct { Chapter, Verse int Text string } type corpus struct { books map[string]map[int][]Verse // book -> chapter -> verses (verse-ordered) } var ( corpora = map[string]*corpus{} corporaMu sync.Mutex ) func load(version string) *corpus { corporaMu.Lock() defer corporaMu.Unlock() if c, ok := corpora[version]; ok { return c } var data []byte if p := userTSVPath(version); p != "" { data, _ = os.ReadFile(p) } if data == nil { data, _ = embReadCorpus(version + ".tsv") } if data == nil { corpora[version] = &corpus{books: map[string]map[int][]Verse{}} return corpora[version] } c := &corpus{books: map[string]map[int][]Verse{}} for _, line := range strings.Split(string(data), "\n") { f := strings.Split(line, "\t") if len(f) != 6 { continue } chap, _ := strconv.Atoi(f[3]) vn, _ := strconv.Atoi(f[4]) book := f[0] if c.books[book] == nil { c.books[book] = map[int][]Verse{} } c.books[book][chap] = append(c.books[book][chap], Verse{chap, vn, f[5]}) } corpora[version] = c return c } // Verses returns all verses of one chapter of a book in a version (may be empty). func Verses(version, book string, chap int) []Verse { return load(version).books[book][chap] } // Chapters returns the chapter numbers present for a book in a version, sorted // ascending (empty if the version has no such book). func Chapters(version, book string) []int { chapMap := load(version).books[book] chaps := make([]int, 0, len(chapMap)) for ch := range chapMap { chaps = append(chaps, ch) } sort.Ints(chaps) return chaps } // CorpusBooks returns the book names present in a version's corpus, sorted // (empty for a version with no embedded corpus, e.g. "bt"). func CorpusBooks(version string) []string { c := load(version) books := make([]string, 0, len(c.books)) for b := range c.books { books = append(books, b) } sort.Strings(books) return books }