codexparser 0.1.41 → 0.1.42

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 +241 -240
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexparser",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
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,115 +65,86 @@ 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
150
 
@@ -182,21 +153,19 @@ class CodexParser {
182
153
  const [start, end] = ref.split("-")
183
154
  const startParts = start.split(":")
184
155
  const endParts = end.split(":")
185
-
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
+ type =
157
+ startParts[0].trim() !== endParts[0].trim()
158
+ ? "multi_chapter_verse_range"
159
+ : "chapter_verse_range"
191
160
  } else if (ref.includes(",")) {
192
- type = "comma_separated_verses" // Example: "8:23,24"
161
+ type = "comma_separated_verses"
193
162
  } else {
194
- type = "chapter_verse" // Example: "8:23"
163
+ type = "chapter_verse"
195
164
  }
196
165
  } else if (ref.includes("-")) {
197
- type = "chapter_range" // Example: "8-9"
166
+ type = "chapter_range"
198
167
  } else {
199
- type = "single_chapter" // Example: "8"
168
+ type = "single_chapter"
200
169
  }
201
170
 
202
171
  this.found.push({
@@ -208,11 +177,11 @@ class CodexParser {
208
177
  })
209
178
  })
210
179
  } else {
211
- i++ // Move to the next character if no book is found
180
+ i++
212
181
  }
213
182
  }
214
183
 
215
- return this // Return the current instance for method chaining
184
+ return this
216
185
  }
217
186
 
218
187
  bibleVersion(version) {
@@ -232,93 +201,143 @@ class CodexParser {
232
201
  * @returns {object} An object with the parsed passage.
233
202
  */
234
203
  parse(reference) {
235
- this.scan(reference)
204
+ this.scan(reference) // Call scan to populate this.found
236
205
 
237
206
  this.passages = this.found.map((passage) => {
238
207
  const book = this.bookify(passage.book)
239
- const testament = this.bible.old.includes(book) ? "old" : "new"
240
-
208
+ const testament = this.bible.old.find((bible) => bible === book) ? "old" : "new"
209
+ // Initialize the parsed passage object
241
210
  const parsedPassage = {
242
- original: `${passage.book} ${passage.reference}`,
243
- book,
211
+ original: passage.book + " " + passage.reference,
212
+ book: book,
244
213
  chapter: null,
245
- verses: [],
246
- type: passage.type,
247
- testament,
214
+ verses: [], // Verse stored as an array
215
+ type: passage.type, // Set type based on reference
216
+ testament: testament,
248
217
  index: passage.index,
249
218
  version: this._handleVersion(passage.version, testament),
250
219
  }
251
- const parts = passage.reference.split(",")
252
- const isSingleChapter = this.singleChapterBook.some((singleChapterBook) => singleChapterBook[book])
220
+
221
+ // Split reference by commas to handle multiple ranges or verses (e.g., "Ge 27:27-29,39-41")
222
+ let parts = passage.reference.split(",")
223
+
224
+ // Check for single chapter books
225
+ const singleChapterBook = this.singleChapterBook.find((bible) => bible[book])
253
226
 
254
227
  parts.forEach((part) => {
255
- part = part.trim()
228
+ part = part.trim() // Clean up spaces
229
+ // Detect whether it uses ":" or "." for chapter:verse separation
256
230
  const separator = part.includes(":") ? ":" : "."
257
231
 
258
232
  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) {
233
+ // Handle ranges (e.g., "27:27-29" or "39-41")
234
+
235
+ let [start, end] = part.split("-")
236
+ // Handle the starting part
237
+ let [startChapter, startVerse] = start.includes(separator)
238
+ ? start.split(separator)
239
+ : [parsedPassage.chapter, start] // Default to same chapter if no chapter is provided
240
+
241
+ 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.
244
+ if (start.includes(separator) && end.includes(separator)) {
245
+ parsedPassage.verses = this.chapterVerses[book][startChapter].slice(
246
+ this.chapterVerses[book][startChapter].indexOf(Number(startVerse))
247
+ )
248
+ }
249
+
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
+ if (Number(endChapter) !== Number(startChapter)) {
254
+ // Cross-chapter range, set 'to' property
255
+ parsedPassage.to = {
256
+ 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
+ )
264
+ }
265
+ parsedPassage.type =
266
+ endChapter !== startChapter ? "multi_chapter_verse_range" : "chapter_verse_range" // Set type to chapter range
267
+ } else {
268
+ // Same-chapter range, just add to the verse array
269
+ parsedPassage.verses.push(`${startVerse}-${endVerse}`)
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]
272
278
  parsedPassage.to = {
273
- book,
274
- chapter: endChapter,
275
- verses: [endVerse],
279
+ book: book,
280
+ chapter: Number(end),
281
+ verses: this.chapterVerses[book][end],
276
282
  }
277
- parsedPassage.verses.push(startVerse)
278
283
  } else {
279
- parsedPassage.verses.push(...this._generateRange(startVerse, endVerse))
284
+ //
285
+ parsedPassage.verses.push(`${startVerse}-${end}`)
280
286
  }
281
287
  } else {
282
- const [start, end] = part.split("-")
283
- parsedPassage.chapter = Number(start)
284
- parsedPassage.to = {
285
- book,
286
- chapter: Number(end),
287
- verses: [],
288
- }
288
+ parsedPassage.chapter = 1
289
+ parsedPassage.verses.push(`${startVerse}-${end}`)
289
290
  }
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
291
  }
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
292
  } else {
302
- if (!isSingleChapter) {
303
- const number = Number(part)
304
- if (!parsedPassage.verses.length) {
305
- parsedPassage.chapter = number
293
+ // Handle individual chapter:verse references (e.g., "27:27")
294
+
295
+ let [chapterPart, versePart] = part.includes(separator)
296
+ ? part.split(separator)
297
+ : [parsedPassage.chapter, part]
298
+ 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
306
305
  }
307
- parsedPassage.verses.push(Number(part))
308
306
  } else {
309
- parsedPassage.chapter = 1
310
- parsedPassage.verses.push(Number(part))
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
+ 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)
318
+ } 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"
327
+ }
328
+ }
311
329
  }
312
330
  }
331
+ parsedPassage.passages = this.populate(parsedPassage)
332
+ parsedPassage.scripture = this.scripturize(parsedPassage)
313
333
  })
314
334
 
315
- parsedPassage.passages = this.populate(parsedPassage)
316
- parsedPassage.scripture = this.scripturize(parsedPassage)
317
335
  parsedPassage.valid = this._isValid(parsedPassage, passage.reference)
336
+
318
337
  return parsedPassage
319
338
  })
320
339
  this.versification()
321
- return this
340
+ return this // Return this instance
322
341
  }
323
342
  /**
324
343
  * Generates an array of numbers representing a range from start to end, inclusive.
@@ -333,6 +352,7 @@ class CodexParser {
333
352
 
334
353
  _searchVersificationDifferences(passage) {
335
354
  const { book, chapter, version } = passage
355
+ if (!this.chapterVerses[book][chapter]) return
336
356
 
337
357
  // Loop through each key-value pair in the dictionary
338
358
  for (const [key, value] of Object.entries(this.versificationDifferences[book])) {
@@ -361,7 +381,6 @@ class CodexParser {
361
381
  versification() {
362
382
  this.passages.forEach((passage) => {
363
383
  const hasVersification = this.versificationDifferences[passage.book]
364
-
365
384
  passage.passages.forEach((subPassage) => {
366
385
  // Apply general versification differences
367
386
  if (hasVersification) {
@@ -412,79 +431,57 @@ class CodexParser {
412
431
  */
413
432
  populate(parsedPassage) {
414
433
  const passages = []
415
- const { book, chapter, verses, type } = parsedPassage
416
- this._setVersion(parsedPassage)
434
+ const { book, chapter, verses, type, to } = parsedPassage
435
+
436
+ this._setVersion(parsedPassage) // Set version data if needed
437
+
417
438
  if (type === "single_chapter") {
418
- // Handle single chapter references
439
+ // Handle entire chapter references
419
440
  if (this.chapterVerses[book] && this.chapterVerses[book][chapter]) {
420
441
  this.chapterVerses[book][chapter].forEach((verse) => {
421
442
  passages.push({ book, chapter: Number(chapter), verse: Number(verse) })
422
443
  })
423
444
  }
424
445
  } else if (type === "comma_separated_verses") {
425
- // Handle only the explicitly mentioned verses
426
- if (verses && this.chapterVerses[book] && this.chapterVerses[book][chapter]) {
446
+ // Handle explicitly mentioned verses (e.g., 3:1,3,6)
447
+ if (this.chapterVerses[book] && this.chapterVerses[book][chapter]) {
427
448
  verses.forEach((verse) => {
428
449
  passages.push({ book, chapter: Number(chapter), verse: Number(verse) })
429
450
  })
430
451
  }
431
452
  } 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,
453
+ // Handle ranges of chapters (e.g., 3-5)
454
+ for (let currentChapter = chapter; currentChapter <= to.chapter; currentChapter++) {
455
+ if (this.chapterVerses[book] && this.chapterVerses[book][currentChapter]) {
456
+ this.chapterVerses[book][currentChapter].forEach((verse) => {
457
+ passages.push({ book, chapter: Number(currentChapter), verse: Number(verse) })
441
458
  })
442
459
  }
443
460
  }
444
461
  } 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
- })
462
+ // Handle multi-chapter verse ranges (e.g., 3:1-5:6)
463
+
464
+ const startChapter = chapter
465
+ const startVerse = verses[0]
466
+ const endChapter = to.chapter
467
+ const endVerse = to.verses[to.verses.length - 1]
468
+
469
+ for (let currentChapter = startChapter; currentChapter <= endChapter; currentChapter++) {
470
+ const chapterVerses = this.chapterVerses[book][currentChapter]
471
+ if (!chapterVerses) continue
472
+
473
+ // Determine start and end verses for each chapter
474
+ const chapterStartVerse = currentChapter === startChapter ? startVerse : 1
475
+ const chapterEndVerse =
476
+ currentChapter === endChapter ? endVerse : chapterVerses[chapterVerses.length - 1]
477
+
478
+ for (let verse = chapterStartVerse; verse <= chapterEndVerse; verse++) {
479
+ passages.push({ book, chapter: currentChapter, verse })
483
480
  }
484
481
  }
485
482
  } 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]) {
483
+ // Handle single chapter:verse or chapter:verse ranges (e.g., 3:1 or 3:1-5)
484
+ if (this.chapterVerses[book] && this.chapterVerses[book][chapter]) {
488
485
  verses.forEach((verse) => {
489
486
  if (typeof verse === "string" && verse.includes("-")) {
490
487
  const [start, end] = verse.split("-").map(Number)
@@ -496,6 +493,12 @@ class CodexParser {
496
493
  }
497
494
  })
498
495
  }
496
+ } else if (type === "single_chapter_book_verse_range") {
497
+ // Handle ranges in single-chapter books (e.g., Jude 5-7)
498
+ const [startVerse, endVerse] = verses[0].split("-").map(Number)
499
+ for (let i = startVerse; i <= endVerse; i++) {
500
+ passages.push({ book, chapter: 1, verse: i })
501
+ }
499
502
  }
500
503
 
501
504
  return passages
@@ -557,64 +560,60 @@ class CodexParser {
557
560
  * @return {object} The object with the human-readable name, chapter and verses and a hash.
558
561
  */
559
562
  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)
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}`
568
+ }
601
569
 
602
- formattedVerses = ranges
603
- .map((range) => (range.length > 1 ? `${range[0]}-${range[range.length - 1]}` : range[0]))
604
- .join(",")
570
+ // Initialize combined passage
571
+ let combined = `${passage.book}`
572
+
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
605
581
  }
606
-
607
- formattedVerses = `${chapter}:${formattedVerses}`
582
+ } else if (passage.type === "chapter_verse_range") {
583
+ // Single-chapter verse range
584
+ combined += ` ${formatChapterVerse(
585
+ passage.chapter,
586
+ passage.verses[0],
587
+ passage.verses[passage.verses.length - 1]
588
+ )}`
589
+ } else if (passage.type === "comma_separated_verses") {
590
+ // Comma-separated verses
591
+ combined += ` ${passage.chapter}:${passage.verses.join(",")}`
592
+ } else if (passage.type === "chapter_range") {
593
+ // Chapter range
594
+ combined += ` ${passage.chapter}:${passage.verses[0]}-${passage.to.chapter}:${
595
+ passage.to.verses[passage.to.verses.length - 1]
596
+ }`
597
+ } else {
598
+ // Single chapter or single verse
599
+ combined += ` ${formatChapterVerse(passage.chapter, passage.verses[0])}`
608
600
  }
609
601
 
610
- // Format the final passage
611
- const full = `${book} ${formattedVerses}`.trim()
612
- const hash = full.toLowerCase().replace(/ /g, "_").replace(/:/g, ".").replace(/-/g, ".").replace(/,/g, ".")
602
+ // Generate chapter:verse for current and "to" objects
603
+ const cv = passage.to
604
+ ? `${formatChapterVerse(passage.chapter, passage.verses[0])}-${formatChapterVerse(
605
+ passage.to.chapter,
606
+ passage.to.verses[passage.to.verses.length - 1]
607
+ )}`
608
+ : formatChapterVerse(passage.chapter, passage.verses[0])
609
+
610
+ // Generate a hash for the passage
611
+ const hash = `${passage.book.toLowerCase()}_${cv.replace(/:/g, "_").replace(/-/g, "_")}`
613
612
 
614
613
  return {
615
- passage: full,
616
- cv: formattedVerses,
617
- hash,
614
+ passage: combined, // Reconstructed passage
615
+ cv: cv, // Chapter:verse range
616
+ hash: hash, // Unique hash
618
617
  }
619
618
  }
620
619
 
@@ -628,13 +627,14 @@ class CodexParser {
628
627
  * @return {object} The combined passage object.
629
628
  */
630
629
  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) {
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) {
634
633
  throw new Error("Passages are not from the same book.")
635
634
  }
636
635
 
637
636
  const newPassages = []
637
+
638
638
  passages.forEach((passageSet) => {
639
639
  passageSet.passages.forEach((passage) => {
640
640
  if (passage.versification) {
@@ -646,7 +646,9 @@ class CodexParser {
646
646
  })
647
647
 
648
648
  const noDuplicates2 = [...new Set(newPassages)]
649
+
649
650
  const parsed = this.parse(noDuplicates2.join(" // ")).getPassages()
651
+
650
652
  return this.join(parsed)
651
653
  }
652
654
 
@@ -662,7 +664,6 @@ class CodexParser {
662
664
 
663
665
  const chapters = {} // Store verses by chapters
664
666
  const uniquePassages = new Set() // Track unique passages to prevent duplicates
665
-
666
667
  // Add initial passages to the unique set to avoid duplication
667
668
  newObject.passages.forEach((p) => {
668
669
  const passageKey = `${p.book}-${p.chapter}-${p.verse}`