codexparser 0.1.79 → 0.1.80

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.443
21
+ - git-diff-check
22
+ - markdownlint@0.45.0
23
+ - osv-scanner@2.0.3
24
+ - prettier@3.5.3
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.80",
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.