codexparser 0.1.55 → 0.1.57

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.
@@ -1,7 +1,8 @@
1
1
  const versified = require("./versified")
2
2
  const bible = require("./bible")
3
3
  const { bookRegex, chapterRegex, verseRegex, scripturesRegex } = require("./regex")
4
- const abbrevations = require("./abbr")
4
+ const abbreviations = require("./abbr")
5
+ const sblAbbreviations = require("./abbr/sbl")
5
6
  const dump = require("./functions").dump
6
7
  const dd = require("./functions").dd
7
8
  const sch = require("./functions").sch
@@ -16,7 +17,8 @@ class CodexParser {
16
17
  this.chapterRegex = chapterRegex
17
18
  this.verseRegex = verseRegex
18
19
  this.scripturesRegex = scripturesRegex
19
- this.abbreviations = abbrevations
20
+ this.abbreviations = abbreviations
21
+ this.sblAbbreviations = sblAbbreviations
20
22
  this.versificationDifferences = versified
21
23
  this.singleChapterBook = [
22
24
  sch("Jude", 25),
@@ -25,43 +27,36 @@ class CodexParser {
25
27
  sch("Obadiah", 21),
26
28
  sch("Philemon", 25),
27
29
  ]
28
-
29
30
  this.chapterVerses = chapter_verses
30
31
  this.error = false
31
32
  this.version = null
33
+ this.SINGLE_CHAPTER = "single_chapter"
34
+ this.CHAPTER_VERSE = "chapter_verse"
35
+ this.CHAPTER_VERSE_RANGE = "chapter_verse_range"
36
+ this.COMMA_SEPARATED = "comma_separated_verses"
37
+ this.CHAPTER_RANGE = "chapter_range"
38
+ this.MULTI_CHAPTER_RANGE = "multi_chapter_verse_range"
39
+ }
40
+
41
+ getChapterVerses(book, chapter) {
42
+ const singleChapterBook = this.singleChapterBook.find((b) => Object.keys(b)[0] === book)
43
+ return singleChapterBook ? singleChapterBook[book][chapter] || [] : this.chapterVerses[book]?.[chapter] || []
32
44
  }
33
45
 
34
- /**
35
- * Scans the given text for Bible references and stores all found references in the `found` property of the instance.
36
- *
37
- * @param {string} text - The text to scan for Bible references.
38
- * @return {CodexParser} - Returns the instance itself, enabling method chaining.
39
- */
40
46
  scan(text) {
41
- // Combine Old and New Testament book names into a single array
42
47
  const fullNames = [...this.bible.old, ...this.bible.new]
43
-
44
- // Retrieve all abbreviation keys from the abbreviations object
45
48
  const abbreviations = Object.keys(this.abbreviations)
46
-
47
- // Initialize the `found` array to store the results
48
49
  this.found = []
49
-
50
- // Preprocess input text: normalize separators while preserving abbreviations
51
50
  let normalizedText = text
52
- .replace(/\.(?=\d)/g, ":") // Convert periods before numbers into colons (e.g., 12.15 -> 12:15)
53
- .replace(/(\b[A-Za-z]+)\.(?=\s|$)/g, "$1") // Remove periods after abbreviations (e.g., Jd. -> Jd)
54
- .replace(/\s+/g, " ") // Normalize multiple spaces to a single space
55
-
56
- // Convert Bible book names, abbreviations, and input text to lowercase for case-insensitive matching
51
+ .replace(/\.(?=\d)/g, ":")
52
+ .replace(/(\b[A-Za-z]+)\.(?=\s|$)/g, "$1")
53
+ .replace(/\s+/g, " ")
57
54
  const lowercaseBibleFullNames = fullNames.map((book) => book.toLowerCase())
58
55
  const lowercaseBibleAbbreviations = abbreviations.map((abbr) => abbr.toLowerCase())
59
56
  const lowerCaseText = normalizedText.toLowerCase()
60
-
61
57
  let i = 0
62
58
 
63
59
  const isValidChapterVerseChar = (char) => /[^A-Za-z]/.test(char)
64
-
65
60
  const isNextBibleBook = (startIndex) => {
66
61
  const textAfterCurrentPosition = lowerCaseText.substring(startIndex).trim()
67
62
  return (
@@ -69,7 +64,6 @@ class CodexParser {
69
64
  lowercaseBibleAbbreviations.some((abbr) => textAfterCurrentPosition.startsWith(abbr))
70
65
  )
71
66
  }
72
-
73
67
  const detectSuffix = (startIndex) => {
74
68
  const suffixMatch = normalizedText.substring(startIndex).match(/\b(LXX|MT)\b/i)
75
69
  return suffixMatch ? suffixMatch[0].toUpperCase() : null
@@ -107,7 +101,6 @@ class CodexParser {
107
101
 
108
102
  while (i < normalizedText.length && isValidChapterVerseChar(normalizedText[i])) {
109
103
  if (isNextBibleBook(i)) break
110
-
111
104
  if (normalizedText[i] === ";") {
112
105
  const formattedReference = chapterVerse.trim().replace(/[^a-zA-Z0-9]+$/, "")
113
106
  if (formattedReference) references.push(formattedReference)
@@ -115,7 +108,6 @@ class CodexParser {
115
108
  i++
116
109
  continue
117
110
  }
118
-
119
111
  chapterVerse += normalizedText[i]
120
112
  i++
121
113
  }
@@ -134,14 +126,12 @@ class CodexParser {
134
126
  const [start, end] = ref.split("-")
135
127
  const startParts = start.split(":")
136
128
  const endParts = end.split(":")
137
-
138
- // Determine type based on the chapter (startParts[0] and endParts[0])
139
129
  type =
140
130
  startParts.length > 1 &&
141
131
  endParts.length > 1 &&
142
132
  startParts[0].trim() !== endParts[0].trim()
143
- ? "multi_chapter_verse_range" // Chapters differ
144
- : "chapter_verse_range" // Same chapter
133
+ ? "multi_chapter_verse_range"
134
+ : "chapter_verse_range"
145
135
  } else if (ref.includes(",")) {
146
136
  type = "comma_separated_verses"
147
137
  } else {
@@ -178,129 +168,78 @@ class CodexParser {
178
168
  return this
179
169
  }
180
170
 
181
- /**
182
- * Parses a given reference and returns an object with the parsed passage,
183
- * including book, chapter, verse, type, testament, index, and version.
184
- *
185
- * @param {string} reference - The reference to parse.
186
- * @returns {object} An object with the parsed passage.
187
- */
188
171
  parse(reference) {
189
- this.scan(reference) // Populate this.found
172
+ this.scan(reference)
190
173
 
191
174
  this.passages = this.found.map((passage) => {
192
175
  const book = this.bookify(passage.book)
193
176
  const testament = this.bible.old.includes(book) ? "old" : "new"
194
- const singleChapterBook = this.singleChapterBook.find((b) => Object.keys(b)[0] === book)
195
177
  const parsedPassage = {
196
- original: passage.book + " " + passage.reference,
197
- book: book,
178
+ original: `${passage.book} ${passage.reference}`,
179
+ book,
198
180
  chapter: null,
199
181
  verses: [],
200
182
  type: passage.type,
201
- testament: testament,
183
+ testament,
202
184
  index: passage.index,
203
185
  version: this._handleVersion(passage.version, testament),
186
+ passages: [],
187
+ scripture: null,
188
+ valid: true,
189
+ start: null,
190
+ end: null,
204
191
  }
205
192
 
206
- // Split reference into parts (e.g., "Matthew 1", "2 John 2", "Matthew 1:1-3,5")
207
- const parts = passage.reference.split(",")
193
+ this.parseReferenceParts(parsedPassage, passage.reference.split(","))
194
+ parsedPassage.passages = this.populate(parsedPassage)
195
+ parsedPassage.scripture = this.scripturize(parsedPassage)
196
+ parsedPassage.valid = this._isValid(parsedPassage, passage.reference)
208
197
 
209
- parts.forEach((part, partIndex) => {
210
- part = part.trim()
198
+ // Add SBL abbreviation as full reference with period, en dashes, and space after commas for comma-separated verses
199
+ const sblBook = this.sblAbbreviations[book] || book
200
+ let abbr = parsedPassage.scripture.passage.replace(book, `${sblBook}.`).replace(/-/g, "–")
201
+ if (parsedPassage.type === "comma_separated_verses") {
202
+ const versePart = parsedPassage.verses.map((v) => `${v}`).join(", ")
203
+ abbr = `${sblBook}. ${parsedPassage.chapter}:${versePart}`
204
+ }
205
+ parsedPassage.abbr = abbr
211
206
 
212
- if (part.includes(":")) {
213
- // Explicit chapter:verse (e.g., "1:1-3")
214
- const [chapterPart, versePart] = part.split(":")
215
- if (partIndex === 0) {
216
- parsedPassage.chapter = Number(chapterPart) // Set chapter only on first part
217
- }
207
+ if (parsedPassage.type === this.MULTI_CHAPTER_RANGE) {
208
+ this.handleMultiChapterRange(parsedPassage, passage.reference)
209
+ } else {
210
+ delete parsedPassage.to
211
+ }
218
212
 
219
- if (versePart.includes("-")) {
220
- parsedPassage.verses.push(versePart) // Add range (e.g., "1-3")
221
- } else {
222
- parsedPassage.verses.push(Number(versePart)) // Add single verse
223
- }
224
- parsedPassage.type = versePart.includes("-") ? "chapter_verse_range" : "chapter_verse"
225
- } else if (singleChapterBook) {
226
- // Handle single-chapter books
227
- const verseCount = singleChapterBook[book][1].length
228
- if (part === "1" && parts.length === 1 && partIndex === 0) {
229
- // "2 John 1" means the whole chapter
230
- parsedPassage.chapter = 1
231
- parsedPassage.type = "single_chapter"
232
- parsedPassage.verses = [`1-${verseCount}`] // e.g., "1-13"
233
- } else if (part.includes("-")) {
234
- // "2 John 2-5" → "2 John 1:2-5"
235
- parsedPassage.chapter = 1
236
- parsedPassage.verses.push(part) // e.g., "2-5"
237
- parsedPassage.type = "chapter_verse_range"
238
- } else {
239
- // "2 John 2" → "2 John 1:2"
240
- const num = Number(part)
241
- if (num > 1 || (num === 1 && parts.length > 1)) {
242
- parsedPassage.chapter = 1
243
- parsedPassage.verses.push(num) // Treat as verse number
244
- parsedPassage.type = "chapter_verse"
245
- }
246
- }
247
- } else if (part.includes("-") && !parsedPassage.chapter) {
248
- // Range without chapter for multi-chapter books (e.g., "Matthew 3-5")
249
- const [start, end] = part.split("-").map(Number)
250
- parsedPassage.chapter = start
251
- parsedPassage.verses = [
252
- `${this.chapterVerses[book][start][0]}-${this.chapterVerses[book][start].slice(-1)[0]}`,
253
- ]
254
- parsedPassage.to = {
255
- book,
256
- chapter: end,
257
- verses: [`${this.chapterVerses[book][end][0]}-${this.chapterVerses[book][end].slice(-1)[0]}`],
258
- }
259
- parsedPassage.type = "chapter_range"
260
- } else if (part.includes("-")) {
261
- // Verse range in current chapter (e.g., "8-9" after "40:3-5")
262
- parsedPassage.verses.push(part)
263
- parsedPassage.type = "chapter_verse_range"
264
- } else {
265
- // Single number (chapter or verse) for multi-chapter books
266
- if (partIndex === 0 && !parsedPassage.chapter) {
267
- parsedPassage.chapter = Number(part)
268
- parsedPassage.type = "single_chapter"
269
- // For multi-chapter books, set verses to full chapter range
270
- if (
271
- !singleChapterBook &&
272
- this.chapterVerses[book] &&
273
- this.chapterVerses[book][parsedPassage.chapter]
274
- ) {
275
- const chapterVerses = this.chapterVerses[book][parsedPassage.chapter]
276
- parsedPassage.verses = [`${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`]
277
- }
278
- } else {
279
- parsedPassage.verses.push(Number(part))
280
- parsedPassage.type = "comma_separated_verses"
281
- }
213
+ if (parsedPassage.passages.length > 0) {
214
+ const sortedPassages = parsedPassage.passages.slice().sort((a, b) => {
215
+ if (a.chapter !== b.chapter) return a.chapter - b.chapter
216
+ return a.verse - b.verse
217
+ })
218
+ const firstPassage = sortedPassages[0]
219
+ const lastPassage = sortedPassages[sortedPassages.length - 1]
220
+ parsedPassage.start = {
221
+ book: firstPassage.book,
222
+ chapter: firstPassage.chapter,
223
+ verse: firstPassage.verse,
282
224
  }
283
- })
284
-
285
- // Populate passages and scripture after processing all parts
286
- parsedPassage.passages = this.populate(parsedPassage)
287
- parsedPassage.scripture = this.scripturize(parsedPassage)
288
- parsedPassage.valid = this._isValid(parsedPassage, passage.reference)
225
+ parsedPassage.end = {
226
+ book: lastPassage.book,
227
+ chapter: lastPassage.chapter,
228
+ verse: lastPassage.verse,
229
+ }
230
+ }
289
231
 
290
- // Handle multi-chapter range if applicable
291
- if (
292
- parsedPassage.type === "multi_chapter_verse_range" &&
293
- parts.some((p) => p.includes(":") && p.split(":")[0] !== String(parsedPassage.chapter))
294
- ) {
295
- const lastPart = parts[parts.length - 1]
296
- const [endChapter, endVerse] = lastPart.split(":")
297
- parsedPassage.to = {
298
- book: book,
299
- chapter: Number(endChapter),
300
- verses: endVerse.includes("-") ? [endVerse] : [Number(endVerse)],
232
+ if (!parsedPassage.version) {
233
+ parsedPassage.version = {
234
+ name: "English",
235
+ value: "ENG",
236
+ abbreviation: "eng",
301
237
  }
302
- } else {
303
- delete parsedPassage.to // Remove erroneous 'to' property
238
+ }
239
+
240
+ // Attach the reference method to this individual passage object
241
+ parsedPassage.reference = function () {
242
+ return this.scripture.passage
304
243
  }
305
244
 
306
245
  return parsedPassage
@@ -309,9 +248,102 @@ class CodexParser {
309
248
  this.versification()
310
249
  return this
311
250
  }
312
- /**
313
- * Generates an array of numbers representing a range from start to end, inclusive.
314
- */
251
+
252
+ parseReferenceParts(passage, parts) {
253
+ const singleChapterBook = this.singleChapterBook.find((b) => Object.keys(b)[0] === passage.book)
254
+
255
+ parts.forEach((part, index) => {
256
+ part = part.trim()
257
+ const isFirstPart = index === 0
258
+
259
+ if (part.includes(":")) {
260
+ this.parseChapterVerse(passage, part, isFirstPart)
261
+ } else if (singleChapterBook) {
262
+ this.parseSingleChapterBook(passage, part, isFirstPart && parts.length === 1)
263
+ } else if (part.includes("-")) {
264
+ this.parseRange(passage, part, isFirstPart)
265
+ } else {
266
+ this.parseSingleNumber(passage, part, isFirstPart)
267
+ }
268
+ })
269
+ }
270
+
271
+ parseChapterVerse(passage, part, isFirstPart) {
272
+ const [chapter, verse] = part.split(":")
273
+ if (isFirstPart) passage.chapter = Number(chapter)
274
+ passage.type = verse.includes("-") ? this.CHAPTER_VERSE_RANGE : this.CHAPTER_VERSE
275
+ passage.verses.push(verse.includes("-") ? verse : Number(verse))
276
+ }
277
+
278
+ parseSingleChapterBook(passage, part, isWholeChapter) {
279
+ const verseCount = this.getChapterVerses(passage.book, 1).length
280
+ if (part === "1" && isWholeChapter) {
281
+ passage.chapter = 1
282
+ passage.type = this.SINGLE_CHAPTER
283
+ passage.verses = [`1-${verseCount}`]
284
+ } else if (part.includes("-")) {
285
+ passage.chapter = 1
286
+ passage.verses.push(part)
287
+ passage.type = this.CHAPTER_VERSE_RANGE
288
+ } else {
289
+ const num = Number(part)
290
+ if (num > 1 || !isWholeChapter) {
291
+ passage.chapter = 1
292
+ passage.verses.push(num)
293
+ passage.type = this.CHAPTER_VERSE
294
+ }
295
+ }
296
+ }
297
+
298
+ parseRange(passage, part, isFirstPart) {
299
+ if (!passage.chapter && isFirstPart) {
300
+ const [start, end] = part.split("-").map(Number)
301
+ passage.chapter = start
302
+ const startVerses = this.getChapterVerses(passage.book, start)
303
+ passage.verses = [`${startVerses[0]}-${startVerses[startVerses.length - 1]}`]
304
+ passage.to = {
305
+ book: passage.book,
306
+ chapter: end,
307
+ verses: [
308
+ `${this.getChapterVerses(passage.book, end)[0]}-${
309
+ this.getChapterVerses(passage.book, end).slice(-1)[0]
310
+ }`,
311
+ ],
312
+ }
313
+ passage.type = this.CHAPTER_RANGE
314
+ } else {
315
+ passage.verses.push(part)
316
+ passage.type = this.CHAPTER_VERSE_RANGE
317
+ }
318
+ }
319
+
320
+ parseSingleNumber(passage, part, isFirstPart) {
321
+ if (isFirstPart && !passage.chapter) {
322
+ passage.chapter = Number(part)
323
+ passage.type = this.SINGLE_CHAPTER
324
+ const chapterVerses = this.getChapterVerses(passage.book, passage.chapter)
325
+ if (chapterVerses.length) {
326
+ passage.verses = [`${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`]
327
+ }
328
+ } else {
329
+ passage.verses.push(Number(part))
330
+ passage.type = this.COMMA_SEPARATED
331
+ }
332
+ }
333
+
334
+ handleMultiChapterRange(passage, reference) {
335
+ const parts = reference.split(",")
336
+ const lastPart = parts[parts.length - 1]
337
+ const [endChapter, endVerse] = lastPart.split(":")
338
+ if (endChapter !== String(passage.chapter)) {
339
+ passage.to = {
340
+ book: passage.book,
341
+ chapter: Number(endChapter),
342
+ verses: endVerse.includes("-") ? [endVerse] : [Number(endVerse)],
343
+ }
344
+ }
345
+ }
346
+
315
347
  _generateRange(start, end) {
316
348
  const range = []
317
349
  for (let i = start; i <= end; i++) {
@@ -324,25 +356,20 @@ class CodexParser {
324
356
  version = version.toLowerCase()
325
357
  if (!this.chapterVerses[book][chapter]) return
326
358
  if (!this.versificationDifferences[book]) return
327
- // Loop through each key-value pair in the dictionary
328
359
  for (const [key, value] of Object.entries(this.versificationDifferences[book])) {
329
- // Check if the key starts with the desired chapter
330
360
  if (value[version].startsWith(`${chapter}:`)) {
331
- // Ensure the version exists in the value object
332
361
  if (value[version]) {
333
- // Extract the verse number from the value
334
362
  const verse = value[version].split(":")[1]
335
363
  this.chapterVerses[book][chapter].push(Number(verse))
336
364
  }
337
365
  }
338
366
  }
339
367
  this.chapterVerses[book][chapter] = Array.from(this.chapterVerses[book][chapter])
340
- return this.chapterVerses // Return the array of verses
368
+ return this.chapterVerses
341
369
  }
342
370
 
343
371
  _setVersion(book, chapter, version) {
344
372
  this.version = version ? version : "eng"
345
-
346
373
  if (this.version !== "eng") {
347
374
  this._searchVersificationDifferences(book, chapter, version)
348
375
  }
@@ -352,31 +379,25 @@ class CodexParser {
352
379
  this.passages.forEach((passage) => {
353
380
  const hasVersification = this.versificationDifferences[passage.book]
354
381
  passage.passages.forEach((subPassage) => {
355
- // Apply general versification differences
356
382
  if (hasVersification) {
357
383
  const key = `${subPassage.chapter}:${subPassage.verse}`
358
384
  if (this.versificationDifferences[passage.book][key]) {
359
385
  subPassage.versification = this.versificationDifferences[passage.book][key]
360
386
  }
361
387
  }
362
-
363
- // Handle specific version adjustments for "lxx" or "mt"
364
388
  if (passage.version) {
365
389
  const versionAbbreviation = passage.version.abbreviation
366
390
  const versionType =
367
391
  versionAbbreviation === "lxx" ? "lxx" : versionAbbreviation === "mt" ? "mt" : null
368
-
369
392
  if (versionType) {
370
393
  const versionReference = `${subPassage.chapter}:${subPassage.verse}`
371
-
372
- // Look for matching versification based on the version type (lxx or mt)
373
394
  for (const versification in this.versificationDifferences[passage.book]) {
374
395
  if (
375
396
  this.versificationDifferences[passage.book][versification][versionType] ===
376
397
  versionReference
377
398
  ) {
378
399
  subPassage.versification = this.versificationDifferences[passage.book][versification]
379
- break // Break once a match is found
400
+ break
380
401
  }
381
402
  }
382
403
  }
@@ -385,96 +406,65 @@ class CodexParser {
385
406
  })
386
407
  }
387
408
 
388
- /**
389
- * Populate all verses from a parsed passage, including all verses in ranges or chapters.
390
- *
391
- * @param {Object} parsedPassage - The parsed passage object containing book, chapter, and verses information.
392
- * @return {Array} An array of passage objects with individual verses.
393
- */
394
- populate(parsedPassage) {
395
- const passages = []
396
- const { book, chapter, verses, type, to } = parsedPassage
397
- const version = parsedPassage.version ? parsedPassage.version.abbreviation : "eng"
398
- this._setVersion(book, chapter, version) // Set version data if needed
409
+ populate(passage) {
410
+ const { book, chapter, verses, type, to } = passage
411
+ const version = passage.version?.abbreviation || "eng"
412
+ this._setVersion(book, chapter, version)
399
413
 
400
- const singleChapterBook = this.singleChapterBook.find((b) => Object.keys(b)[0] === book)
414
+ if (type === this.SINGLE_CHAPTER) {
415
+ const chapterVerses = this.getChapterVerses(book, chapter)
416
+ return this.expandVerses(book, chapter, [`${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`])
417
+ }
401
418
 
402
- if (type === "single_chapter") {
403
- // Handle entire chapter references (e.g., "Isaiah 40" or "2 John 1")
404
- if (singleChapterBook) {
405
- // Single-chapter book: populate all verses from singleChapterBook
406
- const verseCount = singleChapterBook[book][1].length
407
- for (let verse = 1; verse <= verseCount; verse++) {
408
- passages.push({ book, chapter: 1, verse })
409
- }
410
- } else if (this.chapterVerses[book] && this.chapterVerses[book][chapter]) {
411
- // Multi-chapter book: populate all verses in the chapter
412
- this.chapterVerses[book][chapter].forEach((verse) => {
413
- passages.push({ book, chapter: Number(chapter), verse: Number(verse) })
414
- })
415
- }
416
- } else if (type === "comma_separated_verses" || type === "chapter_verse_range") {
417
- // Handle comma-separated verses or single-chapter verse ranges (e.g., "Isaiah 40:3-5,8-9" or "2 John 1:1-3")
418
- verses.forEach((verse) => {
419
- if (typeof verse === "string" && verse.includes("-")) {
420
- const [start, end] = verse.split("-").map(Number)
421
- for (let i = start; i <= end; i++) {
422
- passages.push({ book, chapter: Number(chapter), verse: i })
423
- }
424
- } else {
425
- passages.push({ book, chapter: Number(chapter), verse: Number(verse) })
426
- }
427
- })
428
- } else if (type === "chapter_range") {
429
- // Handle ranges of chapters (e.g., "Isaiah 3-5")
430
- for (let currentChapter = chapter; currentChapter <= to.chapter; currentChapter++) {
431
- if (this.chapterVerses[book] && this.chapterVerses[book][currentChapter]) {
432
- this.chapterVerses[book][currentChapter].forEach((verse) => {
433
- passages.push({ book, chapter: Number(currentChapter), verse: Number(verse) })
434
- })
435
- }
419
+ if (type === this.CHAPTER_VERSE || type === this.COMMA_SEPARATED || type === this.CHAPTER_VERSE_RANGE) {
420
+ return this.expandVerses(book, chapter, verses)
421
+ }
422
+
423
+ if (type === this.CHAPTER_RANGE) {
424
+ const passages = []
425
+ for (let ch = chapter; ch <= to.chapter; ch++) {
426
+ const chapterVerses = this.getChapterVerses(book, ch)
427
+ passages.push(
428
+ ...this.expandVerses(book, ch, [`${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`])
429
+ )
436
430
  }
437
- } else if (type === "multi_chapter_verse_range") {
438
- // Handle multi-chapter verse ranges (e.g., "Isaiah 3:1-5:6")
439
- const startChapter = chapter
431
+ return passages
432
+ }
433
+
434
+ if (type === this.MULTI_CHAPTER_RANGE) {
435
+ const passages = []
440
436
  const startVerse = verses[0].includes("-") ? Number(verses[0].split("-")[0]) : Number(verses[0])
441
- const endChapter = to.chapter
442
437
  const endVerse = to.verses[0].includes("-") ? Number(to.verses[0].split("-")[1]) : Number(to.verses[0])
443
438
 
444
- for (let currentChapter = startChapter; currentChapter <= endChapter; currentChapter++) {
445
- const chapterVerses = this.chapterVerses[book][currentChapter]
446
- if (!chapterVerses) continue
439
+ for (let ch = chapter; ch <= to.chapter; ch++) {
440
+ const chapterVerses = this.getChapterVerses(book, ch)
441
+ const from = ch === chapter ? startVerse : chapterVerses[0]
442
+ const toVerse = ch === to.chapter ? endVerse : chapterVerses[chapterVerses.length - 1]
443
+ passages.push(...this.expandVerses(book, ch, [`${from}-${toVerse}`]))
444
+ }
445
+ return passages
446
+ }
447
+
448
+ return []
449
+ }
447
450
 
448
- const chapterStartVerse = currentChapter === startChapter ? startVerse : chapterVerses[0]
449
- const chapterEndVerse =
450
- currentChapter === endChapter ? endVerse : chapterVerses[chapterVerses.length - 1]
451
+ expandVerses(book, chapter, verses) {
452
+ const passages = []
453
+ const chapterVerses = this.getChapterVerses(book, chapter)
451
454
 
452
- for (let verse = chapterStartVerse; verse <= chapterEndVerse; verse++) {
453
- passages.push({ book, chapter: currentChapter, verse })
455
+ verses.forEach((verse) => {
456
+ if (typeof verse === "string" && verse.includes("-")) {
457
+ const [start, end] = verse.split("-").map(Number)
458
+ for (let i = start; i <= end && i <= chapterVerses[chapterVerses.length - 1]; i++) {
459
+ passages.push({ book, chapter, verse: i })
454
460
  }
461
+ } else {
462
+ passages.push({ book, chapter, verse: Number(verse) })
455
463
  }
456
- } else if (type === "chapter_verse") {
457
- // Handle single chapter:verse references (e.g., "2 John 1:1")
458
- verses.forEach((verse) => {
459
- passages.push({ book, chapter: Number(chapter), verse: Number(verse) })
460
- })
461
- } else if (type === "single_chapter_book_verse_range") {
462
- // Handle ranges in single-chapter books (e.g., "Jude 5-7")
463
- const [startVerse, endVerse] = verses[0].split("-").map(Number)
464
- for (let i = startVerse; i <= endVerse; i++) {
465
- passages.push({ book, chapter: 1, verse: i })
466
- }
467
- }
468
-
464
+ })
469
465
  return passages
470
466
  }
471
467
 
472
- /**
473
- * Converts a book name to its corresponding full name from the bible.
474
- *
475
- * @param {string} book - The abbreviated or partial name of the book.
476
- * @return {string|undefined} The full name of the book if found, otherwise undefined.
477
- */
478
468
  bookify(book) {
479
469
  if (typeof book !== "string") {
480
470
  book = book[0]
@@ -496,78 +486,105 @@ class CodexParser {
496
486
  return bookified
497
487
  }
498
488
 
499
- /**
500
- * Returns the passages stored in the object.
501
- *
502
- * @return {array} The passages stored in the object.
503
- */
504
489
  getPassages() {
505
- // Return the array of passages and add a custom first() method to it
506
- const passagesArray = [...this.passages] // Clone the array to avoid mutation
490
+ const passagesArray = [...this.passages]
507
491
 
508
- // Add first() method directly to the array
509
492
  passagesArray.first = function () {
510
493
  return this.length > 0 ? this[0] : null
511
494
  }
512
495
 
496
+ passagesArray.oldTestament = function () {
497
+ return this.filter((passage) => passage.testament === "old")
498
+ }
499
+
500
+ passagesArray.newTestament = function () {
501
+ return this.filter((passage) => passage.testament === "new")
502
+ }
503
+
504
+ passagesArray.combine = function (options = {}) {
505
+ const { book = true, chapter = true } = options
506
+
507
+ if (!book) {
508
+ return [...this]
509
+ }
510
+
511
+ const parser = new CodexParser()
512
+ const groupedByBook = new Map()
513
+
514
+ this.forEach((passage) => {
515
+ const bookKey = passage.book
516
+ if (!groupedByBook.has(bookKey)) {
517
+ groupedByBook.set(bookKey, [])
518
+ }
519
+ groupedByBook.get(bookKey).push(passage)
520
+ })
521
+
522
+ const combinedPassages = []
523
+
524
+ for (const [book, bookPassages] of groupedByBook) {
525
+ if (chapter) {
526
+ const groupedByChapter = new Map()
527
+ bookPassages.forEach((passage) => {
528
+ const chapterKey = `${passage.book}-${passage.chapter}`
529
+ if (!groupedByChapter.has(chapterKey)) {
530
+ groupedByChapter.set(chapterKey, [])
531
+ }
532
+ groupedByChapter.get(chapterKey).push(passage)
533
+ })
534
+
535
+ for (const passages of groupedByChapter.values()) {
536
+ if (passages.length === 1) {
537
+ combinedPassages.push({ ...passages[0] })
538
+ } else {
539
+ const combined = parser.combine(passages)
540
+ combinedPassages.push(combined)
541
+ }
542
+ }
543
+ } else {
544
+ const combined = parser.combine(bookPassages)
545
+ combinedPassages.push(combined)
546
+ }
547
+ }
548
+
549
+ return combinedPassages
550
+ }
551
+
513
552
  return passagesArray
514
553
  }
515
554
 
516
- // New first() method that can be chained after getPassages()
517
555
  first() {
518
556
  return this.passages.length > 0 ? this.passages[0] : null
519
557
  }
520
558
 
521
- /**
522
- * Converts a passage object into a scripturize object with human-readable name, chapter and verses and a hash.
523
- *
524
- * @param {object} passage - The passage object to scripturize.
525
- * @return {object} The object with the human-readable name, chapter and verses and a hash.
526
- */
527
559
  scripturize(passage) {
528
- // Helper function to format a single chapter:verse combination
529
560
  const formatChapterVerse = (chapter, verses) => {
530
561
  if (!chapter || !verses || verses.length === 0) return ""
531
562
  if (verses.length === 1) {
532
563
  return `${chapter}:${verses[0]}`
533
564
  }
534
-
535
- // Check if verses are continuous (e.g., [1, 2, 3, 4, 5] -> "1-5")
536
565
  const isRange = verses.every((v, i, arr) => i === 0 || v === arr[i - 1] + 1)
537
-
538
566
  if (isRange) {
539
567
  return `${chapter}:${verses[0]}-${verses[verses.length - 1]}`
540
568
  }
541
-
542
- // Comma-separated (e.g., [1, 3, 5] -> "1,3,5")
543
569
  return `${chapter}:${verses.join(",")}`
544
570
  }
545
571
 
546
- // Start constructing the passage string
547
572
  let combined = `${passage.book}`
548
-
549
573
  if (passage.type === "multi_chapter_verse_range" && passage.to) {
550
- // Multi-chapter verse range
551
-
552
574
  combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}-${formatChapterVerse(
553
575
  passage.to.chapter,
554
576
  passage.to.verses
555
577
  )}`
556
578
  } else if (passage.type === "chapter_verse_range") {
557
- // Single-chapter verse range
558
579
  combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
559
580
  } else if (passage.type === "comma_separated_verses") {
560
- // Comma-separated verses
561
581
  combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
562
582
  } else if (passage.type === "chapter_range" && passage.to) {
563
- // Chapter range
564
583
  combined += ` ${passage.chapter}-${passage.to.chapter}`
565
584
  } else {
566
- // Single chapter or single verse
567
585
  combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
568
586
  }
569
587
 
570
- // Generate the chapter:verse (cv) string
571
588
  const cv = passage.to
572
589
  ? `${formatChapterVerse(passage.chapter, passage.verses)}-${formatChapterVerse(
573
590
  passage.to.chapter,
@@ -575,10 +592,8 @@ class CodexParser {
575
592
  )}`
576
593
  : formatChapterVerse(passage.chapter, passage.verses)
577
594
 
578
- // Generate the hash
579
595
  const hash = `${passage.book.toLowerCase()}_${cv.replace(/:/g, ".").replace(/-/g, ".")}`
580
596
 
581
- // Return the final scripture object
582
597
  return {
583
598
  passage: combined,
584
599
  cv: cv,
@@ -586,27 +601,16 @@ class CodexParser {
586
601
  }
587
602
  }
588
603
 
589
- /**
590
- * Combine multiple passages into one. The method checks for duplicates, merges overlapping or adjacent ranges,
591
- * and builds the original and scripture properties.
592
- * **This method will always combine based on English versification. LXX and MT versifications will be reflected in the combined passage.passages.versification.**
593
- * This method will fail if the passages are not to the same book and chapter.
594
- * TODO: Add support for MT and LXX
595
- * @param {array} passages - An array of passage objects to combine.
596
- * @return {object} The combined passage object.
597
- */
598
604
  combine(passages) {
599
605
  if (!passages || passages.length === 0) {
600
606
  throw new Error("No passages provided to join.")
601
607
  }
602
608
 
603
- // Ensure all passages are from the same book
604
609
  const uniqueBooks = [...new Set(passages.map((p) => p.book))]
605
610
  if (uniqueBooks.length > 1) {
606
611
  throw new Error("Passages must be from the same book to join.")
607
612
  }
608
613
 
609
- // Start with the base object
610
614
  const combined = {
611
615
  ...passages[0],
612
616
  verses: [],
@@ -614,36 +618,45 @@ class CodexParser {
614
618
  to: null,
615
619
  scripture: {},
616
620
  type: null,
621
+ start: null,
622
+ end: null,
617
623
  }
618
624
 
619
625
  const chapterVerses = {}
620
626
  let firstChapter = null
621
627
  let lastChapter = null
628
+ let firstVerse = null
629
+ let lastVerse = null
622
630
 
623
- // Collect all verses and passages, grouped by chapter
624
631
  passages.forEach((passage) => {
625
632
  passage.passages.forEach((p) => {
626
633
  if (!chapterVerses[p.chapter]) {
627
634
  chapterVerses[p.chapter] = new Set()
628
635
  }
629
636
  chapterVerses[p.chapter].add(p.verse)
630
- combined.passages.push(p) // Add individual passage
637
+ combined.passages.push(p)
638
+
639
+ if (firstChapter === null || p.chapter < firstChapter) {
640
+ firstChapter = p.chapter
641
+ firstVerse = p.verse
642
+ } else if (p.chapter === firstChapter && (firstVerse === null || p.verse < firstVerse)) {
643
+ firstVerse = p.verse
644
+ }
645
+ if (lastChapter === null || p.chapter > lastChapter) {
646
+ lastChapter = p.chapter
647
+ lastVerse = p.verse
648
+ } else if (p.chapter === lastChapter && (lastVerse === null || p.verse > lastVerse)) {
649
+ lastVerse = p.verse
650
+ }
631
651
  })
632
652
 
633
- // Track first and last chapters
634
653
  const chapters = passage.passages.map((p) => p.chapter)
635
- if (!firstChapter || Math.min(...chapters) < firstChapter) {
636
- firstChapter = Math.min(...chapters)
637
- }
638
- if (!lastChapter || Math.max(...chapters) > lastChapter) {
639
- lastChapter = Math.max(...chapters)
640
- }
654
+ firstChapter = firstChapter === null ? Math.min(...chapters) : Math.min(firstChapter, ...chapters)
655
+ lastChapter = lastChapter === null ? Math.max(...chapters) : Math.max(lastChapter, ...chapters)
641
656
  })
642
657
 
643
- // Ensure unique and sorted passages
644
658
  combined.passages = Array.from(new Set(combined.passages.map(JSON.stringify))).map(JSON.parse)
645
659
 
646
- // Process chapter and verse data
647
660
  const chapterStrings = []
648
661
  const sortedChapters = Object.keys(chapterVerses)
649
662
  .map(Number)
@@ -654,13 +667,12 @@ class CodexParser {
654
667
  const mergedVerses = this.mergeRanges(verses)
655
668
  chapterStrings.push(`${chapter}:${mergedVerses.join(",")}`)
656
669
  if (chapter === firstChapter) {
657
- combined.verses = mergedVerses // First chapter's verses
670
+ combined.verses = mergedVerses
658
671
  }
659
672
  })
660
673
 
661
- // Handle multi-chapter ranges
662
674
  if (firstChapter !== lastChapter) {
663
- combined.type = "multi_chapter_verse_range"
675
+ combined.type = this.MULTI_CHAPTER_RANGE
664
676
  combined.to = {
665
677
  book: combined.book,
666
678
  chapter: lastChapter,
@@ -670,25 +682,37 @@ class CodexParser {
670
682
  ","
671
683
  )}; ${lastChapter}:${combined.to.verses.join(",")}`
672
684
  } else {
673
- // Single-chapter range or comma-separated
674
- if (combined.verses.length > 1) {
675
- combined.type = "chapter_verse_range"
676
- } else {
677
- combined.type = "chapter_verse"
678
- }
685
+ combined.type = combined.verses.length > 1 ? this.CHAPTER_VERSE_RANGE : this.CHAPTER_VERSE
679
686
  combined.original = `${combined.book} ${firstChapter}:${combined.verses.join(",")}`
680
687
  }
681
688
 
682
- // Build the scripture property
683
689
  const chapterString = chapterStrings.join(";")
684
690
  combined.scripture = {
685
691
  passage: `${combined.book} ${chapterString}`,
686
692
  cv: chapterString,
687
- hash: `${combined.book.toLowerCase()}_${chapterString.replace(/:/g, ".").replace(/,|;/g, ".")}`,
693
+ hash: `${combined.book.toLowerCase()}_${chapterString.replace(/:/g, ".").replace(/[,;]/g, ".")}`,
694
+ }
695
+
696
+ combined.start = {
697
+ book: combined.book,
698
+ chapter: firstChapter,
699
+ verse: firstVerse || Math.min(...Array.from(chapterVerses[firstChapter])),
700
+ }
701
+ combined.end = {
702
+ book: combined.book,
703
+ chapter: lastChapter,
704
+ verse: lastVerse || Math.max(...Array.from(chapterVerses[lastChapter])),
688
705
  }
706
+
707
+ // Reattach the reference method to the combined passage
708
+ combined.reference = function () {
709
+ return this.scripture.passage
710
+ }
711
+
689
712
  if (combined.to === null) {
690
713
  delete combined.to
691
714
  }
715
+
692
716
  return combined
693
717
  }
694
718
 
@@ -702,7 +726,6 @@ class CodexParser {
702
726
  if (sortedVerses[i] === end + 1) {
703
727
  end = sortedVerses[i]
704
728
  } else {
705
- // Push range or single verse
706
729
  if (start === end) {
707
730
  merged.push(`${start}`)
708
731
  } else {
@@ -713,7 +736,6 @@ class CodexParser {
713
736
  }
714
737
  }
715
738
 
716
- // Push the final range or single verse
717
739
  if (start === end) {
718
740
  merged.push(`${start}`)
719
741
  } else {
@@ -724,24 +746,17 @@ class CodexParser {
724
746
  }
725
747
 
726
748
  getToc(version = "ESV") {
727
- // Initialize the table of contents (toc)
728
749
  const toc = {}
729
-
730
- // Add Old Testament books and their chapters/verses to toc
731
750
  this.bible.old.forEach((book) => {
732
751
  if (this.chapterVerses[book]) {
733
752
  toc[book] = this.chapterVerses[book]
734
753
  }
735
754
  })
736
-
737
- // Add New Testament books and their chapters/verses to toc
738
755
  this.bible.new.forEach((book) => {
739
756
  if (this.chapterVerses[book]) {
740
757
  toc[book] = this.chapterVerses[book]
741
758
  }
742
759
  })
743
-
744
- // Merge in single-chapter books if not already in toc
745
760
  this.singleChapterBook.forEach((item) => {
746
761
  Object.keys(item).forEach((book) => {
747
762
  if (!toc[book]) {
@@ -749,8 +764,6 @@ class CodexParser {
749
764
  }
750
765
  })
751
766
  })
752
-
753
- // Sort the keys of toc by canonical order
754
767
  const orderedToc = {}
755
768
  const canonicalOrder = [...this.bible.old, ...this.bible.new]
756
769
  canonicalOrder.forEach((book) => {
@@ -758,162 +771,79 @@ class CodexParser {
758
771
  orderedToc[book] = toc[book]
759
772
  }
760
773
  })
761
-
762
774
  return orderedToc
763
775
  }
764
776
 
765
- /**
766
- * Validates a parsed passage to ensure the chapter and verses exist.
767
- *
768
- * @param {Object} passage - The parsed passage object to validate.
769
- * @param {string} reference - The original reference string for error messaging.
770
- * @return {boolean|Object} True if valid, or an error object if invalid.
771
- */
772
777
  _isValid(passage, reference) {
773
- const singleChapterBook = this.singleChapterBook.find((b) => Object.keys(b)[0] === passage.book)
778
+ const { book, chapter, verses, type } = passage
774
779
 
775
- // Check if verses exist at all
776
- if (!passage.verses || passage.verses.length === 0) {
777
- if (passage.type !== "single_chapter") {
778
- return {
779
- error: true,
780
- code: 101,
781
- message: {
782
- chapter_exists: false,
783
- content: "Possible invalid chapter: " + reference,
784
- },
785
- }
786
- }
780
+ if (!verses.length && type !== this.SINGLE_CHAPTER) {
781
+ return this.validationError(101, `Possible invalid chapter: ${reference}`)
787
782
  }
788
783
 
789
- // Handle single-chapter books
790
- if (singleChapterBook) {
791
- const verseCount = singleChapterBook[passage.book][1].length
792
-
793
- if (passage.chapter !== 1) {
794
- return {
795
- error: true,
796
- code: 103,
797
- message: {
798
- chapter_exists: false,
799
- content: `Chapter ${passage.chapter} does not exist in ${passage.book}`,
800
- },
801
- }
802
- }
784
+ const chapterVerses = this.getChapterVerses(book, chapter)
785
+ if (!chapterVerses.length) {
786
+ return this.validationError(102, `Chapter ${chapter} does not exist in ${book}`)
787
+ }
803
788
 
804
- if (passage.type === "single_chapter") {
805
- // For "2 John 1", validate the full range
806
- const [range] = passage.verses // e.g., "1-13"
807
- if (range) {
808
- const [start, end] = range.split("-").map(Number)
809
- if (start < 1 || end > verseCount) {
810
- return {
811
- error: true,
812
- code: 104,
813
- message: {
814
- verse_exists: false,
815
- content: `Verse range ${start}-${end} exceeds available verses (1-${verseCount}) in ${passage.book} 1`,
816
- },
817
- }
818
- }
819
- }
820
- return true // If no specific verses or range matches, it’s valid
821
- }
822
-
823
- // For specific verses in single-chapter books (e.g., "2 John 1:1-3")
824
- for (let i = 0; i < passage.verses.length; i++) {
825
- const verseRange = String(passage.verses[i])
826
- let versesToCheck = verseRange.includes("-") ? verseRange.split("-").map(Number) : [Number(verseRange)]
827
-
828
- if (versesToCheck.length === 2) {
829
- const [start, end] = versesToCheck
830
- versesToCheck = Array.from({ length: end - start + 1 }, (_, idx) => start + idx)
831
- }
832
-
833
- for (const verse of versesToCheck) {
834
- if (verse < 1 || verse > verseCount) {
835
- return {
836
- error: true,
837
- code: 104,
838
- message: {
839
- verse_exists: false,
840
- content: `Verse number ${verse} does not exist in ${passage.book} 1`,
841
- },
842
- }
843
- }
789
+ if (type === this.SINGLE_CHAPTER) {
790
+ const [range] = verses
791
+ if (range) {
792
+ const [start, end] = range.split("-").map(Number)
793
+ if (start < 1 || end > chapterVerses[chapterVerses.length - 1]) {
794
+ return this.validationError(
795
+ 104,
796
+ `Verse range ${start}-${end} exceeds available verses (1-${
797
+ chapterVerses[chapterVerses.length - 1]
798
+ }) in ${book} ${chapter}`
799
+ )
844
800
  }
845
801
  }
846
802
  return true
847
803
  }
848
804
 
849
- // Handle multi-chapter books
850
- if (!this.chapterVerses[passage.book] || !this.chapterVerses[passage.book][passage.chapter]) {
851
- return {
852
- error: true,
853
- code: 102,
854
- message: {
855
- chapter_exists: false,
856
- content: `Chapter ${passage.chapter} does not exist in ${passage.book}`,
857
- },
858
- }
859
- }
860
-
861
- if (passage.type === "single_chapter") {
862
- return true // For multi-chapter books, whole chapter is valid if it exists
863
- }
864
-
865
- for (let i = 0; i < passage.verses.length; i++) {
866
- const passageVerses = String(passage.verses[i])
867
- let verses = passageVerses.includes("-") ? passageVerses.split("-").map(Number) : [Number(passageVerses)]
868
-
869
- if (verses.length === 2) {
870
- // Expand the range if there are two numbers
871
- verses = Array.from({ length: verses[1] - verses[0] + 1 }, (_, index) => verses[0] + index)
872
- }
805
+ return this.validateVerses(book, chapter, verses, reference)
806
+ }
873
807
 
874
- for (const verse of verses) {
875
- const isValidVerse =
876
- this.chapterVerses[passage.book] &&
877
- this.chapterVerses[passage.book][passage.chapter] &&
878
- this.chapterVerses[passage.book][passage.chapter].includes(verse)
879
-
880
- if (!isValidVerse) {
881
- return {
882
- error: true,
883
- code: 104,
884
- message: {
885
- verse_exists: false,
886
- content: `Verse number ${verse} does not exist in ${passage.book} ${passage.chapter}`,
887
- },
888
- }
808
+ validateVerses(book, chapter, verses, reference) {
809
+ const chapterVerses = this.getChapterVerses(book, chapter)
810
+ for (const verse of verses) {
811
+ const verseRange = String(verse)
812
+ const verseNumbers = verseRange.includes("-")
813
+ ? Array.from(
814
+ { length: Number(verseRange.split("-")[1]) - Number(verseRange.split("-")[0]) + 1 },
815
+ (_, i) => Number(verseRange.split("-")[0]) + i
816
+ )
817
+ : [Number(verseRange)]
818
+
819
+ for (const v of verseNumbers) {
820
+ if (!chapterVerses.includes(v)) {
821
+ return this.validationError(104, `Verse number ${v} does not exist in ${book} ${chapter}`)
889
822
  }
890
823
  }
891
824
  }
892
-
893
825
  return true
894
826
  }
895
- _handleVersion(version, testament) {
896
- if (this.version) {
897
- version = this.version
898
- }
899
- if (!version) {
900
- version = "eng"
901
- }
902
- if (version.toLowerCase() === "lxx" && testament.toLowerCase() === "old") {
903
- return {
904
- name: "Septuagint",
905
- value: "LXX",
906
- abbreviation: "lxx",
907
- }
827
+
828
+ validationError(code, message) {
829
+ return {
830
+ error: true,
831
+ code,
832
+ message: { verse_exists: code === 104, chapter_exists: code !== 104, content: message },
908
833
  }
834
+ }
909
835
 
910
- if (version.toLowerCase() === "mt" && testament.toLowerCase() === "old") {
911
- return {
912
- name: "Masoretic Text",
913
- value: "MT",
914
- abbreviation: "mt",
915
- }
836
+ _handleVersion(version, testament) {
837
+ const effectiveVersion = this.version || version || "eng"
838
+ const lowerVersion = effectiveVersion.toLowerCase()
839
+
840
+ if (lowerVersion === "lxx" && testament === "old") {
841
+ return { name: "Septuagint", value: "LXX", abbreviation: "lxx" }
842
+ }
843
+ if (lowerVersion === "mt" && testament === "old") {
844
+ return { name: "Masoretic Text", value: "MT", abbreviation: "mt" }
916
845
  }
846
+ return { name: "English", value: "ENG", abbreviation: "eng" }
917
847
  }
918
848
  }
919
849