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,404 @@
1
+ /**
2
+ * PassageCollection.js
3
+ * Array-like collection with utility methods for passages
4
+ */
5
+
6
+ const PassageUtils = require("../utils/PassageUtils")
7
+ const VersionHandler = require("./VersionHandler")
8
+ const sblAbbreviations = require("../data/abbr/sbl")
9
+ const { formatOsis, formatOsisNumeric } = require("../format/osis")
10
+
11
+ /**
12
+ * Collection of passages with utility methods
13
+ */
14
+ class PassageCollection extends Array {
15
+ /**
16
+ * Gets the first passage
17
+ * @returns {Object|null} First passage or null
18
+ */
19
+ first() {
20
+ return this.length > 0 ? this[0] : null
21
+ }
22
+
23
+ /**
24
+ * Filters passages from Old Testament
25
+ * @returns {PassageCollection} Filtered collection
26
+ */
27
+ oldTestament() {
28
+ const filtered = this.filter((passage) => passage.testament === "old")
29
+ return PassageCollection.from(filtered)
30
+ }
31
+
32
+ /**
33
+ * Filters passages from New Testament
34
+ * @returns {PassageCollection} Filtered collection
35
+ */
36
+ newTestament() {
37
+ const filtered = this.filter((passage) => passage.testament === "new")
38
+ return PassageCollection.from(filtered)
39
+ }
40
+
41
+ /**
42
+ * Combines passages by book and/or chapter
43
+ * @param {Object} options - Combination options
44
+ * @param {boolean} options.book - Group by book
45
+ * @param {boolean} options.chapter - Group by chapter
46
+ * @returns {PassageCollection} Combined passages
47
+ */
48
+ combine(options = {}) {
49
+ const { book = true, chapter = true } = options
50
+
51
+ if (!book) {
52
+ return PassageCollection.from(this)
53
+ }
54
+
55
+ const groupedByBook = new Map()
56
+
57
+ this.forEach((passage) => {
58
+ const bookKey = passage.book
59
+ if (!groupedByBook.has(bookKey)) {
60
+ groupedByBook.set(bookKey, [])
61
+ }
62
+ groupedByBook.get(bookKey).push(passage)
63
+ })
64
+
65
+ const combinedPassages = []
66
+
67
+ for (const [bookName, bookPassages] of groupedByBook) {
68
+ if (chapter) {
69
+ const groupedByChapter = new Map()
70
+ bookPassages.forEach((passage) => {
71
+ const chapterKey = `${passage.book}-${passage.chapter}`
72
+ if (!groupedByChapter.has(chapterKey)) {
73
+ groupedByChapter.set(chapterKey, [])
74
+ }
75
+ groupedByChapter.get(chapterKey).push(passage)
76
+ })
77
+
78
+ for (const passages of groupedByChapter.values()) {
79
+ if (passages.length === 1) {
80
+ combinedPassages.push({ ...passages[0] })
81
+ } else {
82
+ const combined = PassageCollection.combinePassages(passages)
83
+ combinedPassages.push(combined)
84
+ }
85
+ }
86
+ } else {
87
+ const combined = PassageCollection.combinePassages(bookPassages)
88
+ combinedPassages.push(combined)
89
+ }
90
+ }
91
+
92
+ return PassageCollection.from(combinedPassages)
93
+ }
94
+
95
+ /**
96
+ * Converts passages to a different version
97
+ * @param {string} targetVersion - Target version (eng, lxx, mt, bhs)
98
+ * @returns {PassageCollection} Converted passages
99
+ */
100
+ getVersion(targetVersion) {
101
+ const targetAbbr = targetVersion.toLowerCase() === "bhs" ? "mt" : targetVersion.toLowerCase()
102
+ const versionObj = VersionHandler.getVersionObject(targetAbbr)
103
+
104
+ const converted = this.map((passage) => {
105
+ const cloned = JSON.parse(JSON.stringify(passage))
106
+ cloned.version = versionObj
107
+
108
+ cloned.passages.forEach((sub) => {
109
+ if (sub.versification && sub.versification[targetAbbr]) {
110
+ const [ch, v] = sub.versification[targetAbbr].split(":").map(Number)
111
+ sub.chapter = ch
112
+ sub.verse = v
113
+ }
114
+ })
115
+
116
+ // Recompute summary fields
117
+ PassageCollection.#recomputePassageFields(cloned, versionObj)
118
+
119
+ return cloned
120
+ })
121
+
122
+ return PassageCollection.from(converted)
123
+ }
124
+
125
+ /**
126
+ * Converts passages to LXX version (shorthand for getVersion('lxx'))
127
+ * @returns {PassageCollection} Converted passages
128
+ */
129
+ getLXX() {
130
+ return this.getVersion("lxx")
131
+ }
132
+
133
+ /**
134
+ * Converts passages to MT/BHS version (shorthand for getVersion('mt'))
135
+ * @returns {PassageCollection} Converted passages
136
+ */
137
+ getMT() {
138
+ return this.getVersion("mt")
139
+ }
140
+
141
+ /**
142
+ * Converts passages to BHS version (alias for getMT())
143
+ * @returns {PassageCollection} Converted passages
144
+ */
145
+ getBHS() {
146
+ return this.getVersion("mt")
147
+ }
148
+
149
+ /**
150
+ * Converts passages to English version (shorthand for getVersion('eng'))
151
+ * @returns {PassageCollection} Converted passages
152
+ */
153
+ getEnglish() {
154
+ return this.getVersion("eng")
155
+ }
156
+
157
+ /**
158
+ * Combines multiple passages into one
159
+ * @static
160
+ */
161
+ static combinePassages(passages) {
162
+ if (!passages || passages.length === 0) {
163
+ throw new Error("No passages provided to combine.")
164
+ }
165
+
166
+ const uniqueBooks = [...new Set(passages.map((p) => p.book))]
167
+ if (uniqueBooks.length > 1) {
168
+ throw new Error("Passages must be from the same book to combine.")
169
+ }
170
+
171
+ const versions = new Set(passages.map((p) => p.version.abbreviation))
172
+ if (versions.size > 1) {
173
+ throw new Error("Cannot combine passages from different versions.")
174
+ }
175
+
176
+ const combined = {
177
+ ...passages[0],
178
+ verses: [],
179
+ passages: [],
180
+ to: null,
181
+ scripture: {},
182
+ type: null,
183
+ start: null,
184
+ end: null,
185
+ }
186
+
187
+ const chapterVerses = {}
188
+ let firstChapter = null
189
+ let lastChapter = null
190
+ let firstVerse = null
191
+ let lastVerse = null
192
+
193
+ passages.forEach((passage) => {
194
+ passage.passages.forEach((p) => {
195
+ const chapter = p.chapter
196
+ const verse = p.verse
197
+ if (!chapterVerses[chapter]) {
198
+ chapterVerses[chapter] = new Set()
199
+ }
200
+ chapterVerses[chapter].add(verse)
201
+ combined.passages.push({
202
+ book: p.book,
203
+ chapter: p.chapter,
204
+ verse: p.verse,
205
+ versification: p.versification,
206
+ })
207
+
208
+ if (firstChapter === null || chapter < firstChapter) {
209
+ firstChapter = chapter
210
+ firstVerse = verse
211
+ } else if (chapter === firstChapter && (firstVerse === null || verse < firstVerse)) {
212
+ firstVerse = verse
213
+ }
214
+ if (lastChapter === null || chapter > lastChapter) {
215
+ lastChapter = chapter
216
+ lastVerse = verse
217
+ } else if (chapter === lastChapter && (lastVerse === null || verse > lastVerse)) {
218
+ lastVerse = verse
219
+ }
220
+ })
221
+ })
222
+
223
+ combined.passages = Array.from(new Set(combined.passages.map(JSON.stringify))).map(JSON.parse)
224
+
225
+ const chapterStrings = []
226
+ const sortedChapters = Object.keys(chapterVerses)
227
+ .map(Number)
228
+ .sort((a, b) => a - b)
229
+
230
+ sortedChapters.forEach((chapter) => {
231
+ const verses = Array.from(chapterVerses[chapter])
232
+ .map(Number)
233
+ .filter((verse) => verse > 0)
234
+ .sort((a, b) => a - b)
235
+ if (verses.length > 0) {
236
+ const mergedVerses = PassageUtils.mergeRanges(verses)
237
+ chapterStrings.push(`${chapter}:${mergedVerses.join(",")}`)
238
+ if (chapter === firstChapter) {
239
+ combined.verses = mergedVerses
240
+ }
241
+ }
242
+ })
243
+
244
+ if (chapterStrings.length === 0) {
245
+ throw new Error("No valid verses found in passages.")
246
+ }
247
+
248
+ combined.chapter = firstChapter
249
+
250
+ if (firstChapter !== lastChapter) {
251
+ combined.type = "multi_chapter_verse_range"
252
+ combined.to = {
253
+ book: combined.book,
254
+ chapter: lastChapter,
255
+ verses: PassageUtils.mergeRanges(
256
+ Array.from(chapterVerses[lastChapter])
257
+ .filter((verse) => verse > 0)
258
+ .sort((a, b) => a - b)
259
+ ),
260
+ }
261
+ combined.original = `${combined.book} ${chapterStrings.join("; ")}`
262
+ } else {
263
+ combined.type = combined.verses.length > 1 ? "chapter_verse_range" : "chapter_verse"
264
+ combined.original = `${combined.book} ${chapterStrings[0]}`
265
+ }
266
+
267
+ const chapterString = chapterStrings.join(";")
268
+ combined.scripture = {
269
+ passage: `${combined.book} ${chapterString}`,
270
+ cv: chapterString,
271
+ hash: formatOsis(combined),
272
+ osisNumeric: formatOsisNumeric(combined),
273
+ }
274
+
275
+ combined.start = {
276
+ book: combined.book,
277
+ chapter: firstChapter,
278
+ verse:
279
+ firstVerse > 0
280
+ ? firstVerse
281
+ : Math.min(...Array.from(chapterVerses[firstChapter]).filter((verse) => verse > 0)),
282
+ }
283
+ combined.end = {
284
+ book: combined.book,
285
+ chapter: lastChapter,
286
+ verse:
287
+ lastVerse > 0
288
+ ? lastVerse
289
+ : Math.max(...Array.from(chapterVerses[lastChapter]).filter((verse) => verse > 0)),
290
+ }
291
+
292
+ if (combined.to === null) {
293
+ delete combined.to
294
+ }
295
+
296
+ combined.version = passages[0].version
297
+
298
+ // Set abbr with version suffix
299
+ PassageCollection.#setAbbreviation(combined)
300
+
301
+ return combined
302
+ }
303
+
304
+ /**
305
+ * Recomputes passage fields after version change
306
+ * @private
307
+ */
308
+ static #recomputePassageFields(cloned, versionObj) {
309
+ cloned.passages.sort((a, b) => a.chapter - b.chapter || a.verse - b.verse)
310
+
311
+ if (cloned.passages.length > 0) {
312
+ cloned.start = {
313
+ book: cloned.book,
314
+ chapter: cloned.passages[0].chapter,
315
+ verse: cloned.passages[0].verse,
316
+ }
317
+ cloned.end = {
318
+ book: cloned.book,
319
+ chapter: cloned.passages[cloned.passages.length - 1].chapter,
320
+ verse: cloned.passages[cloned.passages.length - 1].verse,
321
+ }
322
+ }
323
+
324
+ const chapterVersesMap = {}
325
+ cloned.passages.forEach((p) => {
326
+ if (!chapterVersesMap[p.chapter]) chapterVersesMap[p.chapter] = new Set()
327
+ chapterVersesMap[p.chapter].add(p.verse)
328
+ })
329
+
330
+ const sortedChs = Object.keys(chapterVersesMap)
331
+ .map(Number)
332
+ .sort((a, b) => a - b)
333
+ const chapterStrs = []
334
+
335
+ sortedChs.forEach((ch) => {
336
+ const vs = Array.from(chapterVersesMap[ch])
337
+ .filter((v) => v > 0)
338
+ .sort((a, b) => a - b)
339
+ if (vs.length > 0) {
340
+ const merged = PassageUtils.mergeRanges(vs)
341
+ chapterStrs.push(`${ch}:${merged.join(",")}`)
342
+ }
343
+ })
344
+
345
+ if (chapterStrs.length === 0) {
346
+ return cloned
347
+ }
348
+
349
+ const firstCh = sortedChs[0]
350
+ const lastCh = sortedChs[sortedChs.length - 1]
351
+ cloned.chapter = firstCh
352
+
353
+ const mergedFirst = PassageUtils.mergeRanges([...chapterVersesMap[firstCh]])
354
+ cloned.verses = mergedFirst
355
+
356
+ if (firstCh !== lastCh) {
357
+ cloned.type = "multi_chapter_verse_range"
358
+ cloned.to = {
359
+ book: cloned.book,
360
+ chapter: lastCh,
361
+ verses: PassageUtils.mergeRanges([...chapterVersesMap[lastCh]]),
362
+ }
363
+ cloned.original = `${cloned.book} ${chapterStrs.join("; ")}`
364
+ } else {
365
+ const hasRangeOrMultiple =
366
+ mergedFirst.length > 1 || (mergedFirst.length === 1 && mergedFirst[0].includes("-"))
367
+ cloned.type = hasRangeOrMultiple ? "chapter_verse_range" : "chapter_verse"
368
+ if (cloned.to) delete cloned.to
369
+ cloned.original = `${cloned.book} ${chapterStrs[0]}`
370
+ }
371
+
372
+ const chString = chapterStrs.join("; ")
373
+ cloned.scripture = {
374
+ passage: `${cloned.book} ${chString}`,
375
+ cv: chString,
376
+ hash: formatOsis(cloned),
377
+ osisNumeric: formatOsisNumeric(cloned),
378
+ }
379
+
380
+ PassageCollection.#setAbbreviation(cloned)
381
+ }
382
+
383
+ /**
384
+ * Sets abbreviation for passage
385
+ * @private
386
+ */
387
+ static #setAbbreviation(passage) {
388
+ const sblEntry = Object.entries(sblAbbreviations).find(
389
+ ([key]) => key.toLowerCase() === passage.book.toLowerCase()
390
+ )
391
+ const suffix = passage.version.abbreviation === "eng" ? "" : ` ${passage.version.value}`
392
+
393
+ if (sblEntry) {
394
+ const { value, abbr } = sblEntry[1]
395
+ passage.abbr = abbr
396
+ ? `${value}. ${passage.scripture.cv}${suffix}`
397
+ : `${value} ${passage.scripture.cv}${suffix}`
398
+ } else {
399
+ passage.abbr = `${passage.book} ${passage.scripture.cv}${suffix}`
400
+ }
401
+ }
402
+ }
403
+
404
+ module.exports = PassageCollection
@@ -0,0 +1,107 @@
1
+ /**
2
+ * PassageValidator.js
3
+ * Handles validation of scripture passages
4
+ */
5
+
6
+ const PassageUtils = require("../utils/PassageUtils")
7
+
8
+ /**
9
+ * Validates scripture passages
10
+ */
11
+ class PassageValidator {
12
+ static ERROR_CODES = {
13
+ INVALID_CHAPTER: 101,
14
+ CHAPTER_NOT_EXIST: 102,
15
+ VERSE_NOT_EXIST: 104,
16
+ }
17
+
18
+ /**
19
+ * Validates a passage for correctness
20
+ * @param {Object} passage - The passage object
21
+ * @param {string} reference - The original reference
22
+ * @returns {boolean|Object} True if valid, error object if invalid
23
+ */
24
+ static validate(passage, reference) {
25
+ const { book, chapter, verses, type } = passage
26
+
27
+ const SINGLE_CHAPTER = "single_chapter"
28
+
29
+ if (!verses.length && type !== SINGLE_CHAPTER) {
30
+ return PassageValidator.#createError(
31
+ PassageValidator.ERROR_CODES.INVALID_CHAPTER,
32
+ `Possible invalid chapter: ${reference}`
33
+ )
34
+ }
35
+
36
+ const chapterVerses = PassageUtils.getChapterVerses(book, chapter)
37
+ if (!chapterVerses.length) {
38
+ return PassageValidator.#createError(
39
+ PassageValidator.ERROR_CODES.CHAPTER_NOT_EXIST,
40
+ `Chapter ${chapter} does not exist in ${book}`
41
+ )
42
+ }
43
+
44
+ if (type === SINGLE_CHAPTER) {
45
+ const [range] = verses
46
+ if (range) {
47
+ const [start, end] = range.split("-").map(Number)
48
+ if (start < 1 || end > chapterVerses[chapterVerses.length - 1]) {
49
+ return PassageValidator.#createError(
50
+ PassageValidator.ERROR_CODES.VERSE_NOT_EXIST,
51
+ `Verse range ${start}-${end} exceeds available verses (1-${
52
+ chapterVerses[chapterVerses.length - 1]
53
+ }) in ${book} ${chapter}`
54
+ )
55
+ }
56
+ }
57
+ return true
58
+ }
59
+
60
+ return PassageValidator.#validateVerses(book, chapter, verses)
61
+ }
62
+
63
+ /**
64
+ * Validates verse numbers for a chapter
65
+ * @private
66
+ */
67
+ static #validateVerses(book, chapter, verses) {
68
+ const chapterVerses = PassageUtils.getChapterVerses(book, chapter)
69
+ for (const verse of verses) {
70
+ const verseRange = String(verse)
71
+ const verseNumbers = verseRange.includes("-")
72
+ ? Array.from(
73
+ { length: Number(verseRange.split("-")[1]) - Number(verseRange.split("-")[0]) + 1 },
74
+ (_, i) => Number(verseRange.split("-")[0]) + i
75
+ )
76
+ : [Number(verseRange)]
77
+
78
+ for (const v of verseNumbers) {
79
+ if (isNaN(v) || v <= 0 || !chapterVerses.includes(v)) {
80
+ return PassageValidator.#createError(
81
+ PassageValidator.ERROR_CODES.VERSE_NOT_EXIST,
82
+ `Verse number ${v} does not exist in ${book} ${chapter}`
83
+ )
84
+ }
85
+ }
86
+ }
87
+ return true
88
+ }
89
+
90
+ /**
91
+ * Creates an error object for validation failures
92
+ * @private
93
+ */
94
+ static #createError(code, message) {
95
+ return {
96
+ error: true,
97
+ code,
98
+ message: {
99
+ verse_exists: code === PassageValidator.ERROR_CODES.VERSE_NOT_EXIST,
100
+ chapter_exists: code !== PassageValidator.ERROR_CODES.VERSE_NOT_EXIST,
101
+ content: message,
102
+ },
103
+ }
104
+ }
105
+ }
106
+
107
+ module.exports = PassageValidator