codexparser 0.1.41 → 0.1.43
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/package.json +1 -1
- package/src/CodexParser.js +342 -310
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codexparser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.43",
|
|
4
4
|
"description": "This is a Javascript Bible parser and text scanner. It will search through texts and collate all scripture references into an array and parse them into objects, and it will parse passages into objects by book, chapter, verse, and testament. ",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
package/src/CodexParser.js
CHANGED
|
@@ -65,138 +65,110 @@ class CodexParser {
|
|
|
65
65
|
// Initialize the `found` array to store the results
|
|
66
66
|
this.found = []
|
|
67
67
|
|
|
68
|
+
// Preprocess input text: normalize separators while preserving abbreviations
|
|
69
|
+
let normalizedText = text
|
|
70
|
+
.replace(/\.(?=\d)/g, ":") // Convert periods before numbers into colons (e.g., 12.15 -> 12:15)
|
|
71
|
+
.replace(/(\b[A-Za-z]+)\.(?=\s|$)/g, "$1") // Remove periods after abbreviations (e.g., Jd. -> Jd)
|
|
72
|
+
.replace(/\s+/g, " ") // Normalize multiple spaces to a single space
|
|
73
|
+
|
|
68
74
|
// Convert Bible book names, abbreviations, and input text to lowercase for case-insensitive matching
|
|
69
75
|
const lowercaseBibleFullNames = fullNames.map((book) => book.toLowerCase())
|
|
70
76
|
const lowercaseBibleAbbreviations = abbreviations.map((abbr) => abbr.toLowerCase())
|
|
71
|
-
const lowerCaseText =
|
|
77
|
+
const lowerCaseText = normalizedText.toLowerCase()
|
|
72
78
|
|
|
73
|
-
let i = 0
|
|
79
|
+
let i = 0
|
|
74
80
|
|
|
75
|
-
/**
|
|
76
|
-
* Helper function to check if a character is part of a chapter or verse reference.
|
|
77
|
-
* Non-word characters (anything not A-Z or a-z) are considered valid.
|
|
78
|
-
*/
|
|
79
81
|
const isValidChapterVerseChar = (char) => /[^A-Za-z]/.test(char)
|
|
80
82
|
|
|
81
|
-
/**
|
|
82
|
-
* Helper function to determine if the text starting at a given index contains
|
|
83
|
-
* the name of a new Bible book.
|
|
84
|
-
*/
|
|
85
83
|
const isNextBibleBook = (startIndex) => {
|
|
86
84
|
const textAfterCurrentPosition = lowerCaseText.substring(startIndex).trim()
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Check for Bible book abbreviations
|
|
94
|
-
for (const abbr of lowercaseBibleAbbreviations) {
|
|
95
|
-
if (textAfterCurrentPosition.startsWith(abbr)) return true
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return false // No match found
|
|
85
|
+
return (
|
|
86
|
+
lowercaseBibleFullNames.some((book) => textAfterCurrentPosition.startsWith(book)) ||
|
|
87
|
+
lowercaseBibleAbbreviations.some((abbr) => textAfterCurrentPosition.startsWith(abbr))
|
|
88
|
+
)
|
|
99
89
|
}
|
|
100
90
|
|
|
101
|
-
/**
|
|
102
|
-
* Helper function to detect suffixes like "LXX" or "MT" in the text after a given index.
|
|
103
|
-
* These suffixes are case-insensitive and indicate the version of the Bible reference.
|
|
104
|
-
*/
|
|
105
91
|
const detectSuffix = (startIndex) => {
|
|
106
|
-
const suffixMatch =
|
|
92
|
+
const suffixMatch = normalizedText.substring(startIndex).match(/\b(LXX|MT)\b/i)
|
|
107
93
|
return suffixMatch ? suffixMatch[0].toUpperCase() : null
|
|
108
94
|
}
|
|
109
95
|
|
|
110
|
-
// Iterate through the input text to detect and process Bible references
|
|
111
96
|
while (i < lowerCaseText.length) {
|
|
112
|
-
let foundBook = null
|
|
113
|
-
let foundIndex = -1
|
|
114
|
-
let matchedLength = 0
|
|
97
|
+
let foundBook = null
|
|
98
|
+
let foundIndex = -1
|
|
99
|
+
let matchedLength = 0
|
|
115
100
|
|
|
116
|
-
// Search for full Bible book names in the text
|
|
117
101
|
for (let j = 0; j < lowercaseBibleFullNames.length; j++) {
|
|
118
102
|
const book = lowercaseBibleFullNames[j]
|
|
119
103
|
if (lowerCaseText.startsWith(book, i) && book.length > matchedLength) {
|
|
120
|
-
foundBook = fullNames[j]
|
|
104
|
+
foundBook = fullNames[j]
|
|
121
105
|
foundIndex = i
|
|
122
|
-
matchedLength = book.length
|
|
106
|
+
matchedLength = book.length
|
|
123
107
|
}
|
|
124
108
|
}
|
|
125
109
|
|
|
126
|
-
// If no full book name is found, search for abbreviations
|
|
127
110
|
if (!foundBook) {
|
|
128
111
|
for (let k = 0; k < lowercaseBibleAbbreviations.length; k++) {
|
|
129
112
|
const abbreviation = lowercaseBibleAbbreviations[k]
|
|
130
|
-
if (lowerCaseText.startsWith(abbreviation, i)) {
|
|
131
|
-
foundBook = abbreviations[k]
|
|
113
|
+
if (lowerCaseText.startsWith(abbreviation, i) && abbreviation.length > matchedLength) {
|
|
114
|
+
foundBook = this.abbreviations[abbreviations[k]]
|
|
132
115
|
foundIndex = i
|
|
133
116
|
matchedLength = abbreviation.length
|
|
134
117
|
}
|
|
135
118
|
}
|
|
136
119
|
}
|
|
137
120
|
|
|
138
|
-
// If a Bible book is found
|
|
139
121
|
if (foundBook) {
|
|
140
|
-
i += matchedLength
|
|
141
|
-
let chapterVerse = ""
|
|
142
|
-
const references = []
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (text[i] === ";") {
|
|
150
|
-
const formattedReference = chapterVerse
|
|
151
|
-
.trim()
|
|
152
|
-
.replace(/\./g, ":")
|
|
153
|
-
.replace(/[^a-zA-Z0-9]+$/, "")
|
|
122
|
+
i += matchedLength
|
|
123
|
+
let chapterVerse = ""
|
|
124
|
+
const references = []
|
|
125
|
+
|
|
126
|
+
while (i < normalizedText.length && isValidChapterVerseChar(normalizedText[i])) {
|
|
127
|
+
if (isNextBibleBook(i)) break
|
|
128
|
+
|
|
129
|
+
if (normalizedText[i] === ";") {
|
|
130
|
+
const formattedReference = chapterVerse.trim().replace(/[^a-zA-Z0-9]+$/, "")
|
|
154
131
|
if (formattedReference) references.push(formattedReference)
|
|
155
|
-
chapterVerse = ""
|
|
132
|
+
chapterVerse = ""
|
|
156
133
|
i++
|
|
157
134
|
continue
|
|
158
135
|
}
|
|
159
136
|
|
|
160
|
-
chapterVerse +=
|
|
137
|
+
chapterVerse += normalizedText[i]
|
|
161
138
|
i++
|
|
162
139
|
}
|
|
163
140
|
|
|
164
|
-
// Process the last detected chapter/verse reference
|
|
165
141
|
if (chapterVerse.trim().length > 0) {
|
|
166
|
-
const formattedReference = chapterVerse
|
|
167
|
-
.trim()
|
|
168
|
-
.replace(/\./g, ":")
|
|
169
|
-
.replace(/[^a-zA-Z0-9]+$/, "")
|
|
142
|
+
const formattedReference = chapterVerse.trim().replace(/[^a-zA-Z0-9]+$/, "")
|
|
170
143
|
if (formattedReference) references.push(formattedReference)
|
|
171
144
|
}
|
|
172
145
|
|
|
173
|
-
// Detect any suffix (e.g., "LXX" or "MT") after the chapter/verse reference
|
|
174
146
|
const suffix = detectSuffix(i)
|
|
175
147
|
|
|
176
|
-
// Add each reference as a separate object to the `found` array with type recognition
|
|
177
148
|
references.forEach((ref) => {
|
|
178
149
|
let type
|
|
179
|
-
|
|
180
150
|
if (ref.includes(":")) {
|
|
181
151
|
if (ref.includes("-")) {
|
|
182
152
|
const [start, end] = ref.split("-")
|
|
183
153
|
const startParts = start.split(":")
|
|
184
154
|
const endParts = end.split(":")
|
|
185
155
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
156
|
+
// Determine type based on the chapter (startParts[0] and endParts[0])
|
|
157
|
+
type =
|
|
158
|
+
startParts.length > 1 &&
|
|
159
|
+
endParts.length > 1 &&
|
|
160
|
+
startParts[0].trim() !== endParts[0].trim()
|
|
161
|
+
? "multi_chapter_verse_range" // Chapters differ
|
|
162
|
+
: "chapter_verse_range" // Same chapter
|
|
191
163
|
} else if (ref.includes(",")) {
|
|
192
|
-
type = "comma_separated_verses"
|
|
164
|
+
type = "comma_separated_verses"
|
|
193
165
|
} else {
|
|
194
|
-
type = "chapter_verse"
|
|
166
|
+
type = "chapter_verse"
|
|
195
167
|
}
|
|
196
168
|
} else if (ref.includes("-")) {
|
|
197
|
-
type = "chapter_range"
|
|
169
|
+
type = "chapter_range"
|
|
198
170
|
} else {
|
|
199
|
-
type = "single_chapter"
|
|
171
|
+
type = "single_chapter"
|
|
200
172
|
}
|
|
201
173
|
|
|
202
174
|
this.found.push({
|
|
@@ -208,11 +180,11 @@ class CodexParser {
|
|
|
208
180
|
})
|
|
209
181
|
})
|
|
210
182
|
} else {
|
|
211
|
-
i++
|
|
183
|
+
i++
|
|
212
184
|
}
|
|
213
185
|
}
|
|
214
186
|
|
|
215
|
-
return this
|
|
187
|
+
return this
|
|
216
188
|
}
|
|
217
189
|
|
|
218
190
|
bibleVersion(version) {
|
|
@@ -232,93 +204,148 @@ class CodexParser {
|
|
|
232
204
|
* @returns {object} An object with the parsed passage.
|
|
233
205
|
*/
|
|
234
206
|
parse(reference) {
|
|
235
|
-
this.scan(reference)
|
|
207
|
+
this.scan(reference) // Call scan to populate this.found
|
|
236
208
|
|
|
237
209
|
this.passages = this.found.map((passage) => {
|
|
238
210
|
const book = this.bookify(passage.book)
|
|
239
|
-
const testament = this.bible.old.
|
|
240
|
-
|
|
211
|
+
const testament = this.bible.old.find((bible) => bible === book) ? "old" : "new"
|
|
212
|
+
// Initialize the parsed passage object
|
|
241
213
|
const parsedPassage = {
|
|
242
|
-
original:
|
|
243
|
-
book,
|
|
214
|
+
original: passage.book + " " + passage.reference,
|
|
215
|
+
book: book,
|
|
244
216
|
chapter: null,
|
|
245
|
-
verses: [],
|
|
246
|
-
type: passage.type,
|
|
247
|
-
testament,
|
|
217
|
+
verses: [], // Verse stored as an array
|
|
218
|
+
type: passage.type, // Set type based on reference
|
|
219
|
+
testament: testament,
|
|
248
220
|
index: passage.index,
|
|
249
221
|
version: this._handleVersion(passage.version, testament),
|
|
250
222
|
}
|
|
251
|
-
|
|
252
|
-
|
|
223
|
+
|
|
224
|
+
// Split reference by commas to handle multiple ranges or verses (e.g., "Ge 27:27-29,39-41")
|
|
225
|
+
let parts = passage.reference.split(",")
|
|
226
|
+
|
|
227
|
+
// Check for single chapter books
|
|
228
|
+
const singleChapterBook = this.singleChapterBook.find((bible) => bible[book])
|
|
253
229
|
|
|
254
230
|
parts.forEach((part) => {
|
|
255
|
-
part = part.trim()
|
|
231
|
+
part = part.trim() // Clean up spaces
|
|
232
|
+
// Detect whether it uses ":" or "." for chapter:verse separation
|
|
256
233
|
const separator = part.includes(":") ? ":" : "."
|
|
257
234
|
|
|
258
235
|
if (part.includes("-")) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
236
|
+
// Handle ranges (e.g., "27:27-29" or "39-41")
|
|
237
|
+
|
|
238
|
+
let [start, end] = part.split("-")
|
|
239
|
+
// Handle the starting part
|
|
240
|
+
let [startChapter, startVerse] = start.includes(separator)
|
|
241
|
+
? start.split(separator)
|
|
242
|
+
: [parsedPassage.chapter, start] // Default to same chapter if no chapter is provided
|
|
243
|
+
|
|
244
|
+
parsedPassage.chapter = Number(startChapter) // Set the chapter
|
|
245
|
+
// Checks to see if we are in a multi chapter verse range, if so, include only relevant verses from the this.chapterVerse to
|
|
246
|
+
// the end of the chapter.
|
|
247
|
+
if (start.includes(separator) && end.includes(separator)) {
|
|
248
|
+
// TODO: Need to update versification and version here.
|
|
249
|
+
this._setVersion(book, startChapter, passage.version)
|
|
250
|
+
parsedPassage.verses = this.chapterVerses[book][startChapter].slice(
|
|
251
|
+
this.chapterVerses[book][startChapter].indexOf(Number(startVerse))
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Handle chapter ranges (e.g., "27:27-29") and multi-chapter ranges (e.g., "Ex 2:1-3:4")
|
|
256
|
+
if (end.includes(separator)) {
|
|
257
|
+
let [endChapter, endVerse] = end.split(separator)
|
|
258
|
+
if (Number(endChapter) !== Number(startChapter)) {
|
|
259
|
+
// Cross-chapter range, set 'to' property
|
|
260
|
+
parsedPassage.to = {
|
|
261
|
+
book: book,
|
|
262
|
+
chapter: Number(endChapter), // End chapter
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (endVerse > 1) {
|
|
266
|
+
parsedPassage.to.verses = this.chapterVerses[book][Number(endChapter)].slice(
|
|
267
|
+
0,
|
|
268
|
+
this.chapterVerses[book][Number(endChapter)].indexOf(Number(endVerse)) + 1
|
|
269
|
+
)
|
|
270
|
+
} else {
|
|
271
|
+
parsedPassage.to.verses = [endVerse]
|
|
272
|
+
}
|
|
273
|
+
parsedPassage.type =
|
|
274
|
+
endChapter !== startChapter ? "multi_chapter_verse_range" : "chapter_verse_range" // Set type to chapter range
|
|
275
|
+
} else {
|
|
276
|
+
// Same-chapter range, just add to the verse array
|
|
277
|
+
parsedPassage.verses.push(`${startVerse}-${endVerse}`)
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
// Single-chapter range (e.g., "27:27-29" or "39-41")
|
|
281
|
+
if (!singleChapterBook) {
|
|
282
|
+
if (!startChapter) {
|
|
283
|
+
// Then we have a chapter range with no verses
|
|
284
|
+
parsedPassage.chapter = Number(start)
|
|
285
|
+
parsedPassage.verses = this.chapterVerses[book][start]
|
|
272
286
|
parsedPassage.to = {
|
|
273
|
-
book,
|
|
274
|
-
chapter:
|
|
275
|
-
verses: [
|
|
287
|
+
book: book,
|
|
288
|
+
chapter: Number(end),
|
|
289
|
+
verses: this.chapterVerses[book][end],
|
|
276
290
|
}
|
|
277
|
-
parsedPassage.verses.push(startVerse)
|
|
278
291
|
} else {
|
|
279
|
-
|
|
292
|
+
//
|
|
293
|
+
parsedPassage.verses.push(`${startVerse}-${end}`)
|
|
280
294
|
}
|
|
281
295
|
} else {
|
|
282
|
-
|
|
283
|
-
parsedPassage.
|
|
284
|
-
parsedPassage.to = {
|
|
285
|
-
book,
|
|
286
|
-
chapter: Number(end),
|
|
287
|
-
verses: [],
|
|
288
|
-
}
|
|
296
|
+
parsedPassage.chapter = 1
|
|
297
|
+
parsedPassage.verses.push(`${startVerse}-${end}`)
|
|
289
298
|
}
|
|
290
|
-
} else {
|
|
291
|
-
part = part.replace(/\d+:/gim, "")
|
|
292
|
-
const [singleChapterStartVerse, singleChapterEndVerse] = part.split("-")
|
|
293
|
-
parsedPassage.chapter = 1
|
|
294
|
-
parsedPassage.verses = [`${singleChapterStartVerse}-${singleChapterEndVerse}`]
|
|
295
|
-
parsedPassage.type = "single_chapter_book_verse_range"
|
|
296
299
|
}
|
|
297
|
-
} else if (part.includes(separator)) {
|
|
298
|
-
const [chapterPart, versePart] = part.split(separator).map(Number)
|
|
299
|
-
parsedPassage.chapter = chapterPart
|
|
300
|
-
if (versePart) parsedPassage.verses.push(versePart)
|
|
301
300
|
} else {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
301
|
+
// Handle individual chapter:verse references (e.g., "27:27")
|
|
302
|
+
|
|
303
|
+
let [chapterPart, versePart] = part.includes(separator)
|
|
304
|
+
? part.split(separator)
|
|
305
|
+
: [parsedPassage.chapter, part]
|
|
306
|
+
if (singleChapterBook) {
|
|
307
|
+
if (!chapterPart) {
|
|
308
|
+
parsedPassage.chapter = 1
|
|
309
|
+
parsedPassage.verses.push(versePart) // Add single verse to array
|
|
310
|
+
} else {
|
|
311
|
+
parsedPassage.chapter = Number(chapterPart)
|
|
312
|
+
parsedPassage.verses.push(versePart) // Add single verse to array
|
|
306
313
|
}
|
|
307
|
-
parsedPassage.verses.push(Number(part))
|
|
308
314
|
} else {
|
|
309
|
-
|
|
310
|
-
|
|
315
|
+
// Need to check if chapterPart is undefined
|
|
316
|
+
// If it's undefined, then versePart actually is the chapter and we need to populate the
|
|
317
|
+
// verses from this.chapterVerses
|
|
318
|
+
|
|
319
|
+
if (chapterPart) {
|
|
320
|
+
parsedPassage.chapter = Number(chapterPart)
|
|
321
|
+
parsedPassage.verses.push(versePart) // Add single verse to array
|
|
322
|
+
} else {
|
|
323
|
+
parsedPassage.chapter = Number(versePart)
|
|
324
|
+
if (!this.chapterVerses[book][parsedPassage.chapter]) {
|
|
325
|
+
parsedPassage.valid = this._isValid(parsedPassage, passage.reference)
|
|
326
|
+
} else {
|
|
327
|
+
parsedPassage.verses = [
|
|
328
|
+
this.chapterVerses[book][parsedPassage.chapter][0] +
|
|
329
|
+
"-" +
|
|
330
|
+
this.chapterVerses[book][parsedPassage.chapter][
|
|
331
|
+
this.chapterVerses[book][parsedPassage.chapter].length - 1
|
|
332
|
+
],
|
|
333
|
+
]
|
|
334
|
+
parsedPassage.type = "single_chapter"
|
|
335
|
+
}
|
|
336
|
+
}
|
|
311
337
|
}
|
|
312
338
|
}
|
|
339
|
+
parsedPassage.passages = this.populate(parsedPassage)
|
|
340
|
+
parsedPassage.scripture = this.scripturize(parsedPassage)
|
|
313
341
|
})
|
|
314
342
|
|
|
315
|
-
parsedPassage.passages = this.populate(parsedPassage)
|
|
316
|
-
parsedPassage.scripture = this.scripturize(parsedPassage)
|
|
317
343
|
parsedPassage.valid = this._isValid(parsedPassage, passage.reference)
|
|
344
|
+
|
|
318
345
|
return parsedPassage
|
|
319
346
|
})
|
|
320
347
|
this.versification()
|
|
321
|
-
return this
|
|
348
|
+
return this // Return this instance
|
|
322
349
|
}
|
|
323
350
|
/**
|
|
324
351
|
* Generates an array of numbers representing a range from start to end, inclusive.
|
|
@@ -331,17 +358,17 @@ class CodexParser {
|
|
|
331
358
|
return range
|
|
332
359
|
}
|
|
333
360
|
|
|
334
|
-
_searchVersificationDifferences(
|
|
335
|
-
|
|
336
|
-
|
|
361
|
+
_searchVersificationDifferences(book, chapter, version) {
|
|
362
|
+
version = version.toLowerCase()
|
|
363
|
+
if (!this.chapterVerses[book][chapter]) return
|
|
337
364
|
// Loop through each key-value pair in the dictionary
|
|
338
365
|
for (const [key, value] of Object.entries(this.versificationDifferences[book])) {
|
|
339
366
|
// Check if the key starts with the desired chapter
|
|
340
|
-
if (value[version
|
|
367
|
+
if (value[version].startsWith(`${chapter}:`)) {
|
|
341
368
|
// Ensure the version exists in the value object
|
|
342
|
-
if (value[version
|
|
369
|
+
if (value[version]) {
|
|
343
370
|
// Extract the verse number from the value
|
|
344
|
-
const verse = value[version
|
|
371
|
+
const verse = value[version].split(":")[1]
|
|
345
372
|
this.chapterVerses[book][chapter].push(Number(verse))
|
|
346
373
|
}
|
|
347
374
|
}
|
|
@@ -350,18 +377,17 @@ class CodexParser {
|
|
|
350
377
|
return this.chapterVerses // Return the array of verses
|
|
351
378
|
}
|
|
352
379
|
|
|
353
|
-
_setVersion(
|
|
354
|
-
this.version =
|
|
380
|
+
_setVersion(book, chapter, version) {
|
|
381
|
+
this.version = version ? version : "eng"
|
|
355
382
|
|
|
356
383
|
if (this.version !== "eng") {
|
|
357
|
-
this._searchVersificationDifferences(
|
|
384
|
+
this._searchVersificationDifferences(book, chapter, version)
|
|
358
385
|
}
|
|
359
386
|
}
|
|
360
387
|
|
|
361
388
|
versification() {
|
|
362
389
|
this.passages.forEach((passage) => {
|
|
363
390
|
const hasVersification = this.versificationDifferences[passage.book]
|
|
364
|
-
|
|
365
391
|
passage.passages.forEach((subPassage) => {
|
|
366
392
|
// Apply general versification differences
|
|
367
393
|
if (hasVersification) {
|
|
@@ -412,79 +438,56 @@ class CodexParser {
|
|
|
412
438
|
*/
|
|
413
439
|
populate(parsedPassage) {
|
|
414
440
|
const passages = []
|
|
415
|
-
const { book, chapter, verses, type } = parsedPassage
|
|
416
|
-
|
|
441
|
+
const { book, chapter, verses, type, to } = parsedPassage
|
|
442
|
+
const version = parsedPassage.version ? parsedPassage.version.abbreviation : "eng"
|
|
443
|
+
this._setVersion(book, chapter, version) // Set version data if needed
|
|
444
|
+
|
|
417
445
|
if (type === "single_chapter") {
|
|
418
|
-
// Handle
|
|
446
|
+
// Handle entire chapter references
|
|
419
447
|
if (this.chapterVerses[book] && this.chapterVerses[book][chapter]) {
|
|
420
448
|
this.chapterVerses[book][chapter].forEach((verse) => {
|
|
421
449
|
passages.push({ book, chapter: Number(chapter), verse: Number(verse) })
|
|
422
450
|
})
|
|
423
451
|
}
|
|
424
452
|
} else if (type === "comma_separated_verses") {
|
|
425
|
-
// Handle
|
|
426
|
-
if (
|
|
453
|
+
// Handle explicitly mentioned verses (e.g., 3:1,3,6)
|
|
454
|
+
if (this.chapterVerses[book] && this.chapterVerses[book][chapter]) {
|
|
427
455
|
verses.forEach((verse) => {
|
|
428
456
|
passages.push({ book, chapter: Number(chapter), verse: Number(verse) })
|
|
429
457
|
})
|
|
430
458
|
}
|
|
431
459
|
} else if (type === "chapter_range") {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
passages.push({
|
|
438
|
-
book,
|
|
439
|
-
chapter: i,
|
|
440
|
-
verse: j,
|
|
460
|
+
// Handle ranges of chapters (e.g., 3-5)
|
|
461
|
+
for (let currentChapter = chapter; currentChapter <= to.chapter; currentChapter++) {
|
|
462
|
+
if (this.chapterVerses[book] && this.chapterVerses[book][currentChapter]) {
|
|
463
|
+
this.chapterVerses[book][currentChapter].forEach((verse) => {
|
|
464
|
+
passages.push({ book, chapter: Number(currentChapter), verse: Number(verse) })
|
|
441
465
|
})
|
|
442
466
|
}
|
|
443
467
|
}
|
|
444
468
|
} else if (type === "multi_chapter_verse_range") {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
// Loop through the range of chapters
|
|
464
|
-
for (let chapter = startChapter; chapter <= endChapter; chapter++) {
|
|
465
|
-
// Determine the starting verse for the current chapter
|
|
466
|
-
const chapterStartVerse = chapter === startChapter ? startVerse : 1
|
|
467
|
-
// Determine the ending verse for the current chapter
|
|
468
|
-
const chapterEndVerse = chapter === endChapter ? endVerse : this.chapterVerses[book][chapter].length
|
|
469
|
-
|
|
470
|
-
// Get the array of verses for the current chapter
|
|
471
|
-
const verses = this.chapterVerses[book][chapter].slice(chapterStartVerse - 1, chapterEndVerse)
|
|
472
|
-
|
|
473
|
-
// Loop through the verses in the current chapter
|
|
474
|
-
for (let j = 0; j < verses.length; j++) {
|
|
475
|
-
const currentVerse = chapterStartVerse + j
|
|
476
|
-
|
|
477
|
-
// Add the verse to the passages array
|
|
478
|
-
passages.push({
|
|
479
|
-
book,
|
|
480
|
-
chapter,
|
|
481
|
-
verse: currentVerse,
|
|
482
|
-
})
|
|
469
|
+
// Handle multi-chapter verse ranges (e.g., 3:1-5:6)
|
|
470
|
+
const startChapter = chapter
|
|
471
|
+
const startVerse = verses[0]
|
|
472
|
+
const endChapter = to.chapter
|
|
473
|
+
const endVerse = to.verses[to.verses.length - 1]
|
|
474
|
+
|
|
475
|
+
for (let currentChapter = startChapter; currentChapter <= endChapter; currentChapter++) {
|
|
476
|
+
const chapterVerses = this.chapterVerses[book][currentChapter]
|
|
477
|
+
if (!chapterVerses) continue
|
|
478
|
+
|
|
479
|
+
// Determine start and end verses for each chapter
|
|
480
|
+
const chapterStartVerse = currentChapter === startChapter ? startVerse : 1
|
|
481
|
+
const chapterEndVerse =
|
|
482
|
+
currentChapter === endChapter ? endVerse : chapterVerses[chapterVerses.length - 1]
|
|
483
|
+
|
|
484
|
+
for (let verse = chapterStartVerse; verse <= chapterEndVerse; verse++) {
|
|
485
|
+
passages.push({ book, chapter: currentChapter, verse })
|
|
483
486
|
}
|
|
484
487
|
}
|
|
485
488
|
} else if (type === "chapter_verse" || type === "chapter_verse_range") {
|
|
486
|
-
// Handle chapter:verse or chapter:verse-
|
|
487
|
-
if (
|
|
489
|
+
// Handle single chapter:verse or chapter:verse ranges (e.g., 3:1 or 3:1-5)
|
|
490
|
+
if (this.chapterVerses[book] && this.chapterVerses[book][chapter]) {
|
|
488
491
|
verses.forEach((verse) => {
|
|
489
492
|
if (typeof verse === "string" && verse.includes("-")) {
|
|
490
493
|
const [start, end] = verse.split("-").map(Number)
|
|
@@ -496,6 +499,12 @@ class CodexParser {
|
|
|
496
499
|
}
|
|
497
500
|
})
|
|
498
501
|
}
|
|
502
|
+
} else if (type === "single_chapter_book_verse_range") {
|
|
503
|
+
// Handle ranges in single-chapter books (e.g., Jude 5-7)
|
|
504
|
+
const [startVerse, endVerse] = verses[0].split("-").map(Number)
|
|
505
|
+
for (let i = startVerse; i <= endVerse; i++) {
|
|
506
|
+
passages.push({ book, chapter: 1, verse: i })
|
|
507
|
+
}
|
|
499
508
|
}
|
|
500
509
|
|
|
501
510
|
return passages
|
|
@@ -557,64 +566,60 @@ class CodexParser {
|
|
|
557
566
|
* @return {object} The object with the human-readable name, chapter and verses and a hash.
|
|
558
567
|
*/
|
|
559
568
|
scripturize(passage) {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (to && to.chapter && to.chapter !== chapter) {
|
|
567
|
-
// Handle multi-chapter range
|
|
568
|
-
const startChapter = chapter
|
|
569
|
-
const startVerses = verses.filter((v) => v.chapter === startChapter).map((v) => v.verse)
|
|
570
|
-
|
|
571
|
-
const endChapter = to.chapter
|
|
572
|
-
const endVerses = verses.filter((v) => v.chapter === endChapter).map((v) => v.verse)
|
|
573
|
-
|
|
574
|
-
const startFormatted =
|
|
575
|
-
startVerses.length > 1 ? `${startVerses[0]}-${startVerses[startVerses.length - 1]}` : startVerses[0]
|
|
576
|
-
|
|
577
|
-
const endFormatted =
|
|
578
|
-
endVerses.length > 1 ? `${endVerses[0]}-${endVerses[endVerses.length - 1]}` : endVerses[0]
|
|
579
|
-
|
|
580
|
-
formattedVerses = `${startChapter}:${startFormatted}-${endChapter}:${endFormatted}`
|
|
581
|
-
} else {
|
|
582
|
-
// Handle single-chapter range
|
|
583
|
-
const startVerses = verses.map((v) => v.verse)
|
|
584
|
-
|
|
585
|
-
if (startVerses.length === 1) {
|
|
586
|
-
formattedVerses = startVerses[0].toString()
|
|
587
|
-
} else {
|
|
588
|
-
// Group consecutive verses into ranges
|
|
589
|
-
let ranges = []
|
|
590
|
-
let tempRange = [startVerses[0]]
|
|
591
|
-
|
|
592
|
-
for (let i = 1; i < startVerses.length; i++) {
|
|
593
|
-
if (startVerses[i] === startVerses[i - 1] + 1) {
|
|
594
|
-
tempRange.push(startVerses[i])
|
|
595
|
-
} else {
|
|
596
|
-
ranges.push(tempRange)
|
|
597
|
-
tempRange = [startVerses[i]]
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
ranges.push(tempRange)
|
|
569
|
+
// Helper to format a single chapter:verse combination
|
|
570
|
+
const formatChapterVerse = (chapter, verseStart, verseEnd = null) => {
|
|
571
|
+
if (!chapter) return ""
|
|
572
|
+
if (!verseStart) return `${chapter}`
|
|
573
|
+
return verseEnd ? `${chapter}:${verseStart}-${verseEnd}` : `${chapter}:${verseStart}`
|
|
574
|
+
}
|
|
601
575
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
576
|
+
// Initialize combined passage
|
|
577
|
+
let combined = `${passage.book}`
|
|
578
|
+
|
|
579
|
+
if (passage.type === "multi_chapter_verse_range") {
|
|
580
|
+
// Multi-chapter verse range handling: first verse of first chapter to last verse of last chapter
|
|
581
|
+
if (passage.to) {
|
|
582
|
+
combined += ` ${formatChapterVerse(passage.chapter, passage.verses[0])}` // Start chapter:verse
|
|
583
|
+
combined += `-${formatChapterVerse(
|
|
584
|
+
passage.to.chapter,
|
|
585
|
+
passage.to.verses[passage.to.verses.length - 1]
|
|
586
|
+
)}` // End chapter:last verse
|
|
605
587
|
}
|
|
606
|
-
|
|
607
|
-
|
|
588
|
+
} else if (passage.type === "chapter_verse_range") {
|
|
589
|
+
// Single-chapter verse range
|
|
590
|
+
combined += ` ${formatChapterVerse(
|
|
591
|
+
passage.chapter,
|
|
592
|
+
passage.verses[0],
|
|
593
|
+
passage.verses[passage.verses.length - 1]
|
|
594
|
+
)}`
|
|
595
|
+
} else if (passage.type === "comma_separated_verses") {
|
|
596
|
+
// Comma-separated verses
|
|
597
|
+
combined += ` ${passage.chapter}:${passage.verses.join(",")}`
|
|
598
|
+
} else if (passage.type === "chapter_range") {
|
|
599
|
+
// Chapter range
|
|
600
|
+
combined += ` ${passage.chapter}:${passage.verses[0]}-${passage.to.chapter}:${
|
|
601
|
+
passage.to.verses[passage.to.verses.length - 1]
|
|
602
|
+
}`
|
|
603
|
+
} else {
|
|
604
|
+
// Single chapter or single verse
|
|
605
|
+
combined += ` ${formatChapterVerse(passage.chapter, passage.verses[0])}`
|
|
608
606
|
}
|
|
609
607
|
|
|
610
|
-
//
|
|
611
|
-
const
|
|
612
|
-
|
|
608
|
+
// Generate chapter:verse for current and "to" objects
|
|
609
|
+
const cv = passage.to
|
|
610
|
+
? `${formatChapterVerse(passage.chapter, passage.verses[0])}-${formatChapterVerse(
|
|
611
|
+
passage.to.chapter,
|
|
612
|
+
passage.to.verses[passage.to.verses.length - 1]
|
|
613
|
+
)}`
|
|
614
|
+
: formatChapterVerse(passage.chapter, passage.verses[0])
|
|
615
|
+
|
|
616
|
+
// Generate a hash for the passage
|
|
617
|
+
const hash = `${passage.book.toLowerCase()}_${cv.replace(/:/g, "_").replace(/-/g, "_")}`
|
|
613
618
|
|
|
614
619
|
return {
|
|
615
|
-
passage:
|
|
616
|
-
cv:
|
|
617
|
-
hash,
|
|
620
|
+
passage: combined, // Reconstructed passage
|
|
621
|
+
cv: cv, // Chapter:verse range
|
|
622
|
+
hash: hash, // Unique hash
|
|
618
623
|
}
|
|
619
624
|
}
|
|
620
625
|
|
|
@@ -628,13 +633,14 @@ class CodexParser {
|
|
|
628
633
|
* @return {object} The combined passage object.
|
|
629
634
|
*/
|
|
630
635
|
combine(passages) {
|
|
631
|
-
// Only check if passages are from the same book
|
|
632
|
-
const
|
|
633
|
-
if (
|
|
636
|
+
// Only check if passages are from the same book)
|
|
637
|
+
const sameBook = [...new Set(passages.map((p) => p.book))]
|
|
638
|
+
if (sameBook.length > 1) {
|
|
634
639
|
throw new Error("Passages are not from the same book.")
|
|
635
640
|
}
|
|
636
641
|
|
|
637
642
|
const newPassages = []
|
|
643
|
+
|
|
638
644
|
passages.forEach((passageSet) => {
|
|
639
645
|
passageSet.passages.forEach((passage) => {
|
|
640
646
|
if (passage.versification) {
|
|
@@ -646,6 +652,7 @@ class CodexParser {
|
|
|
646
652
|
})
|
|
647
653
|
|
|
648
654
|
const noDuplicates2 = [...new Set(newPassages)]
|
|
655
|
+
|
|
649
656
|
const parsed = this.parse(noDuplicates2.join(" // ")).getPassages()
|
|
650
657
|
return this.join(parsed)
|
|
651
658
|
}
|
|
@@ -658,87 +665,100 @@ class CodexParser {
|
|
|
658
665
|
* @return {object} The combined passage object.
|
|
659
666
|
*/
|
|
660
667
|
join(passages) {
|
|
661
|
-
|
|
668
|
+
if (!passages || passages.length === 0) {
|
|
669
|
+
throw new Error("No passages provided to join.")
|
|
670
|
+
}
|
|
662
671
|
|
|
663
|
-
|
|
664
|
-
const
|
|
672
|
+
// Ensure all passages are from the same book
|
|
673
|
+
const uniqueBooks = [...new Set(passages.map((p) => p.book))]
|
|
674
|
+
if (uniqueBooks.length > 1) {
|
|
675
|
+
throw new Error("Passages must be from the same book to join.")
|
|
676
|
+
}
|
|
665
677
|
|
|
666
|
-
//
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
678
|
+
// Start with the base object
|
|
679
|
+
const combined = {
|
|
680
|
+
...passages[0],
|
|
681
|
+
verses: [],
|
|
682
|
+
passages: [],
|
|
683
|
+
to: null,
|
|
684
|
+
scripture: {},
|
|
685
|
+
type: null,
|
|
686
|
+
}
|
|
671
687
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
chapters[passage.chapter] = new Set() // Use Set to avoid duplicates
|
|
676
|
-
}
|
|
688
|
+
const chapterVerses = {}
|
|
689
|
+
let firstChapter = null
|
|
690
|
+
let lastChapter = null
|
|
677
691
|
|
|
678
|
-
|
|
692
|
+
// Collect all verses and passages, grouped by chapter
|
|
693
|
+
passages.forEach((passage) => {
|
|
679
694
|
passage.passages.forEach((p) => {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
// Create a unique key for each passage (book-chapter-verse)
|
|
683
|
-
const passageKey = `${p.book}-${p.chapter}-${p.verse}`
|
|
684
|
-
|
|
685
|
-
// Add to the passages array if it hasn't been added yet
|
|
686
|
-
if (!uniquePassages.has(passageKey)) {
|
|
687
|
-
newObject.passages.push(p) // Add the passage
|
|
688
|
-
uniquePassages.add(passageKey) // Mark it as added
|
|
695
|
+
if (!chapterVerses[p.chapter]) {
|
|
696
|
+
chapterVerses[p.chapter] = new Set()
|
|
689
697
|
}
|
|
698
|
+
chapterVerses[p.chapter].add(p.verse)
|
|
699
|
+
combined.passages.push(p) // Add individual passage
|
|
690
700
|
})
|
|
691
|
-
})
|
|
692
701
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
if (
|
|
696
|
-
|
|
702
|
+
// Track first and last chapters
|
|
703
|
+
const chapters = passage.passages.map((p) => p.chapter)
|
|
704
|
+
if (!firstChapter || Math.min(...chapters) < firstChapter) {
|
|
705
|
+
firstChapter = Math.min(...chapters)
|
|
706
|
+
}
|
|
707
|
+
if (!lastChapter || Math.max(...chapters) > lastChapter) {
|
|
708
|
+
lastChapter = Math.max(...chapters)
|
|
697
709
|
}
|
|
698
|
-
return a.verse - b.verse // Sort by verse within the same chapter
|
|
699
710
|
})
|
|
700
711
|
|
|
701
|
-
//
|
|
712
|
+
// Ensure unique and sorted passages
|
|
713
|
+
combined.passages = Array.from(new Set(combined.passages.map(JSON.stringify))).map(JSON.parse)
|
|
714
|
+
|
|
715
|
+
// Process chapter and verse data
|
|
702
716
|
const chapterStrings = []
|
|
703
|
-
|
|
704
|
-
|
|
717
|
+
const sortedChapters = Object.keys(chapterVerses)
|
|
718
|
+
.map(Number)
|
|
719
|
+
.sort((a, b) => a - b)
|
|
705
720
|
|
|
706
|
-
|
|
707
|
-
const verses = Array.from(
|
|
708
|
-
const mergedVerses = this.mergeRanges(verses)
|
|
721
|
+
sortedChapters.forEach((chapter) => {
|
|
722
|
+
const verses = Array.from(chapterVerses[chapter]).sort((a, b) => a - b)
|
|
723
|
+
const mergedVerses = this.mergeRanges(verses)
|
|
709
724
|
chapterStrings.push(`${chapter}:${mergedVerses.join(",")}`)
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
if (!firstChapter) firstChapter = Number(chapter) // Ensure chapter is a number
|
|
713
|
-
lastChapter = Number(chapter) // Always update to the current chapter as a number
|
|
714
|
-
|
|
715
|
-
// Update the newObject.verses with the merged ranges for the current chapter
|
|
716
|
-
if (Number(chapter) === firstChapter) {
|
|
717
|
-
newObject.verses = mergedVerses
|
|
725
|
+
if (chapter === firstChapter) {
|
|
726
|
+
combined.verses = mergedVerses // First chapter's verses
|
|
718
727
|
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// Build the final combined object with `to` key for multi-chapter passages
|
|
722
|
-
newObject.original = `${newObject.book} ${firstChapter}:${newObject.verses.join(",")}`
|
|
728
|
+
})
|
|
723
729
|
|
|
730
|
+
// Handle multi-chapter ranges
|
|
724
731
|
if (firstChapter !== lastChapter) {
|
|
725
|
-
|
|
726
|
-
|
|
732
|
+
combined.type = "multi_chapter_verse_range"
|
|
733
|
+
combined.to = {
|
|
734
|
+
book: combined.book,
|
|
727
735
|
chapter: lastChapter,
|
|
728
|
-
verses: this.mergeRanges(Array.from(
|
|
736
|
+
verses: this.mergeRanges(Array.from(chapterVerses[lastChapter])),
|
|
737
|
+
}
|
|
738
|
+
combined.original = `${combined.book} ${firstChapter}:${combined.verses.join(
|
|
739
|
+
","
|
|
740
|
+
)}-${lastChapter}:${combined.to.verses.join(",")}`
|
|
741
|
+
} else {
|
|
742
|
+
// Single-chapter range or comma-separated
|
|
743
|
+
if (combined.verses.length > 1) {
|
|
744
|
+
combined.type = "chapter_verse_range"
|
|
745
|
+
} else {
|
|
746
|
+
combined.type = "chapter_verse"
|
|
729
747
|
}
|
|
748
|
+
combined.original = `${combined.book} ${firstChapter}:${combined.verses.join(",")}`
|
|
730
749
|
}
|
|
731
750
|
|
|
732
|
-
// Build the scripture
|
|
733
|
-
const chapterString = chapterStrings.join(",")
|
|
734
|
-
|
|
735
|
-
passage: `${
|
|
751
|
+
// Build the scripture property
|
|
752
|
+
const chapterString = chapterStrings.join(",")
|
|
753
|
+
combined.scripture = {
|
|
754
|
+
passage: `${combined.book} ${chapterString}`,
|
|
736
755
|
cv: chapterString,
|
|
737
|
-
hash: `${
|
|
756
|
+
hash: `${combined.book.toLowerCase()}_${chapterString.replace(/:/g, ".").replace(/,/g, ".")}`,
|
|
738
757
|
}
|
|
739
758
|
|
|
740
|
-
return
|
|
759
|
+
return combined
|
|
741
760
|
}
|
|
761
|
+
|
|
742
762
|
mergeRanges(verses) {
|
|
743
763
|
const sortedVerses = [...new Set(verses)].sort((a, b) => a - b)
|
|
744
764
|
const merged = []
|
|
@@ -749,11 +769,9 @@ class CodexParser {
|
|
|
749
769
|
if (sortedVerses[i] === end + 1) {
|
|
750
770
|
end = sortedVerses[i]
|
|
751
771
|
} else {
|
|
752
|
-
// Push
|
|
772
|
+
// Push range or single verse
|
|
753
773
|
if (start === end) {
|
|
754
774
|
merged.push(`${start}`)
|
|
755
|
-
} else if (end === start + 1) {
|
|
756
|
-
merged.push(`${start},${end}`)
|
|
757
775
|
} else {
|
|
758
776
|
merged.push(`${start}-${end}`)
|
|
759
777
|
}
|
|
@@ -762,11 +780,9 @@ class CodexParser {
|
|
|
762
780
|
}
|
|
763
781
|
}
|
|
764
782
|
|
|
765
|
-
// Push the final range or
|
|
783
|
+
// Push the final range or single verse
|
|
766
784
|
if (start === end) {
|
|
767
785
|
merged.push(`${start}`)
|
|
768
|
-
} else if (end === start + 1) {
|
|
769
|
-
merged.push(`${start},${end}`)
|
|
770
786
|
} else {
|
|
771
787
|
merged.push(`${start}-${end}`)
|
|
772
788
|
}
|
|
@@ -848,6 +864,22 @@ class CodexParser {
|
|
|
848
864
|
}
|
|
849
865
|
}
|
|
850
866
|
}
|
|
867
|
+
for (let i = 0; i < passage.verses.length; i++) {
|
|
868
|
+
let verse = passage.verses[i]
|
|
869
|
+
const searchForVerse = this.chapterVerses[passage.book][passage.chapter].find(
|
|
870
|
+
(v) => Number(v) === Number(verse)
|
|
871
|
+
)
|
|
872
|
+
if (!searchForVerse) {
|
|
873
|
+
return {
|
|
874
|
+
error: true,
|
|
875
|
+
code: 104,
|
|
876
|
+
message: {
|
|
877
|
+
verse_exists: false,
|
|
878
|
+
content: `Verse number ${verse} does not exist in ${passage.book} ${passage.chapter}`,
|
|
879
|
+
},
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
851
883
|
return true
|
|
852
884
|
}
|
|
853
885
|
_handleVersion(version, testament) {
|