codexparser 0.1.42 → 0.1.44

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 +274 -197
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexparser",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
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": {
@@ -147,16 +147,19 @@ class CodexParser {
147
147
 
148
148
  references.forEach((ref) => {
149
149
  let type
150
-
151
150
  if (ref.includes(":")) {
152
151
  if (ref.includes("-")) {
153
152
  const [start, end] = ref.split("-")
154
153
  const startParts = start.split(":")
155
154
  const endParts = end.split(":")
155
+
156
+ // Determine type based on the chapter (startParts[0] and endParts[0])
156
157
  type =
158
+ startParts.length > 1 &&
159
+ endParts.length > 1 &&
157
160
  startParts[0].trim() !== endParts[0].trim()
158
- ? "multi_chapter_verse_range"
159
- : "chapter_verse_range"
161
+ ? "multi_chapter_verse_range" // Chapters differ
162
+ : "chapter_verse_range" // Same chapter
160
163
  } else if (ref.includes(",")) {
161
164
  type = "comma_separated_verses"
162
165
  } else {
@@ -206,136 +209,103 @@ class CodexParser {
206
209
  this.passages = this.found.map((passage) => {
207
210
  const book = this.bookify(passage.book)
208
211
  const testament = this.bible.old.find((bible) => bible === book) ? "old" : "new"
212
+
209
213
  // Initialize the parsed passage object
210
214
  const parsedPassage = {
211
215
  original: passage.book + " " + passage.reference,
212
216
  book: book,
213
217
  chapter: null,
214
- verses: [], // Verse stored as an array
218
+ verses: [], // Verses stored as an array
215
219
  type: passage.type, // Set type based on reference
216
220
  testament: testament,
217
221
  index: passage.index,
218
222
  version: this._handleVersion(passage.version, testament),
219
223
  }
220
224
 
221
- // Split reference by commas to handle multiple ranges or verses (e.g., "Ge 27:27-29,39-41")
225
+ // Split reference by commas to handle multiple ranges or verses (e.g., "Isaiah 8:22-9:1,5")
222
226
  let parts = passage.reference.split(",")
223
227
 
224
- // Check for single chapter books
228
+ // Check for single-chapter books
225
229
  const singleChapterBook = this.singleChapterBook.find((bible) => bible[book])
226
230
 
227
- parts.forEach((part) => {
231
+ parts.forEach((part, partIndex) => {
228
232
  part = part.trim() // Clean up spaces
229
- // Detect whether it uses ":" or "." for chapter:verse separation
230
233
  const separator = part.includes(":") ? ":" : "."
231
234
 
232
235
  if (part.includes("-")) {
233
- // Handle ranges (e.g., "27:27-29" or "39-41")
234
-
236
+ // Handle ranges (e.g., "8:22-9:1")
235
237
  let [start, end] = part.split("-")
238
+
236
239
  // Handle the starting part
237
240
  let [startChapter, startVerse] = start.includes(separator)
238
241
  ? start.split(separator)
239
242
  : [parsedPassage.chapter, start] // Default to same chapter if no chapter is provided
240
243
 
241
244
  parsedPassage.chapter = Number(startChapter) // Set the chapter
242
- // Checks to see if we are in a multi chapter verse range, if so, include only relevant verses from the this.chapterVerse to
243
- // the end of the chapter.
245
+
246
+ // Multi-chapter verse range logic
244
247
  if (start.includes(separator) && end.includes(separator)) {
248
+ let [endChapter, endVerse] = end.split(separator)
249
+
245
250
  parsedPassage.verses = this.chapterVerses[book][startChapter].slice(
246
251
  this.chapterVerses[book][startChapter].indexOf(Number(startVerse))
247
252
  )
248
- }
249
253
 
250
- // Handle same-chapter ranges (e.g., "27:27-29") and multi-chapter ranges (e.g., "Ex 2:1-3:4")
251
- if (end.includes(separator)) {
252
- let [endChapter, endVerse] = end.split(separator)
253
254
  if (Number(endChapter) !== Number(startChapter)) {
254
- // Cross-chapter range, set 'to' property
255
+ const endVerses = this.chapterVerses[book][endChapter].slice(
256
+ 0,
257
+ this.chapterVerses[book][endChapter].indexOf(Number(endVerse)) + 1
258
+ )
259
+
260
+ // Construct ranges for the `to` object
255
261
  parsedPassage.to = {
256
262
  book: book,
257
- chapter: Number(endChapter), // End chapter
258
- }
259
- if (endVerse > 1) {
260
- parsedPassage.to.verses = this.chapterVerses[book][Number(endChapter)].slice(
261
- 0,
262
- this.chapterVerses[book][Number(endChapter)].indexOf(Number(endVerse)) + 1
263
- )
263
+ chapter: Number(endChapter),
264
+ verses: [`${1}-${endVerse}`, ...endVerses.filter((v) => v > endVerse)],
264
265
  }
265
- parsedPassage.type =
266
- endChapter !== startChapter ? "multi_chapter_verse_range" : "chapter_verse_range" // Set type to chapter range
266
+
267
+ parsedPassage.type = "multi_chapter_verse_range"
267
268
  } else {
268
- // Same-chapter range, just add to the verse array
269
269
  parsedPassage.verses.push(`${startVerse}-${endVerse}`)
270
270
  }
271
- } else {
272
- // Single-chapter range (e.g., "27:27-29" or "39-41")
273
- if (!singleChapterBook) {
274
- if (!startChapter) {
275
- // Then we have a chapter range with no verses
276
- parsedPassage.chapter = Number(start)
277
- parsedPassage.verses = this.chapterVerses[book][start]
278
- parsedPassage.to = {
279
- book: book,
280
- chapter: Number(end),
281
- verses: this.chapterVerses[book][end],
282
- }
283
- } else {
284
- //
285
- parsedPassage.verses.push(`${startVerse}-${end}`)
286
- }
287
- } else {
288
- parsedPassage.chapter = 1
289
- parsedPassage.verses.push(`${startVerse}-${end}`)
290
- }
291
271
  }
292
272
  } else {
293
- // Handle individual chapter:verse references (e.g., "27:27")
294
-
273
+ // Handle individual chapter:verse references (e.g., "9:5" in "8:22-9:1,5")
295
274
  let [chapterPart, versePart] = part.includes(separator)
296
275
  ? part.split(separator)
297
276
  : [parsedPassage.chapter, part]
277
+
298
278
  if (singleChapterBook) {
299
- if (!chapterPart) {
300
- parsedPassage.chapter = 1
301
- parsedPassage.verses.push(versePart) // Add single verse to array
302
- } else {
303
- parsedPassage.chapter = Number(chapterPart)
304
- parsedPassage.verses.push(versePart) // Add single verse to array
305
- }
279
+ parsedPassage.chapter = 1
280
+ parsedPassage.verses.push(versePart) // Add single verse to array
306
281
  } else {
307
- // Need to check if chapterPart is undefined
308
- // If it's undefined, then versePart actually is the chapter and we need to populate the
309
- // verses from this.chapterVerses
310
-
311
282
  if (chapterPart) {
312
- parsedPassage.chapter = Number(chapterPart)
313
- parsedPassage.verses.push(versePart) // Add single verse to array
314
- } else {
315
- parsedPassage.chapter = Number(versePart)
316
- if (!this.chapterVerses[book][parsedPassage.chapter]) {
317
- parsedPassage.valid = this._isValid(parsedPassage, passage.reference)
283
+ if (partIndex === parts.length - 1 && parsedPassage.to) {
284
+ // Add additional verses to the last chapter in multi-chapter range
285
+ if (!parsedPassage.to.verses) parsedPassage.to.verses = []
286
+ parsedPassage.to.verses.push(Number(versePart))
318
287
  } else {
319
- parsedPassage.verses = [
320
- this.chapterVerses[book][parsedPassage.chapter][0] +
321
- "-" +
322
- this.chapterVerses[book][parsedPassage.chapter][
323
- this.chapterVerses[book][parsedPassage.chapter].length - 1
324
- ],
325
- ]
326
- parsedPassage.type = "single_chapter"
288
+ parsedPassage.chapter = Number(chapterPart)
289
+ parsedPassage.verses.push(Number(versePart))
327
290
  }
328
291
  }
329
292
  }
330
293
  }
294
+
331
295
  parsedPassage.passages = this.populate(parsedPassage)
332
296
  parsedPassage.scripture = this.scripturize(parsedPassage)
333
297
  })
334
298
 
299
+ // Handle range merging in `to` object
300
+ if (parsedPassage.to && Array.isArray(parsedPassage.to.verses)) {
301
+ parsedPassage.to.verses = this.mergeRanges(parsedPassage.to.verses)
302
+ }
303
+
335
304
  parsedPassage.valid = this._isValid(parsedPassage, passage.reference)
336
305
 
337
306
  return parsedPassage
338
307
  })
308
+
339
309
  this.versification()
340
310
  return this // Return this instance
341
311
  }
@@ -350,18 +320,17 @@ class CodexParser {
350
320
  return range
351
321
  }
352
322
 
353
- _searchVersificationDifferences(passage) {
354
- const { book, chapter, version } = passage
323
+ _searchVersificationDifferences(book, chapter, version) {
324
+ version = version.toLowerCase()
355
325
  if (!this.chapterVerses[book][chapter]) return
356
-
357
326
  // Loop through each key-value pair in the dictionary
358
327
  for (const [key, value] of Object.entries(this.versificationDifferences[book])) {
359
328
  // Check if the key starts with the desired chapter
360
- if (value[version.abbreviation].startsWith(`${chapter}:`)) {
329
+ if (value[version].startsWith(`${chapter}:`)) {
361
330
  // Ensure the version exists in the value object
362
- if (value[version.abbreviation]) {
331
+ if (value[version]) {
363
332
  // Extract the verse number from the value
364
- const verse = value[version.abbreviation].split(":")[1]
333
+ const verse = value[version].split(":")[1]
365
334
  this.chapterVerses[book][chapter].push(Number(verse))
366
335
  }
367
336
  }
@@ -370,11 +339,11 @@ class CodexParser {
370
339
  return this.chapterVerses // Return the array of verses
371
340
  }
372
341
 
373
- _setVersion(passage) {
374
- this.version = passage.version ? passage.version.abbreviation : "eng"
342
+ _setVersion(book, chapter, version) {
343
+ this.version = version ? version : "eng"
375
344
 
376
345
  if (this.version !== "eng") {
377
- this._searchVersificationDifferences(passage)
346
+ this._searchVersificationDifferences(book, chapter, version)
378
347
  }
379
348
  }
380
349
 
@@ -432,8 +401,8 @@ class CodexParser {
432
401
  populate(parsedPassage) {
433
402
  const passages = []
434
403
  const { book, chapter, verses, type, to } = parsedPassage
435
-
436
- this._setVersion(parsedPassage) // Set version data if needed
404
+ const version = parsedPassage.version ? parsedPassage.version.abbreviation : "eng"
405
+ this._setVersion(book, chapter, version) // Set version data if needed
437
406
 
438
407
  if (type === "single_chapter") {
439
408
  // Handle entire chapter references
@@ -460,7 +429,6 @@ class CodexParser {
460
429
  }
461
430
  } else if (type === "multi_chapter_verse_range") {
462
431
  // Handle multi-chapter verse ranges (e.g., 3:1-5:6)
463
-
464
432
  const startChapter = chapter
465
433
  const startVerse = verses[0]
466
434
  const endChapter = to.chapter
@@ -560,60 +528,63 @@ class CodexParser {
560
528
  * @return {object} The object with the human-readable name, chapter and verses and a hash.
561
529
  */
562
530
  scripturize(passage) {
563
- // Helper to format a single chapter:verse combination
564
- const formatChapterVerse = (chapter, verseStart, verseEnd = null) => {
565
- if (!chapter) return ""
566
- if (!verseStart) return `${chapter}`
567
- return verseEnd ? `${chapter}:${verseStart}-${verseEnd}` : `${chapter}:${verseStart}`
531
+ // Helper function to format a single chapter:verse combination
532
+ const formatChapterVerse = (chapter, verses) => {
533
+ if (!chapter || !verses || verses.length === 0) return ""
534
+ if (verses.length === 1) {
535
+ return `${chapter}:${verses[0]}`
536
+ }
537
+
538
+ // Check if verses are continuous (e.g., [1, 2, 3, 4, 5] -> "1-5")
539
+ const isRange = verses.every((v, i, arr) => i === 0 || v === arr[i - 1] + 1)
540
+
541
+ if (isRange) {
542
+ return `${chapter}:${verses[0]}-${verses[verses.length - 1]}`
543
+ }
544
+
545
+ // Comma-separated (e.g., [1, 3, 5] -> "1,3,5")
546
+ return `${chapter}:${verses.join(",")}`
568
547
  }
569
548
 
570
- // Initialize combined passage
549
+ // Start constructing the passage string
571
550
  let combined = `${passage.book}`
572
551
 
573
- if (passage.type === "multi_chapter_verse_range") {
574
- // Multi-chapter verse range handling: first verse of first chapter to last verse of last chapter
575
- if (passage.to) {
576
- combined += ` ${formatChapterVerse(passage.chapter, passage.verses[0])}` // Start chapter:verse
577
- combined += `-${formatChapterVerse(
578
- passage.to.chapter,
579
- passage.to.verses[passage.to.verses.length - 1]
580
- )}` // End chapter:last verse
581
- }
552
+ if (passage.type === "multi_chapter_verse_range" && passage.to) {
553
+ // Multi-chapter verse range
554
+ combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}-${formatChapterVerse(
555
+ passage.to.chapter,
556
+ passage.to.verses
557
+ )}`
582
558
  } else if (passage.type === "chapter_verse_range") {
583
559
  // Single-chapter verse range
584
- combined += ` ${formatChapterVerse(
585
- passage.chapter,
586
- passage.verses[0],
587
- passage.verses[passage.verses.length - 1]
588
- )}`
560
+ combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
589
561
  } else if (passage.type === "comma_separated_verses") {
590
562
  // Comma-separated verses
591
- combined += ` ${passage.chapter}:${passage.verses.join(",")}`
592
- } else if (passage.type === "chapter_range") {
563
+ combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
564
+ } else if (passage.type === "chapter_range" && passage.to) {
593
565
  // Chapter range
594
- combined += ` ${passage.chapter}:${passage.verses[0]}-${passage.to.chapter}:${
595
- passage.to.verses[passage.to.verses.length - 1]
596
- }`
566
+ combined += ` ${passage.chapter}-${passage.to.chapter}`
597
567
  } else {
598
568
  // Single chapter or single verse
599
- combined += ` ${formatChapterVerse(passage.chapter, passage.verses[0])}`
569
+ combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
600
570
  }
601
571
 
602
- // Generate chapter:verse for current and "to" objects
572
+ // Generate the chapter:verse (cv) string
603
573
  const cv = passage.to
604
- ? `${formatChapterVerse(passage.chapter, passage.verses[0])}-${formatChapterVerse(
574
+ ? `${formatChapterVerse(passage.chapter, passage.verses)}-${formatChapterVerse(
605
575
  passage.to.chapter,
606
- passage.to.verses[passage.to.verses.length - 1]
576
+ passage.to.verses
607
577
  )}`
608
- : formatChapterVerse(passage.chapter, passage.verses[0])
578
+ : formatChapterVerse(passage.chapter, passage.verses)
609
579
 
610
- // Generate a hash for the passage
611
- const hash = `${passage.book.toLowerCase()}_${cv.replace(/:/g, "_").replace(/-/g, "_")}`
580
+ // Generate the hash
581
+ const hash = `${passage.book.toLowerCase()}_${cv.replace(/:/g, ".").replace(/-/g, ".")}`
612
582
 
583
+ // Return the final scripture object
613
584
  return {
614
- passage: combined, // Reconstructed passage
615
- cv: cv, // Chapter:verse range
616
- hash: hash, // Unique hash
585
+ passage: combined,
586
+ cv: cv,
587
+ hash: hash,
617
588
  }
618
589
  }
619
590
 
@@ -627,29 +598,96 @@ class CodexParser {
627
598
  * @return {object} The combined passage object.
628
599
  */
629
600
  combine(passages) {
630
- // Only check if passages are from the same book)
631
- const sameBook = [...new Set(passages.map((p) => p.book))]
632
- if (sameBook.length > 1) {
633
- throw new Error("Passages are not from the same book.")
601
+ if (!passages || passages.length === 0) {
602
+ throw new Error("No passages provided to combine.")
603
+ }
604
+
605
+ // Ensure all passages are from the same book
606
+ const uniqueBooks = [...new Set(passages.map((p) => p.book))]
607
+ if (uniqueBooks.length > 1) {
608
+ throw new Error("Passages must be from the same book to combine.")
634
609
  }
635
610
 
636
- const newPassages = []
611
+ // Initialize combined passage object
612
+ const combined = {
613
+ ...passages[0],
614
+ verses: [],
615
+ passages: [],
616
+ to: null,
617
+ type: null,
618
+ }
637
619
 
638
- passages.forEach((passageSet) => {
639
- passageSet.passages.forEach((passage) => {
640
- if (passage.versification) {
641
- newPassages.push(passage.book + " " + passage.versification[this.version])
642
- } else {
643
- newPassages.push(passage.book + " " + passage.chapter + ":" + passage.verse)
620
+ const chapterVerses = {}
621
+ let firstChapter = null
622
+ let lastChapter = null
623
+
624
+ // Collect all verses grouped by chapter
625
+ passages.forEach((passage) => {
626
+ passage.passages.forEach((p) => {
627
+ if (!chapterVerses[p.chapter]) {
628
+ chapterVerses[p.chapter] = new Set()
644
629
  }
630
+ chapterVerses[p.chapter].add(p.verse)
631
+ combined.passages.push(p) // Add individual passage
645
632
  })
633
+
634
+ // Track first and last chapters
635
+ const chapters = passage.passages.map((p) => p.chapter)
636
+ if (!firstChapter || Math.min(...chapters) < firstChapter) {
637
+ firstChapter = Math.min(...chapters)
638
+ }
639
+ if (!lastChapter || Math.max(...chapters) > lastChapter) {
640
+ lastChapter = Math.max(...chapters)
641
+ }
646
642
  })
647
643
 
648
- const noDuplicates2 = [...new Set(newPassages)]
644
+ // Ensure unique and sorted passages
645
+ combined.passages = Array.from(new Set(combined.passages.map(JSON.stringify))).map(JSON.parse)
649
646
 
650
- const parsed = this.parse(noDuplicates2.join(" // ")).getPassages()
647
+ // Process chapter and verse data
648
+ const sortedChapters = Object.keys(chapterVerses)
649
+ .map(Number)
650
+ .sort((a, b) => a - b)
651
+
652
+ const originalParts = []
653
+ sortedChapters.forEach((chapter, index) => {
654
+ const verses = Array.from(chapterVerses[chapter]).sort((a, b) => a - b)
655
+ if (chapter === firstChapter) {
656
+ combined.verses = verses // First chapter's verses
657
+ }
658
+ if (chapter === lastChapter) {
659
+ combined.to = {
660
+ book: combined.book,
661
+ chapter,
662
+ verses,
663
+ }
664
+ }
665
+ originalParts.push(`${chapter}:${verses.join(",")}`)
666
+ })
651
667
 
652
- return this.join(parsed)
668
+ // Ensure `to` is properly set for multi-chapter ranges
669
+ if (firstChapter !== lastChapter) {
670
+ const lastChapterVerses = Array.from(chapterVerses[lastChapter]).sort((a, b) => a - b)
671
+ combined.to = {
672
+ book: combined.book,
673
+ chapter: lastChapter,
674
+ verses: lastChapterVerses,
675
+ }
676
+ }
677
+
678
+ // Determine the passage type
679
+ if (firstChapter !== lastChapter) {
680
+ combined.type = "multi_chapter_verse_range"
681
+ } else if (combined.verses.length > 1) {
682
+ combined.type = "chapter_verse_range"
683
+ } else {
684
+ combined.type = "chapter_verse"
685
+ }
686
+
687
+ // Build the `original` field
688
+ combined.original = `${combined.book} ${originalParts.join("-")}`
689
+
690
+ return this.parse(combined.original).getPassages().first()
653
691
  }
654
692
 
655
693
  /**
@@ -660,86 +698,100 @@ class CodexParser {
660
698
  * @return {object} The combined passage object.
661
699
  */
662
700
  join(passages) {
663
- const newObject = { ...passages[0] } // Start with the first passage
664
-
665
- const chapters = {} // Store verses by chapters
666
- const uniquePassages = new Set() // Track unique passages to prevent duplicates
667
- // Add initial passages to the unique set to avoid duplication
668
- newObject.passages.forEach((p) => {
669
- const passageKey = `${p.book}-${p.chapter}-${p.verse}`
670
- uniquePassages.add(passageKey)
671
- })
701
+ if (!passages || passages.length === 0) {
702
+ throw new Error("No passages provided to join.")
703
+ }
672
704
 
673
- // Iterate through all the passages and group verses by chapter
674
- passages.forEach((passage) => {
675
- if (!chapters[passage.chapter]) {
676
- chapters[passage.chapter] = new Set() // Use Set to avoid duplicates
677
- }
705
+ // Ensure all passages are from the same book
706
+ const uniqueBooks = [...new Set(passages.map((p) => p.book))]
707
+ if (uniqueBooks.length > 1) {
708
+ throw new Error("Passages must be from the same book to join.")
709
+ }
678
710
 
679
- // Add verses to their corresponding chapter
680
- passage.passages.forEach((p) => {
681
- chapters[p.chapter].add(p.verse)
711
+ // Start with the base object
712
+ const combined = {
713
+ ...passages[0],
714
+ verses: [],
715
+ passages: [],
716
+ to: null,
717
+ scripture: {},
718
+ type: null,
719
+ }
682
720
 
683
- // Create a unique key for each passage (book-chapter-verse)
684
- const passageKey = `${p.book}-${p.chapter}-${p.verse}`
721
+ const chapterVerses = {}
722
+ let firstChapter = null
723
+ let lastChapter = null
685
724
 
686
- // Add to the passages array if it hasn't been added yet
687
- if (!uniquePassages.has(passageKey)) {
688
- newObject.passages.push(p) // Add the passage
689
- uniquePassages.add(passageKey) // Mark it as added
725
+ // Collect all verses and passages, grouped by chapter
726
+ passages.forEach((passage) => {
727
+ passage.passages.forEach((p) => {
728
+ if (!chapterVerses[p.chapter]) {
729
+ chapterVerses[p.chapter] = new Set()
690
730
  }
731
+ chapterVerses[p.chapter].add(p.verse)
732
+ combined.passages.push(p) // Add individual passage
691
733
  })
692
- })
693
734
 
694
- // Sort the newObject.passages array by chapter first, then by verse
695
- newObject.passages.sort((a, b) => {
696
- if (a.chapter !== b.chapter) {
697
- return a.chapter - b.chapter // Sort by chapter
735
+ // Track first and last chapters
736
+ const chapters = passage.passages.map((p) => p.chapter)
737
+ if (!firstChapter || Math.min(...chapters) < firstChapter) {
738
+ firstChapter = Math.min(...chapters)
739
+ }
740
+ if (!lastChapter || Math.max(...chapters) > lastChapter) {
741
+ lastChapter = Math.max(...chapters)
698
742
  }
699
- return a.verse - b.verse // Sort by verse within the same chapter
700
743
  })
701
744
 
702
- // Prepare to build the final result
745
+ // Ensure unique and sorted passages
746
+ combined.passages = Array.from(new Set(combined.passages.map(JSON.stringify))).map(JSON.parse)
747
+
748
+ // Process chapter and verse data
703
749
  const chapterStrings = []
704
- let firstChapter = null
705
- let lastChapter = null
750
+ const sortedChapters = Object.keys(chapterVerses)
751
+ .map(Number)
752
+ .sort((a, b) => a - b)
706
753
 
707
- for (const chapter in chapters) {
708
- const verses = Array.from(chapters[chapter]).sort((a, b) => a - b)
709
- const mergedVerses = this.mergeRanges(verses) // Merge adjacent verses into ranges
754
+ sortedChapters.forEach((chapter) => {
755
+ const verses = Array.from(chapterVerses[chapter]).sort((a, b) => a - b)
756
+ const mergedVerses = this.mergeRanges(verses)
710
757
  chapterStrings.push(`${chapter}:${mergedVerses.join(",")}`)
711
-
712
- // Track the first and last chapters for the 'to' key
713
- if (!firstChapter) firstChapter = Number(chapter) // Ensure chapter is a number
714
- lastChapter = Number(chapter) // Always update to the current chapter as a number
715
-
716
- // Update the newObject.verses with the merged ranges for the current chapter
717
- if (Number(chapter) === firstChapter) {
718
- newObject.verses = mergedVerses
758
+ if (chapter === firstChapter) {
759
+ combined.verses = mergedVerses // First chapter's verses
719
760
  }
720
- }
721
-
722
- // Build the final combined object with `to` key for multi-chapter passages
723
- newObject.original = `${newObject.book} ${firstChapter}:${newObject.verses.join(",")}`
761
+ })
724
762
 
763
+ // Handle multi-chapter ranges
725
764
  if (firstChapter !== lastChapter) {
726
- newObject.to = {
727
- book: newObject.book,
765
+ combined.type = "multi_chapter_verse_range"
766
+ combined.to = {
767
+ book: combined.book,
728
768
  chapter: lastChapter,
729
- verses: this.mergeRanges(Array.from(chapters[lastChapter])), // Ensure merged range
769
+ verses: this.mergeRanges(Array.from(chapterVerses[lastChapter])),
770
+ }
771
+ combined.original = `${combined.book} ${firstChapter}:${combined.verses.join(
772
+ ","
773
+ )}-${lastChapter}:${combined.to.verses.join(",")}`
774
+ } else {
775
+ // Single-chapter range or comma-separated
776
+ if (combined.verses.length > 1) {
777
+ combined.type = "chapter_verse_range"
778
+ } else {
779
+ combined.type = "chapter_verse"
730
780
  }
781
+ combined.original = `${combined.book} ${firstChapter}:${combined.verses.join(",")}`
731
782
  }
732
783
 
733
- // Build the scripture string with combined chapters (without spaces after commas)
734
- const chapterString = chapterStrings.join(",") // No space after comma
735
- newObject.scripture = {
736
- passage: `${newObject.book} ${chapterString}`,
784
+ // Build the scripture property
785
+ const chapterString = chapterStrings.join(",")
786
+ combined.scripture = {
787
+ passage: `${combined.book} ${chapterString}`,
737
788
  cv: chapterString,
738
- hash: `${newObject.book.toLowerCase()}_${chapterString.replace(/:/g, ".").replace(/,/g, ".")}`,
789
+ hash: `${combined.book.toLowerCase()}_${chapterString.replace(/:/g, ".").replace(/,/g, ".")}`,
739
790
  }
740
791
 
741
- return newObject
792
+ return combined
742
793
  }
794
+
743
795
  mergeRanges(verses) {
744
796
  const sortedVerses = [...new Set(verses)].sort((a, b) => a - b)
745
797
  const merged = []
@@ -750,11 +802,9 @@ class CodexParser {
750
802
  if (sortedVerses[i] === end + 1) {
751
803
  end = sortedVerses[i]
752
804
  } else {
753
- // Push the current range if it's more than 2 consecutive numbers, otherwise separate by commas
805
+ // Push range or single verse
754
806
  if (start === end) {
755
807
  merged.push(`${start}`)
756
- } else if (end === start + 1) {
757
- merged.push(`${start},${end}`)
758
808
  } else {
759
809
  merged.push(`${start}-${end}`)
760
810
  }
@@ -763,11 +813,9 @@ class CodexParser {
763
813
  }
764
814
  }
765
815
 
766
- // Push the final range or pair
816
+ // Push the final range or single verse
767
817
  if (start === end) {
768
818
  merged.push(`${start}`)
769
- } else if (end === start + 1) {
770
- merged.push(`${start},${end}`)
771
819
  } else {
772
820
  merged.push(`${start}-${end}`)
773
821
  }
@@ -849,6 +897,35 @@ class CodexParser {
849
897
  }
850
898
  }
851
899
  }
900
+ for (let i = 0; i < passage.verses.length; i++) {
901
+ const passageVerses = String(passage.verses[i])
902
+ let verses = passageVerses.split("-").map(Number)
903
+
904
+ if (verses.length === 2) {
905
+ // Expand the range if there are two numbers
906
+ verses = Array.from({ length: verses[1] - verses[0] + 1 }, (_, index) => verses[0] + index)
907
+ }
908
+
909
+ // The verses array now contains all values between the range or remains as a single value
910
+ for (const verse of verses) {
911
+ // Check if the verse exists in the chapterVerses structure
912
+ const isValidVerse =
913
+ this.chapterVerses[passage.book] &&
914
+ this.chapterVerses[passage.book][passage.chapter] &&
915
+ this.chapterVerses[passage.book][passage.chapter].includes(verse)
916
+
917
+ if (!isValidVerse) {
918
+ return {
919
+ error: true,
920
+ code: 104,
921
+ message: {
922
+ verse_exists: false,
923
+ content: `Verse number ${verse} does not exist in ${passage.book} ${passage.chapter}`,
924
+ },
925
+ }
926
+ }
927
+ }
928
+ }
852
929
  return true
853
930
  }
854
931
  _handleVersion(version, testament) {