codexparser 0.1.97 → 0.3.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 (120) hide show
  1. package/.trunk/trunk.yaml +3 -3
  2. package/CHANGELOG.md +18 -0
  3. package/README.md +76 -0
  4. package/REFACTORING.md +214 -0
  5. package/RELEASE_NOTES_v0.2.0.md +5 -0
  6. package/index.js +1 -1
  7. package/package.json +4 -24
  8. package/src/{CodexParser.js → CodexParser.js.backup} +303 -247
  9. package/src/core/CodexParser.js +353 -0
  10. package/src/core/PassageCollection.js +404 -0
  11. package/src/core/PassageValidator.js +107 -0
  12. package/src/core/ReferenceParser.js +638 -0
  13. package/src/core/ScriptureScanner.js +235 -0
  14. package/src/core/VersificationHandler.js +143 -0
  15. package/src/core/VersionHandler.js +77 -0
  16. package/src/data/toc.js +7 -0
  17. package/src/format/osis.js +210 -0
  18. package/src/index.js +12 -0
  19. package/src/utils/PassageUtils.js +117 -0
  20. package/src/utils/chapterVerseCombine.js +65 -0
  21. package/.babelrc +0 -5
  22. package/src/chapterVerseCombine.js +0 -63
  23. package/src/toc.js +0 -2
  24. package/webpack.config.js +0 -38
  25. /package/src/{abbr → data/abbr}/sbl.js +0 -0
  26. /package/src/{bible.js → data/bible.js} +0 -0
  27. /package/src/{chapter_verses → data/chapter_verses}/1chronicles.js +0 -0
  28. /package/src/{chapter_verses → data/chapter_verses}/1corinthians.js +0 -0
  29. /package/src/{chapter_verses → data/chapter_verses}/1john.js +0 -0
  30. /package/src/{chapter_verses → data/chapter_verses}/1kings.js +0 -0
  31. /package/src/{chapter_verses → data/chapter_verses}/1peter.js +0 -0
  32. /package/src/{chapter_verses → data/chapter_verses}/1samuel.js +0 -0
  33. /package/src/{chapter_verses → data/chapter_verses}/1thessalonians.js +0 -0
  34. /package/src/{chapter_verses → data/chapter_verses}/1timothy.js +0 -0
  35. /package/src/{chapter_verses → data/chapter_verses}/2chronicles.js +0 -0
  36. /package/src/{chapter_verses → data/chapter_verses}/2corinthians.js +0 -0
  37. /package/src/{chapter_verses → data/chapter_verses}/2kings.js +0 -0
  38. /package/src/{chapter_verses → data/chapter_verses}/2peter.js +0 -0
  39. /package/src/{chapter_verses → data/chapter_verses}/2samuel.js +0 -0
  40. /package/src/{chapter_verses → data/chapter_verses}/2thessalonians.js +0 -0
  41. /package/src/{chapter_verses → data/chapter_verses}/2timothy.js +0 -0
  42. /package/src/{chapter_verses → data/chapter_verses}/acts.js +0 -0
  43. /package/src/{chapter_verses → data/chapter_verses}/amos.js +0 -0
  44. /package/src/{chapter_verses → data/chapter_verses}/colossians.js +0 -0
  45. /package/src/{chapter_verses → data/chapter_verses}/daniel.js +0 -0
  46. /package/src/{chapter_verses → data/chapter_verses}/deuteronomy.js +0 -0
  47. /package/src/{chapter_verses → data/chapter_verses}/ecclesiastes.js +0 -0
  48. /package/src/{chapter_verses → data/chapter_verses}/ephesians.js +0 -0
  49. /package/src/{chapter_verses → data/chapter_verses}/esther.js +0 -0
  50. /package/src/{chapter_verses → data/chapter_verses}/exodus.js +0 -0
  51. /package/src/{chapter_verses → data/chapter_verses}/ezekiel.js +0 -0
  52. /package/src/{chapter_verses → data/chapter_verses}/ezra.js +0 -0
  53. /package/src/{chapter_verses → data/chapter_verses}/galatians.js +0 -0
  54. /package/src/{chapter_verses → data/chapter_verses}/genesis.js +0 -0
  55. /package/src/{chapter_verses → data/chapter_verses}/habakkuk.js +0 -0
  56. /package/src/{chapter_verses → data/chapter_verses}/haggai.js +0 -0
  57. /package/src/{chapter_verses → data/chapter_verses}/hebrews.js +0 -0
  58. /package/src/{chapter_verses → data/chapter_verses}/hosea.js +0 -0
  59. /package/src/{chapter_verses → data/chapter_verses}/isaiah.js +0 -0
  60. /package/src/{chapter_verses → data/chapter_verses}/james.js +0 -0
  61. /package/src/{chapter_verses → data/chapter_verses}/jeremiah.js +0 -0
  62. /package/src/{chapter_verses → data/chapter_verses}/job.js +0 -0
  63. /package/src/{chapter_verses → data/chapter_verses}/joel.js +0 -0
  64. /package/src/{chapter_verses → data/chapter_verses}/john.js +0 -0
  65. /package/src/{chapter_verses → data/chapter_verses}/jonah.js +0 -0
  66. /package/src/{chapter_verses → data/chapter_verses}/joshua.js +0 -0
  67. /package/src/{chapter_verses → data/chapter_verses}/judges.js +0 -0
  68. /package/src/{chapter_verses → data/chapter_verses}/lamentations.js +0 -0
  69. /package/src/{chapter_verses → data/chapter_verses}/leviticus.js +0 -0
  70. /package/src/{chapter_verses → data/chapter_verses}/luke.js +0 -0
  71. /package/src/{chapter_verses → data/chapter_verses}/malachi.js +0 -0
  72. /package/src/{chapter_verses → data/chapter_verses}/mark.js +0 -0
  73. /package/src/{chapter_verses → data/chapter_verses}/matthew.js +0 -0
  74. /package/src/{chapter_verses → data/chapter_verses}/micah.js +0 -0
  75. /package/src/{chapter_verses → data/chapter_verses}/nahum.js +0 -0
  76. /package/src/{chapter_verses → data/chapter_verses}/nehemiah.js +0 -0
  77. /package/src/{chapter_verses → data/chapter_verses}/numbers.js +0 -0
  78. /package/src/{chapter_verses → data/chapter_verses}/philippians.js +0 -0
  79. /package/src/{chapter_verses → data/chapter_verses}/proverbs.js +0 -0
  80. /package/src/{chapter_verses → data/chapter_verses}/psalms.js +0 -0
  81. /package/src/{chapter_verses → data/chapter_verses}/revelation.js +0 -0
  82. /package/src/{chapter_verses → data/chapter_verses}/romans.js +0 -0
  83. /package/src/{chapter_verses → data/chapter_verses}/ruth.js +0 -0
  84. /package/src/{chapter_verses → data/chapter_verses}/songs.js +0 -0
  85. /package/src/{chapter_verses → data/chapter_verses}/titus.js +0 -0
  86. /package/src/{chapter_verses → data/chapter_verses}/zechariah.js +0 -0
  87. /package/src/{chapter_verses → data/chapter_verses}/zephaniah.js +0 -0
  88. /package/src/{esv.js → data/esv.js} +0 -0
  89. /package/src/{versifications → data/versifications}/1chronicles.js +0 -0
  90. /package/src/{versifications → data/versifications}/1kings.js +0 -0
  91. /package/src/{versifications → data/versifications}/1samuel.js +0 -0
  92. /package/src/{versifications → data/versifications}/2chronicles.js +0 -0
  93. /package/src/{versifications → data/versifications}/2samuel.js +0 -0
  94. /package/src/{versifications → data/versifications}/daniel.js +0 -0
  95. /package/src/{versifications → data/versifications}/deuteronomy.js +0 -0
  96. /package/src/{versifications → data/versifications}/ecclesiastes.js +0 -0
  97. /package/src/{versifications → data/versifications}/exodus.js +0 -0
  98. /package/src/{versifications → data/versifications}/ezekiel.js +0 -0
  99. /package/src/{versifications → data/versifications}/genesis.js +0 -0
  100. /package/src/{versifications → data/versifications}/hosea.js +0 -0
  101. /package/src/{versifications → data/versifications}/isaiah.js +0 -0
  102. /package/src/{versifications → data/versifications}/jeremiah.js +0 -0
  103. /package/src/{versifications → data/versifications}/job.js +0 -0
  104. /package/src/{versifications → data/versifications}/joel.js +0 -0
  105. /package/src/{versifications → data/versifications}/jonah.js +0 -0
  106. /package/src/{versifications → data/versifications}/joshua.js +0 -0
  107. /package/src/{versifications → data/versifications}/leviticus.js +0 -0
  108. /package/src/{versifications → data/versifications}/malachi.js +0 -0
  109. /package/src/{versifications → data/versifications}/micah.js +0 -0
  110. /package/src/{versifications → data/versifications}/nahum.js +0 -0
  111. /package/src/{versifications → data/versifications}/nehemiah.js +0 -0
  112. /package/src/{versifications → data/versifications}/numbers.js +0 -0
  113. /package/src/{versifications → data/versifications}/proverbs.js +0 -0
  114. /package/src/{versifications → data/versifications}/psalms.js +0 -0
  115. /package/src/{versifications → data/versifications}/song.js +0 -0
  116. /package/src/{versifications → data/versifications}/zechariah.js +0 -0
  117. /package/src/{versified.js → data/versified.js} +0 -0
  118. /package/src/{abbr.js → format/abbr.js} +0 -0
  119. /package/src/{functions.js → utils/functions.js} +0 -0
  120. /package/src/{regex.js → utils/regex.js} +0 -0
@@ -0,0 +1,638 @@
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
+ passage.convertVersion = function (targetVersion) {
271
+ const targetAbbr = targetVersion.toLowerCase() === "bhs" ? "mt" : targetVersion.toLowerCase()
272
+
273
+ // Check if any passages have versification data
274
+ const hasVersification = this.passages.some((p) => p.versification)
275
+
276
+ if (!hasVersification) {
277
+ // No versification exists, return a clone with updated version info only
278
+ const cloned = JSON.parse(JSON.stringify(this))
279
+ cloned.version = VersionHandler.getVersionObject(targetAbbr)
280
+ return cloned
281
+ }
282
+
283
+ // Has versification, use the full conversion
284
+ return computeConverted(this, targetAbbr)
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Handles empty or chapter-only references
290
+ * @private
291
+ */
292
+ #handleEmptyReference(passage, cleanReference) {
293
+ const chapterMatch = cleanReference.match(/\d+/) || ["1"]
294
+ const chapter = Number(chapterMatch[0])
295
+ passage.chapter = chapter
296
+ passage.type = ReferenceParser.REFERENCE_TYPES.SINGLE_CHAPTER
297
+
298
+ const chapterVerses = PassageUtils.getChapterVerses(passage.book, chapter)
299
+ if (chapterVerses.length) {
300
+ const startVerse = chapterVerses[0]
301
+ const endVerse = chapterVerses[chapterVerses.length - 1]
302
+ passage.verses = [`${startVerse}-${endVerse}`]
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Handles comma-separated verses
308
+ * @private
309
+ */
310
+ #handleCommaSeparated(passage, cleanReference) {
311
+ const [chapter, verses] = cleanReference.split(":")
312
+ passage.chapter = Number(chapter)
313
+ passage.verses = verses.split(",").map((v) => v.trim())
314
+ }
315
+
316
+ /**
317
+ * Parses reference parts into chapter and verse components
318
+ * @private
319
+ */
320
+ #parseReferenceParts(passage, reference) {
321
+ const singleChapterBook = PassageUtils.SINGLE_CHAPTER_BOOKS.find((b) => Object.keys(b)[0] === passage.book)
322
+ const parts = reference
323
+ .split(",")
324
+ .map((part) => part.trim())
325
+ .filter((part) => part)
326
+
327
+ parts.forEach((part, index) => {
328
+ const isFirstPart = index === 0
329
+
330
+ // Handle multi-chapter ranges (e.g., "2:1-3:19")
331
+ if (part.includes("-") && part.includes(":")) {
332
+ const [start, end] = part.split("-").map((s) => s.trim())
333
+ const startParts = start.split(/[:.]/).map((s) => s.trim())
334
+ const endParts = end.split(/[:.]/).map((s) => s.trim())
335
+
336
+ if (startParts.length > 1 && endParts.length > 1 && startParts[0] !== endParts[0]) {
337
+ passage.type = ReferenceParser.REFERENCE_TYPES.MULTI_CHAPTER_RANGE
338
+ passage.chapter = Number(startParts[0])
339
+ passage.verses = [startParts[1] || "1"]
340
+ passage.to = {
341
+ book: passage.book,
342
+ chapter: Number(endParts[0]),
343
+ verses: [endParts[1] || "1"],
344
+ }
345
+ return
346
+ }
347
+ }
348
+
349
+ // Handle chapter-only references
350
+ if (!part.includes(":") && !part.includes("-") && !singleChapterBook) {
351
+ this.#parseChapterOnly(passage, part)
352
+ } else if (part.includes(":")) {
353
+ this.#parseChapterVerse(passage, part, isFirstPart)
354
+ } else if (singleChapterBook) {
355
+ this.#parseSingleChapterBook(passage, part, isFirstPart && parts.length === 1)
356
+ } else if (part.includes("-")) {
357
+ this.#parseRange(passage, part, isFirstPart)
358
+ } else {
359
+ this.#parseSingleNumber(passage, part, isFirstPart)
360
+ }
361
+ })
362
+ }
363
+
364
+ /**
365
+ * Parses chapter-only reference
366
+ * @private
367
+ */
368
+ #parseChapterOnly(passage, part) {
369
+ const chapter = Number(part.replace(/[^0-9]/g, ""))
370
+ if (chapter > 0) {
371
+ passage.chapter = chapter
372
+ passage.type = ReferenceParser.REFERENCE_TYPES.SINGLE_CHAPTER
373
+ const chapterVerses = PassageUtils.getChapterVerses(passage.book, chapter)
374
+ if (chapterVerses.length) {
375
+ const startVerse = chapterVerses[0]
376
+ const endVerse = chapterVerses[chapterVerses.length - 1]
377
+ passage.verses = [`${startVerse}-${endVerse}`]
378
+ }
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Parses chapter:verse references
384
+ * @private
385
+ */
386
+ #parseChapterVerse(passage, part, isFirstPart) {
387
+ const [chapter, verse] = part.split(/[:.]/).map((s) => s.trim())
388
+ if (isFirstPart) passage.chapter = Number(chapter)
389
+
390
+ passage.type = verse.includes("-")
391
+ ? ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE_RANGE
392
+ : verse.includes(",")
393
+ ? ReferenceParser.REFERENCE_TYPES.COMMA_SEPARATED
394
+ : ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE
395
+
396
+ if (verse.includes(",")) {
397
+ passage.verses.push(...verse.split(",").map((v) => v.trim()))
398
+ } else {
399
+ passage.verses.push(verse)
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Parses references for single-chapter books
405
+ * @private
406
+ */
407
+ #parseSingleChapterBook(passage, part, isWholeChapter) {
408
+ const verseCount = PassageUtils.getChapterVerses(passage.book, 1).length
409
+
410
+ if (part === "1" && isWholeChapter) {
411
+ passage.chapter = 1
412
+ passage.type = ReferenceParser.REFERENCE_TYPES.SINGLE_CHAPTER
413
+ passage.verses = [`1-${verseCount}`]
414
+ } else if (part.includes("-")) {
415
+ passage.chapter = 1
416
+ passage.verses.push(part)
417
+ passage.type = ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE_RANGE
418
+ } else if (part.includes(",")) {
419
+ passage.chapter = 1
420
+ passage.verses.push(...part.split(",").map((v) => v.trim()))
421
+ passage.type = ReferenceParser.REFERENCE_TYPES.COMMA_SEPARATED
422
+ } else {
423
+ const num = Number(part)
424
+ if (num > 0) {
425
+ passage.chapter = 1
426
+ passage.verses.push(num)
427
+ passage.type = ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE
428
+ }
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Parses range references
434
+ * @private
435
+ */
436
+ #parseRange(passage, part, isFirstPart) {
437
+ if (!passage.chapter && isFirstPart) {
438
+ const [start, end] = part.split("-").map(Number)
439
+ passage.chapter = start
440
+ const startVerses = PassageUtils.getChapterVerses(passage.book, start)
441
+ passage.verses = [`${startVerses[0]}-${startVerses[startVerses.length - 1]}`]
442
+ passage.to = {
443
+ book: passage.book,
444
+ chapter: end,
445
+ verses: [
446
+ `${PassageUtils.getChapterVerses(passage.book, end)[0]}-${
447
+ PassageUtils.getChapterVerses(passage.book, end).slice(-1)[0]
448
+ }`,
449
+ ],
450
+ }
451
+ passage.type = ReferenceParser.REFERENCE_TYPES.CHAPTER_RANGE
452
+ } else {
453
+ passage.verses.push(part)
454
+ passage.type = ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE_RANGE
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Parses single number references
460
+ * @private
461
+ */
462
+ #parseSingleNumber(passage, part, isFirstPart) {
463
+ if (isFirstPart && !passage.chapter) {
464
+ passage.chapter = Number(part)
465
+ passage.type = ReferenceParser.REFERENCE_TYPES.SINGLE_CHAPTER
466
+ const chapterVerses = PassageUtils.getChapterVerses(passage.book, passage.chapter)
467
+ if (chapterVerses.length) {
468
+ passage.verses = [`${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`]
469
+ }
470
+ } else {
471
+ passage.verses.push(Number(part))
472
+ passage.type = ReferenceParser.REFERENCE_TYPES.COMMA_SEPARATED
473
+ }
474
+ }
475
+
476
+ /**
477
+ * Handles multi-chapter range references
478
+ * @private
479
+ */
480
+ #handleMultiChapterRange(passage, reference) {
481
+ const parts = reference.split(",")
482
+ const lastPart = parts[parts.length - 1].trim()
483
+ const [endChapter, endVerse] = lastPart.split(/[:.]/).map((s) => s.trim())
484
+
485
+ if (endChapter !== String(passage.chapter)) {
486
+ passage.to = {
487
+ book: passage.book,
488
+ chapter: Number(endChapter),
489
+ verses: endVerse ? [endVerse] : ["1"],
490
+ }
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Populates passage with expanded verse objects
496
+ * @private
497
+ */
498
+ #populatePassage(passage) {
499
+ const { book, chapter, verses, type, to } = passage
500
+
501
+ if (type === ReferenceParser.REFERENCE_TYPES.SINGLE_CHAPTER) {
502
+ const chapterVerses = PassageUtils.getChapterVerses(book, chapter)
503
+ return PassageUtils.expandVerses(book, chapter, [
504
+ `${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`,
505
+ ])
506
+ }
507
+
508
+ if (
509
+ type === ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE ||
510
+ type === ReferenceParser.REFERENCE_TYPES.COMMA_SEPARATED ||
511
+ type === ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE_RANGE
512
+ ) {
513
+ return PassageUtils.expandVerses(book, chapter, verses)
514
+ }
515
+
516
+ if (type === ReferenceParser.REFERENCE_TYPES.CHAPTER_RANGE) {
517
+ const passages = []
518
+ if (to && to.chapter) {
519
+ for (let ch = chapter; ch <= to.chapter; ch++) {
520
+ const chapterVerses = PassageUtils.getChapterVerses(book, ch)
521
+ passages.push(
522
+ ...PassageUtils.expandVerses(book, ch, [
523
+ `${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`,
524
+ ])
525
+ )
526
+ }
527
+ }
528
+ return passages
529
+ }
530
+
531
+ if (type === ReferenceParser.REFERENCE_TYPES.MULTI_CHAPTER_RANGE) {
532
+ const passages = []
533
+ const startVerse = verses[0]?.includes("-") ? Number(verses[0].split("-")[0]) : Number(verses[0]) || 1
534
+ const endVerse = to?.verses?.[0]?.includes("-")
535
+ ? Number(to.verses[0].split("-")[1])
536
+ : Number(to?.verses?.[0]) || 1
537
+ const endChapter = to?.chapter || chapter
538
+
539
+ for (let ch = chapter; ch <= endChapter; ch++) {
540
+ const chapterVerses = PassageUtils.getChapterVerses(book, ch)
541
+ const from = ch === chapter ? startVerse : chapterVerses[0]
542
+ const toVerse = ch === endChapter ? endVerse : chapterVerses[chapterVerses.length - 1]
543
+ passages.push(...PassageUtils.expandVerses(book, ch, [`${from}-${toVerse}`]))
544
+ }
545
+ return passages
546
+ }
547
+
548
+ return []
549
+ }
550
+
551
+ /**
552
+ * Formats a passage into a human-readable reference
553
+ * @private
554
+ */
555
+ #formatScripture(passage) {
556
+ let combined = `${passage.book}`
557
+
558
+ if (passage.type === ReferenceParser.REFERENCE_TYPES.MULTI_CHAPTER_RANGE && passage.to) {
559
+ combined += ` ${PassageUtils.formatChapterVerse(
560
+ passage.chapter,
561
+ passage.verses
562
+ )}-${PassageUtils.formatChapterVerse(passage.to.chapter, passage.to.verses)}`
563
+ } else if (
564
+ passage.type === ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE_RANGE ||
565
+ passage.type === ReferenceParser.REFERENCE_TYPES.COMMA_SEPARATED
566
+ ) {
567
+ combined += ` ${PassageUtils.formatChapterVerse(passage.chapter, passage.verses)}`
568
+ } else if (passage.type === ReferenceParser.REFERENCE_TYPES.CHAPTER_RANGE && passage.to) {
569
+ combined += ` ${passage.chapter}-${passage.to.chapter}`
570
+ } else {
571
+ combined += ` ${PassageUtils.formatChapterVerse(passage.chapter, passage.verses)}`
572
+ }
573
+
574
+ const cv = passage.to
575
+ ? `${PassageUtils.formatChapterVerse(passage.chapter, passage.verses)}-${PassageUtils.formatChapterVerse(
576
+ passage.to.chapter,
577
+ passage.to.verses
578
+ )}`
579
+ : PassageUtils.formatChapterVerse(passage.chapter, passage.verses)
580
+
581
+ const hash = formatOsis(passage)
582
+
583
+ return {
584
+ passage: combined,
585
+ cv: cv,
586
+ hash: hash,
587
+ osisNumeric: formatOsisNumeric(passage),
588
+ }
589
+ }
590
+
591
+ /**
592
+ * Sets the SBL abbreviation for a passage
593
+ * @private
594
+ */
595
+ #setAbbreviation(passage, reference) {
596
+ const sblEntry = Object.entries(sblAbbreviations).find(
597
+ ([key]) => key.toLowerCase() === passage.book.toLowerCase()
598
+ )
599
+
600
+ if (sblEntry) {
601
+ const { value, abbr } = sblEntry[1]
602
+ const ref = reference.replace(/\s*(LXX|MT)$/i, "").trim()
603
+ const suffix = passage.version.value !== "ENG" ? " " + passage.version.value : ""
604
+ passage.abbr = abbr ? `${value}. ${ref}${suffix}` : `${value} ${ref}${suffix}`
605
+ } else {
606
+ passage.abbr = passage.original
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Calculates start and end based on passages array
612
+ * @private
613
+ */
614
+ #calculateStartEnd(passage) {
615
+ if (passage.passages.length > 0) {
616
+ const sortedPassages = passage.passages.slice().sort((a, b) => {
617
+ if (a.chapter !== b.chapter) return a.chapter - b.chapter
618
+ return a.verse - b.verse
619
+ })
620
+
621
+ const firstPassage = sortedPassages[0]
622
+ const lastPassage = sortedPassages[sortedPassages.length - 1]
623
+
624
+ passage.start = {
625
+ book: firstPassage.book,
626
+ chapter: firstPassage.chapter,
627
+ verse: firstPassage.verse,
628
+ }
629
+ passage.end = {
630
+ book: lastPassage.book,
631
+ chapter: lastPassage.chapter,
632
+ verse: lastPassage.verse,
633
+ }
634
+ }
635
+ }
636
+ }
637
+
638
+ module.exports = ReferenceParser