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.
- package/.trunk/trunk.yaml +3 -3
- package/CHANGELOG.md +18 -0
- package/README.md +76 -0
- package/REFACTORING.md +214 -0
- package/RELEASE_NOTES_v0.2.0.md +5 -0
- package/index.js +1 -1
- package/package.json +4 -24
- package/src/{CodexParser.js → CodexParser.js.backup} +303 -247
- package/src/core/CodexParser.js +353 -0
- package/src/core/PassageCollection.js +404 -0
- package/src/core/PassageValidator.js +107 -0
- package/src/core/ReferenceParser.js +638 -0
- package/src/core/ScriptureScanner.js +235 -0
- package/src/core/VersificationHandler.js +143 -0
- package/src/core/VersionHandler.js +77 -0
- package/src/data/toc.js +7 -0
- package/src/format/osis.js +210 -0
- package/src/index.js +12 -0
- package/src/utils/PassageUtils.js +117 -0
- package/src/utils/chapterVerseCombine.js +65 -0
- package/.babelrc +0 -5
- package/src/chapterVerseCombine.js +0 -63
- package/src/toc.js +0 -2
- package/webpack.config.js +0 -38
- /package/src/{abbr → data/abbr}/sbl.js +0 -0
- /package/src/{bible.js → data/bible.js} +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/1chronicles.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/1corinthians.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/1john.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/1kings.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/1peter.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/1samuel.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/1thessalonians.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/1timothy.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/2chronicles.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/2corinthians.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/2kings.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/2peter.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/2samuel.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/2thessalonians.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/2timothy.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/acts.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/amos.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/colossians.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/daniel.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/deuteronomy.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/ecclesiastes.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/ephesians.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/esther.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/exodus.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/ezekiel.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/ezra.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/galatians.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/genesis.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/habakkuk.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/haggai.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/hebrews.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/hosea.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/isaiah.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/james.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/jeremiah.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/job.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/joel.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/john.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/jonah.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/joshua.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/judges.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/lamentations.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/leviticus.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/luke.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/malachi.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/mark.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/matthew.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/micah.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/nahum.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/nehemiah.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/numbers.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/philippians.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/proverbs.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/psalms.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/revelation.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/romans.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/ruth.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/songs.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/titus.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/zechariah.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/zephaniah.js +0 -0
- /package/src/{esv.js → data/esv.js} +0 -0
- /package/src/{versifications → data/versifications}/1chronicles.js +0 -0
- /package/src/{versifications → data/versifications}/1kings.js +0 -0
- /package/src/{versifications → data/versifications}/1samuel.js +0 -0
- /package/src/{versifications → data/versifications}/2chronicles.js +0 -0
- /package/src/{versifications → data/versifications}/2samuel.js +0 -0
- /package/src/{versifications → data/versifications}/daniel.js +0 -0
- /package/src/{versifications → data/versifications}/deuteronomy.js +0 -0
- /package/src/{versifications → data/versifications}/ecclesiastes.js +0 -0
- /package/src/{versifications → data/versifications}/exodus.js +0 -0
- /package/src/{versifications → data/versifications}/ezekiel.js +0 -0
- /package/src/{versifications → data/versifications}/genesis.js +0 -0
- /package/src/{versifications → data/versifications}/hosea.js +0 -0
- /package/src/{versifications → data/versifications}/isaiah.js +0 -0
- /package/src/{versifications → data/versifications}/jeremiah.js +0 -0
- /package/src/{versifications → data/versifications}/job.js +0 -0
- /package/src/{versifications → data/versifications}/joel.js +0 -0
- /package/src/{versifications → data/versifications}/jonah.js +0 -0
- /package/src/{versifications → data/versifications}/joshua.js +0 -0
- /package/src/{versifications → data/versifications}/leviticus.js +0 -0
- /package/src/{versifications → data/versifications}/malachi.js +0 -0
- /package/src/{versifications → data/versifications}/micah.js +0 -0
- /package/src/{versifications → data/versifications}/nahum.js +0 -0
- /package/src/{versifications → data/versifications}/nehemiah.js +0 -0
- /package/src/{versifications → data/versifications}/numbers.js +0 -0
- /package/src/{versifications → data/versifications}/proverbs.js +0 -0
- /package/src/{versifications → data/versifications}/psalms.js +0 -0
- /package/src/{versifications → data/versifications}/song.js +0 -0
- /package/src/{versifications → data/versifications}/zechariah.js +0 -0
- /package/src/{versified.js → data/versified.js} +0 -0
- /package/src/{abbr.js → format/abbr.js} +0 -0
- /package/src/{functions.js → utils/functions.js} +0 -0
- /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
|