codexparser 0.1.79 → 0.1.81

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.
@@ -0,0 +1,2 @@
1
+ # Prettier friendly markdownlint config (all formatting rules disabled)
2
+ extends: markdownlint/style/prettier
@@ -0,0 +1,32 @@
1
+ # This file controls the behavior of Trunk: https://docs.trunk.io/cli
2
+ # To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
3
+ version: 0.1
4
+ cli:
5
+ version: 1.24.0
6
+ # Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
7
+ plugins:
8
+ sources:
9
+ - id: trunk
10
+ ref: v1.7.0
11
+ uri: https://github.com/trunk-io/plugins
12
+ # Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
13
+ runtimes:
14
+ enabled:
15
+ - node@22.16.0
16
+ - python@3.10.8
17
+ # This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
18
+ lint:
19
+ enabled:
20
+ - checkov@3.2.445
21
+ - git-diff-check
22
+ - markdownlint@0.45.0
23
+ - osv-scanner@2.0.3
24
+ - prettier@3.6.0
25
+ - trufflehog@3.89.2
26
+ actions:
27
+ disabled:
28
+ - trunk-announce
29
+ - trunk-check-pre-push
30
+ - trunk-fmt-pre-commit
31
+ enabled:
32
+ - trunk-upgrade-available
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexparser",
3
- "version": "0.1.79",
3
+ "version": "0.1.81",
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": {
@@ -23,7 +23,7 @@ class CodexParser {
23
23
  /**
24
24
  * Initializes the parser with default properties and data.
25
25
  */
26
- constructor() {
26
+ constructor(config = {}) {
27
27
  this.found = [] // Array to store detected references
28
28
  this.passages = [] // Array to store parsed passages
29
29
  this.bible = bible // Bible data (Old/New Testament books)
@@ -53,9 +53,12 @@ class CodexParser {
53
53
  this.CHAPTER_RANGE = "chapter_range"
54
54
  this.MULTI_CHAPTER_RANGE = "multi_chapter_verse_range"
55
55
  this.config = {
56
- booksOnly: false,
56
+ booksOnly: config.booksOnly ?? false,
57
+ invalid_sequence_strategy: config.invalid_sequence_strategy ?? "include",
58
+ invalid_passage_strategy: config.invalid_passage_strategy ?? "include",
57
59
  }
58
60
  }
61
+
59
62
  /**
60
63
  * Sets configuration options for the parser.
61
64
  * @param {Object} config - Configuration options.
@@ -65,6 +68,8 @@ class CodexParser {
65
68
  options(config) {
66
69
  this.config = {
67
70
  booksOnly: config.booksOnly ?? false,
71
+ invalid_sequence_strategy: config.invalid_sequence_strategy ?? this.config.invalid_sequence_strategy,
72
+ invalid_passage_strategy: config.invalid_passage_strategy ?? this.config.invalid_passage_strategy,
68
73
  }
69
74
  return this
70
75
  }
@@ -87,159 +92,213 @@ class CodexParser {
87
92
  */
88
93
  scan(text) {
89
94
  const fullNames = [...this.bible.old, ...this.bible.new]
90
- const abbreviations = Object.keys(this.abbreviations).filter((abbr) => abbr.length >= 3)
95
+ const abbreviations = Object.keys(this.abbreviations)
91
96
  this.found = []
92
-
93
- // Normalize text: remove curly quotes, replace periods before numbers with colons
94
- let normalizedText = text.replace(/[“”]/g, "").replace(/\.(?=\d)/g, ":")
97
+ // Normalize text for parsing but keep original for originalText
98
+ let normalizedText = text
99
+ .replace(/[“”]/g, "") // Remove curly quotes
100
+ .replace(/\.(?=\d)/g, ":") // Replace periods before digits with colons (e.g., "Re13.8" -> "Re13:8")
101
+ .replace(/\s+/g, " ") // Normalize multiple spaces to single
102
+ const lowercaseBibleFullNames = fullNames.map((book) => book.toLowerCase())
103
+ const lowercaseBibleAbbreviations = abbreviations.map((abbr) => abbr.toLowerCase())
95
104
  const lowerCaseText = normalizedText.toLowerCase()
96
105
  let i = 0
97
106
 
107
+ console.log("[Scan] Input text:", text)
108
+ console.log("[Scan] Normalized text:", normalizedText)
109
+
110
+ const isValidChapterVerseChar = (char) => /[^A-Za-z]/.test(char) // Non-letter characters
111
+ const isNextBibleBook = (startIndex) => {
112
+ const textAfterCurrentPosition = lowerCaseText.substring(startIndex).trim()
113
+ // Check if the text starts with a book name or abbreviation followed by a digit
114
+ return (
115
+ lowercaseBibleFullNames.some((book) => {
116
+ if (textAfterCurrentPosition.startsWith(book)) {
117
+ const nextIndex = startIndex + book.length
118
+ const nextChar = lowerCaseText[nextIndex]
119
+ return nextChar && /\d/.test(nextChar)
120
+ }
121
+ return false
122
+ }) ||
123
+ lowercaseBibleAbbreviations.some((abbr) => {
124
+ if (textAfterCurrentPosition.startsWith(abbr)) {
125
+ const nextIndex = startIndex + abbr.length
126
+ const nextChar = lowerCaseText[nextIndex]
127
+ return nextChar && (/\d/.test(nextChar) || /\./.test(nextChar))
128
+ }
129
+ return false
130
+ })
131
+ )
132
+ }
133
+ const detectSuffix = (startIndex, inputText) => {
134
+ const suffixMatch = inputText.substring(startIndex).match(/\b(LXX|MT)\b/i)
135
+ return suffixMatch ? { version: suffixMatch[0].toUpperCase(), length: suffixMatch[0].length } : null
136
+ }
137
+
98
138
  while (i < lowerCaseText.length) {
99
139
  let foundBook = null
100
- let startIndex = i
101
140
  let matchedLength = 0
141
+ let originalBookText = ""
142
+ let startIndex = i
102
143
 
103
- // Check for book names or abbreviations
104
- for (let book of fullNames) {
144
+ // Check full book names
145
+ for (let j = 0; j < lowercaseBibleFullNames.length; j++) {
146
+ const book = lowercaseBibleFullNames[j]
105
147
  if (
106
- lowerCaseText.startsWith(book.toLowerCase(), i) &&
107
- (i + book.length >= lowerCaseText.length || /[\s:;]/.test(lowerCaseText[i + book.length]))
148
+ lowerCaseText.startsWith(book, i) &&
149
+ (i + book.length >= lowerCaseText.length || /\d/.test(lowerCaseText[i + book.length]))
108
150
  ) {
109
- foundBook = book
151
+ foundBook = fullNames[j]
110
152
  matchedLength = book.length
111
- break
153
+ originalBookText = text.slice(i, i + book.length)
154
+ console.log(`[Scan] Matched full book name: "${foundBook}" at index ${i}`)
112
155
  }
113
156
  }
157
+
158
+ // Check abbreviations
114
159
  if (!foundBook) {
115
- for (let abbr of abbreviations) {
116
- if (
117
- lowerCaseText.startsWith(abbr.toLowerCase(), i) &&
118
- (i + abbr.length >= lowerCaseText.length || /[\s:;]/.test(lowerCaseText[i + abbr.length]))
119
- ) {
120
- foundBook = this.abbreviations[abbr]
121
- matchedLength = abbr.length
122
- break
160
+ for (let k = 0; k < lowercaseBibleAbbreviations.length; k++) {
161
+ const abbreviation = lowercaseBibleAbbreviations[k]
162
+ const abbrPattern = abbreviation.replace(/\./g, "\\.?")
163
+ const regex = new RegExp(`^${abbrPattern}(\\.?\\s*\\d)`, "i")
164
+ const match = lowerCaseText.slice(i).match(regex)
165
+ if (match) {
166
+ foundBook = this.abbreviations[abbreviations[k]]
167
+ matchedLength = match[0].length - match[1].length // Exclude chapter-verse part
168
+ originalBookText = text.slice(i, i + matchedLength)
169
+ console.log(
170
+ `[Scan] Matched abbreviation: "${abbreviations[k]}" -> "${foundBook}" at index ${i}`
171
+ )
123
172
  }
124
173
  }
125
174
  }
126
175
 
127
176
  if (foundBook) {
128
- let j = i + matchedLength
129
- let currentBook = foundBook
130
- let currentStartIndex = startIndex
131
-
132
- // Process multiple references for the same book
133
- while (j < lowerCaseText.length) {
134
- let chapterVerse = ""
135
- let hasColon = false
136
- let version = null
137
- let refStart = j
138
-
139
- // Skip spaces
140
- while (j < lowerCaseText.length && /\s/.test(lowerCaseText[j])) {
141
- chapterVerse += normalizedText[j]
142
- j++
177
+ i += matchedLength
178
+ let chapterVerse = ""
179
+ let originalChapterVerseText = ""
180
+ const references = []
181
+
182
+ // Capture chapter-verse until a letter (potential new book) or semicolon
183
+ while (i < normalizedText.length && isValidChapterVerseChar(normalizedText[i])) {
184
+ if (isNextBibleBook(i)) {
185
+ console.log(`[Scan] Detected next book at index ${i}, breaking`)
186
+ break
143
187
  }
144
- refStart = j // Update start after spaces
145
-
146
- // Next character must be a digit or version suffix
147
- if (j < lowerCaseText.length) {
148
- const nextChar = lowerCaseText[j]
149
- const isVersion = lowerCaseText.substring(j).match(/^(lxx|mt)\b/i)
150
- if (!/\d/.test(nextChar) && !isVersion && !this.config.booksOnly) {
151
- break
188
+ if (normalizedText[i] === ";") {
189
+ const formattedReference = chapterVerse.trim().replace(/[^a-zA-Z0-9:,\-]+$/g, "")
190
+ if (formattedReference) {
191
+ // Find the last digit in the reference
192
+ const lastDigitMatch = formattedReference.match(/\d(?=[^0-9]*$)/)
193
+ let endIndex = i - 1 // Default to position before semicolon
194
+ if (lastDigitMatch) {
195
+ const lastDigitIndex = formattedReference.lastIndexOf(lastDigitMatch[0])
196
+ endIndex = startIndex + matchedLength + lastDigitIndex
197
+ }
198
+ references.push({
199
+ reference: formattedReference,
200
+ originalText: (originalBookText + originalChapterVerseText).trim(),
201
+ startIndex,
202
+ endIndex,
203
+ })
152
204
  }
153
- } else if (!this.config.booksOnly) {
154
- break
205
+ chapterVerse = ""
206
+ originalChapterVerseText = ""
207
+ originalBookText = foundBook // Reuse book for semicolon-separated references
208
+ startIndex = i + 1 // Start of next reference
209
+ i++
210
+ continue
155
211
  }
212
+ chapterVerse += normalizedText[i]
213
+ originalChapterVerseText += text[i]
214
+ i++
215
+ }
156
216
 
157
- // Capture chapter-verse
158
- while (j < lowerCaseText.length && /\d/.test(lowerCaseText[j])) {
159
- chapterVerse += normalizedText[j]
160
- j++
161
- }
162
- while (
163
- j < lowerCaseText.length &&
164
- (/[\d:,\-;]/.test(normalizedText[j]) || normalizedText[j] === " ")
165
- ) {
166
- if (normalizedText[j] === ":") hasColon = true
167
- chapterVerse += normalizedText[j]
168
- if (normalizedText[j] === ";") break
169
- j++
217
+ // Add any remaining reference
218
+ if (chapterVerse.trim().length > 0) {
219
+ const formattedReference = chapterVerse.trim().replace(/[^a-zA-Z0-9:,\-]+$/g, "")
220
+ if (formattedReference) {
221
+ // Find the last digit in the reference
222
+ const lastDigitMatch = formattedReference.match(/\d(?=[^0-9]*$)/)
223
+ let endIndex = i - 1 // Default to last character
224
+ if (lastDigitMatch) {
225
+ const lastDigitIndex = formattedReference.lastIndexOf(lastDigitMatch[0])
226
+ endIndex = startIndex + matchedLength + lastDigitIndex
227
+ }
228
+ references.push({
229
+ reference: formattedReference,
230
+ originalText: (originalBookText + originalChapterVerseText).trim(),
231
+ startIndex,
232
+ endIndex,
233
+ })
170
234
  }
235
+ }
171
236
 
172
- // Check for version suffix
173
- let endIndex = j
174
- const suffixMatch = normalizedText.substring(j).match(/\b(LXX|MT)\b/i)
175
- if (suffixMatch) {
176
- version = suffixMatch[0].toUpperCase()
177
- endIndex += suffixMatch[0].length
178
- j += suffixMatch[0].length
237
+ // Process each reference
238
+ references.forEach((refObj) => {
239
+ // Detect version suffix (LXX or MT)
240
+ let version = null
241
+ let originalText = refObj.originalText
242
+ const suffix = detectSuffix(i, text)
243
+ if (suffix) {
244
+ version = suffix.version
245
+ originalText += ` ${version}`
246
+ i += suffix.length
247
+ // Update endIndex if version suffix follows a digit
248
+ if (refObj.endIndex === i - suffix.length - 1) {
249
+ refObj.endIndex = i - 1
250
+ }
179
251
  }
180
252
 
181
- // Store the reference
182
- const ref = chapterVerse.trim()
183
- if (ref.length > 0 || version || this.config.booksOnly) {
184
- let type
185
- if (this.config.booksOnly && !ref) {
186
- type = "book_only"
187
- } else if (ref.includes(":")) {
188
- if (ref.includes("-")) {
189
- const [start, end] = ref.split("-")
190
- const startParts = start.split(":")
191
- const endParts = end.split(":")
192
- type =
193
- startParts.length > 1 &&
194
- endParts.length > 1 &&
195
- startParts[0].trim() !== endParts[0].trim()
196
- ? "multi_chapter_verse_range"
197
- : "chapter_verse_range"
198
- } else if (ref.includes(",")) {
199
- type = "comma_separated_verses"
200
- } else {
201
- type = "chapter_verse"
202
- }
203
- } else if (ref.includes("-")) {
204
- type = "chapter_range"
205
- } else if (/\d/.test(ref)) {
206
- type = "single_chapter"
253
+ let type
254
+ let ref = refObj.reference.replace(/^\.\s*/, "") // Remove leading period and space
255
+ if (this.config.booksOnly && !ref) {
256
+ type = "book_only"
257
+ } else if (ref.includes(":")) {
258
+ if (ref.includes("-")) {
259
+ const [start, end] = ref.split("-").map((s) => s.trim())
260
+ const startParts = start.split(":").map((s) => s.trim())
261
+ const endParts = end.split(":").map((s) => s.trim())
262
+ type =
263
+ startParts.length > 1 && endParts.length > 1 && startParts[0] !== endParts[0]
264
+ ? "multi_chapter_verse_range"
265
+ : "chapter_verse_range"
266
+ } else if (ref.includes(",")) {
267
+ type = "comma_separated_verses"
207
268
  } else {
208
- type = "book_only"
269
+ type = "chapter_verse"
209
270
  }
210
-
211
- this.found.push({
212
- book: currentBook,
213
- reference: ref,
214
- startIndex: currentStartIndex,
215
- endIndex,
216
- version,
217
- type,
218
- originalText: normalizedText.slice(currentStartIndex, endIndex),
219
- })
271
+ } else if (ref.includes("-")) {
272
+ type = "chapter_range"
273
+ } else if (/\d/.test(ref)) {
274
+ type = "single_chapter"
275
+ } else {
276
+ type = "book_only"
220
277
  }
221
278
 
222
- // Handle semicolon for next reference
223
- if (j < lowerCaseText.length && lowerCaseText[j] === ";") {
224
- j++ // Move past semicolon
225
- currentStartIndex = j // Reset start for next reference
226
- // Skip spaces after semicolon
227
- while (j < lowerCaseText.length && /\s/.test(lowerCaseText[j])) {
228
- j++
229
- }
230
- continue // Process next reference
279
+ const referenceObj = {
280
+ book: foundBook,
281
+ reference: ref,
282
+ version,
283
+ type,
284
+ originalText,
285
+ startIndex: refObj.startIndex,
286
+ endIndex: refObj.endIndex,
231
287
  }
288
+ this.found.push(referenceObj)
289
+ console.log(`[Scan] Stored reference: ${JSON.stringify(referenceObj)}`)
290
+ })
232
291
 
233
- // Exit if no semicolon or end of reference
234
- break
292
+ // Skip any trailing spaces after the reference
293
+ while (i < lowerCaseText.length && /\s/.test(lowerCaseText[i])) {
294
+ i++
235
295
  }
236
-
237
- i = j
238
296
  } else {
239
297
  i++
240
298
  }
241
299
  }
242
300
 
301
+ console.log("[Scan] Final found references:", JSON.stringify(this.found, null, 2))
243
302
  return this
244
303
  }
245
304
 
@@ -287,14 +346,16 @@ class CodexParser {
287
346
  abbr: null,
288
347
  }
289
348
 
290
- // Clean reference for parsing, removing version suffix
291
- let cleanReference = passage.reference
292
- if (passage.version) {
293
- cleanReference = cleanReference.replace(/\s*(LXX|MT)$/i, "").trim()
349
+ // Clean reference for parsing
350
+ let cleanReference = passage.reference.replace(/\s*(LXX|MT)$/i, "").trim()
351
+ if (cleanReference.endsWith(",")) {
352
+ cleanReference = cleanReference.slice(0, -1).trim()
294
353
  }
295
354
 
296
- // Handle chapter-only references (e.g., "113 :" or "113")
297
- if (!cleanReference || cleanReference.match(/^\d+\s*[:;]?\s*$/)) {
355
+ // Handle book-only or empty references
356
+ if (!cleanReference && this.config.booksOnly) {
357
+ parsedPassage.type = "book_only"
358
+ } else if (!cleanReference || cleanReference.match(/^\d+\s*[:;]?\s*$/)) {
298
359
  const chapterMatch = cleanReference.match(/\d+/) || ["1"]
299
360
  const chapter = Number(chapterMatch[0])
300
361
  parsedPassage.chapter = chapter
@@ -306,7 +367,7 @@ class CodexParser {
306
367
  parsedPassage.verses = [`${startVerse}-${endVerse}`]
307
368
  }
308
369
  } else {
309
- this.parseReferenceParts(parsedPassage, cleanReference.split(","))
370
+ this.parseReferenceParts(parsedPassage, cleanReference)
310
371
  }
311
372
 
312
373
  parsedPassage.passages = this.populate(parsedPassage)
@@ -371,20 +432,40 @@ class CodexParser {
371
432
  /**
372
433
  * Parses reference parts into chapter and verse components.
373
434
  * @param {Object} passage - The passage object to populate.
374
- * @param {string[]} parts - Array of reference parts.
435
+ * @param {string} reference - The reference string.
375
436
  * @private
376
437
  */
377
- parseReferenceParts(passage, parts) {
438
+ parseReferenceParts(passage, reference) {
378
439
  const singleChapterBook = this.singleChapterBook.find((b) => Object.keys(b)[0] === passage.book)
440
+ const parts = reference
441
+ .split(",")
442
+ .map((part) => part.trim())
443
+ .filter((part) => part)
379
444
 
380
445
  parts.forEach((part, index) => {
381
- part = part.trim()
382
- if (!part) return // Skip empty parts from trailing commas
383
446
  const isFirstPart = index === 0
384
447
 
385
- // Handle chapter-only references (e.g., "113 :" or "113")
448
+ // Handle multi-chapter ranges (e.g., "2:1-3:19")
449
+ if (part.includes("-") && part.includes(":")) {
450
+ const [start, end] = part.split("-").map((s) => s.trim())
451
+ const startParts = start.split(/[:.]/).map((s) => s.trim())
452
+ const endParts = end.split(/[:.]/).map((s) => s.trim())
453
+ if (startParts.length > 1 && endParts.length > 1 && startParts[0] !== endParts[0]) {
454
+ passage.type = this.MULTI_CHAPTER_RANGE
455
+ passage.chapter = Number(startParts[0])
456
+ passage.verses = [startParts[1] || "1"]
457
+ passage.to = {
458
+ book: passage.book,
459
+ chapter: Number(endParts[0]),
460
+ verses: [endParts[1] || "1"],
461
+ }
462
+ return
463
+ }
464
+ }
465
+
466
+ // Handle chapter-only references (e.g., "3")
386
467
  if (!part.includes(":") && !part.includes("-") && !singleChapterBook) {
387
- const chapter = Number(part.replace(/[^0-9]/g, "")) // Extract number, remove trailing colon
468
+ const chapter = Number(part.replace(/[^0-9]/g, ""))
388
469
  if (chapter > 0) {
389
470
  passage.chapter = chapter
390
471
  passage.type = this.SINGLE_CHAPTER
@@ -409,6 +490,7 @@ class CodexParser {
409
490
  }
410
491
  })
411
492
  }
493
+
412
494
  /**
413
495
  * Parses chapter-verse references (e.g., "3:16").
414
496
  * @param {Object} passage - The passage object.
@@ -417,10 +499,18 @@ class CodexParser {
417
499
  * @private
418
500
  */
419
501
  parseChapterVerse(passage, part, isFirstPart) {
420
- const [chapter, verse] = part.split(":")
502
+ const [chapter, verse] = part.split(/[:.]/).map((s) => s.trim())
421
503
  if (isFirstPart) passage.chapter = Number(chapter)
422
- passage.type = verse.includes("-") ? this.CHAPTER_VERSE_RANGE : this.CHAPTER_VERSE
423
- passage.verses.push(verse.includes("-") ? verse : Number(verse))
504
+ passage.type = verse.includes("-")
505
+ ? this.CHAPTER_VERSE_RANGE
506
+ : verse.includes(",")
507
+ ? this.COMMA_SEPARATED
508
+ : this.CHAPTER_VERSE
509
+ if (verse.includes(",")) {
510
+ passage.verses.push(...verse.split(",").map((v) => v.trim()))
511
+ } else {
512
+ passage.verses.push(verse)
513
+ }
424
514
  }
425
515
 
426
516
  /**
@@ -440,9 +530,13 @@ class CodexParser {
440
530
  passage.chapter = 1
441
531
  passage.verses.push(part)
442
532
  passage.type = this.CHAPTER_VERSE_RANGE
533
+ } else if (part.includes(",")) {
534
+ passage.chapter = 1
535
+ passage.verses.push(...part.split(",").map((v) => v.trim()))
536
+ passage.type = this.COMMA_SEPARATED
443
537
  } else {
444
538
  const num = Number(part)
445
- if (num > 1 || !isWholeChapter) {
539
+ if (num > 0) {
446
540
  passage.chapter = 1
447
541
  passage.verses.push(num)
448
542
  passage.type = this.CHAPTER_VERSE
@@ -508,13 +602,13 @@ class CodexParser {
508
602
  */
509
603
  handleMultiChapterRange(passage, reference) {
510
604
  const parts = reference.split(",")
511
- const lastPart = parts[parts.length - 1]
512
- const [endChapter, endVerse] = lastPart.split(":")
605
+ const lastPart = parts[parts.length - 1].trim()
606
+ const [endChapter, endVerse] = lastPart.split(/[:.]/).map((s) => s.trim())
513
607
  if (endChapter !== String(passage.chapter)) {
514
608
  passage.to = {
515
609
  book: passage.book,
516
610
  chapter: Number(endChapter),
517
- verses: endVerse.includes("-") ? [endVerse] : [Number(endVerse)],
611
+ verses: endVerse ? [endVerse] : ["1"],
518
612
  }
519
613
  }
520
614
  }
@@ -563,17 +657,16 @@ class CodexParser {
563
657
  }
564
658
  }
565
659
  }
566
- // Ensure unique verses and update the singleChapterBook entry
567
660
  singleChapterBook[book][chapter] = Array.from(new Set(singleChapterBook[book][chapter]))
568
- return singleChapterBook[book] // Return updated chapter data
661
+ return singleChapterBook[book]
569
662
  }
570
663
 
571
664
  // Handle all other books using chapterVerses
572
665
  if (!this.chapterVerses[book][chapter]) {
573
- return // No data for this book/chapter
666
+ return
574
667
  }
575
668
  if (!this.versificationDifferences[book]) {
576
- return // No versification differences
669
+ return
577
670
  }
578
671
  for (const [key, value] of Object.entries(this.versificationDifferences[book])) {
579
672
  if (value[version].startsWith(`${chapter}:`)) {
@@ -583,8 +676,8 @@ class CodexParser {
583
676
  }
584
677
  }
585
678
  }
586
- this.chapterVerses[book][chapter] = Array.from(this.chapterVerses[book][chapter])
587
- return this.chapterVerses // Return updated chapterVerses
679
+ this.chapterVerses[book][chapter] = Array.from(new Set(this.chapterVerses[book][chapter]))
680
+ return this.chapterVerses
588
681
  }
589
682
 
590
683
  /**
@@ -656,24 +749,31 @@ class CodexParser {
656
749
 
657
750
  if (type === this.CHAPTER_RANGE) {
658
751
  const passages = []
659
- for (let ch = chapter; ch <= to.chapter; ch++) {
660
- const chapterVerses = this.getChapterVerses(book, ch)
661
- passages.push(
662
- ...this.expandVerses(book, ch, [`${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`])
663
- )
752
+ if (to && to.chapter) {
753
+ for (let ch = chapter; ch <= to.chapter; ch++) {
754
+ const chapterVerses = this.getChapterVerses(book, ch)
755
+ passages.push(
756
+ ...this.expandVerses(book, ch, [
757
+ `${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`,
758
+ ])
759
+ )
760
+ }
664
761
  }
665
762
  return passages
666
763
  }
667
764
 
668
765
  if (type === this.MULTI_CHAPTER_RANGE) {
669
766
  const passages = []
670
- const startVerse = verses[0].includes("-") ? Number(verses[0].split("-")[0]) : Number(verses[0])
671
- const endVerse = to.verses[0].includes("-") ? Number(to.verses[0].split("-")[1]) : Number(to.verses[0])
767
+ const startVerse = verses[0]?.includes("-") ? Number(verses[0].split("-")[0]) : Number(verses[0]) || 1
768
+ const endVerse = to?.verses?.[0]?.includes("-")
769
+ ? Number(to.verses[0].split("-")[1])
770
+ : Number(to?.verses?.[0]) || 1
771
+ const endChapter = to?.chapter || chapter
672
772
 
673
- for (let ch = chapter; ch <= to.chapter; ch++) {
773
+ for (let ch = chapter; ch <= endChapter; ch++) {
674
774
  const chapterVerses = this.getChapterVerses(book, ch)
675
775
  const from = ch === chapter ? startVerse : chapterVerses[0]
676
- const toVerse = ch === to.chapter ? endVerse : chapterVerses[chapterVerses.length - 1]
776
+ const toVerse = ch === endChapter ? endVerse : chapterVerses[chapterVerses.length - 1]
677
777
  passages.push(...this.expandVerses(book, ch, [`${from}-${toVerse}`]))
678
778
  }
679
779
  return passages
@@ -700,7 +800,10 @@ class CodexParser {
700
800
  passages.push({ book, chapter, verse: i })
701
801
  }
702
802
  } else {
703
- passages.push({ book, chapter, verse: Number(verse) })
803
+ const verseNum = Number(verse)
804
+ if (!isNaN(verseNum) && verseNum > 0) {
805
+ passages.push({ book, chapter, verse: verseNum })
806
+ }
704
807
  }
705
808
  })
706
809
  return passages
@@ -716,15 +819,13 @@ class CodexParser {
716
819
  book = book[0]
717
820
  }
718
821
  book = book.toLowerCase()
719
- // Check if book is an abbreviation
720
822
  let bookified = this.abbreviations[Object.keys(this.abbreviations).find((abbr) => abbr.toLowerCase() === book)]
721
823
  if (bookified) {
722
824
  return bookified
723
825
  }
724
- // Check if book is a full name
725
826
  bookified =
726
827
  this.bible.new.find((b) => b.toLowerCase() === book) || this.bible.old.find((b) => b.toLowerCase() === book)
727
- return bookified || book // Fallback to input if not found
828
+ return bookified || book
728
829
  }
729
830
 
730
831
  /**
@@ -816,7 +917,7 @@ class CodexParser {
816
917
  if (verses.length === 1) {
817
918
  return `${chapter}:${verses[0]}`
818
919
  }
819
- const isRange = verses.every((v, i, arr) => i === 0 || v === arr[i - 1] + 1)
920
+ const isRange = verses.every((v, i, arr) => i === 0 || Number(v) === Number(arr[i - 1]) + 1)
820
921
  if (isRange) {
821
922
  return `${chapter}:${verses[0]}-${verses[verses.length - 1]}`
822
923
  }
@@ -829,9 +930,7 @@ class CodexParser {
829
930
  passage.to.chapter,
830
931
  passage.to.verses
831
932
  )}`
832
- } else if (passage.type === "chapter_verse_range") {
833
- combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
834
- } else if (passage.type === "comma_separated_verses") {
933
+ } else if (passage.type === "chapter_verse_range" || passage.type === "comma_separated_verses") {
835
934
  combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
836
935
  } else if (passage.type === "chapter_range" && passage.to) {
837
936
  combined += ` ${passage.chapter}-${passage.to.chapter}`
@@ -857,7 +956,7 @@ class CodexParser {
857
956
 
858
957
  /**
859
958
  * Combines multiple passages into a single reference.
860
- * @param {Object[]} [passages=this.passages] - Array of passages to combine, defaults to this.passages.
959
+ * @param {Object[]} [passages=this.passages] - Array of passages to combine.
861
960
  * @returns {Object} Combined passage object.
862
961
  */
863
962
  combine(passages = this.passages) {
@@ -920,7 +1019,7 @@ class CodexParser {
920
1019
  sortedChapters.forEach((chapter) => {
921
1020
  const verses = Array.from(chapterVerses[chapter])
922
1021
  .map(Number)
923
- .filter((verse) => verse > 0) // Exclude invalid verse 0
1022
+ .filter((verse) => verse > 0)
924
1023
  .sort((a, b) => a - b)
925
1024
  if (verses.length > 0) {
926
1025
  const mergedVerses = this.mergeRanges(verses)
@@ -972,7 +1071,6 @@ class CodexParser {
972
1071
  : Math.max(...Array.from(chapterVerses[lastChapter]).filter((verse) => verse > 0)),
973
1072
  }
974
1073
 
975
- // Reattach the reference method to the combined passage
976
1074
  combined.reference = function () {
977
1075
  return this.scripture.passage
978
1076
  }
@@ -1111,7 +1209,7 @@ class CodexParser {
1111
1209
  : [Number(verseRange)]
1112
1210
 
1113
1211
  for (const v of verseNumbers) {
1114
- if (!chapterVerses.includes(v)) {
1212
+ if (isNaN(v) || v <= 0 || !chapterVerses.includes(v)) {
1115
1213
  return this.validationError(104, `Verse number ${v} does not exist in ${book} ${chapter}`)
1116
1214
  }
1117
1215
  }
@@ -1171,21 +1269,18 @@ class CodexParser {
1171
1269
  const { originalText, abbr, original } = passage
1172
1270
  const newReference = useAbbreviations ? abbr : original
1173
1271
 
1174
- const regex = new RegExp(`${originalText}`, "g")
1272
+ const regex = new RegExp(`${originalText.replace(/([.*+?^${}()|[\]\\])/g, "\\$1")}`, "g")
1175
1273
 
1176
- // Find all matches
1177
1274
  const matches = [...result.matchAll(regex)]
1178
1275
  if (matches.length > 0) {
1179
- // Process matches in reverse to avoid index shifting
1180
1276
  for (let j = matches.length - 1; j >= 0; j--) {
1181
1277
  const match = matches[j]
1182
1278
  const startIndex = match.index
1183
1279
  const endIndex = startIndex + match[0].length
1184
- const leadingSpace = match[1] || "" // Capture leading spaces
1280
+ const leadingSpace = match[1] || ""
1185
1281
  const hasOpeningParen = match[2] === "("
1186
1282
  const hasClosingParen = match[3] === ")"
1187
- const trailingSpace = match[4] || " " // Capture trailing spaces
1188
- // Preserve parentheses if present
1283
+ const trailingSpace = match[4] || " "
1189
1284
  const replacement =
1190
1285
  hasOpeningParen && hasClosingParen
1191
1286
  ? `${leadingSpace}(${newReference})${trailingSpace}`
@@ -1197,6 +1292,7 @@ class CodexParser {
1197
1292
 
1198
1293
  return result
1199
1294
  }
1295
+
1200
1296
  /**
1201
1297
  * Checks if all references in the passages array are from the same book.
1202
1298
  * @returns {boolean} True if all passages are from the same book, false otherwise.
@@ -6807,269 +6807,264 @@ module.exports = {
6807
6807
  eng: "88:19",
6808
6808
  },
6809
6809
  "89:1": {
6810
- lxx: "88:1",
6811
- mt: "89:1",
6812
- eng: "89:1",
6813
- },
6814
- "89:2": {
6815
6810
  lxx: "88:2",
6816
6811
  mt: "89:2",
6817
- eng: "89:2",
6812
+ eng: "89:1",
6818
6813
  },
6819
- "89:3": {
6814
+ "89:2": {
6820
6815
  lxx: "88:3",
6821
6816
  mt: "89:3",
6822
- eng: "89:3",
6817
+ eng: "89:2",
6823
6818
  },
6824
- "89:4": {
6819
+ "89:3": {
6825
6820
  lxx: "88:4",
6826
6821
  mt: "89:4",
6827
- eng: "89:4",
6822
+ eng: "89:3",
6828
6823
  },
6829
- "89:5": {
6824
+ "89:4": {
6830
6825
  lxx: "88:5",
6831
6826
  mt: "89:5",
6832
- eng: "89:5",
6827
+ eng: "89:4",
6833
6828
  },
6834
- "89:6": {
6829
+ "89:5": {
6835
6830
  lxx: "88:6",
6836
6831
  mt: "89:6",
6837
- eng: "89:6",
6832
+ eng: "89:5",
6838
6833
  },
6839
- "89:7": {
6834
+ "89:6": {
6840
6835
  lxx: "88:7",
6841
6836
  mt: "89:7",
6842
- eng: "89:7",
6837
+ eng: "89:6",
6843
6838
  },
6844
- "89:8": {
6839
+ "89:7": {
6845
6840
  lxx: "88:8",
6846
6841
  mt: "89:8",
6847
- eng: "89:8",
6842
+ eng: "89:7",
6848
6843
  },
6849
- "89:9": {
6844
+ "89:8": {
6850
6845
  lxx: "88:9",
6851
6846
  mt: "89:9",
6852
- eng: "89:9",
6847
+ eng: "89:8",
6853
6848
  },
6854
- "89:10": {
6849
+ "89:9": {
6855
6850
  lxx: "88:10",
6856
6851
  mt: "89:10",
6857
- eng: "89:10",
6852
+ eng: "89:9",
6858
6853
  },
6859
- "89:11": {
6854
+ "89:10": {
6860
6855
  lxx: "88:11",
6861
6856
  mt: "89:11",
6862
- eng: "89:11",
6857
+ eng: "89:10",
6863
6858
  },
6864
- "89:12": {
6859
+ "89:11": {
6865
6860
  lxx: "88:12",
6866
6861
  mt: "89:12",
6867
- eng: "89:12",
6862
+ eng: "89:11",
6868
6863
  },
6869
- "89:13": {
6864
+ "89:12": {
6870
6865
  lxx: "88:13",
6871
6866
  mt: "89:13",
6872
- eng: "89:13",
6867
+ eng: "89:12",
6873
6868
  },
6874
- "89:14": {
6869
+ "89:13": {
6875
6870
  lxx: "88:14",
6876
6871
  mt: "89:14",
6877
- eng: "89:14",
6872
+ eng: "89:13",
6878
6873
  },
6879
- "89:15": {
6874
+ "89:14": {
6880
6875
  lxx: "88:15",
6881
6876
  mt: "89:15",
6882
- eng: "89:15",
6877
+ eng: "89:14",
6883
6878
  },
6884
- "89:16": {
6879
+ "89:15": {
6885
6880
  lxx: "88:16",
6886
6881
  mt: "89:16",
6887
- eng: "89:16",
6882
+ eng: "89:15",
6888
6883
  },
6889
- "89:17": {
6884
+ "89:16": {
6890
6885
  lxx: "88:17",
6891
6886
  mt: "89:17",
6892
- eng: "89:17",
6887
+ eng: "89:16",
6893
6888
  },
6894
- "89:18": {
6889
+ "89:17": {
6895
6890
  lxx: "88:18",
6896
6891
  mt: "89:18",
6897
- eng: "89:18",
6892
+ eng: "89:17",
6898
6893
  },
6899
- "89:19": {
6894
+ "89:18": {
6900
6895
  lxx: "88:19",
6901
6896
  mt: "89:19",
6902
- eng: "89:19",
6897
+ eng: "89:18",
6903
6898
  },
6904
- "89:20": {
6899
+ "89:19": {
6905
6900
  lxx: "88:20",
6906
6901
  mt: "89:20",
6907
- eng: "89:20",
6902
+ eng: "89:19",
6908
6903
  },
6909
- "89:21": {
6904
+ "89:20": {
6910
6905
  lxx: "88:21",
6911
6906
  mt: "89:21",
6912
- eng: "89:21",
6907
+ eng: "89:20",
6913
6908
  },
6914
- "89:22": {
6909
+ "89:21": {
6915
6910
  lxx: "88:22",
6916
6911
  mt: "89:22",
6917
- eng: "89:22",
6912
+ eng: "89:21",
6918
6913
  },
6919
- "89:23": {
6914
+ "89:22": {
6920
6915
  lxx: "88:23",
6921
6916
  mt: "89:23",
6922
- eng: "89:23",
6917
+ eng: "89:22",
6923
6918
  },
6924
- "89:24": {
6919
+ "89:23": {
6925
6920
  lxx: "88:24",
6926
6921
  mt: "89:24",
6927
- eng: "89:24",
6922
+ eng: "89:23",
6928
6923
  },
6929
- "89:25": {
6924
+ "89:24": {
6930
6925
  lxx: "88:25",
6931
6926
  mt: "89:25",
6932
- eng: "89:25",
6927
+ eng: "89:24",
6933
6928
  },
6934
- "89:26": {
6929
+ "89:25": {
6935
6930
  lxx: "88:26",
6936
6931
  mt: "89:26",
6937
- eng: "89:26",
6932
+ eng: "89:25",
6938
6933
  },
6939
- "89:27": {
6934
+ "89:26": {
6940
6935
  lxx: "88:27",
6941
6936
  mt: "89:27",
6942
- eng: "89:27",
6937
+ eng: "89:26",
6943
6938
  },
6944
- "89:28": {
6939
+ "89:27": {
6945
6940
  lxx: "88:28",
6946
6941
  mt: "89:28",
6947
- eng: "89:28",
6942
+ eng: "89:27",
6948
6943
  },
6949
- "89:29": {
6944
+ "89:28": {
6950
6945
  lxx: "88:29",
6951
6946
  mt: "89:29",
6952
- eng: "89:29",
6947
+ eng: "89:28",
6953
6948
  },
6954
- "89:30": {
6949
+ "89:29": {
6955
6950
  lxx: "88:30",
6956
6951
  mt: "89:30",
6957
- eng: "89:30",
6952
+ eng: "89:29",
6958
6953
  },
6959
- "89:31": {
6954
+ "89:30": {
6960
6955
  lxx: "88:31",
6961
6956
  mt: "89:31",
6962
- eng: "89:31",
6957
+ eng: "89:30",
6963
6958
  },
6964
- "89:32": {
6959
+ "89:31": {
6965
6960
  lxx: "88:32",
6966
6961
  mt: "89:32",
6967
- eng: "89:32",
6962
+ eng: "89:31",
6968
6963
  },
6969
- "89:33": {
6964
+ "89:32": {
6970
6965
  lxx: "88:33",
6971
6966
  mt: "89:33",
6972
- eng: "89:33",
6967
+ eng: "89:32",
6973
6968
  },
6974
- "89:34": {
6969
+ "89:33": {
6975
6970
  lxx: "88:34",
6976
6971
  mt: "89:34",
6977
- eng: "89:34",
6972
+ eng: "89:33",
6978
6973
  },
6979
- "89:35": {
6974
+ "89:34": {
6980
6975
  lxx: "88:35",
6981
6976
  mt: "89:35",
6982
- eng: "89:35",
6977
+ eng: "89:34",
6983
6978
  },
6984
- "89:36": {
6979
+ "89:35": {
6985
6980
  lxx: "88:36",
6986
6981
  mt: "89:36",
6987
- eng: "89:36",
6982
+ eng: "89:35",
6988
6983
  },
6989
- "89:37": {
6984
+ "89:36": {
6990
6985
  lxx: "88:37",
6991
6986
  mt: "89:37",
6992
- eng: "89:37",
6987
+ eng: "89:36",
6993
6988
  },
6994
- "89:38": {
6989
+ "89:37": {
6995
6990
  lxx: "88:38",
6996
6991
  mt: "89:38",
6997
- eng: "89:38",
6992
+ eng: "89:37",
6998
6993
  },
6999
- "89:39": {
6994
+ "89:38": {
7000
6995
  lxx: "88:39",
7001
6996
  mt: "89:39",
7002
- eng: "89:39",
6997
+ eng: "89:38",
7003
6998
  },
7004
- "89:40": {
6999
+ "89:39": {
7005
7000
  lxx: "88:40",
7006
7001
  mt: "89:40",
7007
- eng: "89:40",
7002
+ eng: "89:39",
7008
7003
  },
7009
- "89:41": {
7004
+ "89:40": {
7010
7005
  lxx: "88:41",
7011
7006
  mt: "89:41",
7012
- eng: "89:41",
7007
+ eng: "89:40",
7013
7008
  },
7014
- "89:42": {
7009
+ "89:41": {
7015
7010
  lxx: "88:42",
7016
7011
  mt: "89:42",
7017
- eng: "89:42",
7012
+ eng: "89:41",
7018
7013
  },
7019
- "89:43": {
7014
+ "89:42": {
7020
7015
  lxx: "88:43",
7021
7016
  mt: "89:43",
7022
- eng: "89:43",
7017
+ eng: "89:42",
7023
7018
  },
7024
- "89:44": {
7019
+ "89:43": {
7025
7020
  lxx: "88:44",
7026
7021
  mt: "89:44",
7027
- eng: "89:44",
7022
+ eng: "89:43",
7028
7023
  },
7029
- "89:45": {
7024
+ "89:44": {
7030
7025
  lxx: "88:45",
7031
7026
  mt: "89:45",
7032
- eng: "89:45",
7027
+ eng: "89:44",
7033
7028
  },
7034
- "89:46": {
7029
+ "89:45": {
7035
7030
  lxx: "88:46",
7036
7031
  mt: "89:46",
7037
- eng: "89:46",
7032
+ eng: "89:45",
7038
7033
  },
7039
- "89:47": {
7034
+ "89:46": {
7040
7035
  lxx: "88:47",
7041
7036
  mt: "89:47",
7042
- eng: "89:47",
7037
+ eng: "89:46",
7043
7038
  },
7044
- "89:48": {
7039
+ "89:47": {
7045
7040
  lxx: "88:48",
7046
7041
  mt: "89:48",
7047
- eng: "89:48",
7042
+ eng: "89:47",
7048
7043
  },
7049
- "89:49": {
7044
+ "89:48": {
7050
7045
  lxx: "88:49",
7051
7046
  mt: "89:49",
7052
- eng: "89:49",
7047
+ eng: "89:48",
7053
7048
  },
7054
- "89:50": {
7049
+ "89:49": {
7055
7050
  lxx: "88:50",
7056
7051
  mt: "89:50",
7057
- eng: "89:50",
7052
+ eng: "89:49",
7058
7053
  },
7059
- "89:51": {
7054
+ "89:50": {
7060
7055
  lxx: "88:51",
7061
7056
  mt: "89:51",
7062
- eng: "89:51",
7057
+ eng: "89:50",
7063
7058
  },
7064
- "89:52": {
7059
+ "89:51": {
7065
7060
  lxx: "88:52",
7066
7061
  mt: "89:52",
7067
- eng: "89:52",
7062
+ eng: "89:51",
7068
7063
  },
7069
- "89:53": {
7064
+ "89:52": {
7070
7065
  lxx: "88:53",
7071
7066
  mt: "89:53",
7072
- eng: "89:53",
7067
+ eng: "89:52",
7073
7068
  },
7074
7069
  "90:1": {
7075
7070
  lxx: "89:1",