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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/CodexParser.js +342 -310
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexparser",
3
- "version": "0.1.41",
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": {
@@ -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 = text.toLowerCase()
77
+ const lowerCaseText = normalizedText.toLowerCase()
72
78
 
73
- let i = 0 // Index pointer to iterate through the input text
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
- // Check for full Bible book names
89
- for (const book of lowercaseBibleFullNames) {
90
- if (textAfterCurrentPosition.startsWith(book)) return true
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 = text.substring(startIndex).match(/\b(LXX|MT)\b/i)
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 // Placeholder for the detected book name
113
- let foundIndex = -1 // Index in the text where the book name starts
114
- let matchedLength = 0 // Length of the matched book name or abbreviation
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] // Store the original book name (case-sensitive)
104
+ foundBook = fullNames[j]
121
105
  foundIndex = i
122
- matchedLength = book.length // Update the match 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 // Move the index pointer forward by the length of the book name
141
- let chapterVerse = "" // Placeholder for chapter and verse data
142
- const references = [] // Array to store multiple chapter/verse references for the same book
143
-
144
- // Extract chapter and verse references
145
- while (i < text.length && isValidChapterVerseChar(text[i])) {
146
- if (isNextBibleBook(i)) break // Stop if a new Bible book is detected
147
-
148
- // Handle semicolon-separated references (indicates a new reference)
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 = "" // Reset for the next reference
132
+ chapterVerse = ""
156
133
  i++
157
134
  continue
158
135
  }
159
136
 
160
- chapterVerse += text[i]
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
- if (startParts.length > 1 && endParts.length > 1 && startParts[0] !== endParts[0]) {
187
- type = "multi_chapter_verse_range" // Example: "8:23-9:1"
188
- } else {
189
- type = "chapter_verse_range" // Example: "8:23-25"
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" // Example: "8:23,24"
164
+ type = "comma_separated_verses"
193
165
  } else {
194
- type = "chapter_verse" // Example: "8:23"
166
+ type = "chapter_verse"
195
167
  }
196
168
  } else if (ref.includes("-")) {
197
- type = "chapter_range" // Example: "8-9"
169
+ type = "chapter_range"
198
170
  } else {
199
- type = "single_chapter" // Example: "8"
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++ // Move to the next character if no book is found
183
+ i++
212
184
  }
213
185
  }
214
186
 
215
- return this // Return the current instance for method chaining
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.includes(book) ? "old" : "new"
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: `${passage.book} ${passage.reference}`,
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
- const parts = passage.reference.split(",")
252
- const isSingleChapter = this.singleChapterBook.some((singleChapterBook) => singleChapterBook[book])
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
- if (!isSingleChapter) {
260
- if (part.includes(":")) {
261
- let [start, end] = part.split("-")
262
- const [startChapter, startVerse] = start.includes(separator)
263
- ? start.split(separator).map(Number)
264
- : [parsedPassage.chapter, Number(start)]
265
- const [endChapter, endVerse] = end.includes(separator)
266
- ? end.split(separator).map(Number)
267
- : [startChapter, Number(end)]
268
-
269
- parsedPassage.chapter = startChapter
270
-
271
- if (startChapter !== endChapter) {
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: endChapter,
275
- verses: [endVerse],
287
+ book: book,
288
+ chapter: Number(end),
289
+ verses: this.chapterVerses[book][end],
276
290
  }
277
- parsedPassage.verses.push(startVerse)
278
291
  } else {
279
- parsedPassage.verses.push(...this._generateRange(startVerse, endVerse))
292
+ //
293
+ parsedPassage.verses.push(`${startVerse}-${end}`)
280
294
  }
281
295
  } else {
282
- const [start, end] = part.split("-")
283
- parsedPassage.chapter = Number(start)
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
- if (!isSingleChapter) {
303
- const number = Number(part)
304
- if (!parsedPassage.verses.length) {
305
- parsedPassage.chapter = number
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
- parsedPassage.chapter = 1
310
- parsedPassage.verses.push(Number(part))
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(passage) {
335
- const { book, chapter, version } = passage
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.abbreviation].startsWith(`${chapter}:`)) {
367
+ if (value[version].startsWith(`${chapter}:`)) {
341
368
  // Ensure the version exists in the value object
342
- if (value[version.abbreviation]) {
369
+ if (value[version]) {
343
370
  // Extract the verse number from the value
344
- const verse = value[version.abbreviation].split(":")[1]
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(passage) {
354
- this.version = passage.version ? passage.version.abbreviation : "eng"
380
+ _setVersion(book, chapter, version) {
381
+ this.version = version ? version : "eng"
355
382
 
356
383
  if (this.version !== "eng") {
357
- this._searchVersificationDifferences(passage)
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
- this._setVersion(parsedPassage)
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 single chapter references
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 only the explicitly mentioned verses
426
- if (verses && this.chapterVerses[book] && this.chapterVerses[book][chapter]) {
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
- const { to } = parsedPassage
433
-
434
- for (let i = chapter; i <= to.chapter; i++) {
435
- const verses = this.chapterVerses[book][i]
436
- for (let j = verses[0]; j < verses.length; j++) {
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
- const { to } = parsedPassage
446
- // Create an array of reference objects for the start and end of the range
447
- const refs = [
448
- {
449
- chapter: Number(parsedPassage.chapter),
450
- verse: Number(parsedPassage.verses[0]),
451
- },
452
- {
453
- chapter: Number(to.chapter),
454
- verse: Number(to.verses[to.verses.length - 1]),
455
- },
456
- ]
457
-
458
- const startChapter = refs[0].chapter
459
- const startVerse = refs[0].verse
460
- const endChapter = refs[refs.length - 1].chapter
461
- const endVerse = refs[refs.length - 1].verse
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-range references
487
- if (verses && this.chapterVerses[book] && this.chapterVerses[book][chapter]) {
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
- const { book, chapter, passages, to } = passage
561
-
562
- // Extract verses from the passages array
563
- const verses = passages.map((p) => ({ chapter: p.chapter, verse: p.verse }))
564
- let formattedVerses = ""
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
- formattedVerses = ranges
603
- .map((range) => (range.length > 1 ? `${range[0]}-${range[range.length - 1]}` : range[0]))
604
- .join(",")
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
- formattedVerses = `${chapter}:${formattedVerses}`
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
- // Format the final passage
611
- const full = `${book} ${formattedVerses}`.trim()
612
- const hash = full.toLowerCase().replace(/ /g, "_").replace(/:/g, ".").replace(/-/g, ".").replace(/,/g, ".")
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: full,
616
- cv: formattedVerses,
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 noDuplicates = [...new Set(passages.map((p) => p.book))]
633
- if (noDuplicates.length > 1) {
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
- const newObject = { ...passages[0] } // Start with the first passage
668
+ if (!passages || passages.length === 0) {
669
+ throw new Error("No passages provided to join.")
670
+ }
662
671
 
663
- const chapters = {} // Store verses by chapters
664
- const uniquePassages = new Set() // Track unique passages to prevent duplicates
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
- // Add initial passages to the unique set to avoid duplication
667
- newObject.passages.forEach((p) => {
668
- const passageKey = `${p.book}-${p.chapter}-${p.verse}`
669
- uniquePassages.add(passageKey)
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
- // Iterate through all the passages and group verses by chapter
673
- passages.forEach((passage) => {
674
- if (!chapters[passage.chapter]) {
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
- // Add verses to their corresponding chapter
692
+ // Collect all verses and passages, grouped by chapter
693
+ passages.forEach((passage) => {
679
694
  passage.passages.forEach((p) => {
680
- chapters[p.chapter].add(p.verse)
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
- // Sort the newObject.passages array by chapter first, then by verse
694
- newObject.passages.sort((a, b) => {
695
- if (a.chapter !== b.chapter) {
696
- return a.chapter - b.chapter // Sort by chapter
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
- // Prepare to build the final result
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
- let firstChapter = null
704
- let lastChapter = null
717
+ const sortedChapters = Object.keys(chapterVerses)
718
+ .map(Number)
719
+ .sort((a, b) => a - b)
705
720
 
706
- for (const chapter in chapters) {
707
- const verses = Array.from(chapters[chapter]).sort((a, b) => a - b)
708
- const mergedVerses = this.mergeRanges(verses) // Merge adjacent verses into ranges
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
- // Track the first and last chapters for the 'to' key
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
- newObject.to = {
726
- book: newObject.book,
732
+ combined.type = "multi_chapter_verse_range"
733
+ combined.to = {
734
+ book: combined.book,
727
735
  chapter: lastChapter,
728
- verses: this.mergeRanges(Array.from(chapters[lastChapter])), // Ensure merged range
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 string with combined chapters (without spaces after commas)
733
- const chapterString = chapterStrings.join(",") // No space after comma
734
- newObject.scripture = {
735
- passage: `${newObject.book} ${chapterString}`,
751
+ // Build the scripture property
752
+ const chapterString = chapterStrings.join(",")
753
+ combined.scripture = {
754
+ passage: `${combined.book} ${chapterString}`,
736
755
  cv: chapterString,
737
- hash: `${newObject.book.toLowerCase()}_${chapterString.replace(/:/g, ".").replace(/,/g, ".")}`,
756
+ hash: `${combined.book.toLowerCase()}_${chapterString.replace(/:/g, ".").replace(/,/g, ".")}`,
738
757
  }
739
758
 
740
- return newObject
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 the current range if it's more than 2 consecutive numbers, otherwise separate by commas
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 pair
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) {