json-bible 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { Bible } from "./lib/Bible"
2
2
  import { BIBLE_SIZE, getBookCategory, getDefault, getDefaultBooks, NT_SIZE, OT_SIZE } from "./lib/defaults"
3
- import { _getBible, _getBook, _getChapter, _getCloseChapter, _getHTML, _getMetadata, _getRandomVerse, _getShortName, _getText, _getVerse, formatText, getBookIndex, getChapterIndex, getVerseIndex } from "./lib/get"
3
+ import { _getBible, _getBook, _getChapter, _getCloseChapter, _getHTML, _getMetadata, _getRandomVerse, _getShortName, _getText, _getVerse, formatText, getBookAbbreviation, getBookIndex, getChapterIndex, getVerseIndex } from "./lib/get"
4
4
  import { getReferenceString, getVerseReferences } from "./lib/reference"
5
5
  import { _bookSearch, _textSearch } from "./lib/search"
6
+ import "./lib/api/index" // api alternative reader (same structure)
6
7
 
7
8
  /**
8
9
  * JSON Bible Helper
@@ -11,7 +12,7 @@ import { _bookSearch, _textSearch } from "./lib/search"
11
12
  export default async function Bible(bibleOrPath: Bible | string) {
12
13
  const bible = await _getBible(bibleOrPath)
13
14
 
14
- return { data: bible, getBook, getAbbreviation, getMetadata, getDefaultBooks, getDefault, getOT, getNT, getRandom, getFromReference, bookSearch, textSearch }
15
+ return { data: bible, getBook, getAbbreviation, getMetadata, getBooksData, getDefaultBooks, getDefault, getOT, getNT, getRandom, getFromReference, bookSearch, textSearch }
15
16
 
16
17
  /**
17
18
  * Get a specified book or the first one if not provided
@@ -156,8 +157,7 @@ export default async function Bible(bibleOrPath: Bible | string) {
156
157
  * @return e.g. "Genesis" = "Gen"
157
158
  */
158
159
  function getNameShort() {
159
- const abbr = getDefaultBooks().ids[book.number - 1]
160
- return abbr[0] + abbr.slice(1).toLowerCase()
160
+ return getBookAbbreviation(bible, bookIndex)
161
161
  }
162
162
 
163
163
  /**
@@ -184,6 +184,23 @@ export default async function Bible(bibleOrPath: Bible | string) {
184
184
  return _getMetadata(bible)
185
185
  }
186
186
 
187
+ /**
188
+ * Get books data with category info and calculated abbreviation
189
+ * @return array with book data
190
+ */
191
+ function getBooksData() {
192
+ return bible.books.map((book, bookIndex) => {
193
+ const category = getBookCategory(book)
194
+ return {
195
+ id: book.id,
196
+ name: book.name,
197
+ number: book.number,
198
+ abbreviation: getBookAbbreviation(bible, bookIndex),
199
+ category
200
+ }
201
+ })
202
+ }
203
+
187
204
  /**
188
205
  * Get all books in the Bible from the Old Testament
189
206
  * @return list of max 39 books
package/lib/Bible.ts CHANGED
@@ -9,6 +9,7 @@ export interface Bible {
9
9
  export type Book = {
10
10
  number: number // starting from 1
11
11
  name: string // e.g. "Genesis"
12
+ abbreviation?: string // e.g. "Gen"
12
13
  id?: string // e.g. "GEN"
13
14
 
14
15
  chapters: Chapter[]
@@ -0,0 +1,147 @@
1
+ // BIBLES
2
+
3
+ // Bible.API format
4
+ export interface BibleListContent {
5
+ id: string
6
+ dblId: string // id without "-01"
7
+ relatedDbl: string | null
8
+ name: string
9
+ nameLocal: string // usually same as name
10
+ abbreviation: string
11
+ abbreviationLocal: string // usually same as abbreviation
12
+ description: string
13
+ descriptionLocal: string // usually same as description
14
+ language: {
15
+ id: string // e.g., "eng"
16
+ name: string // English name
17
+ nameLocal: string // native name
18
+ script: string // e.g., Latin
19
+ scriptDirection: string // e.g., LTR
20
+ }
21
+ countries: { id: string; name: string; nameLocal: string }[]
22
+ type: string // "text" or "audio"
23
+ updatedAt: string // ISO date string
24
+ audioBibles: any[]
25
+ }
26
+
27
+ // contentapi.churchapps.org/bibles format
28
+ export interface CustomBibleListContent {
29
+ id: string // custom id
30
+ abbreviation: string
31
+ name: string
32
+ nameLocal: string
33
+ description: string
34
+ source: string // "api.bible"
35
+ sourceKey: string // api.bible id
36
+ language: string // language.id
37
+ copyright: string // custom deals with copyright info
38
+ attributionRequired: boolean // custom deal that needs to show attribution string
39
+ attributionString: string // custom attribution string
40
+ countryList: string[] // countries.id (lowercase)
41
+ }
42
+
43
+ // BOOK
44
+
45
+ export interface BibleBookContent {
46
+ id: string
47
+ bibleId: string
48
+ abbreviation: string
49
+ name: string
50
+ nameLong: string
51
+ }
52
+
53
+ // contentapi.churchapps.org format
54
+ export interface CustomBibleBookContent {
55
+ id: string // custom id
56
+ translationKey: string // bibleId
57
+ keyName: string // id
58
+ abbreviation: string
59
+ name: string
60
+ sort: number // book index
61
+ }
62
+
63
+ // CHAPTER
64
+
65
+ export interface BibleChapterContent {
66
+ id: string // e.g., GEN.1
67
+ bibleId: string
68
+ bookId: string // e.g., GEN
69
+ number: string // can be "intro"
70
+ reference: string
71
+ }
72
+
73
+ // contentapi.churchapps.org format
74
+ export interface CustomChapterContent {
75
+ id: string // custom id
76
+ translationKey: string // bibleId
77
+ bookKey: string // bookId
78
+ keyName: string // id
79
+ number: number // chapter number
80
+ }
81
+
82
+ // VERSE
83
+
84
+ export interface BibleVerseListContent {
85
+ id: string // e.g., GEN.1.1
86
+ orgId: string
87
+ bookId: string // e.g., GEN
88
+ chapterId: string // e.g., GEN.1
89
+ bibleId: string
90
+ reference: string
91
+ }
92
+
93
+ // contentapi.churchapps.org format
94
+ export interface CustomVerseListContent {
95
+ id: string // custom id
96
+ translationKey: string // bibleId
97
+ chapterKey: string // chapterId
98
+ keyName: string // id
99
+ number: number // verse number
100
+ }
101
+
102
+ export interface BibleVerseContent {
103
+ id: string // e.g., GEN.1.1
104
+ orgId: string
105
+ bookId: string // e.g., GEN
106
+ chapterId: string // e.g., GEN.1
107
+ bibleId: string
108
+ reference: string
109
+ content: string // HTML content
110
+ verseCount: number
111
+ copyright: string
112
+ next: { id: string; number: string } | null
113
+ previous: { id: string; number: string } | null
114
+ }
115
+
116
+ // contentapi.churchapps.org format
117
+ export interface CustomVerseContent {
118
+ id: string // custom id
119
+ translationKey: string // bibleId
120
+ verseKey: string // id
121
+ bookKey: string // bookId
122
+ chapterNumber: number // chapter number
123
+ verseNumber: number // verse number
124
+ content: string // plain text content
125
+ newParagraph: boolean // if new paragraph starts here
126
+ }
127
+
128
+ // SEARCH
129
+
130
+ export interface BibleContentSearchResult {
131
+ query: string
132
+ limit: number
133
+ offset: number
134
+ total: number
135
+ verseCount: number
136
+ verses: BibleVerseSearchContent[]
137
+ }
138
+
139
+ export interface BibleVerseSearchContent {
140
+ id: string // e.g., GEN.1.1
141
+ orgId: string // usually same as id
142
+ bookId: string // e.g., GEN
143
+ bibleId: string // api bible id
144
+ chapterId: string // e.g., GEN.1
145
+ reference: string // e.g., Genesis 1:1
146
+ text: string // plain text content
147
+ }
@@ -0,0 +1,120 @@
1
+ import type { Verse } from "../Bible"
2
+ import type { BibleBookContent, BibleChapterContent, BibleListContent, BibleVerseContent, BibleVerseListContent, CustomBibleBookContent, CustomBibleListContent, CustomChapterContent, CustomVerseContent, CustomVerseListContent } from "./ApiBible"
3
+
4
+ export function getCustomBibleListContent(data: (BibleListContent | CustomBibleListContent)[]): CustomBibleListContent[] {
5
+ if (!data?.length) return []
6
+ if ((data[0] as CustomBibleListContent).sourceKey) return data as CustomBibleListContent[]
7
+
8
+ return (data as BibleListContent[]).map((item) => {
9
+ return {
10
+ id: item.id,
11
+ abbreviation: item.abbreviation,
12
+ name: item.name,
13
+ nameLocal: item.nameLocal,
14
+ description: item.description,
15
+ source: "api.bible",
16
+ sourceKey: item.id,
17
+ language: item.language.id,
18
+ copyright: "",
19
+ attributionRequired: false,
20
+ attributionString: "",
21
+ countryList: item.countries.map((country) => country.id.toLowerCase())
22
+ } as CustomBibleListContent
23
+ })
24
+ }
25
+
26
+ export function getCustomBookContent(data: (BibleBookContent | CustomBibleBookContent)[]): CustomBibleBookContent[] {
27
+ if (!data?.length) return []
28
+ if ((data[0] as CustomBibleBookContent).keyName) return data as CustomBibleBookContent[]
29
+
30
+ return (data as BibleBookContent[]).map((book, i) => {
31
+ return {
32
+ id: book.id,
33
+ translationKey: book.bibleId,
34
+ keyName: book.id,
35
+ abbreviation: book.abbreviation,
36
+ name: book.name,
37
+ sort: i
38
+ } as CustomBibleBookContent
39
+ })
40
+ }
41
+
42
+ export function getCustomChapterContent(data: (BibleChapterContent | CustomChapterContent)[]): CustomChapterContent[] {
43
+ if (!data?.length) return []
44
+ if ((data[0] as CustomChapterContent).keyName) return data as CustomChapterContent[]
45
+
46
+ return (data as BibleChapterContent[]).map((chapter) => {
47
+ return {
48
+ id: chapter.id,
49
+ translationKey: chapter.bibleId,
50
+ bookKey: chapter.bookId,
51
+ keyName: chapter.id,
52
+ number: parseInt(chapter.number)
53
+ } as CustomChapterContent
54
+ })
55
+ }
56
+
57
+ export function getCustomVerseContent(data: (BibleVerseListContent | CustomVerseListContent)[]): CustomVerseListContent[] {
58
+ if (!data?.length) return []
59
+ if ((data as CustomVerseListContent[])[0]?.keyName) return data as CustomVerseListContent[]
60
+
61
+ return (data as BibleVerseListContent[]).map((verse) => {
62
+ return {
63
+ keyName: verse.id,
64
+ number: parseInt(verse.id.split(".")[2]),
65
+ id: verse.id,
66
+ translationKey: verse.bibleId,
67
+ chapterKey: verse.chapterId
68
+ } as CustomVerseListContent
69
+ })
70
+ }
71
+
72
+ export function convertVerseTextToJson(verses: BibleVerseContent | CustomVerseContent[]) {
73
+ // custom data format
74
+ if (Array.isArray(verses)) {
75
+ return verses.map((verse) => {
76
+ const verseNumber = verse.verseNumber
77
+ const plainText = verse.content || ""
78
+ return { number: verseNumber, text: plainText } as Verse
79
+ })
80
+ }
81
+
82
+ // Parse HTML content to extract verses with their numbers
83
+ const text = verses.content || ""
84
+ const verseRegex = /<span data-number="(\d+)"[^>]*class="v"[^>]*>\d+<\/span>/g
85
+ const versesArray: Verse[] = []
86
+
87
+ let match: RegExpExecArray | null
88
+ while ((match = verseRegex.exec(text)) !== null) {
89
+ const verseNumber = parseInt(match[1])
90
+ const startIndex = match.index + match[0].length
91
+
92
+ // Find the next verse or end of text
93
+ const nextMatch = verseRegex.exec(text)
94
+ const endIndex = nextMatch ? nextMatch.index : text.length
95
+
96
+ // Reset regex position to continue from where we left off
97
+ verseRegex.lastIndex = startIndex
98
+
99
+ // Extract text between current verse and next verse (or end)
100
+ let verseText = text.substring(startIndex, endIndex)
101
+
102
+ // Clean up HTML tags and normalize whitespace
103
+ verseText = verseText
104
+ .replace(/<\/p>/g, " ") // Replace closing p tags with space
105
+ .replace(/<[^>]*>/g, " ") // Remove all other HTML tags
106
+ .replace(/\s+/g, " ") // Normalize whitespace
107
+ .trim()
108
+
109
+ if (verseText) {
110
+ versesArray.push({ number: verseNumber, text: verseText } as Verse)
111
+ }
112
+
113
+ // If we found a next match, we need to process it in the next iteration
114
+ if (nextMatch) {
115
+ verseRegex.lastIndex = nextMatch.index
116
+ }
117
+ }
118
+
119
+ return versesArray
120
+ }
package/lib/api/get.ts ADDED
@@ -0,0 +1,156 @@
1
+ import type { Bible, Book, Chapter } from "../Bible"
2
+ import type { BibleContentSearchResult, BibleVerseContent, CustomVerseContent } from "./ApiBible"
3
+ import { convertVerseTextToJson, getCustomBibleListContent, getCustomBookContent, getCustomChapterContent, getCustomVerseContent } from "./converters"
4
+
5
+ const APIBIBLE_URL = "https://api.scripture.api.bible/v1/bibles"
6
+
7
+ export default function ApiBibleHelper(key: string, customApiUrl?: string) {
8
+ if (!key) throw new Error("No API key!")
9
+
10
+ let apiUrl = customApiUrl || APIBIBLE_URL
11
+ if (apiUrl.endsWith("/")) apiUrl = apiUrl.slice(0, -1)
12
+
13
+ const headers: { [key: string]: string } = { "api-key": key }
14
+
15
+ return { getBibles, bible, clearCache }
16
+
17
+ async function getBibles() {
18
+ return getCustomBibleListContent(await fetchWrapper(apiUrl, headers, 3))
19
+ }
20
+
21
+ async function bible(bibleId: string) {
22
+ const bibleUrl = `${apiUrl}/${bibleId}`
23
+
24
+ // BOOKS
25
+
26
+ const booksData = await getApiBooks()
27
+
28
+ const json: Bible = {
29
+ name: "",
30
+ abbreviation: "",
31
+ metadata: {},
32
+ books: booksData.map((book, i) => {
33
+ return { number: i + 1, name: book.name, id: book.keyName, chapters: [] } as Book
34
+ })
35
+ }
36
+
37
+ return { json, booksData, getChapters, getVerses, contentSearch }
38
+
39
+ async function getApiBooks() {
40
+ const booksUrl = `${bibleUrl}/books`
41
+ return getCustomBookContent(await fetchWrapper(booksUrl, headers, 60))
42
+ }
43
+
44
+ // CHAPTERS
45
+
46
+ // GEN
47
+ async function getChapters(bookId: string) {
48
+ const chaptersData = await getApiChapters(bookId)
49
+
50
+ const json = chaptersData.map((chapter) => {
51
+ return { number: chapter.number, verses: [] } as Chapter
52
+ })
53
+
54
+ return { json, chaptersData }
55
+ }
56
+
57
+ async function getApiChapters(bookId: string) {
58
+ const chaptersUrl = customApiUrl ? `${bibleUrl}/${bookId}/chapters` : `${bibleUrl}/books/${bookId}/chapters`
59
+ return getCustomChapterContent(await fetchWrapper(chaptersUrl, headers, 60)).filter((a) => !a.keyName.includes("intro"))
60
+ }
61
+
62
+ // VERSES
63
+
64
+ // GEN.1
65
+ async function getVerses(chapterId: string) {
66
+ const versesData = await getApiVerses(chapterId)
67
+ const reference = versesData[0].keyName + "-" + versesData[versesData.length - 1].keyName
68
+ const versesContent = await getApiVersesContent(reference)
69
+
70
+ const json = convertVerseTextToJson(versesContent)
71
+
72
+ return { json, versesData }
73
+ }
74
+
75
+ async function getApiVerses(chapterId: string) {
76
+ const versesUrl = `${bibleUrl}/chapters/${chapterId}/verses`
77
+ return getCustomVerseContent(await fetchWrapper(versesUrl, headers, 60))
78
+ }
79
+
80
+ // GEN.1.1-GEN.1.10
81
+ async function getApiVersesContent(reference: string) {
82
+ const versesReferenceUrl = `${bibleUrl}/verses/${reference}`
83
+ return (await fetchWrapper(versesReferenceUrl, headers, 60)) as BibleVerseContent | CustomVerseContent[]
84
+ }
85
+
86
+ // SEARCH
87
+
88
+ // no api key needed, just the bible id
89
+ async function contentSearch(query: string, { limit } = { limit: 20 }) {
90
+ const url = `${bibleUrl}/search?query=${query}&limit=${limit}`
91
+ return ((await fetchWrapper(url, headers, 14)) as BibleContentSearchResult).verses
92
+ }
93
+ }
94
+
95
+ function clearCache(key: string) {
96
+ clearCachedContent(key)
97
+ }
98
+ }
99
+
100
+ // HTTP
101
+
102
+ export function fetchWrapper(url: string, headers: any, cacheTimeDays: number) {
103
+ if (getCachedContent(url)) return Promise.resolve(getCachedContent(url, cacheTimeDays))
104
+
105
+ // console.info("Fetching:", url)
106
+ return fetch(url, { headers })
107
+ .then((response) => {
108
+ if (response.status !== 200) {
109
+ throw new Error("Bad response from server: " + response.status)
110
+ }
111
+
112
+ return response.json()
113
+ })
114
+ .then((data) => {
115
+ if (!Array.isArray(data) && data?.data) data = data.data
116
+ if (!data) throw new Error("No data found")
117
+
118
+ cacheContent(url, data)
119
+ return data
120
+ })
121
+ .catch((e) => {
122
+ throw new Error(e)
123
+ })
124
+ }
125
+
126
+ // CACHE
127
+
128
+ export function cacheContent(key: string, data: any) {
129
+ if (typeof localStorage === "undefined") return
130
+
131
+ const cacheEntry = { data, timestamp: Date.now() }
132
+ localStorage.setItem(key, JSON.stringify(cacheEntry))
133
+ }
134
+
135
+ const ONE_DAY_MS = 24 * 60 * 60 * 1000
136
+ export function getCachedContent(key: string, maxAgeDays: number = 7) {
137
+ if (typeof localStorage === "undefined") return null
138
+
139
+ const cached = localStorage.getItem(key)
140
+ if (!cached) return null
141
+
142
+ const { data, timestamp } = JSON.parse(cached)
143
+ const age = Date.now() - timestamp
144
+
145
+ if (age > maxAgeDays * ONE_DAY_MS) {
146
+ clearCachedContent(key)
147
+ return null
148
+ }
149
+
150
+ return data
151
+ }
152
+
153
+ export function clearCachedContent(key: string) {
154
+ if (typeof localStorage === "undefined") return
155
+ localStorage.removeItem(key)
156
+ }
@@ -0,0 +1,293 @@
1
+ import { BIBLE_SIZE, getBookCategory, getDefault, getDefaultBooks, NT_SIZE, OT_SIZE } from "../defaults"
2
+ import { _getBook, _getChapter, _getCloseChapter, _getHTML, _getMetadata, _getShortName, _getText, _getVerse, formatText, getBookAbbreviation, getBookIndex, getChapterIndex, getVerseIndex } from "../get"
3
+ import { getReferenceString, getVerseReferences, VerseReference } from "../reference"
4
+ import { _bookSearch } from "../search"
5
+ import ApiBibleHelper from "./get"
6
+
7
+ ///// API BIBLE (requests one part at a time) /////
8
+ // Works with the Bible.API format
9
+ // Any custom API URL uses a smaller, but similar format (see ApiBible.ts)
10
+ // Used by FreeShow Presentation Software through the ChurchApps ContentAPI
11
+
12
+ /**
13
+ * API.Bible Bibles List
14
+ * @param apiKey API key from https://api.bible
15
+ * @param apiUrl custom api URL
16
+ * @returns
17
+ */
18
+ export function ApiBiblesList(apiKey: string, apiUrl?: string) {
19
+ return ApiBibleHelper(apiKey, apiUrl).getBibles()
20
+ }
21
+
22
+ /**
23
+ * API.Bible Helper
24
+ * @param apiKey API key from https://api.bible
25
+ * @param apiUrl custom api URL
26
+ */
27
+ export async function ApiBible(apiKey: string, bibleKey: string, apiUrl?: string) {
28
+ const apiData = await ApiBibleHelper(apiKey, apiUrl)
29
+ const bibleData = await apiData.bible(bibleKey)
30
+ const bible = bibleData.json
31
+
32
+ return { data: bible, getBook, getAbbreviation, getMetadata, getBooksData, getDefaultBooks, getDefault, getOT, getNT, getFromReference, bookSearch, textSearch }
33
+
34
+ /**
35
+ * Get a specified book or the first one if not provided
36
+ * @param number book number/name/id
37
+ * @return book data
38
+ */
39
+ async function getBook(numberOrId?: string | number) {
40
+ if (numberOrId === undefined) numberOrId = bible.books[0].number
41
+ let bookIndex = getBookIndex(bible, numberOrId)
42
+ const book = _getBook(bible, bookIndex)
43
+
44
+ const bookId = bible.books[bookIndex]?.id || ""
45
+ const chapters = await bibleData.getChapters(bookId)
46
+ book.chapters = chapters.json
47
+
48
+ return { data: book, index: bookIndex, number: book.number, name: book.name, getChapter, getAbbreviation: getNameShort, getCategory }
49
+
50
+ /**
51
+ * Get a specified chapter or the first one if no number provided
52
+ * @param number chapter number
53
+ * @return chapter data
54
+ */
55
+ async function getChapter(number?: number) {
56
+ if (number === undefined) number = book.chapters[0].number
57
+ let chapterIndex = getChapterIndex(book, number)
58
+ const chapter = _getChapter(book, chapterIndex)
59
+
60
+ const chapterId = `${bookId}.${number}`
61
+ const verses = await bibleData.getVerses(chapterId)
62
+ chapter.verses = verses.json
63
+
64
+ return { data: chapter, index: chapterIndex, number, getVerse, getVerses, getNext, getPrevious, getReference }
65
+
66
+ /**
67
+ * Get a specified verse or the first one if no number provided
68
+ * @param number verse number
69
+ * @return verse data
70
+ */
71
+ function getVerse(number?: number) {
72
+ if (number === undefined) number = chapter.verses[0].number
73
+ let verseIndex = getVerseIndex(chapter, number)
74
+ const verse = _getVerse(chapter, verseIndex)
75
+
76
+ return { data: verse, index: verseIndex, number, getText, getHTML, getReference }
77
+
78
+ /**
79
+ * A plain text string of the verse
80
+ * @param includeNumbers should verse numbers be included
81
+ * @return verse text
82
+ */
83
+ function getText(includeNumber: boolean = false) {
84
+ return _getText(verse, includeNumber)
85
+ }
86
+
87
+ /**
88
+ * A HTML string of the verse with parsed markdown
89
+ * @param includeNumbers should verse numbers be included
90
+ * @return HTML string value
91
+ */
92
+ function getHTML(includeNumber: boolean = false) {
93
+ return _getHTML(verse, includeNumber)
94
+ }
95
+
96
+ /**
97
+ * Get verse reference
98
+ * @param addBibleVersion add Bible abbreviation at the end of the reference
99
+ * @return reference string, e.g. "Genesis 1:1"
100
+ */
101
+ function getReference(addBibleVersion: boolean = false) {
102
+ return _getReference([verse.number], addBibleVersion)
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Get a selection of verses based in an array of numbers
108
+ * @param verseNumbers verse numbers
109
+ * @param html should markdown text be parsed to HTML
110
+ * @return verses data
111
+ */
112
+ function getVerses(verseNumbers: number[] = [], html: boolean = false) {
113
+ let verses = chapter.verses
114
+ if (verseNumbers.length) verses = verses.filter((a) => verseNumbers.includes(a.number))
115
+
116
+ const data = verses.reduce((obj, { number, text }) => ({ ...obj, [number]: formatText(text, html) }), {} as { [key: number]: string })
117
+ const numbers = verseNumbers.length ? verseNumbers : verses.map((a) => a.number)
118
+
119
+ return { data, numbers, getText, getHTML, getReference }
120
+
121
+ /**
122
+ * A plain text string of the verses
123
+ * @param includeNumbers should verse numbers be included
124
+ * @return verse text
125
+ */
126
+ function getText(includeNumbers: boolean = false) {
127
+ return verses.map((v) => _getText(v, includeNumbers)).join(" ")
128
+ }
129
+
130
+ /**
131
+ * A HTML string of the verses with parsed markdown
132
+ * @param includeNumbers should verse numbers be included
133
+ * @return HTML string value
134
+ */
135
+ function getHTML(includeNumbers: boolean = false) {
136
+ return verses.map((v, i) => _getHTML(v, includeNumbers, i === 0)).join(" ")
137
+ }
138
+
139
+ /**
140
+ * Get verses reference
141
+ * @param addBibleVersion add Bible abbreviation at the end of the reference
142
+ * @return reference string, e.g. "Genesis 1:1-3"
143
+ */
144
+ function getReference(addBibleVersion: boolean = false) {
145
+ return _getReference(numbers, addBibleVersion)
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Get next chapter relative to the one currently selected
151
+ * @return a reference to the next chapter, including possibly changed book - null if none available
152
+ */
153
+ function getNext() {
154
+ return _getCloseChapter(bible, book.number, chapter.number, true)
155
+ }
156
+
157
+ /**
158
+ * Get previous chapter relative to the one currently selected
159
+ * @return a reference to the previous chapter, including possibly changed book - null if none available
160
+ */
161
+ function getPrevious() {
162
+ return _getCloseChapter(bible, book.number, chapter.number, false)
163
+ }
164
+
165
+ /**
166
+ * Get chapter reference
167
+ * @param addBibleVersion add Bible abbreviation at the end of the reference
168
+ * @return reference string, e.g. "Genesis 1"
169
+ */
170
+ function getReference(addBibleVersion: boolean = false) {
171
+ return _getReference([], addBibleVersion)
172
+ }
173
+
174
+ // HELPER
175
+ function _getReference(verses: number[], addBibleVersion: boolean = false) {
176
+ return getReferenceString({ book: book.number, chapter: chapter.number, verses }, bible) + (addBibleVersion ? ` ${getAbbreviation()}` : "")
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Get three letter book abbreviation/short name
182
+ * @return e.g. "Genesis" = "Gen"
183
+ */
184
+ function getNameShort() {
185
+ return getBookAbbreviation(bible, bookIndex)
186
+ }
187
+
188
+ /**
189
+ * Get current book category name/color, all categories are:
190
+ * "The Law", "History", "Poetry & Wisdom", "Prophets", "The Gospels & Acts", "Letters" & "Apocalyptic"
191
+ * @return book category object
192
+ */
193
+ function getCategory() {
194
+ return getBookCategory(book)
195
+ }
196
+ }
197
+
198
+ /**
199
+ * @return bible name abbreviation/identifier
200
+ */
201
+ function getAbbreviation() {
202
+ return _getShortName(bible)
203
+ }
204
+
205
+ /**
206
+ * @return trimmed metadata object
207
+ */
208
+ function getMetadata() {
209
+ return _getMetadata(bible)
210
+ }
211
+
212
+ /**
213
+ * Get books data with category info and calculated abbreviation
214
+ * @return array with book data
215
+ */
216
+ function getBooksData() {
217
+ return bible.books.map((book, bookIndex) => {
218
+ const category = getBookCategory(book)
219
+ return {
220
+ id: book.id,
221
+ name: book.name,
222
+ number: book.number,
223
+ abbreviation: getBookAbbreviation(bible, bookIndex),
224
+ category
225
+ }
226
+ })
227
+ }
228
+
229
+ /**
230
+ * Get all books in the Bible from the Old Testament
231
+ * @return list of max 39 books
232
+ */
233
+ function getOT() {
234
+ if (bible.books.length === OT_SIZE) return bible.books
235
+ if (bible.books.length === BIBLE_SIZE) return bible.books.slice(0, OT_SIZE)
236
+ return bible.books.filter((a) => a.number >= 1 && a.number <= OT_SIZE)
237
+ }
238
+
239
+ /**
240
+ * Get all books in the Bible from the New Testament
241
+ * @return list of max 27 books
242
+ */
243
+ function getNT() {
244
+ if (bible.books.length === NT_SIZE) return bible.books
245
+ if (bible.books.length === BIBLE_SIZE) return bible.books.slice(OT_SIZE)
246
+ return bible.books.filter((a) => a.number > OT_SIZE && a.number <= BIBLE_SIZE)
247
+ }
248
+
249
+ /**
250
+ * Get book/chapter/verses from a string reference
251
+ * @param value e.g. "Genesis 1:1-3" / "GEN.1.2-3" / "1.1.1"
252
+ * @return matching content
253
+ */
254
+ function getFromReference(value: string) {
255
+ return getVerseReferences(bible, value)
256
+ }
257
+
258
+ /**
259
+ * Search for book/chapter/verses with autocomplete
260
+ * @param value e.g. "Genesis 1:1-3"
261
+ * @return matching content
262
+ */
263
+ function bookSearch(value: string) {
264
+ return _bookSearch(bible, value)
265
+ }
266
+
267
+ /**
268
+ * Search for text content in the entire Bible
269
+ * @param value search string
270
+ * @param limit max results, lower number means quicker search
271
+ * @param bookNumber search only in a specified book
272
+ * @return an array of verses
273
+ */
274
+ async function textSearch(value: string, limit: number = 50) {
275
+ const result = await bibleData.contentSearch(value, { limit })
276
+
277
+ // convert result to VerseReference[]
278
+ return result.map((r) => {
279
+ const bookIndex = bible.books.findIndex((b) => b.id === r.bookId)
280
+ const book = bookIndex >= 0 ? bookIndex + 1 : r.bookId
281
+ const chapter = Number(r.id.split(".")[1])
282
+ const verse = Number(r.id.split(".")[2])
283
+
284
+ return {
285
+ book,
286
+ chapter,
287
+ verse: { number: verse, text: r.text },
288
+ reference: r.reference,
289
+ id: `${r.bookId}.${chapter}.${verse}`
290
+ } as VerseReference
291
+ })
292
+ }
293
+ }
package/lib/defaults.ts CHANGED
@@ -32,6 +32,7 @@ export function getDefaultMetadata() {
32
32
  // BOOKS
33
33
 
34
34
  const BOOKS = {
35
+ // GT
35
36
  GEN: "Genesis",
36
37
  EXO: "Exodus",
37
38
  LEV: "Leviticus",
@@ -57,21 +58,22 @@ const BOOKS = {
57
58
  ISA: "Isaiah",
58
59
  JER: "Jeremiah",
59
60
  LAM: "Lamentations",
60
- EZE: "Ezekiel",
61
+ EZK: "Ezekiel",
61
62
  DAN: "Daniel",
62
63
  HOS: "Hosea",
63
- JOE: "Joel",
64
+ JOL: "Joel",
64
65
  AMO: "Amos",
65
66
  OBA: "Obadiah",
66
67
  JON: "Jonah",
67
68
  MIC: "Micah",
68
- NAH: "Nahum",
69
+ NAM: "Nahum",
69
70
  HAB: "Habakkuk",
70
71
  ZEP: "Zephaniah",
71
72
  HAG: "Haggai",
72
73
  ZEC: "Zechariah",
73
74
  MAL: "Malachi",
74
75
 
76
+ // NT
75
77
  MAT: "Matthew",
76
78
  MRK: "Mark",
77
79
  LUK: "Luke",
@@ -98,7 +100,18 @@ const BOOKS = {
98
100
  "2JN": "2 John",
99
101
  "3JN": "3 John",
100
102
  JUD: "Jude",
101
- REV: "Revelation"
103
+ REV: "Revelation",
104
+
105
+ // APOCRYPHA
106
+ TOB: "Tobit",
107
+ JDT: "Judith",
108
+ ESG: "Esther (Greek)",
109
+ WIS: "Wisdom",
110
+ SIR: "Sirach",
111
+ BAR: "Baruch",
112
+ LJE: "Letter of Jeremiah",
113
+ "1MA": "1 Maccabees",
114
+ "2MA": "2 Maccabees"
102
115
  }
103
116
  type BookId = keyof typeof BOOKS
104
117
 
@@ -109,7 +122,8 @@ const categories = [
109
122
  { id: "prophets", start: "ISA", name: "Prophets", color: "#42e84d" },
110
123
  { id: "gospels", start: "MAT", name: "The Gospels & Acts", color: "#42c4e8" },
111
124
  { id: "letters", start: "ROM", name: "Letters", color: "#e8de42" }, // #b542e8
112
- { id: "prophecy", start: "REV", name: "Apocalyptic", color: "#e842e5" }
125
+ { id: "prophecy", start: "REV", name: "Apocalyptic", color: "#e842e5" },
126
+ { id: "apocrypha", start: "TOB", name: "Apocrypha", color: "#8269fa" }
113
127
  ]
114
128
 
115
129
  export function getDefaultBooks() {
@@ -128,9 +142,23 @@ export function getDefaultBooks() {
128
142
  }
129
143
 
130
144
  export function getBookCategory(book: Book) {
131
- return categories.find((a, i) => {
132
- const startNumber = Object.keys(BOOKS).indexOf(a.start) + 1
133
- const endNumber = Object.keys(BOOKS).indexOf(categories[i + 1]?.start) + 1 || startNumber + 1
134
- return book.number >= startNumber && book.number < endNumber
135
- })
145
+ const bookId = book.id
146
+ const bookNumber = book.number
147
+
148
+ const defaults = getDefaultBooks()
149
+
150
+ let currentIndex = defaults.ids.indexOf(bookId as any)
151
+ if (bookId?.length === 3 && currentIndex < 0) return null
152
+
153
+ if (currentIndex < 0) currentIndex = bookNumber - 1
154
+ if (currentIndex > defaults.names.length - 1) return null
155
+
156
+ // find category based on current index
157
+ let categoryIndex = 0
158
+ for (let i = 0; i < categories.length; i++) {
159
+ const startIndex = defaults.ids.indexOf(categories[i].start as any)
160
+ if (currentIndex >= startIndex) categoryIndex = i
161
+ }
162
+
163
+ return categories[categoryIndex]
136
164
  }
package/lib/get.ts CHANGED
@@ -34,28 +34,28 @@ export function _getShortName(bible: Bible) {
34
34
  // MAIN //
35
35
 
36
36
  export function getBookIndex(bible: Bible, numberOrName: string | number) {
37
- if (isNumber(numberOrName)) return bible.books.findIndex((a) => a.number === Number(numberOrName))
37
+ if (isNumber(numberOrName)) return bible.books.findIndex((a) => Number(a.number) === Number(numberOrName))
38
38
  return bible.books.findIndex((a) => a.name === numberOrName || a.id === numberOrName)
39
39
  }
40
40
 
41
41
  export function _getBook(bible: Bible, index: number) {
42
- return bible.books[index] || getDefault().book
42
+ return bible.books[index] || bible.books[0] || getDefault().book
43
43
  }
44
44
 
45
45
  export function getChapterIndex(book: Book, number: number) {
46
- return book.chapters.findIndex((a) => a.number === Number(number))
46
+ return book.chapters.findIndex((a) => Number(a.number) === Number(number))
47
47
  }
48
48
 
49
49
  export function _getChapter(book: Book, index: number) {
50
- return book.chapters[index] || getDefault().chapter
50
+ return book.chapters[index] || book.chapters[0] || getDefault().chapter
51
51
  }
52
52
 
53
53
  export function getVerseIndex(chapter: Chapter, number: number) {
54
- return chapter.verses.findIndex((a) => a.number === number || (a.endNumber && number > a.number && number <= a.endNumber))
54
+ return chapter.verses.findIndex((a) => Number(a.number) === Number(number) || (a.endNumber && Number(number) > Number(a.number) && Number(number) <= Number(a.endNumber)))
55
55
  }
56
56
 
57
57
  export function _getVerse(chapter: Chapter, index: number) {
58
- return chapter.verses[index] || getDefault().verse
58
+ return chapter.verses[index] || chapter.verses[0] || getDefault().verse
59
59
  }
60
60
 
61
61
  // BOOK //
@@ -76,6 +76,30 @@ export function getBookName(numberOrId: number | string, bible?: Bible) {
76
76
  return getDefaultBooks().byId(numberOrId.toString())
77
77
  }
78
78
 
79
+ export function getBookAbbreviation(bible: Bible, bookIndex: number) {
80
+ const currentBook = bible.books[bookIndex]
81
+ if (!currentBook) return ""
82
+
83
+ if (currentBook.abbreviation) return currentBook.abbreviation
84
+
85
+ const abbr = currentBook.id || ""
86
+ const name = currentBook.name
87
+ const defaultBookName = (getDefaultBooks().data as any)[abbr]
88
+
89
+ if (name === defaultBookName) return abbr[0] + abbr.slice(1).toLowerCase()
90
+
91
+ const hasNumber = isNaN(parseInt(name[0]))
92
+ let shortName = hasNumber ? name.slice(0, 3) : name.replace(/[^\w]/g, "").slice(0, 4)
93
+
94
+ // use four characters if same short name ("Jud"ges="Jud"e)
95
+ if (shortName.length === 3 && bible.books.some((a) => a.abbreviation === shortName)) {
96
+ shortName = name.slice(0, 4)
97
+ }
98
+
99
+ currentBook.abbreviation = shortName
100
+ return shortName
101
+ }
102
+
79
103
  // CHAPTER //
80
104
 
81
105
  export function _getCloseChapter(bible: Bible, currentBookNumberOrName: number | string, currentChapterNumber: number, next: boolean) {
package/lib/reference.ts CHANGED
@@ -110,7 +110,7 @@ function parseUniversalReference(ref: UniversalReference) {
110
110
 
111
111
  // Genesis 1:1-3 / GEN 1:1-3 / GEN.1.1-3 = 1.1.1-3
112
112
  function referenceStringToId(ref: string) {
113
- const regex = /^(?:[A-Z]+|[0-9]+)(?:\.[0-9]+)?(?:\.[0-9+-]+)?$/
113
+ const regex = /^(?:[\p{Lu}]+|[0-9]+)(?:\.[0-9]+)?(?:\.[0-9+-]+)?$/u
114
114
  const isId = ref.match(regex) !== null
115
115
  if (isId) return parseReferenceId(ref)
116
116
 
@@ -127,7 +127,7 @@ function referenceStringToId(ref: string) {
127
127
  // "Genesis 1:1-3" = { book: "Genesis ", chapter: 1, verses: [1, 2, 3] }
128
128
  function splitReferenceString(ref: string) {
129
129
  // (:,.) allowed between chapter/verse - (-+) allowed between verses
130
- const regex = /(?<book>[1-3]?\s?[A-Za-z]+)(?:\s(?<chapter>\d+))?(?:[:,.](?<verses>[0-9,.\-+]+))?/
130
+ const regex = /(?<book>(?:\d+\.?\s?)?[\p{L}](?:[\p{L}\s']*[\p{L}])?)(?:\s(?<chapter>\d+))?(?:[:,.](?<verses>[0-9,.\-+]+))?/u
131
131
  const match = ref.match(regex)
132
132
 
133
133
  if (!match) return { book: "", chapter: "", verses: "" }
@@ -143,9 +143,14 @@ function splitReferenceString(ref: string) {
143
143
  // VERSE REFERENCE //
144
144
 
145
145
  // [1, 2, 3] = "1-3" / [4, 2, 7, 1, 9, 10] = "1-2+4+7+9-10"
146
- function getVerseReference(verses: number[]) {
146
+ // WIP reference array might also contain subverses e.g. ["1_b", 2, "6_a"], which will become "1b-2+6a"
147
+ function getVerseReference(verses: (number | string)[]) {
147
148
  // sort in ascending order
148
- verses.sort((a, b) => a - b)
149
+ verses.sort((a, b) => {
150
+ const numA = typeof a === "number" ? a : Number(a.split("_")[0])
151
+ const numB = typeof b === "number" ? b : Number(b.split("_")[0])
152
+ return numA - numB
153
+ })
149
154
 
150
155
  let verseRef = ""
151
156
  let i = 0
@@ -153,7 +158,7 @@ function getVerseReference(verses: number[]) {
153
158
  while (i < verses.length) {
154
159
  // get consecutive verses
155
160
  let start = verses[i]
156
- while (i < verses.length - 1 && verses[i] + 1 === verses[i + 1]) i++
161
+ while (i < verses.length - 1 && Number(verses[i]) + 1 === Number(verses[i + 1])) i++
157
162
  let end = verses[i]
158
163
 
159
164
  // if start and end are the same add the number, if not add the range
package/lib/search.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Bible } from "./Bible"
2
+ import { getDefaultBooks } from "./defaults"
2
3
  import { getBookIndex } from "./get"
3
4
  import { getReferenceFromSearchString, getReferenceString, getVerseReferences, SearchReference, VerseReference } from "./reference"
4
5
 
@@ -31,9 +32,25 @@ export function _bookSearch(bible: Bible, searchValue: string) {
31
32
  returnValue.chapter = chapter.number
32
33
 
33
34
  const verses = findVerses(reference.verses)
35
+ if (!verses.length) {
36
+ // add divider (:) automatically if space at end
37
+ if (searchValue.endsWith(" ") && !searchValue.trim().includes(":")) returnValue.autocompleted = returnValue.autocompleted.trim() + ":"
38
+ else returnValue.autocompleted = returnValue.autocompleted.trim()
39
+
40
+ return finish()
41
+ }
34
42
  returnValue.verses = verses.map(({ number }) => number)
35
43
  returnValue.versesContent = verses
36
44
 
45
+ // add divider (+/-) automatically if space at end
46
+ if (searchValue.endsWith(" ") && !searchValue.trim().endsWith("-") && !searchValue.trim().endsWith("+")) {
47
+ const minus = (searchValue.match(/-/g) || []).length
48
+ const plus = (searchValue.match(/\+/g) || []).length
49
+ returnValue.autocompleted = returnValue.autocompleted.trim() + (minus === plus ? "-" : "+")
50
+ } else {
51
+ returnValue.autocompleted = returnValue.autocompleted.trim()
52
+ }
53
+
37
54
  return finish()
38
55
 
39
56
  /////
@@ -44,16 +61,21 @@ export function _bookSearch(bible: Bible, searchValue: string) {
44
61
  }
45
62
 
46
63
  function findBooks(name: string) {
47
- const formatText = (a: string) => a.replace(/\s/g, "").toLowerCase()
48
- name = formatText(name)
64
+ name = removeSpaces(formatText(name))
49
65
 
50
66
  let matches = []
51
67
  for (let book of bible.books) {
52
- const bookName = formatText(book.name)
68
+ const bookName = removeSpaces(formatText(book.name))
53
69
  if (bookName === name) return [book]
54
70
  if (bookName.includes(name)) matches.push(book)
55
71
  }
56
72
 
73
+ // find any abbreviation matches
74
+ for (let book of bible.books) {
75
+ let abbr = getDefaultBooks().ids[book.number - 1] || ""
76
+ if (abbr.toLowerCase() === name) return [book]
77
+ }
78
+
57
79
  // remove books with numbers if no number at search start (John)
58
80
  const hasNum = (str: string) => /\d/.test(str)
59
81
  if (!hasNum(name[0])) matches = matches.filter((book) => !hasNum(book.name))
@@ -62,11 +84,11 @@ export function _bookSearch(bible: Bible, searchValue: string) {
62
84
  }
63
85
 
64
86
  function findChapter(number: number) {
65
- return book.chapters.find((a) => a.number === number)
87
+ return book.chapters.find((a) => Number(a.number) === number)
66
88
  }
67
89
 
68
90
  function findVerses(verses: number[]) {
69
- return chapter?.verses.filter((a) => verses.includes(a.number)) || []
91
+ return chapter?.verses.filter((a) => verses.includes(Number(a.number))) || []
70
92
  }
71
93
  }
72
94
 
@@ -74,7 +96,6 @@ export function _bookSearch(bible: Bible, searchValue: string) {
74
96
 
75
97
  let textSearchCache: { [key: string]: string } = {}
76
98
  export function _textSearch(bible: Bible, searchValue: string, limit: number, bookNumber?: number) {
77
- const formatText = (a: string) => a.replace(/[`!*()-?;:'",.]/gi, "").toLowerCase()
78
99
  searchValue = formatText(searchValue).trim()
79
100
  if (!searchValue.length) return []
80
101
 
@@ -120,3 +141,20 @@ export function _textSearch(bible: Bible, searchValue: string, limit: number, bo
120
141
  return matches
121
142
  }
122
143
  }
144
+
145
+ // HELPERS //
146
+
147
+ function formatText(text: string) {
148
+ return (
149
+ text
150
+ // replace diacritic values like á -> a & ö -> o
151
+ .normalize("NFD")
152
+ .replace(/\p{Diacritic}/gu, "")
153
+ // remove special characters
154
+ .replace(/[`!*()\-?;:'",.]/gi, "")
155
+ .toLowerCase()
156
+ )
157
+ }
158
+ function removeSpaces(text: string) {
159
+ return text.replace(/\s/g, "")
160
+ }
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
- {
2
- "name": "json-bible",
3
- "description": "Universal JSON Bible Format",
4
- "version": "1.0.0",
5
- "main": "index.js",
6
- "type": "module",
7
- "scripts": {
8
- "dev": "node dist/index.js",
9
- "build": "tsc && node add-js-extension.js",
10
- "test": "echo \"Error: no test specified\" && exit 1"
11
- },
12
- "author": "vassbo",
13
- "license": "ISC",
14
- "devDependencies": {
15
- "@types/node": "^22.7.6",
16
- "typescript": "^5.6.2"
17
- }
18
- }
1
+ {
2
+ "name": "json-bible",
3
+ "description": "Universal JSON Bible Format",
4
+ "version": "1.1.0",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "node dist/index.js",
9
+ "build": "tsc && node add-js-extension.js",
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "author": "vassbo",
13
+ "license": "ISC",
14
+ "devDependencies": {
15
+ "@types/node": "^22.7.6",
16
+ "typescript": "^5.6.2"
17
+ }
18
+ }