json-bible 1.0.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/lib/get.ts ADDED
@@ -0,0 +1,164 @@
1
+ import { Bible, Book, Chapter, Metadata, Verse } from "./Bible"
2
+ import { getDefault, getDefaultBooks, getDefaultMetadata } from "./defaults"
3
+ import { bibleFromFile, validateBible } from "./load"
4
+ import { parseMarkdown, stripMarkdown } from "./markdown"
5
+ import { getVerseReferences } from "./reference"
6
+ import { isNumber, stripText } from "./util"
7
+
8
+ // BIBLE //
9
+
10
+ export async function _getBible(bibleOrPath: Bible | string) {
11
+ let bible: Bible
12
+ if (typeof bibleOrPath === "string") bible = await bibleFromFile(bibleOrPath)
13
+ else bible = bibleOrPath
14
+
15
+ validateBible(bible)
16
+
17
+ return bible
18
+ }
19
+
20
+ export function _getShortName(bible: Bible) {
21
+ if (bible.abbreviation) return bible.abbreviation
22
+ if (bible.metadata.identifier) return bible.metadata.identifier
23
+ if (bible.name.length < 5) return bible.name.toUpperCase()
24
+
25
+ return bible.name
26
+ .trim()
27
+ .split(" ")
28
+ .filter((word) => isNaN(Number(word))) // filter out numbers
29
+ .map((word) => word[0]) // get the first letter of each word
30
+ .join("")
31
+ .toUpperCase()
32
+ }
33
+
34
+ // MAIN //
35
+
36
+ export function getBookIndex(bible: Bible, numberOrName: string | number) {
37
+ if (isNumber(numberOrName)) return bible.books.findIndex((a) => a.number === Number(numberOrName))
38
+ return bible.books.findIndex((a) => a.name === numberOrName || a.id === numberOrName)
39
+ }
40
+
41
+ export function _getBook(bible: Bible, index: number) {
42
+ return bible.books[index] || getDefault().book
43
+ }
44
+
45
+ export function getChapterIndex(book: Book, number: number) {
46
+ return book.chapters.findIndex((a) => a.number === Number(number))
47
+ }
48
+
49
+ export function _getChapter(book: Book, index: number) {
50
+ return book.chapters[index] || getDefault().chapter
51
+ }
52
+
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))
55
+ }
56
+
57
+ export function _getVerse(chapter: Chapter, index: number) {
58
+ return chapter.verses[index] || getDefault().verse
59
+ }
60
+
61
+ // BOOK //
62
+
63
+ export function getBookNumber(numberOrNameOrId: number | string, bible?: Bible) {
64
+ if (isNumber(numberOrNameOrId)) return Number(numberOrNameOrId)
65
+ if (bible?.books) return bible.books.findIndex((a) => a.name === numberOrNameOrId || a.id === numberOrNameOrId)
66
+
67
+ const index = Object.entries(getDefaultBooks().data).findIndex(([id, name]) => name === numberOrNameOrId || id === numberOrNameOrId)
68
+ return index + 1
69
+ }
70
+
71
+ export function getBookName(numberOrId: number | string, bible?: Bible) {
72
+ let bibleName = bible?.books.find((a) => a.number === numberOrId || a.id === numberOrId)
73
+ if (bibleName?.name) return bibleName.name
74
+
75
+ if (isNumber(numberOrId)) return getDefaultBooks().byNumber(Number(numberOrId))
76
+ return getDefaultBooks().byId(numberOrId.toString())
77
+ }
78
+
79
+ // CHAPTER //
80
+
81
+ export function _getCloseChapter(bible: Bible, currentBookNumberOrName: number | string, currentChapterNumber: number, next: boolean) {
82
+ let bookIndex = getBookIndex(bible, currentBookNumberOrName)
83
+ let book = _getBook(bible, bookIndex)
84
+ let chapterIndex = getChapterIndex(book, Number(currentChapterNumber))
85
+ let chapter = book.chapters[chapterIndex + (next ? 1 : -1)]
86
+
87
+ if (!chapter) {
88
+ book = _getBook(bible, bookIndex + (next ? 1 : -1))
89
+ chapter = book.chapters[next ? 0 : book.chapters.length - 1]
90
+ }
91
+ if (!chapter) return null
92
+
93
+ return getVerseReferences(bible, { book: book.number, chapter: chapter.number, verses: [] })[0]
94
+ }
95
+
96
+ // VERSE //
97
+
98
+ export function _getText(verse: Verse, includeNumber: boolean = false) {
99
+ let text = stripMarkdown(stripText(verse.text))
100
+ if (includeNumber) text = `${getVerseNumber(verse)} ${text.trim()}`
101
+ return text
102
+ }
103
+
104
+ export function _getHTML(verse: Verse, includeNumber: boolean = false, bigNumber: boolean = false) {
105
+ let html = parseMarkdown(verse.text || "")
106
+ if (includeNumber) html = `<span class="number${bigNumber ? " big" : ""}">${getVerseNumber(verse)}</span> ${html.trim()}`
107
+ return html
108
+ }
109
+
110
+ export function formatText(value: string, html: boolean = false) {
111
+ value = stripText(value)
112
+ if (html) value = parseMarkdown(value)
113
+ return value
114
+ }
115
+
116
+ function getVerseNumber(verse: Verse) {
117
+ let number = verse.number.toString()
118
+ if (verse.endNumber) number += `-${verse.endNumber}`
119
+ return number
120
+ }
121
+
122
+ export function _getRandomVerse(bible: Bible) {
123
+ let bookIndex = randomNum(bible.books.length)
124
+ const book = bible.books[bookIndex]
125
+ let chapterIndex = randomNum(book.chapters.length)
126
+ const chapter = book.chapters[chapterIndex]
127
+ let verseIndex = randomNum(chapter.verses.length)
128
+ const verse = chapter.verses[verseIndex]
129
+
130
+ return getVerseReferences(bible, { book: book.number, chapter: chapter.number, verses: [verse.number] })[0]
131
+
132
+ function randomNum(end: number) {
133
+ return Math.floor(Math.random() * end)
134
+ }
135
+ }
136
+
137
+ // METADATA //
138
+
139
+ export function _getMetadata(bible: Bible) {
140
+ if (typeof bible.metadata !== "object") throw new Error("Missing metadata!")
141
+
142
+ // trim metadata values
143
+ let metadata: Metadata = {}
144
+ Object.keys(bible.metadata).forEach((key) => {
145
+ let value = trim(bible.metadata[key])
146
+ if (value.length) metadata[key] = value
147
+ })
148
+
149
+ // add all default values
150
+ const defaultMetadata = getDefaultMetadata()
151
+ Object.keys(defaultMetadata).forEach((key) => {
152
+ if (!metadata[key]) metadata[key] = defaultMetadata[key]
153
+ })
154
+
155
+ // add known values
156
+ if (!metadata.title) metadata.title = bible.name
157
+ if (!metadata.identifier) metadata.identifier = _getShortName(bible)
158
+
159
+ return metadata
160
+
161
+ function trim(value: string) {
162
+ return stripText(value).trim()
163
+ }
164
+ }
package/lib/load.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { Bible } from "./Bible"
2
+ import { getDefaultBooks } from "./defaults"
3
+ import { getBookName, getBookNumber } from "./get"
4
+
5
+ export async function bibleFromFile(filePath: string) {
6
+ let content = ""
7
+
8
+ if (filePath.match(/^https?:\/\//)) {
9
+ content = await fetchFile(filePath)
10
+ } else {
11
+ try {
12
+ content = await loadFileNode(filePath)
13
+ } catch (_) {
14
+ // not node environment
15
+ content = await fetchFile(filePath)
16
+ }
17
+ }
18
+
19
+ if (!content) throw new Error("No file content!")
20
+
21
+ let bible: Bible
22
+ try {
23
+ bible = JSON.parse(content)
24
+ } catch (err) {
25
+ throw new Error("Error parsing JSON: " + err)
26
+ }
27
+
28
+ return bible
29
+ }
30
+
31
+ async function loadFileNode(filePath: string) {
32
+ const fs = require("fs")
33
+ try {
34
+ return await fs.readFile(filePath, "utf8")
35
+ } catch (err) {
36
+ throw new Error("Error getting file: " + err)
37
+ }
38
+ }
39
+
40
+ async function fetchFile(filePath: string) {
41
+ try {
42
+ const response = await fetch(filePath)
43
+ return await response.text()
44
+ } catch (err) {
45
+ throw new Error("Error getting file: " + err)
46
+ }
47
+ }
48
+
49
+ export function validateBible(bible: Bible) {
50
+ if (!bible.name) incomplete("Missing name!")
51
+ if (!bible.books?.length) incomplete("No books!")
52
+
53
+ // this only checks first
54
+ if (!bible.books[0]?.chapters?.length) incomplete("No initial chapters!")
55
+ if (!bible.books[0]?.chapters[0]?.verses?.length) incomplete("No initial verses!")
56
+ if (!bible.books[0]?.chapters[0]?.verses[0]?.text?.length) incomplete("No initial text!")
57
+
58
+ // set book names/id if missing
59
+ bible.books = bible.books.map((book) => {
60
+ if (!book.name) book.name = getBookName(book.id || book.number)
61
+ if (!book.number) book.number = getBookNumber(book.name)
62
+ if (!book.id) book.id = getDefaultBooks().ids[book.number - 1]
63
+
64
+ return book
65
+ })
66
+
67
+ function incomplete(message: string) {
68
+ throw new Error("Incorrect Bible format: " + message)
69
+ }
70
+ }
@@ -0,0 +1,63 @@
1
+ export function parseMarkdown(input: string) {
2
+ // Undertitle: # Text #
3
+ input = input.replace(/#\s*(.*?)\s*#/g, "<h4>$1</h4>")
4
+
5
+ // Cross Reference & Notes: *{text}* | *{[reference](id) text}*
6
+ input = parseCrossReferences()
7
+
8
+ // Jesus Red Words: !{text}!
9
+ input = input.replace(/!\{(.*?)\}!/g, '<span style="color:red;">$1</span>')
10
+
11
+ // Uncertain: [text]
12
+ input = input.replace(/\[(.*?)\]/g, '<span class="uncertain">[$1]</span>')
13
+
14
+ ///// https://www.markdownguide.org/basic-syntax/
15
+
16
+ // Bold: **text** or __text__
17
+ input = input.replace(/(\*\*|__)(.*?)\1/g, "<strong>$2</strong>")
18
+
19
+ // Italic: *text* or _text_
20
+ input = input.replace(/(\*|_)(.*?)\1/g, "<em>$2</em>")
21
+
22
+ // Underline: ++text++
23
+ input = input.replace(/\+\+(.*?)\+\+/g, "<u>$1</u>")
24
+
25
+ // Strikethrough: ~~text~~
26
+ input = input.replace(/~~(.*?)~~/g, "<del>$1</del>")
27
+
28
+ /////
29
+
30
+ // Quote: "text"
31
+ input = input.replace(/(?<!<[^>]*?)"([^"]*?)"(?![^<]*?>)/g, "<q>$1</q>")
32
+
33
+ // Line Break: \n
34
+ input = input.replace(/\n/g, "<br>")
35
+
36
+ // Paragraph: ¶
37
+ input = input.replace(/¶/g, "<br><br>")
38
+
39
+ return input
40
+
41
+ function parseCrossReferences() {
42
+ return input.replace(/\*\{(.*?)\}\*/g, (_, content) => {
43
+ const transformedContent = content.replace(/\[(.*?)\]\((.*?)\)/g, '<span id="$2" class="cross-ref-id">$1</span>')
44
+ return `<span class="cross-ref"><span class="content">${transformedContent}</span></span>`
45
+ })
46
+ }
47
+ }
48
+
49
+ export function stripMarkdown(input: string) {
50
+ input = input.replace(/#\s*(.*?)\s*#/g, "")
51
+ input = input.replace(/\*\{(.*?)\}\*/g, "$1")
52
+ input = input.replace(/!\{(.*?)\}!/g, "$1")
53
+ // input = input.replace(/\[(.*?)\]/g, "[$1]")
54
+ input = input.replace(/(\*\*|__)(.*?)\1/g, "$2")
55
+ input = input.replace(/(\*|_)(.*?)\1/g, "$2")
56
+ input = input.replace(/\+\+(.*?)\+\+/g, "$1")
57
+ input = input.replace(/~~(.*?)~~/g, "$1")
58
+ input = input.replace(/"([^"]*?)"/g, "$1")
59
+ input = input.replace(/\n/g, "")
60
+ input = input.replace(/¶/g, "")
61
+
62
+ return input
63
+ }
@@ -0,0 +1,205 @@
1
+ import { Bible, Verse } from "./Bible"
2
+ import { getBookName, getBookNumber } from "./get"
3
+
4
+ // TYPES //
5
+
6
+ type Reference = {
7
+ book: number // 1
8
+ chapter: number // 1
9
+ verses: number[] // 1, 2, 3
10
+ }
11
+
12
+ type UniversalReference = {
13
+ book: number | string // 1 / "GEN" / "Genesis"
14
+ chapter: number // 1
15
+ verse?: number // 1
16
+ verses?: number[] | string // [1, 2, 3] / "1-3"
17
+ }
18
+
19
+ export type VerseReference = {
20
+ book: number // 1
21
+ chapter: number // 1
22
+ verse: Verse // { number: 1, text: "" }
23
+ reference: string // "Genesis 1:1"
24
+ id: string // "1.1.1"
25
+ }
26
+
27
+ export interface SearchReference extends Reference {
28
+ autocompleted: string // "Genesis ..."
29
+ versesContent: Verse[] // [{ text: "", ... }]
30
+ }
31
+
32
+ // MAIN //
33
+
34
+ // OUTPUT: "1" | "1.1" | "1.1.1"
35
+ export function getReferenceId(ref: UniversalReference | Reference) {
36
+ return referenceBuilder(ref as UniversalReference, [".", "."])
37
+ }
38
+
39
+ // OUTPUT: "Genesis" | "Genesis 1" | "Genesis 1:1"
40
+ export function getReferenceString(ref: UniversalReference | Reference, bible?: Bible) {
41
+ return referenceBuilder(ref as UniversalReference, [" ", ":"], { bookName: true, bible })
42
+ }
43
+
44
+ export function getVerseReferences(bible: Bible, ref: Reference | string) {
45
+ if (!ref) return []
46
+ if (typeof ref === "string") ref = getReference(ref)
47
+ let verses = getVersesFromReference(bible, ref)
48
+ if (!verses.length) verses.push({ number: 0, text: "" })
49
+
50
+ let verseReferences: VerseReference[] = verses.map((a) => ({
51
+ book: ref.book,
52
+ chapter: ref.chapter,
53
+ verse: a,
54
+ reference: getReferenceString({ ...ref, verse: a.number }, bible),
55
+ id: getReferenceId({ ...ref, verse: a.number })
56
+ }))
57
+
58
+ return verseReferences
59
+ }
60
+
61
+ // search input
62
+ export function getReferenceFromSearchString(searchRef: string, bible?: Bible) {
63
+ let { book, chapter, verses } = splitReferenceString(searchRef)
64
+
65
+ const reference: Reference = {
66
+ book: getBookNumber(book, bible),
67
+ chapter: Number(chapter || 0),
68
+ verses: verses ? extractVerseReference(verses) : []
69
+ }
70
+
71
+ return { name: book.trim(), reference }
72
+ }
73
+
74
+ // GETTERS //
75
+
76
+ function getReference(referenceString: string) {
77
+ const referenceId = referenceStringToId(referenceString)
78
+ const split = referenceId.split(".")
79
+ const ref: Reference = { book: Number(split[0]), chapter: Number(split[1]), verses: extractVerseReference(split[2] || "") }
80
+ return ref
81
+ }
82
+
83
+ function referenceBuilder(ref: UniversalReference, [chapterSeperator, verseSeperator]: string[], options: { bookName?: boolean; bible?: Bible } = {}) {
84
+ const { book, chapter, verses } = parseUniversalReference(ref)
85
+
86
+ let reference = ""
87
+ if (!book) return reference
88
+
89
+ reference += `${options.bookName ? getBookName(book, options.bible) : book}`
90
+ if (!chapter) return reference
91
+
92
+ reference += `${chapterSeperator}${chapter}`
93
+ if (!verses.length) return reference
94
+
95
+ reference += `${verseSeperator}${getVerseReference(verses)}`
96
+ return reference
97
+ }
98
+
99
+ function parseUniversalReference(ref: UniversalReference) {
100
+ const book = getBookNumber(ref.book)
101
+ const chapter = ref.chapter
102
+ const verses = ref.verse ? [ref.verse] : getVerses(ref.verses || [])
103
+ return { book, chapter, verses } as Reference
104
+
105
+ function getVerses(verses: number[] | string) {
106
+ if (Array.isArray(verses)) return verses
107
+ return extractVerseReference(verses)
108
+ }
109
+ }
110
+
111
+ // Genesis 1:1-3 / GEN 1:1-3 / GEN.1.1-3 = 1.1.1-3
112
+ function referenceStringToId(ref: string) {
113
+ const regex = /^(?:[A-Z]+|[0-9]+)(?:\.[0-9]+)?(?:\.[0-9+-]+)?$/
114
+ const isId = ref.match(regex) !== null
115
+ if (isId) return parseReferenceId(ref)
116
+
117
+ let { book, chapter, verses } = splitReferenceString(ref)
118
+ return `${getBookNumber(book)}.${chapter}.${verses}`
119
+
120
+ function parseReferenceId(ref: string) {
121
+ const split = ref.split(".")
122
+ split[0] = getBookNumber(split[0]).toString()
123
+ return split.join(".")
124
+ }
125
+ }
126
+
127
+ // "Genesis 1:1-3" = { book: "Genesis ", chapter: 1, verses: [1, 2, 3] }
128
+ function splitReferenceString(ref: string) {
129
+ // (:,.) allowed between chapter/verse - (-+) allowed between verses
130
+ const regex = /(?<book>[1-3]?\s?[A-Za-z]+)(?:\s(?<chapter>\d+))?(?:[:,.](?<verses>[0-9,.\-+]+))?/
131
+ const match = ref.match(regex)
132
+
133
+ if (!match) return { book: "", chapter: "", verses: "" }
134
+ const groups = match.groups || {}
135
+
136
+ const book = groups.book?.trim() || ""
137
+ const chapter = groups.chapter || ""
138
+ const verses = groups.verses || ""
139
+
140
+ return { book, chapter, verses }
141
+ }
142
+
143
+ // VERSE REFERENCE //
144
+
145
+ // [1, 2, 3] = "1-3" / [4, 2, 7, 1, 9, 10] = "1-2+4+7+9-10"
146
+ function getVerseReference(verses: number[]) {
147
+ // sort in ascending order
148
+ verses.sort((a, b) => a - b)
149
+
150
+ let verseRef = ""
151
+ let i = 0
152
+
153
+ while (i < verses.length) {
154
+ // get consecutive verses
155
+ let start = verses[i]
156
+ while (i < verses.length - 1 && verses[i] + 1 === verses[i + 1]) i++
157
+ let end = verses[i]
158
+
159
+ // if start and end are the same add the number, if not add the range
160
+ verseRef += start === end ? `${start}` : `${start}-${end}`
161
+
162
+ i++
163
+
164
+ // add a '+' if there are more verses
165
+ if (i < verses.length) verseRef += "+"
166
+ }
167
+
168
+ return verseRef
169
+ }
170
+
171
+ // "1-3+5" = [1, 2, 3, 5]
172
+ function extractVerseReference(verseRef: string) {
173
+ const result: number[] = []
174
+
175
+ verseRef.split("+").forEach((part) => {
176
+ if (part.includes("-")) {
177
+ let [start, end] = part.split("-").filter(Boolean).map(Number)
178
+
179
+ // ending in + or -
180
+ if (isNaN(end)) result.push(start)
181
+ else {
182
+ // swap inverted numbers
183
+ if (start > end) [start, end] = [end, start]
184
+
185
+ for (let i = start; i <= end; i++) {
186
+ result.push(i)
187
+ }
188
+ }
189
+ } else if (part) {
190
+ result.push(Number(part))
191
+ }
192
+ })
193
+
194
+ return result
195
+ }
196
+
197
+ function getVersesFromReference(bible: Bible, ref: Reference | string) {
198
+ if (typeof ref === "string") ref = getReference(ref)
199
+
200
+ const bookIndex = bible.books.findIndex((a) => a.number === ref.book)
201
+ const chapterIndex = bible.books[bookIndex]?.chapters.findIndex((a) => a.number === ref.chapter)
202
+ const verses = (bible.books[bookIndex]?.chapters[chapterIndex]?.verses || []).filter((a) => ref.verses.includes(a.number))
203
+
204
+ return verses
205
+ }
package/lib/search.ts ADDED
@@ -0,0 +1,122 @@
1
+ import { Bible } from "./Bible"
2
+ import { getBookIndex } from "./get"
3
+ import { getReferenceFromSearchString, getReferenceString, getVerseReferences, SearchReference, VerseReference } from "./reference"
4
+
5
+ // BOOK SEARCH //
6
+
7
+ let previousSearch: string = ""
8
+ export function _bookSearch(bible: Bible, searchValue: string) {
9
+ const returnValue: SearchReference = { autocompleted: searchValue, book: 0, chapter: 0, verses: [], versesContent: [] }
10
+ if (!searchValue.length) return finish()
11
+
12
+ const { name, reference } = getReferenceFromSearchString(searchValue)
13
+ if (!reference || !name) return finish()
14
+
15
+ let books = findBooks(name)
16
+ if (books.length !== 1) return finish()
17
+
18
+ const book = books[0]
19
+ returnValue.book = book.number
20
+
21
+ // autocomplete book name
22
+ // this will also "disallow" more text input after full book name
23
+ if (!reference.chapter && previousSearch.length <= searchValue.length) {
24
+ reference.book = book.number
25
+ returnValue.autocompleted = getReferenceString(reference, bible) + " "
26
+ searchValue = returnValue.autocompleted
27
+ }
28
+
29
+ const chapter = findChapter(reference.chapter)
30
+ if (!chapter) return finish()
31
+ returnValue.chapter = chapter.number
32
+
33
+ const verses = findVerses(reference.verses)
34
+ returnValue.verses = verses.map(({ number }) => number)
35
+ returnValue.versesContent = verses
36
+
37
+ return finish()
38
+
39
+ /////
40
+
41
+ function finish() {
42
+ previousSearch = searchValue
43
+ return returnValue
44
+ }
45
+
46
+ function findBooks(name: string) {
47
+ const formatText = (a: string) => a.replace(/\s/g, "").toLowerCase()
48
+ name = formatText(name)
49
+
50
+ let matches = []
51
+ for (let book of bible.books) {
52
+ const bookName = formatText(book.name)
53
+ if (bookName === name) return [book]
54
+ if (bookName.includes(name)) matches.push(book)
55
+ }
56
+
57
+ // remove books with numbers if no number at search start (John)
58
+ const hasNum = (str: string) => /\d/.test(str)
59
+ if (!hasNum(name[0])) matches = matches.filter((book) => !hasNum(book.name))
60
+
61
+ return matches
62
+ }
63
+
64
+ function findChapter(number: number) {
65
+ return book.chapters.find((a) => a.number === number)
66
+ }
67
+
68
+ function findVerses(verses: number[]) {
69
+ return chapter?.verses.filter((a) => verses.includes(a.number)) || []
70
+ }
71
+ }
72
+
73
+ // TEXT SEARCH //
74
+
75
+ let textSearchCache: { [key: string]: string } = {}
76
+ export function _textSearch(bible: Bible, searchValue: string, limit: number, bookNumber?: number) {
77
+ const formatText = (a: string) => a.replace(/[`!*()-?;:'",.]/gi, "").toLowerCase()
78
+ searchValue = formatText(searchValue).trim()
79
+ if (!searchValue.length) return []
80
+
81
+ const cacheId = searchValue + limit + (bookNumber ?? "")
82
+ if (textSearchCache[cacheId]) return JSON.parse(textSearchCache[cacheId]) as VerseReference[]
83
+
84
+ const matches = bibleSearch().slice(0, limit)
85
+
86
+ textSearchCache[cacheId] = JSON.stringify(matches)
87
+ return matches
88
+
89
+ /////
90
+
91
+ function bibleSearch() {
92
+ const searchWords = searchValue.split(" ")
93
+ const books = bookNumber === undefined ? bible.books : [bible.books[getBookIndex(bible, bookNumber)]]
94
+
95
+ let matches: VerseReference[] = []
96
+
97
+ for (let book of books) {
98
+ for (let chapter of book.chapters) {
99
+ let verses: number[] = []
100
+
101
+ for (let verse of chapter.verses) {
102
+ const verseValue = formatText(verse.text || "")
103
+
104
+ // check if the full verse, or one of the words contains the search value
105
+ if (verseValue.includes(searchValue) || searchWords.every((word) => verseValue.includes(word))) {
106
+ verses.push(verse.number)
107
+ }
108
+ }
109
+
110
+ if (verses.length) {
111
+ const reference = getVerseReferences(bible, { book: book.number, chapter: chapter.number, verses })
112
+ matches.push(...reference)
113
+
114
+ // return early if we have reached the limit
115
+ if (matches.length >= limit) return matches
116
+ }
117
+ }
118
+ }
119
+
120
+ return matches
121
+ }
122
+ }
package/lib/util.ts ADDED
@@ -0,0 +1,21 @@
1
+ export function clone<T>(object: T): T {
2
+ if (typeof object !== "object") return object
3
+ return JSON.parse(JSON.stringify(object))
4
+ }
5
+
6
+ export function isNumber(value: any) {
7
+ return typeof value === "number" || !isNaN(Number(value))
8
+ }
9
+
10
+ export function stripText(value: string) {
11
+ if (!value) return ""
12
+
13
+ // remove any HTML tags
14
+ value = value.replace(/<[^>]*>/g, "")
15
+ // value = value.replace(/(<([^>]+)>)/g, "")
16
+
17
+ // remove [1], not [text]
18
+ value = value.replace(/ *\[[0-9\]]*]/g, "")
19
+
20
+ return value
21
+ }
package/package.json ADDED
@@ -0,0 +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
+ }