codexparser 0.3.0 → 0.4.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/CHANGELOG.md +47 -0
- package/README.md +94 -2
- package/RELEASE_NOTES_v0.4.0.md +94 -0
- package/package.json +11 -1
- package/src/core/CodexParser.js +21 -0
- package/src/core/ReferenceParser.js +131 -28
- package/src/data/chapter_verses/daniel.js +4 -0
- package/src/data/chapter_verses/esther.js +12 -1
- package/src/data/lxx-editions.js +58 -0
- package/src/data/versifications/1samuel.js +1 -1
- package/src/data/versifications/2kings.js +117 -0
- package/src/data/versifications/2samuel.js +1 -1
- package/src/data/versifications/daniel.js +28 -0
- package/src/data/versifications/esther.js +114 -0
- package/src/data/versifications/ezekiel.js +37 -37
- package/src/data/versifications/genesis.js +37 -39
- package/src/data/versifications/micah.js +19 -16
- package/src/data/versifications/numbers.js +83 -1
- package/src/data/versifications/psalms.js +12 -12
- package/src/data/versified.js +6 -1
- package/.trunk/configs/.markdownlint.yaml +0 -2
- package/.trunk/trunk.yaml +0 -32
- package/REFACTORING.md +0 -214
- package/RELEASE_NOTES_v0.2.0.md +0 -5
- package/bibles/kjv.json +0 -194080
- package/bibles/updated_kjv.json +0 -194080
- package/passage-generator.js +0 -25
- package/src/CodexParser.js.backup +0 -1713
|
@@ -1,1713 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CodexParser.js
|
|
3
|
-
* A modern ES6+ class for scanning and parsing scripture references from text.
|
|
4
|
-
* Supports various formats (single verses, ranges, multi-chapter references) with
|
|
5
|
-
* validation and version-specific versification.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const bible = require("./bible");
|
|
9
|
-
const { bookRegex, chapterRegex, verseRegex, scripturesRegex } = require("./regex");
|
|
10
|
-
const ScriptureScanner = require("./ScriptureScanner");
|
|
11
|
-
const ReferenceParser = require("./ReferenceParser");
|
|
12
|
-
const VersificationHandler = require("./VersificationHandler");
|
|
13
|
-
const PassageCollection = require("./PassageCollection");
|
|
14
|
-
const PassageUtils = require("./PassageUtils");
|
|
15
|
-
const VersionHandler = require("./VersionHandler");
|
|
16
|
-
const chapter_verses = require("./chapterVerseCombine");
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Main class for parsing and validating scripture references
|
|
20
|
-
* @class
|
|
21
|
-
*/
|
|
22
|
-
class CodexParser {
|
|
23
|
-
// Private fields using ES6+ syntax
|
|
24
|
-
#scanner;
|
|
25
|
-
#parser;
|
|
26
|
-
#versificationHandler;
|
|
27
|
-
#config;
|
|
28
|
-
#version;
|
|
29
|
-
#found;
|
|
30
|
-
#passages;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Initializes the parser with configuration
|
|
34
|
-
* @param {Object} config - Configuration options
|
|
35
|
-
*/
|
|
36
|
-
constructor(config = {}) {
|
|
37
|
-
this.#config = {
|
|
38
|
-
booksOnly: config.booksOnly ?? false,
|
|
39
|
-
invalid_sequence_strategy: config.invalid_sequence_strategy ?? "include",
|
|
40
|
-
invalid_passage_strategy: config.invalid_passage_strategy ?? "include",
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
this.#scanner = new ScriptureScanner(this.#config);
|
|
44
|
-
this.#parser = new ReferenceParser(this.#config);
|
|
45
|
-
this.#versificationHandler = new VersificationHandler();
|
|
46
|
-
this.#version = null;
|
|
47
|
-
this.#found = [];
|
|
48
|
-
this.#passages = [];
|
|
49
|
-
|
|
50
|
-
// Legacy public properties for backward compatibility
|
|
51
|
-
this.bible = bible;
|
|
52
|
-
this.bookRegex = bookRegex;
|
|
53
|
-
this.chapterRegex = chapterRegex;
|
|
54
|
-
this.verseRegex = verseRegex;
|
|
55
|
-
this.scripturesRegex = scripturesRegex;
|
|
56
|
-
this.chapterVerses = chapter_verses;
|
|
57
|
-
this.error = false;
|
|
58
|
-
|
|
59
|
-
// Legacy constants
|
|
60
|
-
this.SINGLE_CHAPTER = "single_chapter";
|
|
61
|
-
this.CHAPTER_VERSE = "chapter_verse";
|
|
62
|
-
this.CHAPTER_VERSE_RANGE = "chapter_verse_range";
|
|
63
|
-
this.COMMA_SEPARATED = "comma_separated_verses";
|
|
64
|
-
this.CHAPTER_RANGE = "chapter_range";
|
|
65
|
-
this.MULTI_CHAPTER_RANGE = "multi_chapter_verse_range";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Gets the found references (legacy getter)
|
|
70
|
-
*/
|
|
71
|
-
get found() {
|
|
72
|
-
return this.#found;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Sets the found references (legacy setter)
|
|
77
|
-
*/
|
|
78
|
-
set found(value) {
|
|
79
|
-
this.#found = value;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Gets the parsed passages (legacy getter)
|
|
84
|
-
*/
|
|
85
|
-
get passages() {
|
|
86
|
-
return this.#passages;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Sets the parsed passages (legacy setter)
|
|
91
|
-
*/
|
|
92
|
-
set passages(value) {
|
|
93
|
-
this.#passages = value;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Gets the current version (legacy getter)
|
|
98
|
-
*/
|
|
99
|
-
get version() {
|
|
100
|
-
return this.#version;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Sets the current version (legacy setter)
|
|
105
|
-
*/
|
|
106
|
-
set version(value) {
|
|
107
|
-
this.#version = value;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Gets the current config (legacy getter)
|
|
112
|
-
*/
|
|
113
|
-
get config() {
|
|
114
|
-
return this.#config;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Sets the current config (legacy setter)
|
|
119
|
-
*/
|
|
120
|
-
set config(value) {
|
|
121
|
-
this.#config = value;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Sets configuration options for the parser
|
|
126
|
-
* @param {Object} config - Configuration options
|
|
127
|
-
* @param {boolean} [config.booksOnly=false] - Whether to capture book names without references
|
|
128
|
-
* @returns {CodexParser} The parser instance for method chaining
|
|
129
|
-
*/
|
|
130
|
-
options(config) {
|
|
131
|
-
this.#config = {
|
|
132
|
-
booksOnly: config.booksOnly ?? this.#config.booksOnly,
|
|
133
|
-
invalid_sequence_strategy: config.invalid_sequence_strategy ?? this.#config.invalid_sequence_strategy,
|
|
134
|
-
invalid_passage_strategy: config.invalid_passage_strategy ?? this.#config.invalid_passage_strategy,
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
// Update scanner and parser configs
|
|
138
|
-
this.#scanner = new ScriptureScanner(this.#config);
|
|
139
|
-
this.#parser = new ReferenceParser(this.#config);
|
|
140
|
-
|
|
141
|
-
return this;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Retrieves available verses for a given book and chapter (legacy method)
|
|
146
|
-
* @param {string} book - The book name
|
|
147
|
-
* @param {number} chapter - The chapter number
|
|
148
|
-
* @returns {number[]} Array of valid verse numbers
|
|
149
|
-
*/
|
|
150
|
-
getChapterVerses(book, chapter) {
|
|
151
|
-
return PassageUtils.getChapterVerses(book, chapter);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Scans text for scripture references
|
|
156
|
-
* @param {string} text - The text to scan
|
|
157
|
-
* @returns {CodexParser} The parser instance for method chaining
|
|
158
|
-
*/
|
|
159
|
-
scan(text) {
|
|
160
|
-
this.#found = this.#scanner.scan(text);
|
|
161
|
-
return this;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Sets the Bible version for parsing
|
|
166
|
-
* @param {string} version - The version (e.g., "lxx", "mt", "eng")
|
|
167
|
-
* @returns {CodexParser} The parser instance
|
|
168
|
-
*/
|
|
169
|
-
bibleVersion(version) {
|
|
170
|
-
this.#version = VersionHandler.normalizeVersion(version);
|
|
171
|
-
return this;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Parses a scripture reference into structured passage objects
|
|
176
|
-
* @param {string} reference - The reference to parse (e.g., "John 3:16")
|
|
177
|
-
* @returns {CodexParser} The parser instance
|
|
178
|
-
*/
|
|
179
|
-
parse(reference) {
|
|
180
|
-
this.scan(reference);
|
|
181
|
-
this.#passages = this.#parser.parse(this.#found, this.#version);
|
|
182
|
-
this.#passages = this.#versificationHandler.applyVersification(this.#passages);
|
|
183
|
-
return this;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Returns parsed passages with utility methods
|
|
188
|
-
* @returns {PassageCollection} Collection of passages with utility methods
|
|
189
|
-
*/
|
|
190
|
-
getPassages() {
|
|
191
|
-
return PassageCollection.from(this.#passages);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Returns the first parsed passage
|
|
196
|
-
* @returns {Object|null} The first passage or null
|
|
197
|
-
*/
|
|
198
|
-
first() {
|
|
199
|
-
return this.#passages.length > 0 ? this.#passages[0] : null;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Normalizes book names using abbreviations or full names (legacy method)
|
|
204
|
-
* @param {string|Array} book - The book name or array
|
|
205
|
-
* @returns {string} Normalized book name
|
|
206
|
-
*/
|
|
207
|
-
bookify(book) {
|
|
208
|
-
return this.#parser.constructor.prototype.normalizeBookName?.(book) || book;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Combines multiple passages into a single reference
|
|
213
|
-
* @param {Object[]} [passages=this.passages] - Array of passages to combine
|
|
214
|
-
* @returns {Object} Combined passage object
|
|
215
|
-
*/
|
|
216
|
-
combine(passages = this.#passages) {
|
|
217
|
-
return PassageCollection.combinePassages(passages);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Merges verses into ranges or comma-separated lists (legacy method)
|
|
222
|
-
* @param {number[]} verses - Array of verse numbers
|
|
223
|
-
* @returns {string[]} Array of verse strings
|
|
224
|
-
*/
|
|
225
|
-
mergeRanges(verses) {
|
|
226
|
-
return PassageUtils.mergeRanges(verses);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Generates a table of contents for the Bible
|
|
231
|
-
* @param {string} [version="ESV"] - The Bible version
|
|
232
|
-
* @returns {Object} TOC with book-chapter-verse mappings
|
|
233
|
-
*/
|
|
234
|
-
getToc(version = "ESV") {
|
|
235
|
-
const toc = {};
|
|
236
|
-
|
|
237
|
-
bible.old.forEach((book) => {
|
|
238
|
-
if (chapter_verses[book]) {
|
|
239
|
-
toc[book] = chapter_verses[book];
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
bible.new.forEach((book) => {
|
|
244
|
-
if (chapter_verses[book]) {
|
|
245
|
-
toc[book] = chapter_verses[book];
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
PassageUtils.SINGLE_CHAPTER_BOOKS.forEach((item) => {
|
|
250
|
-
Object.keys(item).forEach((book) => {
|
|
251
|
-
if (!toc[book]) {
|
|
252
|
-
toc[book] = item[book];
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
const orderedToc = {};
|
|
258
|
-
const canonicalOrder = [...bible.old, ...bible.new];
|
|
259
|
-
canonicalOrder.forEach((book) => {
|
|
260
|
-
if (toc[book]) {
|
|
261
|
-
orderedToc[book] = toc[book];
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
return orderedToc;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Replaces scripture references in text with formatted references
|
|
270
|
-
* @param {string} text - The original text
|
|
271
|
-
* @param {boolean} useAbbreviations - Whether to use abbreviated book names
|
|
272
|
-
* @returns {string} Text with replaced references
|
|
273
|
-
*/
|
|
274
|
-
replace(text, useAbbreviations = true) {
|
|
275
|
-
if (!this.#passages.length) {
|
|
276
|
-
return text;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
let result = text;
|
|
280
|
-
for (let i = this.#passages.length - 1; i >= 0; i--) {
|
|
281
|
-
const passage = this.#passages[i];
|
|
282
|
-
const { originalText, abbr, original } = passage;
|
|
283
|
-
const newReference = useAbbreviations ? abbr : original;
|
|
284
|
-
|
|
285
|
-
const regex = new RegExp(`${originalText.replace(/([.*+?^${}()|[\]\\])/g, "\\$1")}`, "g");
|
|
286
|
-
|
|
287
|
-
const matches = [...result.matchAll(regex)];
|
|
288
|
-
if (matches.length > 0) {
|
|
289
|
-
for (let j = matches.length - 1; j >= 0; j--) {
|
|
290
|
-
const match = matches[j];
|
|
291
|
-
const startIndex = match.index;
|
|
292
|
-
const endIndex = startIndex + match[0].length;
|
|
293
|
-
const leadingSpace = match[1] || "";
|
|
294
|
-
const hasOpeningParen = match[2] === "(";
|
|
295
|
-
const hasClosingParen = match[3] === ")";
|
|
296
|
-
const trailingSpace = match[4] || " ";
|
|
297
|
-
const replacement =
|
|
298
|
-
hasOpeningParen && hasClosingParen
|
|
299
|
-
? `${leadingSpace}(${newReference})${trailingSpace}`
|
|
300
|
-
: `${leadingSpace}${newReference}${trailingSpace}`;
|
|
301
|
-
result = result.slice(0, startIndex) + replacement + result.slice(endIndex);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return result;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Checks if all references in the passages array are from the same book
|
|
311
|
-
* @returns {boolean} True if all passages are from the same book
|
|
312
|
-
*/
|
|
313
|
-
same() {
|
|
314
|
-
if (this.#passages.length <= 1) {
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const firstBook = this.#passages[0].book.toLowerCase();
|
|
319
|
-
return this.#passages.every((passage) => passage.book.toLowerCase() === firstBook);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Legacy methods for backward compatibility
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Expands verse references into individual verse objects (legacy)
|
|
326
|
-
*/
|
|
327
|
-
expandVerses(book, chapter, verses) {
|
|
328
|
-
return PassageUtils.expandVerses(book, chapter, verses);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Populates passage with expanded verse objects (legacy)
|
|
333
|
-
*/
|
|
334
|
-
populate(passage) {
|
|
335
|
-
return this.#parser.constructor.prototype._populatePassage?.(passage) || [];
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Formats a passage into a human-readable reference (legacy)
|
|
340
|
-
*/
|
|
341
|
-
scripturize(passage) {
|
|
342
|
-
return this.#parser.constructor.prototype._formatScripture?.(passage) || {};
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Applies versification differences to parsed passages (legacy)
|
|
347
|
-
*/
|
|
348
|
-
versification() {
|
|
349
|
-
this.#passages = this.#versificationHandler.applyVersification(this.#passages);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
module.exports = CodexParser;
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
testament,
|
|
358
|
-
startIndex: passage.startIndex,
|
|
359
|
-
endIndex: passage.endIndex,
|
|
360
|
-
originalText: passage.originalText,
|
|
361
|
-
version: this._handleVersion(passage.version, testament),
|
|
362
|
-
passages: [],
|
|
363
|
-
scripture: null,
|
|
364
|
-
valid: true,
|
|
365
|
-
start: null,
|
|
366
|
-
end: null,
|
|
367
|
-
abbr: null,
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Clean reference for parsing
|
|
371
|
-
let cleanReference = passage.reference.replace(/\s*(LXX|MT)$/i, "").trim()
|
|
372
|
-
if (cleanReference.endsWith(",")) {
|
|
373
|
-
cleanReference = cleanReference.slice(0, -1).trim()
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Handle book-only or empty references
|
|
377
|
-
if (!cleanReference && this.config.booksOnly) {
|
|
378
|
-
parsedPassage.type = "book_only"
|
|
379
|
-
} else if (!cleanReference || cleanReference.match(/^\d+\s*[:;]?\s*$/)) {
|
|
380
|
-
const chapterMatch = cleanReference.match(/\d+/) || ["1"]
|
|
381
|
-
const chapter = Number(chapterMatch[0])
|
|
382
|
-
parsedPassage.chapter = chapter
|
|
383
|
-
parsedPassage.type = this.SINGLE_CHAPTER
|
|
384
|
-
const chapterVerses = this.getChapterVerses(book, chapter)
|
|
385
|
-
if (chapterVerses.length) {
|
|
386
|
-
const startVerse = chapterVerses[0]
|
|
387
|
-
const endVerse = chapterVerses[chapterVerses.length - 1]
|
|
388
|
-
parsedPassage.verses = [`${startVerse}-${endVerse}`]
|
|
389
|
-
}
|
|
390
|
-
} else if (passage.type === "comma_separated_verses") {
|
|
391
|
-
// Handle comma-separated verses (e.g., "1:7,18")
|
|
392
|
-
const [chapter, verses] = cleanReference.split(":")
|
|
393
|
-
parsedPassage.chapter = Number(chapter)
|
|
394
|
-
parsedPassage.verses = verses.split(",").map((v) => v.trim())
|
|
395
|
-
} else {
|
|
396
|
-
this.parseReferenceParts(parsedPassage, cleanReference)
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
parsedPassage.passages = this.populate(parsedPassage)
|
|
400
|
-
parsedPassage.scripture = this.scripturize(parsedPassage)
|
|
401
|
-
parsedPassage.valid = this._isValid(parsedPassage, cleanReference)
|
|
402
|
-
|
|
403
|
-
// Set abbr property using SBL-style abbreviations
|
|
404
|
-
const sblEntry = Object.entries(this.sblAbbreviations).find(
|
|
405
|
-
([key]) => key.toLowerCase() === book.toLowerCase()
|
|
406
|
-
)
|
|
407
|
-
if (sblEntry) {
|
|
408
|
-
const { value, abbr } = sblEntry[1]
|
|
409
|
-
const ref = passage.reference.replace(/\s*(LXX|MT)$/i, "").trim()
|
|
410
|
-
parsedPassage.abbr = abbr
|
|
411
|
-
? `${value}. ${ref}${
|
|
412
|
-
parsedPassage.version.value !== "ENG" ? " " + parsedPassage.version.value : ""
|
|
413
|
-
}`
|
|
414
|
-
: `${value} ${ref}${parsedPassage.version.value !== "ENG" ? " " + parsedPassage.version.value : ""}`
|
|
415
|
-
} else {
|
|
416
|
-
parsedPassage.abbr = parsedPassage.original
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (parsedPassage.type === this.MULTI_CHAPTER_RANGE) {
|
|
420
|
-
this.handleMultiChapterRange(parsedPassage, cleanReference)
|
|
421
|
-
} else {
|
|
422
|
-
delete parsedPassage.to
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Calculate start and end based on passages array
|
|
426
|
-
if (parsedPassage.passages.length > 0) {
|
|
427
|
-
const sortedPassages = parsedPassage.passages.slice().sort((a, b) => {
|
|
428
|
-
if (a.chapter !== b.chapter) return a.chapter - b.chapter
|
|
429
|
-
return a.verse - b.verse
|
|
430
|
-
})
|
|
431
|
-
const firstPassage = sortedPassages[0]
|
|
432
|
-
const lastPassage = sortedPassages[sortedPassages.length - 1]
|
|
433
|
-
parsedPassage.start = {
|
|
434
|
-
book: firstPassage.book,
|
|
435
|
-
chapter: firstPassage.chapter,
|
|
436
|
-
verse: firstPassage.verse,
|
|
437
|
-
}
|
|
438
|
-
parsedPassage.end = {
|
|
439
|
-
book: lastPassage.book,
|
|
440
|
-
chapter: lastPassage.chapter,
|
|
441
|
-
verse: lastPassage.verse,
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (!parsedPassage.version) {
|
|
446
|
-
parsedPassage.version = {
|
|
447
|
-
name: "English",
|
|
448
|
-
value: "ENG",
|
|
449
|
-
abbreviation: "eng",
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
return parsedPassage
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
this.versification()
|
|
457
|
-
return this
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Parses reference parts into chapter and verse components.
|
|
461
|
-
* @param {Object} passage - The passage object to populate.
|
|
462
|
-
* @param {string} reference - The reference string.
|
|
463
|
-
* @private
|
|
464
|
-
*/
|
|
465
|
-
parseReferenceParts(passage, reference) {
|
|
466
|
-
const singleChapterBook = this.singleChapterBook.find((b) => Object.keys(b)[0] === passage.book)
|
|
467
|
-
const parts = reference
|
|
468
|
-
.split(",")
|
|
469
|
-
.map((part) => part.trim())
|
|
470
|
-
.filter((part) => part)
|
|
471
|
-
|
|
472
|
-
parts.forEach((part, index) => {
|
|
473
|
-
const isFirstPart = index === 0
|
|
474
|
-
|
|
475
|
-
// Handle multi-chapter ranges (e.g., "2:1-3:19")
|
|
476
|
-
if (part.includes("-") && part.includes(":")) {
|
|
477
|
-
const [start, end] = part.split("-").map((s) => s.trim())
|
|
478
|
-
const startParts = start.split(/[:.]/).map((s) => s.trim())
|
|
479
|
-
const endParts = end.split(/[:.]/).map((s) => s.trim())
|
|
480
|
-
if (startParts.length > 1 && endParts.length > 1 && startParts[0] !== endParts[0]) {
|
|
481
|
-
passage.type = this.MULTI_CHAPTER_RANGE
|
|
482
|
-
passage.chapter = Number(startParts[0])
|
|
483
|
-
passage.verses = [startParts[1] || "1"]
|
|
484
|
-
passage.to = {
|
|
485
|
-
book: passage.book,
|
|
486
|
-
chapter: Number(endParts[0]),
|
|
487
|
-
verses: [endParts[1] || "1"],
|
|
488
|
-
}
|
|
489
|
-
return
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// Handle chapter-only references (e.g., "3")
|
|
494
|
-
if (!part.includes(":") && !part.includes("-") && !singleChapterBook) {
|
|
495
|
-
const chapter = Number(part.replace(/[^0-9]/g, ""))
|
|
496
|
-
if (chapter > 0) {
|
|
497
|
-
passage.chapter = chapter
|
|
498
|
-
passage.type = this.SINGLE_CHAPTER
|
|
499
|
-
const chapterVerses = this.getChapterVerses(passage.book, chapter)
|
|
500
|
-
if (chapterVerses.length) {
|
|
501
|
-
const startVerse = chapterVerses[0]
|
|
502
|
-
const endVerse = chapterVerses[chapterVerses.length - 1]
|
|
503
|
-
passage.verses = [`${startVerse}-${endVerse}`]
|
|
504
|
-
}
|
|
505
|
-
return
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
if (part.includes(":")) {
|
|
510
|
-
this.parseChapterVerse(passage, part, isFirstPart)
|
|
511
|
-
} else if (singleChapterBook) {
|
|
512
|
-
this.parseSingleChapterBook(passage, part, isFirstPart && parts.length === 1)
|
|
513
|
-
} else if (part.includes("-")) {
|
|
514
|
-
this.parseRange(passage, part, isFirstPart)
|
|
515
|
-
} else {
|
|
516
|
-
this.parseSingleNumber(passage, part, isFirstPart)
|
|
517
|
-
}
|
|
518
|
-
})
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* Parses chapter-verse references (e.g., "3:16").
|
|
523
|
-
* @param {Object} passage - The passage object.
|
|
524
|
-
* @param {string} part - The reference part.
|
|
525
|
-
* @param {boolean} isFirstPart - Whether this is the first part.
|
|
526
|
-
* @private
|
|
527
|
-
*/
|
|
528
|
-
parseChapterVerse(passage, part, isFirstPart) {
|
|
529
|
-
const [chapter, verse] = part.split(/[:.]/).map((s) => s.trim())
|
|
530
|
-
if (isFirstPart) passage.chapter = Number(chapter)
|
|
531
|
-
passage.type = verse.includes("-")
|
|
532
|
-
? this.CHAPTER_VERSE_RANGE
|
|
533
|
-
: verse.includes(",")
|
|
534
|
-
? this.COMMA_SEPARATED
|
|
535
|
-
: this.CHAPTER_VERSE
|
|
536
|
-
if (verse.includes(",")) {
|
|
537
|
-
passage.verses.push(...verse.split(",").map((v) => v.trim()))
|
|
538
|
-
} else {
|
|
539
|
-
passage.verses.push(verse)
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* Parses references for single-chapter books (e.g., "Jude 5").
|
|
545
|
-
* @param {Object} passage - The passage object.
|
|
546
|
-
* @param {string} part - The reference part.
|
|
547
|
-
* @param {boolean} isWholeChapter - Whether the reference is for the whole chapter.
|
|
548
|
-
* @private
|
|
549
|
-
*/
|
|
550
|
-
parseSingleChapterBook(passage, part, isWholeChapter) {
|
|
551
|
-
const verseCount = this.getChapterVerses(passage.book, 1).length
|
|
552
|
-
if (part === "1" && isWholeChapter) {
|
|
553
|
-
passage.chapter = 1
|
|
554
|
-
passage.type = this.SINGLE_CHAPTER
|
|
555
|
-
passage.verses = [`1-${verseCount}`]
|
|
556
|
-
} else if (part.includes("-")) {
|
|
557
|
-
passage.chapter = 1
|
|
558
|
-
passage.verses.push(part)
|
|
559
|
-
passage.type = this.CHAPTER_VERSE_RANGE
|
|
560
|
-
} else if (part.includes(",")) {
|
|
561
|
-
passage.chapter = 1
|
|
562
|
-
passage.verses.push(...part.split(",").map((v) => v.trim()))
|
|
563
|
-
passage.type = this.COMMA_SEPARATED
|
|
564
|
-
} else {
|
|
565
|
-
const num = Number(part)
|
|
566
|
-
if (num > 0) {
|
|
567
|
-
passage.chapter = 1
|
|
568
|
-
passage.verses.push(num)
|
|
569
|
-
passage.type = this.CHAPTER_VERSE
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/**
|
|
575
|
-
* Parses range references (e.g., "1-3").
|
|
576
|
-
* @param {Object} passage - The passage object.
|
|
577
|
-
* @param {string} part - The reference part.
|
|
578
|
-
* @param {boolean} isFirstPart - Whether this is the first part.
|
|
579
|
-
* @private
|
|
580
|
-
*/
|
|
581
|
-
parseRange(passage, part, isFirstPart) {
|
|
582
|
-
if (!passage.chapter && isFirstPart) {
|
|
583
|
-
const [start, end] = part.split("-").map(Number)
|
|
584
|
-
passage.chapter = start
|
|
585
|
-
const startVerses = this.getChapterVerses(passage.book, start)
|
|
586
|
-
passage.verses = [`${startVerses[0]}-${startVerses[startVerses.length - 1]}`]
|
|
587
|
-
passage.to = {
|
|
588
|
-
book: passage.book,
|
|
589
|
-
chapter: end,
|
|
590
|
-
verses: [
|
|
591
|
-
`${this.getChapterVerses(passage.book, end)[0]}-${
|
|
592
|
-
this.getChapterVerses(passage.book, end).slice(-1)[0]
|
|
593
|
-
}`,
|
|
594
|
-
],
|
|
595
|
-
}
|
|
596
|
-
passage.type = this.CHAPTER_RANGE
|
|
597
|
-
} else {
|
|
598
|
-
passage.verses.push(part)
|
|
599
|
-
passage.type = this.CHAPTER_VERSE_RANGE
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
/**
|
|
604
|
-
* Parses single number references (e.g., "3" for chapter or verse).
|
|
605
|
-
* @param {Object} passage - The passage object.
|
|
606
|
-
* @param {string} part - The reference part.
|
|
607
|
-
* @param {boolean} isFirstPart - Whether this is the first part.
|
|
608
|
-
* @private
|
|
609
|
-
*/
|
|
610
|
-
parseSingleNumber(passage, part, isFirstPart) {
|
|
611
|
-
if (isFirstPart && !passage.chapter) {
|
|
612
|
-
passage.chapter = Number(part)
|
|
613
|
-
passage.type = this.SINGLE_CHAPTER
|
|
614
|
-
const chapterVerses = this.getChapterVerses(passage.book, passage.chapter)
|
|
615
|
-
if (chapterVerses.length) {
|
|
616
|
-
passage.verses = [`${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`]
|
|
617
|
-
}
|
|
618
|
-
} else {
|
|
619
|
-
passage.verses.push(Number(part))
|
|
620
|
-
passage.type = this.COMMA_SEPARATED
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Handles multi-chapter range references (e.g., "3:16-4:5").
|
|
626
|
-
* @param {Object} passage - The passage object.
|
|
627
|
-
* @param {string} reference - The full reference string.
|
|
628
|
-
* @private
|
|
629
|
-
*/
|
|
630
|
-
handleMultiChapterRange(passage, reference) {
|
|
631
|
-
const parts = reference.split(",")
|
|
632
|
-
const lastPart = parts[parts.length - 1].trim()
|
|
633
|
-
const [endChapter, endVerse] = lastPart.split(/[:.]/).map((s) => s.trim())
|
|
634
|
-
if (endChapter !== String(passage.chapter)) {
|
|
635
|
-
passage.to = {
|
|
636
|
-
book: passage.book,
|
|
637
|
-
chapter: Number(endChapter),
|
|
638
|
-
verses: endVerse ? [endVerse] : ["1"],
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Generates a range of numbers.
|
|
645
|
-
* @param {number} start - Start number.
|
|
646
|
-
* @param {number} end - End number.
|
|
647
|
-
* @returns {number[]} Array of numbers.
|
|
648
|
-
* @private
|
|
649
|
-
*/
|
|
650
|
-
_generateRange(start, end) {
|
|
651
|
-
const range = []
|
|
652
|
-
for (let i = start; i <= end; i++) {
|
|
653
|
-
range.push(i)
|
|
654
|
-
}
|
|
655
|
-
return range
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
/**
|
|
659
|
-
* Searches for versification differences for a book and chapter.
|
|
660
|
-
* @param {string} book - The book name.
|
|
661
|
-
* @param {number} chapter - The chapter number.
|
|
662
|
-
* @param {string} version - The Bible version.
|
|
663
|
-
* @returns {Object|undefined} Updated chapter verses or undefined.
|
|
664
|
-
* @private
|
|
665
|
-
*/
|
|
666
|
-
_searchVersificationDifferences(book, chapter, version) {
|
|
667
|
-
version = version.toLowerCase()
|
|
668
|
-
|
|
669
|
-
// Handle single-chapter book "Obadiah"
|
|
670
|
-
if (book === "Obadiah") {
|
|
671
|
-
const singleChapterBook = this.singleChapterBook.find((b) => Object.keys(b)[0] === "Obadiah")
|
|
672
|
-
if (!singleChapterBook || !singleChapterBook[book][chapter]) {
|
|
673
|
-
return // No data for Obadiah or chapter
|
|
674
|
-
}
|
|
675
|
-
if (!this.versificationDifferences[book]) {
|
|
676
|
-
return // No versification differences for Obadiah
|
|
677
|
-
}
|
|
678
|
-
// Process versification differences
|
|
679
|
-
for (const [key, value] of Object.entries(this.versificationDifferences[book])) {
|
|
680
|
-
if (value[version].startsWith(`${chapter}:`)) {
|
|
681
|
-
if (value[version]) {
|
|
682
|
-
const verse = value[version].split(":")[1]
|
|
683
|
-
singleChapterBook[book][chapter].push(Number(verse))
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
singleChapterBook[book][chapter] = Array.from(new Set(singleChapterBook[book][chapter]))
|
|
688
|
-
return singleChapterBook[book]
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// Handle all other books using chapterVerses
|
|
692
|
-
if (!this.chapterVerses[book][chapter]) {
|
|
693
|
-
return
|
|
694
|
-
}
|
|
695
|
-
if (!this.versificationDifferences[book]) {
|
|
696
|
-
return
|
|
697
|
-
}
|
|
698
|
-
for (const [key, value] of Object.entries(this.versificationDifferences[book])) {
|
|
699
|
-
if (value[version].startsWith(`${chapter}:`)) {
|
|
700
|
-
if (value[version]) {
|
|
701
|
-
const verse = value[version].split(":")[1]
|
|
702
|
-
this.chapterVerses[book][chapter].push(Number(verse))
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
this.chapterVerses[book][chapter] = Array.from(new Set(this.chapterVerses[book][chapter]))
|
|
707
|
-
return this.chapterVerses
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
/**
|
|
711
|
-
* Sets the Bible version and applies versification differences.
|
|
712
|
-
* @param {string} book - The book name.
|
|
713
|
-
* @param {number} chapter - The chapter number.
|
|
714
|
-
* @param {string} version - The Bible version.
|
|
715
|
-
* @private
|
|
716
|
-
*/
|
|
717
|
-
_setVersion(book, chapter, version) {
|
|
718
|
-
this.version = version ? version : "eng"
|
|
719
|
-
if (this.version !== "eng") {
|
|
720
|
-
this._searchVersificationDifferences(book, chapter, version)
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
/**
|
|
725
|
-
* Applies versification differences to parsed passages.
|
|
726
|
-
*/
|
|
727
|
-
versification() {
|
|
728
|
-
this.passages.forEach((passage) => {
|
|
729
|
-
const hasVersification = this.versificationDifferences[passage.book]
|
|
730
|
-
passage.passages.forEach((subPassage) => {
|
|
731
|
-
if (hasVersification) {
|
|
732
|
-
const key = `${subPassage.chapter}:${subPassage.verse}`
|
|
733
|
-
if (this.versificationDifferences[passage.book][key]) {
|
|
734
|
-
subPassage.versification = this.versificationDifferences[passage.book][key]
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
if (passage.version) {
|
|
738
|
-
const versionAbbreviation = passage.version.abbreviation
|
|
739
|
-
const versionType =
|
|
740
|
-
versionAbbreviation === "lxx" ? "lxx" : versionAbbreviation === "mt" ? "mt" : null
|
|
741
|
-
if (versionType) {
|
|
742
|
-
const versionReference = `${subPassage.chapter}:${subPassage.verse}`
|
|
743
|
-
for (const versification in this.versificationDifferences[passage.book]) {
|
|
744
|
-
if (
|
|
745
|
-
this.versificationDifferences[passage.book][versification][versionType] ===
|
|
746
|
-
versionReference
|
|
747
|
-
) {
|
|
748
|
-
subPassage.versification = this.versificationDifferences[passage.book][versification]
|
|
749
|
-
break
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
})
|
|
755
|
-
})
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
/**
|
|
759
|
-
* Populates passage with expanded verse objects.
|
|
760
|
-
* @param {Object} passage - The passage object.
|
|
761
|
-
* @returns {Object[]} Array of verse objects.
|
|
762
|
-
*/
|
|
763
|
-
populate(passage) {
|
|
764
|
-
const { book, chapter, verses, type, to } = passage
|
|
765
|
-
const version = passage.version?.abbreviation || "eng"
|
|
766
|
-
this._setVersion(book, chapter, version)
|
|
767
|
-
|
|
768
|
-
if (type === this.SINGLE_CHAPTER) {
|
|
769
|
-
const chapterVerses = this.getChapterVerses(book, chapter)
|
|
770
|
-
return this.expandVerses(book, chapter, [`${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`])
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
if (type === this.CHAPTER_VERSE || type === this.COMMA_SEPARATED || type === this.CHAPTER_VERSE_RANGE) {
|
|
774
|
-
return this.expandVerses(book, chapter, verses)
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
if (type === this.CHAPTER_RANGE) {
|
|
778
|
-
const passages = []
|
|
779
|
-
if (to && to.chapter) {
|
|
780
|
-
for (let ch = chapter; ch <= to.chapter; ch++) {
|
|
781
|
-
const chapterVerses = this.getChapterVerses(book, ch)
|
|
782
|
-
passages.push(
|
|
783
|
-
...this.expandVerses(book, ch, [
|
|
784
|
-
`${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`,
|
|
785
|
-
])
|
|
786
|
-
)
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
return passages
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
if (type === this.MULTI_CHAPTER_RANGE) {
|
|
793
|
-
const passages = []
|
|
794
|
-
const startVerse = verses[0]?.includes("-") ? Number(verses[0].split("-")[0]) : Number(verses[0]) || 1
|
|
795
|
-
const endVerse = to?.verses?.[0]?.includes("-")
|
|
796
|
-
? Number(to.verses[0].split("-")[1])
|
|
797
|
-
: Number(to?.verses?.[0]) || 1
|
|
798
|
-
const endChapter = to?.chapter || chapter
|
|
799
|
-
|
|
800
|
-
for (let ch = chapter; ch <= endChapter; ch++) {
|
|
801
|
-
const chapterVerses = this.getChapterVerses(book, ch)
|
|
802
|
-
const from = ch === chapter ? startVerse : chapterVerses[0]
|
|
803
|
-
const toVerse = ch === endChapter ? endVerse : chapterVerses[chapterVerses.length - 1]
|
|
804
|
-
passages.push(...this.expandVerses(book, ch, [`${from}-${toVerse}`]))
|
|
805
|
-
}
|
|
806
|
-
return passages
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
return []
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
/**
|
|
813
|
-
* Expands verse references into individual verse objects.
|
|
814
|
-
* @param {string} book - The book name.
|
|
815
|
-
* @param {number} chapter - The chapter number.
|
|
816
|
-
* @param {Array<string|number>} verses - Array of verses or ranges.
|
|
817
|
-
* @returns {Object[]} Array of verse objects.
|
|
818
|
-
*/
|
|
819
|
-
expandVerses(book, chapter, verses) {
|
|
820
|
-
const passages = []
|
|
821
|
-
const chapterVerses = this.getChapterVerses(book, chapter)
|
|
822
|
-
|
|
823
|
-
verses.forEach((verse) => {
|
|
824
|
-
if (typeof verse === "string" && verse.includes("-")) {
|
|
825
|
-
const [start, end] = verse.split("-").map(Number)
|
|
826
|
-
for (let i = start; i <= end && i <= chapterVerses[chapterVerses.length - 1]; i++) {
|
|
827
|
-
passages.push({ book, chapter, verse: i })
|
|
828
|
-
}
|
|
829
|
-
} else {
|
|
830
|
-
const verseNum = Number(verse)
|
|
831
|
-
if (!isNaN(verseNum) && verseNum > 0) {
|
|
832
|
-
passages.push({ book, chapter, verse: verseNum })
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
})
|
|
836
|
-
return passages
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
/**
|
|
840
|
-
* Normalizes book names using abbreviations or full names.
|
|
841
|
-
* @param {string|Array} book - The book name or array.
|
|
842
|
-
* @returns {string} Normalized book name.
|
|
843
|
-
*/
|
|
844
|
-
bookify(book) {
|
|
845
|
-
if (typeof book !== "string") {
|
|
846
|
-
book = book[0]
|
|
847
|
-
}
|
|
848
|
-
book = book.toLowerCase()
|
|
849
|
-
let bookified = this.abbreviations[Object.keys(this.abbreviations).find((abbr) => abbr.toLowerCase() === book)]
|
|
850
|
-
if (bookified) {
|
|
851
|
-
return bookified
|
|
852
|
-
}
|
|
853
|
-
bookified =
|
|
854
|
-
this.bible.new.find((b) => b.toLowerCase() === book) || this.bible.old.find((b) => b.toLowerCase() === book)
|
|
855
|
-
return bookified || book
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
/**
|
|
859
|
-
* Returns parsed passages with utility methods.
|
|
860
|
-
* @returns {Object[]} Array of passages with methods.
|
|
861
|
-
*/
|
|
862
|
-
getPassages() {
|
|
863
|
-
const passagesArray = [...this.passages]
|
|
864
|
-
const self = this
|
|
865
|
-
|
|
866
|
-
passagesArray.first = function () {
|
|
867
|
-
return this.length > 0 ? this[0] : null
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
passagesArray.oldTestament = function () {
|
|
871
|
-
return this.filter((passage) => passage.testament === "old")
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
passagesArray.newTestament = function () {
|
|
875
|
-
return this.filter((passage) => passage.testament === "new")
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
passagesArray.combine = function (options = {}) {
|
|
879
|
-
const { book = true, chapter = true } = options
|
|
880
|
-
|
|
881
|
-
if (!book) {
|
|
882
|
-
return [...this]
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
const groupedByBook = new Map()
|
|
886
|
-
|
|
887
|
-
this.forEach((passage) => {
|
|
888
|
-
const bookKey = passage.book
|
|
889
|
-
if (!groupedByBook.has(bookKey)) {
|
|
890
|
-
groupedByBook.set(bookKey, [])
|
|
891
|
-
}
|
|
892
|
-
groupedByBook.get(bookKey).push(passage)
|
|
893
|
-
})
|
|
894
|
-
|
|
895
|
-
const combinedPassages = []
|
|
896
|
-
|
|
897
|
-
for (const [book, bookPassages] of groupedByBook) {
|
|
898
|
-
if (chapter) {
|
|
899
|
-
const groupedByChapter = new Map()
|
|
900
|
-
bookPassages.forEach((passage) => {
|
|
901
|
-
const chapterKey = `${passage.book}-${passage.chapter}`
|
|
902
|
-
if (!groupedByChapter.has(chapterKey)) {
|
|
903
|
-
groupedByChapter.set(chapterKey, [])
|
|
904
|
-
}
|
|
905
|
-
groupedByChapter.get(chapterKey).push(passage)
|
|
906
|
-
})
|
|
907
|
-
|
|
908
|
-
for (const passages of groupedByChapter.values()) {
|
|
909
|
-
if (passages.length === 1) {
|
|
910
|
-
combinedPassages.push({ ...passages[0] })
|
|
911
|
-
} else {
|
|
912
|
-
const combined = self.combine(passages)
|
|
913
|
-
combinedPassages.push(combined)
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
} else {
|
|
917
|
-
const combined = self.combine(bookPassages)
|
|
918
|
-
combinedPassages.push(combined)
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
return combinedPassages
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
passagesArray.getVersion = function (targetVersion) {
|
|
926
|
-
const targetAbbr = targetVersion.toLowerCase() === "bhs" ? "mt" : targetVersion.toLowerCase()
|
|
927
|
-
let versionObj
|
|
928
|
-
if (targetAbbr === "eng") {
|
|
929
|
-
versionObj = { name: "English", value: "ENG", abbreviation: "eng" }
|
|
930
|
-
} else if (targetAbbr === "mt") {
|
|
931
|
-
versionObj = { name: "Masoretic Text", value: "MT", abbreviation: "mt" }
|
|
932
|
-
} else if (targetAbbr === "lxx") {
|
|
933
|
-
versionObj = { name: "Septuagint", value: "LXX", abbreviation: "lxx" }
|
|
934
|
-
} else {
|
|
935
|
-
throw new Error("Invalid version: must be one of 'eng', 'mt', 'bhs', 'lxx'")
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
const converted = this.map((passage) => {
|
|
939
|
-
const cloned = JSON.parse(JSON.stringify(passage))
|
|
940
|
-
cloned.version = versionObj
|
|
941
|
-
|
|
942
|
-
cloned.passages.forEach((sub) => {
|
|
943
|
-
if (sub.versification && sub.versification[targetAbbr]) {
|
|
944
|
-
const [ch, v] = sub.versification[targetAbbr].split(":").map(Number)
|
|
945
|
-
sub.chapter = ch
|
|
946
|
-
sub.verse = v
|
|
947
|
-
}
|
|
948
|
-
// else remain unchanged
|
|
949
|
-
})
|
|
950
|
-
|
|
951
|
-
// Recompute summary fields
|
|
952
|
-
cloned.passages.sort((a, b) => a.chapter - b.chapter || a.verse - b.verse)
|
|
953
|
-
|
|
954
|
-
if (cloned.passages.length > 0) {
|
|
955
|
-
cloned.start = {
|
|
956
|
-
book: cloned.book,
|
|
957
|
-
chapter: cloned.passages[0].chapter,
|
|
958
|
-
verse: cloned.passages[0].verse,
|
|
959
|
-
}
|
|
960
|
-
cloned.end = {
|
|
961
|
-
book: cloned.book,
|
|
962
|
-
chapter: cloned.passages[cloned.passages.length - 1].chapter,
|
|
963
|
-
verse: cloned.passages[cloned.passages.length - 1].verse,
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
const chapterVersesMap = {}
|
|
968
|
-
cloned.passages.forEach((p) => {
|
|
969
|
-
if (!chapterVersesMap[p.chapter]) chapterVersesMap[p.chapter] = new Set()
|
|
970
|
-
chapterVersesMap[p.chapter].add(p.verse)
|
|
971
|
-
})
|
|
972
|
-
|
|
973
|
-
const sortedChs = Object.keys(chapterVersesMap)
|
|
974
|
-
.map(Number)
|
|
975
|
-
.sort((a, b) => a - b)
|
|
976
|
-
const chapterStrs = []
|
|
977
|
-
|
|
978
|
-
const mergeFunc = (verses) => {
|
|
979
|
-
const sorted = [...verses].sort((a, b) => a - b)
|
|
980
|
-
const merged = []
|
|
981
|
-
if (sorted.length === 0) return merged
|
|
982
|
-
let start = sorted[0]
|
|
983
|
-
let end = sorted[0]
|
|
984
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
985
|
-
if (sorted[i] === end + 1) {
|
|
986
|
-
end = sorted[i]
|
|
987
|
-
} else {
|
|
988
|
-
merged.push(start === end ? `${start}` : `${start}-${end}`)
|
|
989
|
-
start = end = sorted[i]
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
merged.push(start === end ? `${start}` : `${start}-${end}`)
|
|
993
|
-
return merged
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
sortedChs.forEach((ch) => {
|
|
997
|
-
const vs = Array.from(chapterVersesMap[ch])
|
|
998
|
-
.filter((v) => v > 0)
|
|
999
|
-
.sort((a, b) => a - b)
|
|
1000
|
-
if (vs.length > 0) {
|
|
1001
|
-
const merged = mergeFunc(vs)
|
|
1002
|
-
chapterStrs.push(`${ch}:${merged.join(",")}`)
|
|
1003
|
-
}
|
|
1004
|
-
})
|
|
1005
|
-
|
|
1006
|
-
if (chapterStrs.length === 0) {
|
|
1007
|
-
return cloned // no verses, perhaps error but return as is
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
const firstCh = sortedChs[0]
|
|
1011
|
-
const lastCh = sortedChs[sortedChs.length - 1]
|
|
1012
|
-
cloned.chapter = firstCh
|
|
1013
|
-
|
|
1014
|
-
const mergedFirst = mergeFunc(chapterVersesMap[firstCh] || new Set())
|
|
1015
|
-
cloned.verses = mergedFirst
|
|
1016
|
-
|
|
1017
|
-
if (firstCh !== lastCh) {
|
|
1018
|
-
cloned.type = this.MULTI_CHAPTER_RANGE
|
|
1019
|
-
cloned.to = {
|
|
1020
|
-
book: cloned.book,
|
|
1021
|
-
chapter: lastCh,
|
|
1022
|
-
verses: mergeFunc(chapterVersesMap[lastCh] || new Set()),
|
|
1023
|
-
}
|
|
1024
|
-
cloned.original = `${cloned.book} ${chapterStrs.join("; ")}`
|
|
1025
|
-
} else {
|
|
1026
|
-
const hasRangeOrMultiple =
|
|
1027
|
-
mergedFirst.length > 1 || (mergedFirst.length === 1 && mergedFirst[0].includes("-"))
|
|
1028
|
-
cloned.type = hasRangeOrMultiple ? this.CHAPTER_VERSE_RANGE : this.CHAPTER_VERSE
|
|
1029
|
-
if (cloned.to) delete cloned.to
|
|
1030
|
-
cloned.original = `${cloned.book} ${chapterStrs[0]}`
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
const chString = chapterStrs.join("; ")
|
|
1034
|
-
cloned.scripture = {
|
|
1035
|
-
passage: `${cloned.book} ${chString}`,
|
|
1036
|
-
cv: chString,
|
|
1037
|
-
hash: `${cloned.book.toLowerCase()}_${chString
|
|
1038
|
-
.replace(/:/g, ".")
|
|
1039
|
-
.replace(/-/g, ".")
|
|
1040
|
-
.replace(/[,; ]/g, ".")}`,
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
// Set abbr
|
|
1044
|
-
const sblEntry = Object.entries(self.sblAbbreviations).find(
|
|
1045
|
-
([key]) => key.toLowerCase() === cloned.book.toLowerCase()
|
|
1046
|
-
)
|
|
1047
|
-
const suffix = versionObj.abbreviation === "eng" ? "" : ` ${versionObj.value}`
|
|
1048
|
-
if (sblEntry) {
|
|
1049
|
-
const { value, abbr } = sblEntry[1]
|
|
1050
|
-
cloned.abbr = abbr
|
|
1051
|
-
? `${value}. ${cloned.scripture.cv}${suffix}`
|
|
1052
|
-
: `${value} ${cloned.scripture.cv}${suffix}`
|
|
1053
|
-
} else {
|
|
1054
|
-
cloned.abbr = `${cloned.book} ${cloned.scripture.cv}${suffix}`
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
return cloned
|
|
1058
|
-
})
|
|
1059
|
-
|
|
1060
|
-
const newArray = [...converted]
|
|
1061
|
-
|
|
1062
|
-
newArray.first = function () {
|
|
1063
|
-
return this.length > 0 ? this[0] : null
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
newArray.oldTestament = function () {
|
|
1067
|
-
return this.filter((passage) => passage.testament === "old")
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
newArray.newTestament = function () {
|
|
1071
|
-
return this.filter((passage) => passage.testament === "new")
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
newArray.combine = function (options = {}) {
|
|
1075
|
-
const { book = true, chapter = true } = options
|
|
1076
|
-
|
|
1077
|
-
if (!book) {
|
|
1078
|
-
return [...this]
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
const groupedByBook = new Map()
|
|
1082
|
-
|
|
1083
|
-
this.forEach((passage) => {
|
|
1084
|
-
const bookKey = passage.book
|
|
1085
|
-
if (!groupedByBook.has(bookKey)) {
|
|
1086
|
-
groupedByBook.set(bookKey, [])
|
|
1087
|
-
}
|
|
1088
|
-
groupedByBook.get(bookKey).push(passage)
|
|
1089
|
-
})
|
|
1090
|
-
|
|
1091
|
-
const combinedPassages = []
|
|
1092
|
-
|
|
1093
|
-
for (const [book, bookPassages] of groupedByBook) {
|
|
1094
|
-
if (chapter) {
|
|
1095
|
-
const groupedByChapter = new Map()
|
|
1096
|
-
bookPassages.forEach((passage) => {
|
|
1097
|
-
const chapterKey = `${passage.book}-${passage.chapter}`
|
|
1098
|
-
if (!groupedByChapter.has(chapterKey)) {
|
|
1099
|
-
groupedByChapter.set(chapterKey, [])
|
|
1100
|
-
}
|
|
1101
|
-
groupedByChapter.get(chapterKey).push(passage)
|
|
1102
|
-
})
|
|
1103
|
-
|
|
1104
|
-
for (const passages of groupedByChapter.values()) {
|
|
1105
|
-
if (passages.length === 1) {
|
|
1106
|
-
combinedPassages.push({ ...passages[0] })
|
|
1107
|
-
} else {
|
|
1108
|
-
const combined = self.combine(passages)
|
|
1109
|
-
combinedPassages.push(combined)
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
} else {
|
|
1113
|
-
const combined = self.combine(bookPassages)
|
|
1114
|
-
combinedPassages.push(combined)
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
return combinedPassages
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
newArray.getVersion = passagesArray.getVersion
|
|
1122
|
-
|
|
1123
|
-
return newArray
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
return passagesArray
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
/**
|
|
1130
|
-
* Returns the first parsed passage.
|
|
1131
|
-
* @returns {Object|null} The first passage or null.
|
|
1132
|
-
*/
|
|
1133
|
-
first() {
|
|
1134
|
-
return this.passages.length > 0 ? this.passages[0] : null
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
/**
|
|
1138
|
-
* Formats a passage into a human-readable reference.
|
|
1139
|
-
* @param {Object} passage - The passage object.
|
|
1140
|
-
* @returns {Object} Formatted passage data.
|
|
1141
|
-
*/
|
|
1142
|
-
scripturize(passage) {
|
|
1143
|
-
const formatChapterVerse = (chapter, verses) => {
|
|
1144
|
-
if (!chapter || !verses || verses.length === 0) return ""
|
|
1145
|
-
if (verses.length === 1) {
|
|
1146
|
-
return `${chapter}:${verses[0]}`
|
|
1147
|
-
}
|
|
1148
|
-
const isRange = verses.every((v, i, arr) => i === 0 || Number(v) === Number(arr[i - 1]) + 1)
|
|
1149
|
-
if (isRange) {
|
|
1150
|
-
return `${chapter}:${verses[0]}-${verses[verses.length - 1]}`
|
|
1151
|
-
}
|
|
1152
|
-
return `${chapter}:${verses.join(",")}`
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
let combined = `${passage.book}`
|
|
1156
|
-
if (passage.type === "multi_chapter_verse_range" && passage.to) {
|
|
1157
|
-
combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}-${formatChapterVerse(
|
|
1158
|
-
passage.to.chapter,
|
|
1159
|
-
passage.to.verses
|
|
1160
|
-
)}`
|
|
1161
|
-
} else if (passage.type === "chapter_verse_range" || passage.type === "comma_separated_verses") {
|
|
1162
|
-
combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
|
|
1163
|
-
} else if (passage.type === "chapter_range" && passage.to) {
|
|
1164
|
-
combined += ` ${passage.chapter}-${passage.to.chapter}`
|
|
1165
|
-
} else {
|
|
1166
|
-
combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
const cv = passage.to
|
|
1170
|
-
? `${formatChapterVerse(passage.chapter, passage.verses)}-${formatChapterVerse(
|
|
1171
|
-
passage.to.chapter,
|
|
1172
|
-
passage.to.verses
|
|
1173
|
-
)}`
|
|
1174
|
-
: formatChapterVerse(passage.chapter, passage.verses)
|
|
1175
|
-
|
|
1176
|
-
const hash = `${passage.book.toLowerCase()}_${cv.replace(/:/g, ".").replace(/-/g, ".")}`
|
|
1177
|
-
|
|
1178
|
-
return {
|
|
1179
|
-
passage: combined,
|
|
1180
|
-
cv: cv,
|
|
1181
|
-
hash: hash,
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
/**
|
|
1186
|
-
* Combines multiple passages into a single reference.
|
|
1187
|
-
* @param {Object[]} [passages=this.passages] - Array of passages to combine.
|
|
1188
|
-
* @returns {Object} Combined passage object.
|
|
1189
|
-
*/
|
|
1190
|
-
combine(passages = this.passages) {
|
|
1191
|
-
if (!passages || passages.length === 0) {
|
|
1192
|
-
throw new Error("No passages provided to join.")
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
const uniqueBooks = [...new Set(passages.map((p) => p.book))]
|
|
1196
|
-
if (uniqueBooks.length > 1) {
|
|
1197
|
-
throw new Error("Passages must be from the same book to join.")
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
const versions = new Set(passages.map((p) => p.version.abbreviation))
|
|
1201
|
-
if (versions.size > 1) {
|
|
1202
|
-
throw new Error("Cannot combine passages from different versions.")
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
const combined = {
|
|
1206
|
-
...passages[0],
|
|
1207
|
-
verses: [],
|
|
1208
|
-
passages: [],
|
|
1209
|
-
to: null,
|
|
1210
|
-
scripture: {},
|
|
1211
|
-
type: null,
|
|
1212
|
-
start: null,
|
|
1213
|
-
end: null,
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
const chapterVerses = {}
|
|
1217
|
-
let firstChapter = null
|
|
1218
|
-
let lastChapter = null
|
|
1219
|
-
let firstVerse = null
|
|
1220
|
-
let lastVerse = null
|
|
1221
|
-
|
|
1222
|
-
passages.forEach((passage) => {
|
|
1223
|
-
passage.passages.forEach((p) => {
|
|
1224
|
-
const chapter = p.chapter
|
|
1225
|
-
const verse = p.verse
|
|
1226
|
-
if (!chapterVerses[chapter]) {
|
|
1227
|
-
chapterVerses[chapter] = new Set()
|
|
1228
|
-
}
|
|
1229
|
-
chapterVerses[chapter].add(verse)
|
|
1230
|
-
combined.passages.push({
|
|
1231
|
-
book: p.book,
|
|
1232
|
-
chapter: p.chapter,
|
|
1233
|
-
verse: p.verse,
|
|
1234
|
-
versification: p.versification,
|
|
1235
|
-
})
|
|
1236
|
-
|
|
1237
|
-
if (firstChapter === null || chapter < firstChapter) {
|
|
1238
|
-
firstChapter = chapter
|
|
1239
|
-
firstVerse = verse
|
|
1240
|
-
} else if (chapter === firstChapter && (firstVerse === null || verse < firstVerse)) {
|
|
1241
|
-
firstVerse = verse
|
|
1242
|
-
}
|
|
1243
|
-
if (lastChapter === null || chapter > lastChapter) {
|
|
1244
|
-
lastChapter = chapter
|
|
1245
|
-
lastVerse = verse
|
|
1246
|
-
} else if (chapter === lastChapter && (lastVerse === null || verse > lastVerse)) {
|
|
1247
|
-
lastVerse = verse
|
|
1248
|
-
}
|
|
1249
|
-
})
|
|
1250
|
-
})
|
|
1251
|
-
|
|
1252
|
-
combined.passages = Array.from(new Set(combined.passages.map(JSON.stringify))).map(JSON.parse)
|
|
1253
|
-
|
|
1254
|
-
const chapterStrings = []
|
|
1255
|
-
const sortedChapters = Object.keys(chapterVerses)
|
|
1256
|
-
.map(Number)
|
|
1257
|
-
.sort((a, b) => a - b)
|
|
1258
|
-
|
|
1259
|
-
sortedChapters.forEach((chapter) => {
|
|
1260
|
-
const verses = Array.from(chapterVerses[chapter])
|
|
1261
|
-
.map(Number)
|
|
1262
|
-
.filter((verse) => verse > 0)
|
|
1263
|
-
.sort((a, b) => a - b)
|
|
1264
|
-
if (verses.length > 0) {
|
|
1265
|
-
const mergedVerses = this.mergeRanges(verses)
|
|
1266
|
-
chapterStrings.push(`${chapter}:${mergedVerses.join(",")}`)
|
|
1267
|
-
if (chapter === firstChapter) {
|
|
1268
|
-
combined.verses = mergedVerses
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
})
|
|
1272
|
-
|
|
1273
|
-
if (chapterStrings.length === 0) {
|
|
1274
|
-
throw new Error("No valid verses found in passages.")
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
combined.chapter = firstChapter
|
|
1278
|
-
|
|
1279
|
-
if (firstChapter !== lastChapter) {
|
|
1280
|
-
combined.type = this.MULTI_CHAPTER_RANGE
|
|
1281
|
-
combined.to = {
|
|
1282
|
-
book: combined.book,
|
|
1283
|
-
chapter: lastChapter,
|
|
1284
|
-
verses: this.mergeRanges(
|
|
1285
|
-
Array.from(chapterVerses[lastChapter])
|
|
1286
|
-
.filter((verse) => verse > 0)
|
|
1287
|
-
.sort((a, b) => a - b)
|
|
1288
|
-
),
|
|
1289
|
-
}
|
|
1290
|
-
combined.original = `${combined.book} ${chapterStrings.join("; ")}`
|
|
1291
|
-
} else {
|
|
1292
|
-
combined.type = combined.verses.length > 1 ? this.CHAPTER_VERSE_RANGE : this.CHAPTER_VERSE
|
|
1293
|
-
combined.original = `${combined.book} ${chapterStrings[0]}`
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
const chapterString = chapterStrings.join(";")
|
|
1297
|
-
combined.scripture = {
|
|
1298
|
-
passage: `${combined.book} ${chapterString}`,
|
|
1299
|
-
cv: chapterString,
|
|
1300
|
-
hash: `${combined.book.toLowerCase()}_${chapterString
|
|
1301
|
-
.replace(/:/g, ".")
|
|
1302
|
-
.replace(/-/g, ".")
|
|
1303
|
-
.replace(/[,;]/g, ".")}`,
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
combined.start = {
|
|
1307
|
-
book: combined.book,
|
|
1308
|
-
chapter: firstChapter,
|
|
1309
|
-
verse:
|
|
1310
|
-
firstVerse > 0
|
|
1311
|
-
? firstVerse
|
|
1312
|
-
: Math.min(...Array.from(chapterVerses[firstChapter]).filter((verse) => verse > 0)),
|
|
1313
|
-
}
|
|
1314
|
-
combined.end = {
|
|
1315
|
-
book: combined.book,
|
|
1316
|
-
chapter: lastChapter,
|
|
1317
|
-
verse:
|
|
1318
|
-
lastVerse > 0
|
|
1319
|
-
? lastVerse
|
|
1320
|
-
: Math.max(...Array.from(chapterVerses[lastChapter]).filter((verse) => verse > 0)),
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
combined.reference = function () {
|
|
1324
|
-
return this.scripture.passage
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
if (combined.to === null) {
|
|
1328
|
-
delete combined.to
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
// Respect the common version
|
|
1332
|
-
combined.version = passages[0].version
|
|
1333
|
-
|
|
1334
|
-
// Set abbr with version suffix if not ENG
|
|
1335
|
-
const sblEntry = Object.entries(this.sblAbbreviations).find(
|
|
1336
|
-
([key]) => key.toLowerCase() === combined.book.toLowerCase()
|
|
1337
|
-
)
|
|
1338
|
-
const suffix = combined.version.abbreviation === "eng" ? "" : ` ${combined.version.value}`
|
|
1339
|
-
if (sblEntry) {
|
|
1340
|
-
const { value, abbr } = sblEntry[1]
|
|
1341
|
-
combined.abbr = abbr
|
|
1342
|
-
? `${value}. ${combined.scripture.cv}${suffix}`
|
|
1343
|
-
: `${value} ${combined.scripture.cv}${suffix}`
|
|
1344
|
-
} else {
|
|
1345
|
-
combined.abbr = `${combined.book} ${combined.scripture.cv}${suffix}`
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
const self = this
|
|
1349
|
-
combined.getVersion = function (targetVersion) {
|
|
1350
|
-
const cloned = JSON.parse(JSON.stringify(this))
|
|
1351
|
-
const targetAbbr = targetVersion.toLowerCase() === "bhs" ? "mt" : targetVersion.toLowerCase()
|
|
1352
|
-
let versionObj
|
|
1353
|
-
if (targetAbbr === "eng") {
|
|
1354
|
-
versionObj = { name: "English", value: "ENG", abbreviation: "eng" }
|
|
1355
|
-
} else if (targetAbbr === "mt") {
|
|
1356
|
-
versionObj = { name: "Masoretic Text", value: "MT", abbreviation: "mt" }
|
|
1357
|
-
} else if (targetAbbr === "lxx") {
|
|
1358
|
-
versionObj = { name: "Septuagint", value: "LXX", abbreviation: "lxx" }
|
|
1359
|
-
} else {
|
|
1360
|
-
throw new Error("Invalid version: must be one of 'eng', 'mt', 'bhs', 'lxx'")
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
cloned.version = versionObj
|
|
1364
|
-
|
|
1365
|
-
cloned.passages.forEach((sub) => {
|
|
1366
|
-
if (sub.versification && sub.versification[targetAbbr]) {
|
|
1367
|
-
const [ch, v] = sub.versification[targetAbbr].split(":").map(Number)
|
|
1368
|
-
sub.chapter = ch
|
|
1369
|
-
sub.verse = v
|
|
1370
|
-
}
|
|
1371
|
-
// else remain unchanged
|
|
1372
|
-
})
|
|
1373
|
-
|
|
1374
|
-
// Recompute summary fields
|
|
1375
|
-
cloned.passages.sort((a, b) => a.chapter - b.chapter || a.verse - b.verse)
|
|
1376
|
-
|
|
1377
|
-
if (cloned.passages.length > 0) {
|
|
1378
|
-
cloned.start = {
|
|
1379
|
-
book: cloned.book,
|
|
1380
|
-
chapter: cloned.passages[0].chapter,
|
|
1381
|
-
verse: cloned.passages[0].verse,
|
|
1382
|
-
}
|
|
1383
|
-
cloned.end = {
|
|
1384
|
-
book: cloned.book,
|
|
1385
|
-
chapter: cloned.passages[cloned.passages.length - 1].chapter,
|
|
1386
|
-
verse: cloned.passages[cloned.passages.length - 1].verse,
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
const chapterVersesMap = {}
|
|
1391
|
-
cloned.passages.forEach((p) => {
|
|
1392
|
-
if (!chapterVersesMap[p.chapter]) chapterVersesMap[p.chapter] = new Set()
|
|
1393
|
-
chapterVersesMap[p.chapter].add(p.verse)
|
|
1394
|
-
})
|
|
1395
|
-
|
|
1396
|
-
const sortedChs = Object.keys(chapterVersesMap)
|
|
1397
|
-
.map(Number)
|
|
1398
|
-
.sort((a, b) => a - b)
|
|
1399
|
-
const chapterStrs = []
|
|
1400
|
-
|
|
1401
|
-
const mergeFunc = (verses) => {
|
|
1402
|
-
const sorted = [...verses].sort((a, b) => a - b)
|
|
1403
|
-
const merged = []
|
|
1404
|
-
if (sorted.length === 0) return merged
|
|
1405
|
-
let start = sorted[0]
|
|
1406
|
-
let end = sorted[0]
|
|
1407
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
1408
|
-
if (sorted[i] === end + 1) {
|
|
1409
|
-
end = sorted[i]
|
|
1410
|
-
} else {
|
|
1411
|
-
merged.push(start === end ? `${start}` : `${start}-${end}`)
|
|
1412
|
-
start = end = sorted[i]
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
merged.push(start === end ? `${start}` : `${start}-${end}`)
|
|
1416
|
-
return merged
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
sortedChs.forEach((ch) => {
|
|
1420
|
-
const vs = Array.from(chapterVersesMap[ch])
|
|
1421
|
-
.filter((v) => v > 0)
|
|
1422
|
-
.sort((a, b) => a - b)
|
|
1423
|
-
if (vs.length > 0) {
|
|
1424
|
-
const merged = mergeFunc(vs)
|
|
1425
|
-
chapterStrs.push(`${ch}:${merged.join(",")}`)
|
|
1426
|
-
}
|
|
1427
|
-
})
|
|
1428
|
-
|
|
1429
|
-
if (chapterStrs.length === 0) {
|
|
1430
|
-
return cloned // no verses, perhaps error but return as is
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
const firstCh = sortedChs[0]
|
|
1434
|
-
const lastCh = sortedChs[sortedChs.length - 1]
|
|
1435
|
-
cloned.chapter = firstCh
|
|
1436
|
-
|
|
1437
|
-
const mergedFirst = mergeFunc(chapterVersesMap[firstCh] || new Set())
|
|
1438
|
-
cloned.verses = mergedFirst
|
|
1439
|
-
|
|
1440
|
-
if (firstCh !== lastCh) {
|
|
1441
|
-
cloned.type = "multi_chapter_verse_range"
|
|
1442
|
-
cloned.to = {
|
|
1443
|
-
book: cloned.book,
|
|
1444
|
-
chapter: lastCh,
|
|
1445
|
-
verses: mergeFunc(chapterVersesMap[lastCh] || new Set()),
|
|
1446
|
-
}
|
|
1447
|
-
cloned.original = `${cloned.book} ${chapterStrs.join("; ")}`
|
|
1448
|
-
} else {
|
|
1449
|
-
const hasRangeOrMultiple =
|
|
1450
|
-
mergedFirst.length > 1 || (mergedFirst.length === 1 && mergedFirst[0].includes("-"))
|
|
1451
|
-
cloned.type = hasRangeOrMultiple ? "chapter_verse_range" : "chapter_verse"
|
|
1452
|
-
if (cloned.to) delete cloned.to
|
|
1453
|
-
cloned.original = `${cloned.book} ${chapterStrs[0]}`
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
const chString = chapterStrs.join("; ")
|
|
1457
|
-
cloned.scripture = {
|
|
1458
|
-
passage: `${cloned.book} ${chString}`,
|
|
1459
|
-
cv: chString,
|
|
1460
|
-
hash: `${cloned.book.toLowerCase()}_${chString
|
|
1461
|
-
.replace(/:/g, ".")
|
|
1462
|
-
.replace(/-/g, ".")
|
|
1463
|
-
.replace(/[,; ]/g, ".")}`,
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
// Set abbr
|
|
1467
|
-
const sblEntry = Object.entries(self.sblAbbreviations).find(
|
|
1468
|
-
([key]) => key.toLowerCase() === cloned.book.toLowerCase()
|
|
1469
|
-
)
|
|
1470
|
-
const suffix = versionObj.abbreviation === "eng" ? "" : ` ${versionObj.value}`
|
|
1471
|
-
if (sblEntry) {
|
|
1472
|
-
const { value, abbr } = sblEntry[1]
|
|
1473
|
-
cloned.abbr = abbr
|
|
1474
|
-
? `${value}. ${cloned.scripture.cv}${suffix}`
|
|
1475
|
-
: `${value} ${cloned.scripture.cv}${suffix}`
|
|
1476
|
-
} else {
|
|
1477
|
-
cloned.abbr = `${cloned.book} ${cloned.scripture.cv}${suffix}`
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
cloned.getVersion = this.getVersion
|
|
1481
|
-
|
|
1482
|
-
return cloned
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
return combined
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
/**
|
|
1489
|
-
* Merges verses into ranges or comma-separated lists.
|
|
1490
|
-
* @param {number[]} verses - Array of verse numbers.
|
|
1491
|
-
* @returns {string[]} Array of verse strings.
|
|
1492
|
-
*/
|
|
1493
|
-
mergeRanges(verses) {
|
|
1494
|
-
const sortedVerses = [...new Set(verses)].sort((a, b) => a - b)
|
|
1495
|
-
const merged = []
|
|
1496
|
-
let start = sortedVerses[0]
|
|
1497
|
-
let end = sortedVerses[0]
|
|
1498
|
-
|
|
1499
|
-
for (let i = 1; i < sortedVerses.length; i++) {
|
|
1500
|
-
if (sortedVerses[i] === end + 1) {
|
|
1501
|
-
end = sortedVerses[i]
|
|
1502
|
-
} else {
|
|
1503
|
-
if (start === end) {
|
|
1504
|
-
merged.push(`${start}`)
|
|
1505
|
-
} else {
|
|
1506
|
-
merged.push(`${start}-${end}`)
|
|
1507
|
-
}
|
|
1508
|
-
start = sortedVerses[i]
|
|
1509
|
-
end = sortedVerses[i]
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
if (start === end) {
|
|
1514
|
-
merged.push(`${start}`)
|
|
1515
|
-
} else {
|
|
1516
|
-
merged.push(`${start}-${end}`)
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
return merged
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
/**
|
|
1523
|
-
* Generates a table of contents for the Bible.
|
|
1524
|
-
* @param {string} [version="ESV"] - The Bible version.
|
|
1525
|
-
* @returns {Object} TOC with book-chapter-verse mappings.
|
|
1526
|
-
*/
|
|
1527
|
-
getToc(version = "ESV") {
|
|
1528
|
-
const toc = {}
|
|
1529
|
-
this.bible.old.forEach((book) => {
|
|
1530
|
-
if (this.chapterVerses[book]) {
|
|
1531
|
-
toc[book] = this.chapterVerses[book]
|
|
1532
|
-
}
|
|
1533
|
-
})
|
|
1534
|
-
this.bible.new.forEach((book) => {
|
|
1535
|
-
if (this.chapterVerses[book]) {
|
|
1536
|
-
toc[book] = this.chapterVerses[book]
|
|
1537
|
-
}
|
|
1538
|
-
})
|
|
1539
|
-
this.singleChapterBook.forEach((item) => {
|
|
1540
|
-
Object.keys(item).forEach((book) => {
|
|
1541
|
-
if (!toc[book]) {
|
|
1542
|
-
toc[book] = item[book]
|
|
1543
|
-
}
|
|
1544
|
-
})
|
|
1545
|
-
})
|
|
1546
|
-
const orderedToc = {}
|
|
1547
|
-
const canonicalOrder = [...this.bible.old, ...this.bible.new]
|
|
1548
|
-
canonicalOrder.forEach((book) => {
|
|
1549
|
-
if (toc[book]) {
|
|
1550
|
-
orderedToc[book] = toc[book]
|
|
1551
|
-
}
|
|
1552
|
-
})
|
|
1553
|
-
return orderedToc
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
/**
|
|
1557
|
-
* Validates a passage for correctness.
|
|
1558
|
-
* @param {Object} passage - The passage object.
|
|
1559
|
-
* @param {string} reference - The original reference.
|
|
1560
|
-
* @returns {boolean|Object} True if valid, error object if invalid.
|
|
1561
|
-
* @private
|
|
1562
|
-
*/
|
|
1563
|
-
_isValid(passage, reference) {
|
|
1564
|
-
const { book, chapter, verses, type } = passage
|
|
1565
|
-
|
|
1566
|
-
if (!verses.length && type !== this.SINGLE_CHAPTER) {
|
|
1567
|
-
return this.validationError(101, `Possible invalid chapter: ${reference}`)
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
const chapterVerses = this.getChapterVerses(book, chapter)
|
|
1571
|
-
if (!chapterVerses.length) {
|
|
1572
|
-
return this.validationError(102, `Chapter ${chapter} does not exist in ${book}`)
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
if (type === this.SINGLE_CHAPTER) {
|
|
1576
|
-
const [range] = verses
|
|
1577
|
-
if (range) {
|
|
1578
|
-
const [start, end] = range.split("-").map(Number)
|
|
1579
|
-
if (start < 1 || end > chapterVerses[chapterVerses.length - 1]) {
|
|
1580
|
-
return this.validationError(
|
|
1581
|
-
104,
|
|
1582
|
-
`Verse range ${start}-${end} exceeds available verses (1-${
|
|
1583
|
-
chapterVerses[chapterVerses.length - 1]
|
|
1584
|
-
}) in ${book} ${chapter}`
|
|
1585
|
-
)
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
return true
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
return this.validateVerses(book, chapter, verses, reference)
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
/**
|
|
1595
|
-
* Validates verse numbers for a chapter.
|
|
1596
|
-
* @param {string} book - The book name.
|
|
1597
|
-
* @param {number} chapter - The chapter number.
|
|
1598
|
-
* @param {Array<string|number>} verses - Array of verses or ranges.
|
|
1599
|
-
* @param {string} reference - The original reference.
|
|
1600
|
-
* @returns {boolean|Object} True if valid, error object if invalid.
|
|
1601
|
-
* @private
|
|
1602
|
-
*/
|
|
1603
|
-
validateVerses(book, chapter, verses, reference) {
|
|
1604
|
-
const chapterVerses = this.getChapterVerses(book, chapter)
|
|
1605
|
-
for (const verse of verses) {
|
|
1606
|
-
const verseRange = String(verse)
|
|
1607
|
-
const verseNumbers = verseRange.includes("-")
|
|
1608
|
-
? Array.from(
|
|
1609
|
-
{ length: Number(verseRange.split("-")[1]) - Number(verseRange.split("-")[0]) + 1 },
|
|
1610
|
-
(_, i) => Number(verseRange.split("-")[0]) + i
|
|
1611
|
-
)
|
|
1612
|
-
: [Number(verseRange)]
|
|
1613
|
-
|
|
1614
|
-
for (const v of verseNumbers) {
|
|
1615
|
-
if (isNaN(v) || v <= 0 || !chapterVerses.includes(v)) {
|
|
1616
|
-
return this.validationError(104, `Verse number ${v} does not exist in ${book} ${chapter}`)
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
return true
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
/**
|
|
1624
|
-
* Creates an error object for validation failures.
|
|
1625
|
-
* @param {number} code - Error code.
|
|
1626
|
-
* @param {string} message - Error message.
|
|
1627
|
-
* @returns {Object} Error object.
|
|
1628
|
-
* @private
|
|
1629
|
-
*/
|
|
1630
|
-
validationError(code, message) {
|
|
1631
|
-
return {
|
|
1632
|
-
error: true,
|
|
1633
|
-
code,
|
|
1634
|
-
message: { verse_exists: code === 104, chapter_exists: code !== 104, content: message },
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
/**
|
|
1639
|
-
* Determines the Bible version for a passage.
|
|
1640
|
-
* @param {string} version - The version (e.g., "lxx").
|
|
1641
|
-
* @param {string} testament - The testament ("old" or "new").
|
|
1642
|
-
* @returns {Object} Version object.
|
|
1643
|
-
* @private
|
|
1644
|
-
*/
|
|
1645
|
-
_handleVersion(version, testament) {
|
|
1646
|
-
const effectiveVersion = this.version || version || "eng"
|
|
1647
|
-
const lowerVersion = effectiveVersion.toLowerCase()
|
|
1648
|
-
|
|
1649
|
-
if (lowerVersion === "lxx" && testament === "old") {
|
|
1650
|
-
return { name: "Septuagint", value: "LXX", abbreviation: "lxx" }
|
|
1651
|
-
}
|
|
1652
|
-
if (lowerVersion === "mt" && testament === "old") {
|
|
1653
|
-
return { name: "Masoretic Text", value: "MT", abbreviation: "mt" }
|
|
1654
|
-
}
|
|
1655
|
-
return { name: "English", value: "ENG", abbreviation: "eng" }
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
/**
|
|
1659
|
-
* Replaces scripture references in text with formatted references.
|
|
1660
|
-
* @param {string} text - The original text.
|
|
1661
|
-
* @param {boolean} useAbbreviations - Whether to use abbreviated book names.
|
|
1662
|
-
* @returns {string} Text with replaced references.
|
|
1663
|
-
*/
|
|
1664
|
-
replace(text, useAbbreviations = true) {
|
|
1665
|
-
if (!this.passages.length) {
|
|
1666
|
-
return text
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
let result = text
|
|
1670
|
-
for (let i = this.passages.length - 1; i >= 0; i--) {
|
|
1671
|
-
const passage = this.passages[i]
|
|
1672
|
-
const { originalText, abbr, original } = passage
|
|
1673
|
-
const newReference = useAbbreviations ? abbr : original
|
|
1674
|
-
|
|
1675
|
-
const regex = new RegExp(`${originalText.replace(/([.*+?^${}()|[\]\\])/g, "\\$1")}`, "g")
|
|
1676
|
-
|
|
1677
|
-
const matches = [...result.matchAll(regex)]
|
|
1678
|
-
if (matches.length > 0) {
|
|
1679
|
-
for (let j = matches.length - 1; j >= 0; j--) {
|
|
1680
|
-
const match = matches[j]
|
|
1681
|
-
const startIndex = match.index
|
|
1682
|
-
const endIndex = startIndex + match[0].length
|
|
1683
|
-
const leadingSpace = match[1] || ""
|
|
1684
|
-
const hasOpeningParen = match[2] === "("
|
|
1685
|
-
const hasClosingParen = match[3] === ")"
|
|
1686
|
-
const trailingSpace = match[4] || " "
|
|
1687
|
-
const replacement =
|
|
1688
|
-
hasOpeningParen && hasClosingParen
|
|
1689
|
-
? `${leadingSpace}(${newReference})${trailingSpace}`
|
|
1690
|
-
: `${leadingSpace}${newReference}${trailingSpace}`
|
|
1691
|
-
result = result.slice(0, startIndex) + replacement + result.slice(endIndex)
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
return result
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
/**
|
|
1700
|
-
* Checks if all references in the passages array are from the same book.
|
|
1701
|
-
* @returns {boolean} True if all passages are from the same book, false otherwise.
|
|
1702
|
-
*/
|
|
1703
|
-
same() {
|
|
1704
|
-
if (this.passages.length <= 1) {
|
|
1705
|
-
return true
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
const firstBook = this.passages[0].book.toLowerCase()
|
|
1709
|
-
return this.passages.every((passage) => passage.book.toLowerCase() === firstBook)
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
module.exports = CodexParser
|