aboutsummaryrefslogtreecommitdiff
path: root/internal/bible/bible.go
blob: 1fb29f2639924d3eeb3c4dddb89476e38bde1277 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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
// <code>)" note, and users may drop any <code>.tsv/<code>.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
}