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/JSON_bible_icon.png +0 -0
- package/README.md +119 -0
- package/add-js-extension.js +42 -0
- package/index.ts +243 -0
- package/lib/Bible.ts +45 -0
- package/lib/defaults.ts +136 -0
- package/lib/get.ts +164 -0
- package/lib/load.ts +70 -0
- package/lib/markdown.ts +63 -0
- package/lib/reference.ts +205 -0
- package/lib/search.ts +122 -0
- package/lib/util.ts +21 -0
- package/package.json +18 -0
- package/preview/bible.html +162 -0
- package/preview/domHelper.js +60 -0
- package/preview/style.css +216 -0
- package/tsconfig.json +13 -0
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
|
+
}
|
package/lib/markdown.ts
ADDED
|
@@ -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
|
+
}
|
package/lib/reference.ts
ADDED
|
@@ -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
|
+
}
|