codexparser 0.1.96 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.trunk/trunk.yaml +4 -4
  2. package/REFACTORING.md +214 -0
  3. package/index.js +1 -1
  4. package/package.json +4 -24
  5. package/src/{CodexParser.js → CodexParser.js.backup} +333 -274
  6. package/src/core/CodexParser.js +353 -0
  7. package/src/core/PassageCollection.js +404 -0
  8. package/src/core/PassageValidator.js +107 -0
  9. package/src/core/ReferenceParser.js +622 -0
  10. package/src/core/ScriptureScanner.js +235 -0
  11. package/src/core/VersificationHandler.js +143 -0
  12. package/src/core/VersionHandler.js +77 -0
  13. package/src/data/toc.js +7 -0
  14. package/src/format/osis.js +210 -0
  15. package/src/index.js +12 -0
  16. package/src/utils/PassageUtils.js +117 -0
  17. package/src/utils/chapterVerseCombine.js +65 -0
  18. package/.babelrc +0 -5
  19. package/src/chapterVerseCombine.js +0 -63
  20. package/src/toc.js +0 -2
  21. package/webpack.config.js +0 -38
  22. /package/src/{abbr → data/abbr}/sbl.js +0 -0
  23. /package/src/{bible.js → data/bible.js} +0 -0
  24. /package/src/{chapter_verses → data/chapter_verses}/1chronicles.js +0 -0
  25. /package/src/{chapter_verses → data/chapter_verses}/1corinthians.js +0 -0
  26. /package/src/{chapter_verses → data/chapter_verses}/1john.js +0 -0
  27. /package/src/{chapter_verses → data/chapter_verses}/1kings.js +0 -0
  28. /package/src/{chapter_verses → data/chapter_verses}/1peter.js +0 -0
  29. /package/src/{chapter_verses → data/chapter_verses}/1samuel.js +0 -0
  30. /package/src/{chapter_verses → data/chapter_verses}/1thessalonians.js +0 -0
  31. /package/src/{chapter_verses → data/chapter_verses}/1timothy.js +0 -0
  32. /package/src/{chapter_verses → data/chapter_verses}/2chronicles.js +0 -0
  33. /package/src/{chapter_verses → data/chapter_verses}/2corinthians.js +0 -0
  34. /package/src/{chapter_verses → data/chapter_verses}/2kings.js +0 -0
  35. /package/src/{chapter_verses → data/chapter_verses}/2peter.js +0 -0
  36. /package/src/{chapter_verses → data/chapter_verses}/2samuel.js +0 -0
  37. /package/src/{chapter_verses → data/chapter_verses}/2thessalonians.js +0 -0
  38. /package/src/{chapter_verses → data/chapter_verses}/2timothy.js +0 -0
  39. /package/src/{chapter_verses → data/chapter_verses}/acts.js +0 -0
  40. /package/src/{chapter_verses → data/chapter_verses}/amos.js +0 -0
  41. /package/src/{chapter_verses → data/chapter_verses}/colossians.js +0 -0
  42. /package/src/{chapter_verses → data/chapter_verses}/daniel.js +0 -0
  43. /package/src/{chapter_verses → data/chapter_verses}/deuteronomy.js +0 -0
  44. /package/src/{chapter_verses → data/chapter_verses}/ecclesiastes.js +0 -0
  45. /package/src/{chapter_verses → data/chapter_verses}/ephesians.js +0 -0
  46. /package/src/{chapter_verses → data/chapter_verses}/esther.js +0 -0
  47. /package/src/{chapter_verses → data/chapter_verses}/exodus.js +0 -0
  48. /package/src/{chapter_verses → data/chapter_verses}/ezekiel.js +0 -0
  49. /package/src/{chapter_verses → data/chapter_verses}/ezra.js +0 -0
  50. /package/src/{chapter_verses → data/chapter_verses}/galatians.js +0 -0
  51. /package/src/{chapter_verses → data/chapter_verses}/genesis.js +0 -0
  52. /package/src/{chapter_verses → data/chapter_verses}/habakkuk.js +0 -0
  53. /package/src/{chapter_verses → data/chapter_verses}/haggai.js +0 -0
  54. /package/src/{chapter_verses → data/chapter_verses}/hebrews.js +0 -0
  55. /package/src/{chapter_verses → data/chapter_verses}/hosea.js +0 -0
  56. /package/src/{chapter_verses → data/chapter_verses}/isaiah.js +0 -0
  57. /package/src/{chapter_verses → data/chapter_verses}/james.js +0 -0
  58. /package/src/{chapter_verses → data/chapter_verses}/jeremiah.js +0 -0
  59. /package/src/{chapter_verses → data/chapter_verses}/job.js +0 -0
  60. /package/src/{chapter_verses → data/chapter_verses}/joel.js +0 -0
  61. /package/src/{chapter_verses → data/chapter_verses}/john.js +0 -0
  62. /package/src/{chapter_verses → data/chapter_verses}/jonah.js +0 -0
  63. /package/src/{chapter_verses → data/chapter_verses}/joshua.js +0 -0
  64. /package/src/{chapter_verses → data/chapter_verses}/judges.js +0 -0
  65. /package/src/{chapter_verses → data/chapter_verses}/lamentations.js +0 -0
  66. /package/src/{chapter_verses → data/chapter_verses}/leviticus.js +0 -0
  67. /package/src/{chapter_verses → data/chapter_verses}/luke.js +0 -0
  68. /package/src/{chapter_verses → data/chapter_verses}/malachi.js +0 -0
  69. /package/src/{chapter_verses → data/chapter_verses}/mark.js +0 -0
  70. /package/src/{chapter_verses → data/chapter_verses}/matthew.js +0 -0
  71. /package/src/{chapter_verses → data/chapter_verses}/micah.js +0 -0
  72. /package/src/{chapter_verses → data/chapter_verses}/nahum.js +0 -0
  73. /package/src/{chapter_verses → data/chapter_verses}/nehemiah.js +0 -0
  74. /package/src/{chapter_verses → data/chapter_verses}/numbers.js +0 -0
  75. /package/src/{chapter_verses → data/chapter_verses}/philippians.js +0 -0
  76. /package/src/{chapter_verses → data/chapter_verses}/proverbs.js +0 -0
  77. /package/src/{chapter_verses → data/chapter_verses}/psalms.js +0 -0
  78. /package/src/{chapter_verses → data/chapter_verses}/revelation.js +0 -0
  79. /package/src/{chapter_verses → data/chapter_verses}/romans.js +0 -0
  80. /package/src/{chapter_verses → data/chapter_verses}/ruth.js +0 -0
  81. /package/src/{chapter_verses → data/chapter_verses}/songs.js +0 -0
  82. /package/src/{chapter_verses → data/chapter_verses}/titus.js +0 -0
  83. /package/src/{chapter_verses → data/chapter_verses}/zechariah.js +0 -0
  84. /package/src/{chapter_verses → data/chapter_verses}/zephaniah.js +0 -0
  85. /package/src/{esv.js → data/esv.js} +0 -0
  86. /package/src/{versifications → data/versifications}/1chronicles.js +0 -0
  87. /package/src/{versifications → data/versifications}/1kings.js +0 -0
  88. /package/src/{versifications → data/versifications}/1samuel.js +0 -0
  89. /package/src/{versifications → data/versifications}/2chronicles.js +0 -0
  90. /package/src/{versifications → data/versifications}/2samuel.js +0 -0
  91. /package/src/{versifications → data/versifications}/daniel.js +0 -0
  92. /package/src/{versifications → data/versifications}/deuteronomy.js +0 -0
  93. /package/src/{versifications → data/versifications}/ecclesiastes.js +0 -0
  94. /package/src/{versifications → data/versifications}/exodus.js +0 -0
  95. /package/src/{versifications → data/versifications}/ezekiel.js +0 -0
  96. /package/src/{versifications → data/versifications}/genesis.js +0 -0
  97. /package/src/{versifications → data/versifications}/hosea.js +0 -0
  98. /package/src/{versifications → data/versifications}/isaiah.js +0 -0
  99. /package/src/{versifications → data/versifications}/jeremiah.js +0 -0
  100. /package/src/{versifications → data/versifications}/job.js +0 -0
  101. /package/src/{versifications → data/versifications}/joel.js +0 -0
  102. /package/src/{versifications → data/versifications}/jonah.js +0 -0
  103. /package/src/{versifications → data/versifications}/joshua.js +0 -0
  104. /package/src/{versifications → data/versifications}/leviticus.js +0 -0
  105. /package/src/{versifications → data/versifications}/malachi.js +0 -0
  106. /package/src/{versifications → data/versifications}/micah.js +0 -0
  107. /package/src/{versifications → data/versifications}/nahum.js +0 -0
  108. /package/src/{versifications → data/versifications}/nehemiah.js +0 -0
  109. /package/src/{versifications → data/versifications}/numbers.js +0 -0
  110. /package/src/{versifications → data/versifications}/proverbs.js +0 -0
  111. /package/src/{versifications → data/versifications}/psalms.js +0 -0
  112. /package/src/{versifications → data/versifications}/song.js +0 -0
  113. /package/src/{versifications → data/versifications}/zechariah.js +0 -0
  114. /package/src/{versified.js → data/versified.js} +0 -0
  115. /package/src/{abbr.js → format/abbr.js} +0 -0
  116. /package/src/{functions.js → utils/functions.js} +0 -0
  117. /package/src/{regex.js → utils/regex.js} +0 -0
@@ -0,0 +1,622 @@
1
+ /**
2
+ * ReferenceParser.js
3
+ * Parses scripture references into structured passage objects
4
+ */
5
+
6
+ const bible = require("../data/bible")
7
+ const abbreviations = require("../format/abbr")
8
+ const sblAbbreviations = require("../data/abbr/sbl")
9
+ const PassageUtils = require("../utils/PassageUtils")
10
+ const PassageValidator = require("./PassageValidator")
11
+ const VersionHandler = require("./VersionHandler")
12
+ const { formatOsis, formatOsisNumeric } = require("../format/osis")
13
+
14
+ /**
15
+ * Parses scripture references
16
+ */
17
+ class ReferenceParser {
18
+ static REFERENCE_TYPES = {
19
+ SINGLE_CHAPTER: "single_chapter",
20
+ CHAPTER_VERSE: "chapter_verse",
21
+ CHAPTER_VERSE_RANGE: "chapter_verse_range",
22
+ COMMA_SEPARATED: "comma_separated_verses",
23
+ CHAPTER_RANGE: "chapter_range",
24
+ MULTI_CHAPTER_RANGE: "multi_chapter_verse_range",
25
+ BOOK_ONLY: "book_only",
26
+ }
27
+
28
+ #config
29
+
30
+ constructor(config = {}) {
31
+ this.#config = config
32
+ }
33
+
34
+ /**
35
+ * Parses found references into structured passage objects
36
+ * @param {Array} foundReferences - Array of found references from scanner
37
+ * @param {string} currentVersion - Current Bible version
38
+ * @returns {Array} Array of parsed passage objects
39
+ */
40
+ parse(foundReferences, currentVersion = null) {
41
+ return foundReferences.map((reference) => {
42
+ const book = this.#normalizeBookName(reference.book)
43
+ const testament = bible.old.includes(book) ? "old" : "new"
44
+
45
+ const parsedPassage = {
46
+ original: `${reference.book} ${reference.reference}`,
47
+ book,
48
+ chapter: null,
49
+ verses: [],
50
+ type: reference.type,
51
+ testament,
52
+ startIndex: reference.startIndex,
53
+ endIndex: reference.endIndex,
54
+ originalText: reference.originalText,
55
+ version: VersionHandler.getVersion(reference.version || currentVersion, testament),
56
+ passages: [],
57
+ scripture: null,
58
+ valid: true,
59
+ start: null,
60
+ end: null,
61
+ abbr: null,
62
+ }
63
+
64
+ // Clean reference for parsing
65
+ let cleanReference = reference.reference.replace(/\s*(LXX|MT)$/i, "").trim()
66
+ if (cleanReference.endsWith(",")) {
67
+ cleanReference = cleanReference.slice(0, -1).trim()
68
+ }
69
+
70
+ // Handle book-only or empty references
71
+ if (!cleanReference && this.#config.booksOnly) {
72
+ parsedPassage.type = ReferenceParser.REFERENCE_TYPES.BOOK_ONLY
73
+ } else if (!cleanReference || cleanReference.match(/^\d+\s*[:;]?\s*$/)) {
74
+ this.#handleEmptyReference(parsedPassage, cleanReference)
75
+ } else if (reference.type === ReferenceParser.REFERENCE_TYPES.COMMA_SEPARATED) {
76
+ this.#handleCommaSeparated(parsedPassage, cleanReference)
77
+ } else {
78
+ this.#parseReferenceParts(parsedPassage, cleanReference)
79
+ }
80
+
81
+ // Populate passages and scripture
82
+ parsedPassage.passages = this.#populatePassage(parsedPassage)
83
+ parsedPassage.scripture = this.#formatScripture(parsedPassage)
84
+ parsedPassage.valid = PassageValidator.validate(parsedPassage, cleanReference)
85
+ // Set abbreviation
86
+ this.#setAbbreviation(parsedPassage, reference.reference)
87
+
88
+ // Handle multi-chapter range
89
+ if (parsedPassage.type === ReferenceParser.REFERENCE_TYPES.MULTI_CHAPTER_RANGE) {
90
+ this.#handleMultiChapterRange(parsedPassage, cleanReference)
91
+ } else {
92
+ delete parsedPassage.to
93
+ }
94
+
95
+ // Calculate start and end
96
+ this.#calculateStartEnd(parsedPassage)
97
+
98
+ // Attach per-passage version helpers for backward compatibility
99
+ this.#attachVersionHelpers(parsedPassage)
100
+
101
+ return parsedPassage
102
+ })
103
+ }
104
+
105
+ /**
106
+ * Normalizes book names using abbreviations or full names
107
+ * @private
108
+ */
109
+ #normalizeBookName(book) {
110
+ if (typeof book !== "string") {
111
+ book = book[0]
112
+ }
113
+ book = book.toLowerCase()
114
+
115
+ const bookified = abbreviations[Object.keys(abbreviations).find((abbr) => abbr.toLowerCase() === book)]
116
+ if (bookified) {
117
+ return bookified
118
+ }
119
+
120
+ return (
121
+ bible.new.find((b) => b.toLowerCase() === book) || bible.old.find((b) => b.toLowerCase() === book) || book
122
+ )
123
+ }
124
+
125
+ /**
126
+ * Attaches version helper methods to a single passage object
127
+ * @private
128
+ */
129
+ #attachVersionHelpers(passage) {
130
+ const self = this
131
+ const computeConverted = (srcPassage, targetAbbr) => {
132
+ const versionObj = VersionHandler.getVersionObject(targetAbbr)
133
+ const cloned = JSON.parse(JSON.stringify(srcPassage))
134
+ cloned.version = versionObj
135
+
136
+ // Remap chapters/verses according to versification, if present
137
+ cloned.passages.forEach((sub) => {
138
+ if (sub.versification && sub.versification[targetAbbr]) {
139
+ const [ch, v] = sub.versification[targetAbbr].split(":").map(Number)
140
+ sub.chapter = ch
141
+ sub.verse = v
142
+ }
143
+ })
144
+
145
+ // Sort and recompute summary fields
146
+ cloned.passages.sort((a, b) => a.chapter - b.chapter || a.verse - b.verse)
147
+
148
+ if (cloned.passages.length > 0) {
149
+ cloned.start = {
150
+ book: cloned.book,
151
+ chapter: cloned.passages[0].chapter,
152
+ verse: cloned.passages[0].verse,
153
+ }
154
+ cloned.end = {
155
+ book: cloned.book,
156
+ chapter: cloned.passages[cloned.passages.length - 1].chapter,
157
+ verse: cloned.passages[cloned.passages.length - 1].verse,
158
+ }
159
+ }
160
+
161
+ const chapterVersesMap = {}
162
+ cloned.passages.forEach((p) => {
163
+ if (!chapterVersesMap[p.chapter]) chapterVersesMap[p.chapter] = new Set()
164
+ chapterVersesMap[p.chapter].add(p.verse)
165
+ })
166
+
167
+ const sortedChs = Object.keys(chapterVersesMap)
168
+ .map(Number)
169
+ .sort((a, b) => a - b)
170
+ const chapterStrs = []
171
+
172
+ const mergeRanges = (verses) => {
173
+ const sorted = [...verses].sort((a, b) => a - b)
174
+ const merged = []
175
+ if (sorted.length === 0) return merged
176
+ let start = sorted[0]
177
+ let end = sorted[0]
178
+ for (let i = 1; i < sorted.length; i++) {
179
+ if (sorted[i] === end + 1) {
180
+ end = sorted[i]
181
+ } else {
182
+ merged.push(start === end ? `${start}` : `${start}-${end}`)
183
+ start = end = sorted[i]
184
+ }
185
+ }
186
+ merged.push(start === end ? `${start}` : `${start}-${end}`)
187
+ return merged
188
+ }
189
+
190
+ sortedChs.forEach((ch) => {
191
+ const vs = Array.from(chapterVersesMap[ch])
192
+ .filter((v) => v > 0)
193
+ .sort((a, b) => a - b)
194
+ if (vs.length > 0) {
195
+ const merged = mergeRanges(vs)
196
+ chapterStrs.push(`${ch}:${merged.join(",")}`)
197
+ }
198
+ })
199
+
200
+ if (chapterStrs.length === 0) {
201
+ return cloned // no verses, return as-is
202
+ }
203
+
204
+ const firstCh = sortedChs[0]
205
+ const lastCh = sortedChs[sortedChs.length - 1]
206
+ cloned.chapter = firstCh
207
+
208
+ const mergedFirst = mergeRanges(chapterVersesMap[firstCh] || new Set())
209
+ cloned.verses = mergedFirst
210
+
211
+ if (firstCh !== lastCh) {
212
+ cloned.type = ReferenceParser.REFERENCE_TYPES.MULTI_CHAPTER_RANGE
213
+ cloned.to = {
214
+ book: cloned.book,
215
+ chapter: lastCh,
216
+ verses: mergeRanges(chapterVersesMap[lastCh] || new Set()),
217
+ }
218
+ cloned.original = `${cloned.book} ${chapterStrs.join("; ")}`
219
+ } else {
220
+ const hasRangeOrMultiple =
221
+ mergedFirst.length > 1 || (mergedFirst.length === 1 && mergedFirst[0].includes("-"))
222
+ cloned.type = hasRangeOrMultiple
223
+ ? ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE_RANGE
224
+ : ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE
225
+ if (cloned.to) delete cloned.to
226
+ cloned.original = `${cloned.book} ${chapterStrs[0]}`
227
+ }
228
+
229
+ const chString = chapterStrs.join("; ")
230
+ cloned.scripture = {
231
+ passage: `${cloned.book} ${chString}`,
232
+ cv: chString,
233
+ hash: formatOsis(cloned),
234
+ osisNumeric: formatOsisNumeric(cloned),
235
+ }
236
+
237
+ // Set abbr
238
+ const sblEntry = Object.entries(sblAbbreviations).find(
239
+ ([key]) => key.toLowerCase() === cloned.book.toLowerCase()
240
+ )
241
+ const suffix = versionObj.abbreviation === "eng" ? "" : ` ${versionObj.value}`
242
+ if (sblEntry) {
243
+ const { value, abbr } = sblEntry[1]
244
+ cloned.abbr = abbr
245
+ ? `${value}. ${cloned.scripture.cv}${suffix}`
246
+ : `${value} ${cloned.scripture.cv}${suffix}`
247
+ } else {
248
+ cloned.abbr = `${cloned.book} ${cloned.scripture.cv}${suffix}`
249
+ }
250
+
251
+ return cloned
252
+ }
253
+
254
+ passage.getVersion = function (targetVersion) {
255
+ const targetAbbr = targetVersion.toLowerCase() === "bhs" ? "mt" : targetVersion.toLowerCase()
256
+ return computeConverted(this, targetAbbr)
257
+ }
258
+ passage.getLXX = function () {
259
+ return this.getVersion("lxx")
260
+ }
261
+ passage.getMT = function () {
262
+ return this.getVersion("mt")
263
+ }
264
+ passage.getBHS = function () {
265
+ return this.getVersion("mt")
266
+ }
267
+ passage.getEnglish = function () {
268
+ return this.getVersion("eng")
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Handles empty or chapter-only references
274
+ * @private
275
+ */
276
+ #handleEmptyReference(passage, cleanReference) {
277
+ const chapterMatch = cleanReference.match(/\d+/) || ["1"]
278
+ const chapter = Number(chapterMatch[0])
279
+ passage.chapter = chapter
280
+ passage.type = ReferenceParser.REFERENCE_TYPES.SINGLE_CHAPTER
281
+
282
+ const chapterVerses = PassageUtils.getChapterVerses(passage.book, chapter)
283
+ if (chapterVerses.length) {
284
+ const startVerse = chapterVerses[0]
285
+ const endVerse = chapterVerses[chapterVerses.length - 1]
286
+ passage.verses = [`${startVerse}-${endVerse}`]
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Handles comma-separated verses
292
+ * @private
293
+ */
294
+ #handleCommaSeparated(passage, cleanReference) {
295
+ const [chapter, verses] = cleanReference.split(":")
296
+ passage.chapter = Number(chapter)
297
+ passage.verses = verses.split(",").map((v) => v.trim())
298
+ }
299
+
300
+ /**
301
+ * Parses reference parts into chapter and verse components
302
+ * @private
303
+ */
304
+ #parseReferenceParts(passage, reference) {
305
+ const singleChapterBook = PassageUtils.SINGLE_CHAPTER_BOOKS.find((b) => Object.keys(b)[0] === passage.book)
306
+ const parts = reference
307
+ .split(",")
308
+ .map((part) => part.trim())
309
+ .filter((part) => part)
310
+
311
+ parts.forEach((part, index) => {
312
+ const isFirstPart = index === 0
313
+
314
+ // Handle multi-chapter ranges (e.g., "2:1-3:19")
315
+ if (part.includes("-") && part.includes(":")) {
316
+ const [start, end] = part.split("-").map((s) => s.trim())
317
+ const startParts = start.split(/[:.]/).map((s) => s.trim())
318
+ const endParts = end.split(/[:.]/).map((s) => s.trim())
319
+
320
+ if (startParts.length > 1 && endParts.length > 1 && startParts[0] !== endParts[0]) {
321
+ passage.type = ReferenceParser.REFERENCE_TYPES.MULTI_CHAPTER_RANGE
322
+ passage.chapter = Number(startParts[0])
323
+ passage.verses = [startParts[1] || "1"]
324
+ passage.to = {
325
+ book: passage.book,
326
+ chapter: Number(endParts[0]),
327
+ verses: [endParts[1] || "1"],
328
+ }
329
+ return
330
+ }
331
+ }
332
+
333
+ // Handle chapter-only references
334
+ if (!part.includes(":") && !part.includes("-") && !singleChapterBook) {
335
+ this.#parseChapterOnly(passage, part)
336
+ } else if (part.includes(":")) {
337
+ this.#parseChapterVerse(passage, part, isFirstPart)
338
+ } else if (singleChapterBook) {
339
+ this.#parseSingleChapterBook(passage, part, isFirstPart && parts.length === 1)
340
+ } else if (part.includes("-")) {
341
+ this.#parseRange(passage, part, isFirstPart)
342
+ } else {
343
+ this.#parseSingleNumber(passage, part, isFirstPart)
344
+ }
345
+ })
346
+ }
347
+
348
+ /**
349
+ * Parses chapter-only reference
350
+ * @private
351
+ */
352
+ #parseChapterOnly(passage, part) {
353
+ const chapter = Number(part.replace(/[^0-9]/g, ""))
354
+ if (chapter > 0) {
355
+ passage.chapter = chapter
356
+ passage.type = ReferenceParser.REFERENCE_TYPES.SINGLE_CHAPTER
357
+ const chapterVerses = PassageUtils.getChapterVerses(passage.book, chapter)
358
+ if (chapterVerses.length) {
359
+ const startVerse = chapterVerses[0]
360
+ const endVerse = chapterVerses[chapterVerses.length - 1]
361
+ passage.verses = [`${startVerse}-${endVerse}`]
362
+ }
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Parses chapter:verse references
368
+ * @private
369
+ */
370
+ #parseChapterVerse(passage, part, isFirstPart) {
371
+ const [chapter, verse] = part.split(/[:.]/).map((s) => s.trim())
372
+ if (isFirstPart) passage.chapter = Number(chapter)
373
+
374
+ passage.type = verse.includes("-")
375
+ ? ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE_RANGE
376
+ : verse.includes(",")
377
+ ? ReferenceParser.REFERENCE_TYPES.COMMA_SEPARATED
378
+ : ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE
379
+
380
+ if (verse.includes(",")) {
381
+ passage.verses.push(...verse.split(",").map((v) => v.trim()))
382
+ } else {
383
+ passage.verses.push(verse)
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Parses references for single-chapter books
389
+ * @private
390
+ */
391
+ #parseSingleChapterBook(passage, part, isWholeChapter) {
392
+ const verseCount = PassageUtils.getChapterVerses(passage.book, 1).length
393
+
394
+ if (part === "1" && isWholeChapter) {
395
+ passage.chapter = 1
396
+ passage.type = ReferenceParser.REFERENCE_TYPES.SINGLE_CHAPTER
397
+ passage.verses = [`1-${verseCount}`]
398
+ } else if (part.includes("-")) {
399
+ passage.chapter = 1
400
+ passage.verses.push(part)
401
+ passage.type = ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE_RANGE
402
+ } else if (part.includes(",")) {
403
+ passage.chapter = 1
404
+ passage.verses.push(...part.split(",").map((v) => v.trim()))
405
+ passage.type = ReferenceParser.REFERENCE_TYPES.COMMA_SEPARATED
406
+ } else {
407
+ const num = Number(part)
408
+ if (num > 0) {
409
+ passage.chapter = 1
410
+ passage.verses.push(num)
411
+ passage.type = ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE
412
+ }
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Parses range references
418
+ * @private
419
+ */
420
+ #parseRange(passage, part, isFirstPart) {
421
+ if (!passage.chapter && isFirstPart) {
422
+ const [start, end] = part.split("-").map(Number)
423
+ passage.chapter = start
424
+ const startVerses = PassageUtils.getChapterVerses(passage.book, start)
425
+ passage.verses = [`${startVerses[0]}-${startVerses[startVerses.length - 1]}`]
426
+ passage.to = {
427
+ book: passage.book,
428
+ chapter: end,
429
+ verses: [
430
+ `${PassageUtils.getChapterVerses(passage.book, end)[0]}-${
431
+ PassageUtils.getChapterVerses(passage.book, end).slice(-1)[0]
432
+ }`,
433
+ ],
434
+ }
435
+ passage.type = ReferenceParser.REFERENCE_TYPES.CHAPTER_RANGE
436
+ } else {
437
+ passage.verses.push(part)
438
+ passage.type = ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE_RANGE
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Parses single number references
444
+ * @private
445
+ */
446
+ #parseSingleNumber(passage, part, isFirstPart) {
447
+ if (isFirstPart && !passage.chapter) {
448
+ passage.chapter = Number(part)
449
+ passage.type = ReferenceParser.REFERENCE_TYPES.SINGLE_CHAPTER
450
+ const chapterVerses = PassageUtils.getChapterVerses(passage.book, passage.chapter)
451
+ if (chapterVerses.length) {
452
+ passage.verses = [`${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`]
453
+ }
454
+ } else {
455
+ passage.verses.push(Number(part))
456
+ passage.type = ReferenceParser.REFERENCE_TYPES.COMMA_SEPARATED
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Handles multi-chapter range references
462
+ * @private
463
+ */
464
+ #handleMultiChapterRange(passage, reference) {
465
+ const parts = reference.split(",")
466
+ const lastPart = parts[parts.length - 1].trim()
467
+ const [endChapter, endVerse] = lastPart.split(/[:.]/).map((s) => s.trim())
468
+
469
+ if (endChapter !== String(passage.chapter)) {
470
+ passage.to = {
471
+ book: passage.book,
472
+ chapter: Number(endChapter),
473
+ verses: endVerse ? [endVerse] : ["1"],
474
+ }
475
+ }
476
+ }
477
+
478
+ /**
479
+ * Populates passage with expanded verse objects
480
+ * @private
481
+ */
482
+ #populatePassage(passage) {
483
+ const { book, chapter, verses, type, to } = passage
484
+
485
+ if (type === ReferenceParser.REFERENCE_TYPES.SINGLE_CHAPTER) {
486
+ const chapterVerses = PassageUtils.getChapterVerses(book, chapter)
487
+ return PassageUtils.expandVerses(book, chapter, [
488
+ `${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`,
489
+ ])
490
+ }
491
+
492
+ if (
493
+ type === ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE ||
494
+ type === ReferenceParser.REFERENCE_TYPES.COMMA_SEPARATED ||
495
+ type === ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE_RANGE
496
+ ) {
497
+ return PassageUtils.expandVerses(book, chapter, verses)
498
+ }
499
+
500
+ if (type === ReferenceParser.REFERENCE_TYPES.CHAPTER_RANGE) {
501
+ const passages = []
502
+ if (to && to.chapter) {
503
+ for (let ch = chapter; ch <= to.chapter; ch++) {
504
+ const chapterVerses = PassageUtils.getChapterVerses(book, ch)
505
+ passages.push(
506
+ ...PassageUtils.expandVerses(book, ch, [
507
+ `${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`,
508
+ ])
509
+ )
510
+ }
511
+ }
512
+ return passages
513
+ }
514
+
515
+ if (type === ReferenceParser.REFERENCE_TYPES.MULTI_CHAPTER_RANGE) {
516
+ const passages = []
517
+ const startVerse = verses[0]?.includes("-") ? Number(verses[0].split("-")[0]) : Number(verses[0]) || 1
518
+ const endVerse = to?.verses?.[0]?.includes("-")
519
+ ? Number(to.verses[0].split("-")[1])
520
+ : Number(to?.verses?.[0]) || 1
521
+ const endChapter = to?.chapter || chapter
522
+
523
+ for (let ch = chapter; ch <= endChapter; ch++) {
524
+ const chapterVerses = PassageUtils.getChapterVerses(book, ch)
525
+ const from = ch === chapter ? startVerse : chapterVerses[0]
526
+ const toVerse = ch === endChapter ? endVerse : chapterVerses[chapterVerses.length - 1]
527
+ passages.push(...PassageUtils.expandVerses(book, ch, [`${from}-${toVerse}`]))
528
+ }
529
+ return passages
530
+ }
531
+
532
+ return []
533
+ }
534
+
535
+ /**
536
+ * Formats a passage into a human-readable reference
537
+ * @private
538
+ */
539
+ #formatScripture(passage) {
540
+ let combined = `${passage.book}`
541
+
542
+ if (passage.type === ReferenceParser.REFERENCE_TYPES.MULTI_CHAPTER_RANGE && passage.to) {
543
+ combined += ` ${PassageUtils.formatChapterVerse(
544
+ passage.chapter,
545
+ passage.verses
546
+ )}-${PassageUtils.formatChapterVerse(passage.to.chapter, passage.to.verses)}`
547
+ } else if (
548
+ passage.type === ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE_RANGE ||
549
+ passage.type === ReferenceParser.REFERENCE_TYPES.COMMA_SEPARATED
550
+ ) {
551
+ combined += ` ${PassageUtils.formatChapterVerse(passage.chapter, passage.verses)}`
552
+ } else if (passage.type === ReferenceParser.REFERENCE_TYPES.CHAPTER_RANGE && passage.to) {
553
+ combined += ` ${passage.chapter}-${passage.to.chapter}`
554
+ } else {
555
+ combined += ` ${PassageUtils.formatChapterVerse(passage.chapter, passage.verses)}`
556
+ }
557
+
558
+ const cv = passage.to
559
+ ? `${PassageUtils.formatChapterVerse(passage.chapter, passage.verses)}-${PassageUtils.formatChapterVerse(
560
+ passage.to.chapter,
561
+ passage.to.verses
562
+ )}`
563
+ : PassageUtils.formatChapterVerse(passage.chapter, passage.verses)
564
+
565
+ const hash = formatOsis(passage)
566
+
567
+ return {
568
+ passage: combined,
569
+ cv: cv,
570
+ hash: hash,
571
+ osisNumeric: formatOsisNumeric(passage),
572
+ }
573
+ }
574
+
575
+ /**
576
+ * Sets the SBL abbreviation for a passage
577
+ * @private
578
+ */
579
+ #setAbbreviation(passage, reference) {
580
+ const sblEntry = Object.entries(sblAbbreviations).find(
581
+ ([key]) => key.toLowerCase() === passage.book.toLowerCase()
582
+ )
583
+
584
+ if (sblEntry) {
585
+ const { value, abbr } = sblEntry[1]
586
+ const ref = reference.replace(/\s*(LXX|MT)$/i, "").trim()
587
+ const suffix = passage.version.value !== "ENG" ? " " + passage.version.value : ""
588
+ passage.abbr = abbr ? `${value}. ${ref}${suffix}` : `${value} ${ref}${suffix}`
589
+ } else {
590
+ passage.abbr = passage.original
591
+ }
592
+ }
593
+
594
+ /**
595
+ * Calculates start and end based on passages array
596
+ * @private
597
+ */
598
+ #calculateStartEnd(passage) {
599
+ if (passage.passages.length > 0) {
600
+ const sortedPassages = passage.passages.slice().sort((a, b) => {
601
+ if (a.chapter !== b.chapter) return a.chapter - b.chapter
602
+ return a.verse - b.verse
603
+ })
604
+
605
+ const firstPassage = sortedPassages[0]
606
+ const lastPassage = sortedPassages[sortedPassages.length - 1]
607
+
608
+ passage.start = {
609
+ book: firstPassage.book,
610
+ chapter: firstPassage.chapter,
611
+ verse: firstPassage.verse,
612
+ }
613
+ passage.end = {
614
+ book: lastPassage.book,
615
+ chapter: lastPassage.chapter,
616
+ verse: lastPassage.verse,
617
+ }
618
+ }
619
+ }
620
+ }
621
+
622
+ module.exports = ReferenceParser