codexparser 0.1.78 → 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.78",
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
  }
@@ -89,138 +94,173 @@ class CodexParser {
89
94
  const fullNames = [...this.bible.old, ...this.bible.new]
90
95
  const abbreviations = Object.keys(this.abbreviations)
91
96
  this.found = []
92
-
93
- // Normalize text: remove curly quotes, replace periods before numbers with colons
97
+ // Normalize text for parsing but keep original for originalText
94
98
  let normalizedText = text
95
99
  .replace(/[“”]/g, "") // Remove curly quotes
96
- .replace(/\.(?=\d)/g, ":")
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())
97
104
  const lowerCaseText = normalizedText.toLowerCase()
98
105
  let i = 0
99
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
+
100
138
  while (i < lowerCaseText.length) {
101
139
  let foundBook = null
102
- let startIndex = -1
103
140
  let matchedLength = 0
104
- let hasOpeningParen = false
105
- let parenStartIndex = -1
106
-
107
- // Skip whitespace
108
- while (i < lowerCaseText.length && /\s/.test(lowerCaseText[i])) {
109
- i++
110
- }
111
- if (i >= lowerCaseText.length) break
112
-
113
- // Check for opening parenthesis
114
- if (i < lowerCaseText.length && lowerCaseText[i] === "(") {
115
- hasOpeningParen = true
116
- parenStartIndex = i
117
- i++
118
- }
119
-
120
- // Record potential start of reference
121
- startIndex = i
141
+ let originalBookText = ""
142
+ let startIndex = i
122
143
 
123
- // Check for book names or abbreviations
124
- for (let book of fullNames) {
144
+ // Check full book names
145
+ for (let j = 0; j < lowercaseBibleFullNames.length; j++) {
146
+ const book = lowercaseBibleFullNames[j]
125
147
  if (
126
- lowerCaseText.startsWith(book.toLowerCase(), i) &&
127
- (i + book.length >= lowerCaseText.length || /[\s:;\d]/.test(lowerCaseText[i + book.length]))
148
+ lowerCaseText.startsWith(book, i) &&
149
+ (i + book.length >= lowerCaseText.length || /\d/.test(lowerCaseText[i + book.length]))
128
150
  ) {
129
- foundBook = book
151
+ foundBook = fullNames[j]
130
152
  matchedLength = book.length
131
- break
153
+ originalBookText = text.slice(i, i + book.length)
154
+ console.log(`[Scan] Matched full book name: "${foundBook}" at index ${i}`)
132
155
  }
133
156
  }
157
+
158
+ // Check abbreviations
134
159
  if (!foundBook) {
135
- for (let abbr of abbreviations) {
136
- if (
137
- lowerCaseText.startsWith(abbr.toLowerCase(), i) &&
138
- (i + abbr.length >= lowerCaseText.length || /[\s:;\d]/.test(lowerCaseText[i + abbr.length]))
139
- ) {
140
- foundBook = this.abbreviations[abbr]
141
- matchedLength = abbr.length
142
- 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
+ )
143
172
  }
144
173
  }
145
174
  }
146
175
 
147
176
  if (foundBook) {
148
- // Check if book is followed by a valid reference or version when booksOnly is false
149
- let isFollowedByReference = false
150
- let j = i + matchedLength
151
- // Skip spaces
152
- while (j < lowerCaseText.length && /\s/.test(lowerCaseText[j])) {
153
- j++
154
- }
155
- // Check for digit (chapter number) or version suffix (LXX/MT)
156
- if (
157
- j < lowerCaseText.length &&
158
- (/\d/.test(lowerCaseText[j]) || lowerCaseText.substring(j).match(/^(lxx|mt)\b/i))
159
- ) {
160
- isFollowedByReference = true
161
- }
162
-
163
- if (!this.config.booksOnly && !hasOpeningParen && !isFollowedByReference) {
164
- i++
165
- continue
166
- }
167
-
168
177
  i += matchedLength
169
178
  let chapterVerse = ""
170
- let hasColon = false
171
-
172
- // Capture space after book
173
- if (i < normalizedText.length && normalizedText[i] === " ") {
174
- chapterVerse += " "
175
- i++
176
- }
179
+ let originalChapterVerseText = ""
180
+ const references = []
177
181
 
178
- // Capture chapter-verse (allow digits, colons, commas, dashes, spaces)
179
- while (i < lowerCaseText.length && (/[\d:,\-]/.test(normalizedText[i]) || normalizedText[i] === " ")) {
180
- if (normalizedText[i] === ":") hasColon = true
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
187
+ }
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
+ })
204
+ }
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
211
+ }
181
212
  chapterVerse += normalizedText[i]
213
+ originalChapterVerseText += text[i]
182
214
  i++
183
215
  }
184
216
 
185
- // Only proceed if valid reference or booksOnly is true
186
- if (
187
- (chapterVerse.trim().length > 0 && (hasColon || /\d/.test(chapterVerse.trim()))) ||
188
- (this.config.booksOnly && !chapterVerse.trim())
189
- ) {
190
- let endIndex = i
191
- let version = null
192
-
193
- // Detect suffix
194
- const suffixMatch = normalizedText.substring(i).match(/\b(LXX|MT)\b/i)
195
- if (suffixMatch) {
196
- version = suffixMatch[0].toUpperCase()
197
- endIndex += suffixMatch[0].length
198
- i += suffixMatch[0].length
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
+ })
199
234
  }
235
+ }
200
236
 
201
- // Handle closing parenthesis
202
- if (hasOpeningParen && i < lowerCaseText.length && normalizedText[i] === ")") {
203
- endIndex = i + 1
204
- i++
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
+ }
205
251
  }
206
252
 
207
- // Use original text for reference only (exclude parentheses)
208
- const originalText = normalizedText.slice(startIndex, hasOpeningParen ? endIndex - 1 : endIndex)
209
-
210
- // Determine type
211
253
  let type
212
- const ref = chapterVerse.trim()
254
+ let ref = refObj.reference.replace(/^\.\s*/, "") // Remove leading period and space
213
255
  if (this.config.booksOnly && !ref) {
214
256
  type = "book_only"
215
257
  } else if (ref.includes(":")) {
216
258
  if (ref.includes("-")) {
217
- const [start, end] = ref.split("-")
218
- const startParts = start.split(":")
219
- const endParts = end.split(":")
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())
220
262
  type =
221
- startParts.length > 1 &&
222
- endParts.length > 1 &&
223
- startParts[0].trim() !== endParts[0].trim()
263
+ startParts.length > 1 && endParts.length > 1 && startParts[0] !== endParts[0]
224
264
  ? "multi_chapter_verse_range"
225
265
  : "chapter_verse_range"
226
266
  } else if (ref.includes(",")) {
@@ -236,23 +276,29 @@ class CodexParser {
236
276
  type = "book_only"
237
277
  }
238
278
 
239
- this.found.push({
279
+ const referenceObj = {
240
280
  book: foundBook,
241
281
  reference: ref,
242
- startIndex: hasOpeningParen ? parenStartIndex : startIndex,
243
- endIndex,
244
282
  version,
245
283
  type,
246
284
  originalText,
247
- })
248
- } else {
249
- i = startIndex + 1
285
+ startIndex: refObj.startIndex,
286
+ endIndex: refObj.endIndex,
287
+ }
288
+ this.found.push(referenceObj)
289
+ console.log(`[Scan] Stored reference: ${JSON.stringify(referenceObj)}`)
290
+ })
291
+
292
+ // Skip any trailing spaces after the reference
293
+ while (i < lowerCaseText.length && /\s/.test(lowerCaseText[i])) {
294
+ i++
250
295
  }
251
296
  } else {
252
297
  i++
253
298
  }
254
299
  }
255
300
 
301
+ console.log("[Scan] Final found references:", JSON.stringify(this.found, null, 2))
256
302
  return this
257
303
  }
258
304
 
@@ -300,14 +346,16 @@ class CodexParser {
300
346
  abbr: null,
301
347
  }
302
348
 
303
- // Clean reference for parsing, removing version suffix
304
- let cleanReference = passage.reference
305
- if (passage.version) {
306
- 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()
307
353
  }
308
354
 
309
- // Handle chapter-only references (e.g., "113 :" or "113")
310
- 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*$/)) {
311
359
  const chapterMatch = cleanReference.match(/\d+/) || ["1"]
312
360
  const chapter = Number(chapterMatch[0])
313
361
  parsedPassage.chapter = chapter
@@ -319,7 +367,7 @@ class CodexParser {
319
367
  parsedPassage.verses = [`${startVerse}-${endVerse}`]
320
368
  }
321
369
  } else {
322
- this.parseReferenceParts(parsedPassage, cleanReference.split(","))
370
+ this.parseReferenceParts(parsedPassage, cleanReference)
323
371
  }
324
372
 
325
373
  parsedPassage.passages = this.populate(parsedPassage)
@@ -384,20 +432,40 @@ class CodexParser {
384
432
  /**
385
433
  * Parses reference parts into chapter and verse components.
386
434
  * @param {Object} passage - The passage object to populate.
387
- * @param {string[]} parts - Array of reference parts.
435
+ * @param {string} reference - The reference string.
388
436
  * @private
389
437
  */
390
- parseReferenceParts(passage, parts) {
438
+ parseReferenceParts(passage, reference) {
391
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)
392
444
 
393
445
  parts.forEach((part, index) => {
394
- part = part.trim()
395
- if (!part) return // Skip empty parts from trailing commas
396
446
  const isFirstPart = index === 0
397
447
 
398
- // 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")
399
467
  if (!part.includes(":") && !part.includes("-") && !singleChapterBook) {
400
- const chapter = Number(part.replace(/[^0-9]/g, "")) // Extract number, remove trailing colon
468
+ const chapter = Number(part.replace(/[^0-9]/g, ""))
401
469
  if (chapter > 0) {
402
470
  passage.chapter = chapter
403
471
  passage.type = this.SINGLE_CHAPTER
@@ -422,6 +490,7 @@ class CodexParser {
422
490
  }
423
491
  })
424
492
  }
493
+
425
494
  /**
426
495
  * Parses chapter-verse references (e.g., "3:16").
427
496
  * @param {Object} passage - The passage object.
@@ -430,10 +499,18 @@ class CodexParser {
430
499
  * @private
431
500
  */
432
501
  parseChapterVerse(passage, part, isFirstPart) {
433
- const [chapter, verse] = part.split(":")
502
+ const [chapter, verse] = part.split(/[:.]/).map((s) => s.trim())
434
503
  if (isFirstPart) passage.chapter = Number(chapter)
435
- passage.type = verse.includes("-") ? this.CHAPTER_VERSE_RANGE : this.CHAPTER_VERSE
436
- 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
+ }
437
514
  }
438
515
 
439
516
  /**
@@ -453,9 +530,13 @@ class CodexParser {
453
530
  passage.chapter = 1
454
531
  passage.verses.push(part)
455
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
456
537
  } else {
457
538
  const num = Number(part)
458
- if (num > 1 || !isWholeChapter) {
539
+ if (num > 0) {
459
540
  passage.chapter = 1
460
541
  passage.verses.push(num)
461
542
  passage.type = this.CHAPTER_VERSE
@@ -521,13 +602,13 @@ class CodexParser {
521
602
  */
522
603
  handleMultiChapterRange(passage, reference) {
523
604
  const parts = reference.split(",")
524
- const lastPart = parts[parts.length - 1]
525
- const [endChapter, endVerse] = lastPart.split(":")
605
+ const lastPart = parts[parts.length - 1].trim()
606
+ const [endChapter, endVerse] = lastPart.split(/[:.]/).map((s) => s.trim())
526
607
  if (endChapter !== String(passage.chapter)) {
527
608
  passage.to = {
528
609
  book: passage.book,
529
610
  chapter: Number(endChapter),
530
- verses: endVerse.includes("-") ? [endVerse] : [Number(endVerse)],
611
+ verses: endVerse ? [endVerse] : ["1"],
531
612
  }
532
613
  }
533
614
  }
@@ -576,17 +657,16 @@ class CodexParser {
576
657
  }
577
658
  }
578
659
  }
579
- // Ensure unique verses and update the singleChapterBook entry
580
660
  singleChapterBook[book][chapter] = Array.from(new Set(singleChapterBook[book][chapter]))
581
- return singleChapterBook[book] // Return updated chapter data
661
+ return singleChapterBook[book]
582
662
  }
583
663
 
584
664
  // Handle all other books using chapterVerses
585
665
  if (!this.chapterVerses[book][chapter]) {
586
- return // No data for this book/chapter
666
+ return
587
667
  }
588
668
  if (!this.versificationDifferences[book]) {
589
- return // No versification differences
669
+ return
590
670
  }
591
671
  for (const [key, value] of Object.entries(this.versificationDifferences[book])) {
592
672
  if (value[version].startsWith(`${chapter}:`)) {
@@ -596,8 +676,8 @@ class CodexParser {
596
676
  }
597
677
  }
598
678
  }
599
- this.chapterVerses[book][chapter] = Array.from(this.chapterVerses[book][chapter])
600
- return this.chapterVerses // Return updated chapterVerses
679
+ this.chapterVerses[book][chapter] = Array.from(new Set(this.chapterVerses[book][chapter]))
680
+ return this.chapterVerses
601
681
  }
602
682
 
603
683
  /**
@@ -669,24 +749,31 @@ class CodexParser {
669
749
 
670
750
  if (type === this.CHAPTER_RANGE) {
671
751
  const passages = []
672
- for (let ch = chapter; ch <= to.chapter; ch++) {
673
- const chapterVerses = this.getChapterVerses(book, ch)
674
- passages.push(
675
- ...this.expandVerses(book, ch, [`${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`])
676
- )
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
+ }
677
761
  }
678
762
  return passages
679
763
  }
680
764
 
681
765
  if (type === this.MULTI_CHAPTER_RANGE) {
682
766
  const passages = []
683
- const startVerse = verses[0].includes("-") ? Number(verses[0].split("-")[0]) : Number(verses[0])
684
- 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
685
772
 
686
- for (let ch = chapter; ch <= to.chapter; ch++) {
773
+ for (let ch = chapter; ch <= endChapter; ch++) {
687
774
  const chapterVerses = this.getChapterVerses(book, ch)
688
775
  const from = ch === chapter ? startVerse : chapterVerses[0]
689
- const toVerse = ch === to.chapter ? endVerse : chapterVerses[chapterVerses.length - 1]
776
+ const toVerse = ch === endChapter ? endVerse : chapterVerses[chapterVerses.length - 1]
690
777
  passages.push(...this.expandVerses(book, ch, [`${from}-${toVerse}`]))
691
778
  }
692
779
  return passages
@@ -713,7 +800,10 @@ class CodexParser {
713
800
  passages.push({ book, chapter, verse: i })
714
801
  }
715
802
  } else {
716
- 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
+ }
717
807
  }
718
808
  })
719
809
  return passages
@@ -729,15 +819,13 @@ class CodexParser {
729
819
  book = book[0]
730
820
  }
731
821
  book = book.toLowerCase()
732
- // Check if book is an abbreviation
733
822
  let bookified = this.abbreviations[Object.keys(this.abbreviations).find((abbr) => abbr.toLowerCase() === book)]
734
823
  if (bookified) {
735
824
  return bookified
736
825
  }
737
- // Check if book is a full name
738
826
  bookified =
739
827
  this.bible.new.find((b) => b.toLowerCase() === book) || this.bible.old.find((b) => b.toLowerCase() === book)
740
- return bookified || book // Fallback to input if not found
828
+ return bookified || book
741
829
  }
742
830
 
743
831
  /**
@@ -829,7 +917,7 @@ class CodexParser {
829
917
  if (verses.length === 1) {
830
918
  return `${chapter}:${verses[0]}`
831
919
  }
832
- 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)
833
921
  if (isRange) {
834
922
  return `${chapter}:${verses[0]}-${verses[verses.length - 1]}`
835
923
  }
@@ -842,9 +930,7 @@ class CodexParser {
842
930
  passage.to.chapter,
843
931
  passage.to.verses
844
932
  )}`
845
- } else if (passage.type === "chapter_verse_range") {
846
- combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
847
- } else if (passage.type === "comma_separated_verses") {
933
+ } else if (passage.type === "chapter_verse_range" || passage.type === "comma_separated_verses") {
848
934
  combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
849
935
  } else if (passage.type === "chapter_range" && passage.to) {
850
936
  combined += ` ${passage.chapter}-${passage.to.chapter}`
@@ -870,7 +956,7 @@ class CodexParser {
870
956
 
871
957
  /**
872
958
  * Combines multiple passages into a single reference.
873
- * @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.
874
960
  * @returns {Object} Combined passage object.
875
961
  */
876
962
  combine(passages = this.passages) {
@@ -933,7 +1019,7 @@ class CodexParser {
933
1019
  sortedChapters.forEach((chapter) => {
934
1020
  const verses = Array.from(chapterVerses[chapter])
935
1021
  .map(Number)
936
- .filter((verse) => verse > 0) // Exclude invalid verse 0
1022
+ .filter((verse) => verse > 0)
937
1023
  .sort((a, b) => a - b)
938
1024
  if (verses.length > 0) {
939
1025
  const mergedVerses = this.mergeRanges(verses)
@@ -985,7 +1071,6 @@ class CodexParser {
985
1071
  : Math.max(...Array.from(chapterVerses[lastChapter]).filter((verse) => verse > 0)),
986
1072
  }
987
1073
 
988
- // Reattach the reference method to the combined passage
989
1074
  combined.reference = function () {
990
1075
  return this.scripture.passage
991
1076
  }
@@ -1124,7 +1209,7 @@ class CodexParser {
1124
1209
  : [Number(verseRange)]
1125
1210
 
1126
1211
  for (const v of verseNumbers) {
1127
- if (!chapterVerses.includes(v)) {
1212
+ if (isNaN(v) || v <= 0 || !chapterVerses.includes(v)) {
1128
1213
  return this.validationError(104, `Verse number ${v} does not exist in ${book} ${chapter}`)
1129
1214
  }
1130
1215
  }
@@ -1184,21 +1269,18 @@ class CodexParser {
1184
1269
  const { originalText, abbr, original } = passage
1185
1270
  const newReference = useAbbreviations ? abbr : original
1186
1271
 
1187
- const regex = new RegExp(`${originalText}`, "g")
1272
+ const regex = new RegExp(`${originalText.replace(/([.*+?^${}()|[\]\\])/g, "\\$1")}`, "g")
1188
1273
 
1189
- // Find all matches
1190
1274
  const matches = [...result.matchAll(regex)]
1191
1275
  if (matches.length > 0) {
1192
- // Process matches in reverse to avoid index shifting
1193
1276
  for (let j = matches.length - 1; j >= 0; j--) {
1194
1277
  const match = matches[j]
1195
1278
  const startIndex = match.index
1196
1279
  const endIndex = startIndex + match[0].length
1197
- const leadingSpace = match[1] || "" // Capture leading spaces
1280
+ const leadingSpace = match[1] || ""
1198
1281
  const hasOpeningParen = match[2] === "("
1199
1282
  const hasClosingParen = match[3] === ")"
1200
- const trailingSpace = match[4] || " " // Capture trailing spaces
1201
- // Preserve parentheses if present
1283
+ const trailingSpace = match[4] || " "
1202
1284
  const replacement =
1203
1285
  hasOpeningParen && hasClosingParen
1204
1286
  ? `${leadingSpace}(${newReference})${trailingSpace}`
@@ -1210,6 +1292,7 @@ class CodexParser {
1210
1292
 
1211
1293
  return result
1212
1294
  }
1295
+
1213
1296
  /**
1214
1297
  * Checks if all references in the passages array are from the same book.
1215
1298
  * @returns {boolean} True if all passages are from the same book, false otherwise.