codexparser 0.1.54 → 0.1.56

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/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # CodexParser: The Ultimate Bible Reference Parser 📖✨
2
+
3
+ Welcome to **CodexParser**, a powerful and flexible Node.js library crafted to parse, validate, and structure Bible references with ease. Whether you're extracting verses from a sermon, building a scripture app, or analyzing biblical texts, CodexParser transforms raw references like "John 3:16" or "Genesis 1:1-5, 10" into rich, actionable data—complete with start and end points, versification support, and more. Dive into the Word like never before!
4
+
5
+ Built with precision and passion, CodexParser handles single verses, ranges, multi-chapter spans, and even single-chapter books (looking at you, Jude!). It’s your trusty companion for navigating the sacred texts, supporting English, Septuagint (LXX), and Masoretic Text (MT) versions. Let’s unleash its power!
6
+
7
+ ---
8
+
9
+ ## Features 🌟
10
+
11
+ - **Parse Any Reference**: From "Jn 3:16" to "Exodus 20:1-17; 21:1-5", it’s got you covered.
12
+ - **Structured Output**: Get book, chapter, verse, testament, start/end points, and more in a clean object.
13
+ - **Versification Support**: Handles differences between English, LXX, and MT texts.
14
+ - **Validation**: Ensures references are legit—no more phantom verses!
15
+ - **Combine Passages**: Merge multiple references into a single, cohesive range.
16
+ - **Chainable API**: Fluent, intuitive method chaining for a smooth workflow.
17
+
18
+ ---
19
+
20
+ ## Installation 🚀
21
+
22
+ Grab CodexParser via npm and start parsing scripture in minutes:
23
+
24
+ ```bash
25
+ npm install codexparser
26
+ ```
27
+
28
+ Or clone it from GitHub and dive into the source:
29
+
30
+ ```bash
31
+ git clone https://github.com/your-username/CodexParser.git
32
+ cd CodexParser
33
+ npm install
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Quick Start ⚡
39
+
40
+ Here’s how to wield CodexParser’s might:
41
+
42
+ ```javascript
43
+ const CodexParser = require("codex-parser")
44
+
45
+ const parser = new CodexParser()
46
+
47
+ // Parse a simple reference
48
+ parser.parse("John 3:16")
49
+ console.log(parser.getPassages().first())
50
+ // Output: {
51
+ // original: "John 3:16",
52
+ // book: "John",
53
+ // chapter: 3,
54
+ // verses: [16],
55
+ // type: "chapter_verse",
56
+ // testament: "new",
57
+ // passages: [{ book: "John", chapter: 3, verse: 16 }],
58
+ // scripture: { passage: "John 3:16", cv: "3:16", hash: "john_3.16" },
59
+ // start: { book: "John", chapter: 3, verse: 16 },
60
+ // end: { book: "John", chapter: 3, verse: 16 },
61
+ // valid: true,
62
+ // version: { name: "English", value: "ENG", abbreviation: "eng" }
63
+ // }
64
+
65
+ // Chain it up!
66
+ console.log(parser.parse("Genesis 1:1-5, 10; 2:1-3").getPassages().combine())
67
+ // Combines into a single passage with start/end spanning the range!
68
+ ```
69
+
70
+ ---
71
+
72
+ ## API: Your Codex Arsenal 🛠️
73
+
74
+ Here’s the breakdown of CodexParser’s key methods—your tools for mastering scripture:
75
+
76
+ ### `new CodexParser()`
77
+
78
+ - **What it does**: Creates a new parser instance, ready to tackle any reference.
79
+ - **Usage**: `const parser = new CodexParser();`
80
+
81
+ ### `.scan(text)`
82
+
83
+ - **What it does**: Scans a string for Bible references, storing raw matches in `this.found`. It’s the first step in parsing—think of it as your scripture radar.
84
+ - **Args**: `text` (string) - The text to search (e.g., "Preaching from Jn 3:16 today").
85
+ - **Returns**: The parser instance for chaining.
86
+ - **Example**: `parser.scan("Jn 3:16; Gen 1:1");`
87
+
88
+ ### `.parse(reference)`
89
+
90
+ - **What it does**: Takes a reference string, scans it, and builds structured passage objects with `start`, `end`, `passages`, and more. This is your main parsing powerhouse.
91
+ - **Args**: `reference` (string) - The Bible reference (e.g., "John 3:16-18").
92
+ - **Returns**: The parser instance for chaining.
93
+ - **Example**: `parser.parse("Exodus 20:1-5").getPassages();`
94
+
95
+ ### `.bibleVersion(version)`
96
+
97
+ - **What it does**: Sets the Bible version (e.g., "lxx", "mt", "bhs") to adjust versification. Great for Old Testament nerds!
98
+ - **Args**: `version` (string) - Version code ("lxx", "mt", "bhs", etc.).
99
+ - **Returns**: The parser instance for chaining.
100
+ - **Example**: `parser.bibleVersion("lxx").parse("Psalm 23:1");`
101
+
102
+ ### `.getPassages()`
103
+
104
+ - **What it does**: Returns an array of parsed passage objects with handy methods like `.first()`, `.oldTestament()`, `.newTestament()`, and `.combine()`.
105
+ - **Returns**: Array of passage objects with extra methods.
106
+ - **Example**: `parser.parse("Matt 5:3-5").getPassages();`
107
+
108
+ ### `.first()`
109
+
110
+ - **What it does**: Grabs the first parsed passage—perfect for single-reference parsing.
111
+ - **Returns**: The first passage object or `null` if none exist.
112
+ - **Example**: `parser.parse("Luke 2:1").first();`
113
+
114
+ ### `.combine(passages)`
115
+
116
+ - **What it does**: Merges multiple passages from the same book into a single passage, calculating a unified range with `start` and `end`. Ideal for consolidating overlapping references.
117
+ - **Args**: `passages` (array) - Array of passage objects to combine.
118
+ - **Returns**: A combined passage object.
119
+ - **Example**:
120
+ ```javascript
121
+ const passages = parser.parse("John 3:16, 3:17-18").getPassages()
122
+ const combined = parser.combine(passages)
123
+ // Result: A single "John 3:16-18" passage
124
+ ```
125
+
126
+ ### `.getToc(version)`
127
+
128
+ - **What it does**: Generates a table of contents with books and their chapter/verse counts. Useful for reference or validation.
129
+ - **Args**: `version` (string, optional) - Bible version (defaults to "ESV").
130
+ - **Returns**: Object mapping books to chapter/verse data.
131
+ - **Example**: `console.log(parser.getToc());`
132
+
133
+ ### Passage Object Structure
134
+
135
+ Each parsed passage looks like this:
136
+
137
+ ```javascript
138
+ {
139
+ original: "John 3:16-18", // Original input
140
+ book: "John", // Full book name
141
+ chapter: 3, // Starting chapter
142
+ verses: ["16-18"], // Verse range or list
143
+ type: "chapter_verse_range", // Reference type
144
+ testament: "new", // Old or New Testament
145
+ index: 0, // Position in text
146
+ version: { name: "English", value: "ENG", abbreviation: "eng" }, // Version info
147
+ passages: [{ book: "John", chapter: 3, verse: 16 }, ...], // Expanded verses
148
+ scripture: { passage: "John 3:16-18", cv: "3:16-18", hash: "john_3.16.18" }, // Formatted output
149
+ valid: true, // Validation status
150
+ start: { book: "John", chapter: 3, verse: 16 }, // First verse
151
+ end: { book: "John", chapter: 3, verse: 18 } // Last verse
152
+ }
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Supported Reference Types 📜
158
+
159
+ - **Single Chapter**: `Jude 1` (whole chapter of a single-chapter book).
160
+ - **Chapter Verse**: `John 3:16` (one verse).
161
+ - **Chapter Verse Range**: `Genesis 1:1-5` (verse range in one chapter).
162
+ - **Comma Separated Verses**: `Matthew 5:3, 5, 7` (multiple verses in one chapter).
163
+ - **Chapter Range**: `Exodus 20-22` (full chapters).
164
+ - **Multi-Chapter Verse Range**: `Psalm 119:1-120:5` (spans chapters).
165
+
166
+ ---
167
+
168
+ ## Contributing 🙌
169
+
170
+ Want to enhance CodexParser? Fork it, tweak it, and send a pull request! Issues and ideas are welcome on the [GitHub Issues page](https://github.com/jeremyam/CodexParser/issues).
171
+
172
+ ---
173
+
174
+ ## License ⚖️
175
+
176
+ [MIT License](LICENSE) - Free to use, modify, and share. Spread the Word!
177
+
178
+ ---
179
+
180
+ ## Acknowledgements 🌍
181
+
182
+ Built with love by [jeremyam], powered by coffee and scripture.
183
+
184
+ ---
185
+
186
+ Let’s parse the scriptures together—happy coding! ✝️📚
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexparser",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "description": "This is a Javascript Bible parser and text scanner. It will search through texts and collate all scripture references into an array and parse them into objects, and it will parse passages into objects by book, chapter, verse, and testament. ",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -25,43 +25,36 @@ class CodexParser {
25
25
  sch("Obadiah", 21),
26
26
  sch("Philemon", 25),
27
27
  ]
28
-
29
28
  this.chapterVerses = chapter_verses
30
29
  this.error = false
31
30
  this.version = null
31
+ this.SINGLE_CHAPTER = "single_chapter"
32
+ this.CHAPTER_VERSE = "chapter_verse"
33
+ this.CHAPTER_VERSE_RANGE = "chapter_verse_range"
34
+ this.COMMA_SEPARATED = "comma_separated_verses"
35
+ this.CHAPTER_RANGE = "chapter_range"
36
+ this.MULTI_CHAPTER_RANGE = "multi_chapter_verse_range"
37
+ }
38
+
39
+ getChapterVerses(book, chapter) {
40
+ const singleChapterBook = this.singleChapterBook.find((b) => Object.keys(b)[0] === book)
41
+ return singleChapterBook ? singleChapterBook[book][chapter] || [] : this.chapterVerses[book]?.[chapter] || []
32
42
  }
33
43
 
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
44
  scan(text) {
41
- // Combine Old and New Testament book names into a single array
42
45
  const fullNames = [...this.bible.old, ...this.bible.new]
43
-
44
- // Retrieve all abbreviation keys from the abbreviations object
45
46
  const abbreviations = Object.keys(this.abbreviations)
46
-
47
- // Initialize the `found` array to store the results
48
47
  this.found = []
49
-
50
- // Preprocess input text: normalize separators while preserving abbreviations
51
48
  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
49
+ .replace(/\.(?=\d)/g, ":")
50
+ .replace(/(\b[A-Za-z]+)\.(?=\s|$)/g, "$1")
51
+ .replace(/\s+/g, " ")
57
52
  const lowercaseBibleFullNames = fullNames.map((book) => book.toLowerCase())
58
53
  const lowercaseBibleAbbreviations = abbreviations.map((abbr) => abbr.toLowerCase())
59
54
  const lowerCaseText = normalizedText.toLowerCase()
60
-
61
55
  let i = 0
62
56
 
63
57
  const isValidChapterVerseChar = (char) => /[^A-Za-z]/.test(char)
64
-
65
58
  const isNextBibleBook = (startIndex) => {
66
59
  const textAfterCurrentPosition = lowerCaseText.substring(startIndex).trim()
67
60
  return (
@@ -69,7 +62,6 @@ class CodexParser {
69
62
  lowercaseBibleAbbreviations.some((abbr) => textAfterCurrentPosition.startsWith(abbr))
70
63
  )
71
64
  }
72
-
73
65
  const detectSuffix = (startIndex) => {
74
66
  const suffixMatch = normalizedText.substring(startIndex).match(/\b(LXX|MT)\b/i)
75
67
  return suffixMatch ? suffixMatch[0].toUpperCase() : null
@@ -107,7 +99,6 @@ class CodexParser {
107
99
 
108
100
  while (i < normalizedText.length && isValidChapterVerseChar(normalizedText[i])) {
109
101
  if (isNextBibleBook(i)) break
110
-
111
102
  if (normalizedText[i] === ";") {
112
103
  const formattedReference = chapterVerse.trim().replace(/[^a-zA-Z0-9]+$/, "")
113
104
  if (formattedReference) references.push(formattedReference)
@@ -115,7 +106,6 @@ class CodexParser {
115
106
  i++
116
107
  continue
117
108
  }
118
-
119
109
  chapterVerse += normalizedText[i]
120
110
  i++
121
111
  }
@@ -134,14 +124,12 @@ class CodexParser {
134
124
  const [start, end] = ref.split("-")
135
125
  const startParts = start.split(":")
136
126
  const endParts = end.split(":")
137
-
138
- // Determine type based on the chapter (startParts[0] and endParts[0])
139
127
  type =
140
128
  startParts.length > 1 &&
141
129
  endParts.length > 1 &&
142
130
  startParts[0].trim() !== endParts[0].trim()
143
- ? "multi_chapter_verse_range" // Chapters differ
144
- : "chapter_verse_range" // Same chapter
131
+ ? "multi_chapter_verse_range"
132
+ : "chapter_verse_range"
145
133
  } else if (ref.includes(",")) {
146
134
  type = "comma_separated_verses"
147
135
  } else {
@@ -178,120 +166,69 @@ class CodexParser {
178
166
  return this
179
167
  }
180
168
 
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
169
  parse(reference) {
189
- this.scan(reference) // Populate this.found
170
+ this.scan(reference)
190
171
 
191
172
  this.passages = this.found.map((passage) => {
192
173
  const book = this.bookify(passage.book)
193
174
  const testament = this.bible.old.includes(book) ? "old" : "new"
194
- const singleChapterBook = this.singleChapterBook.find((b) => Object.keys(b)[0] === book)
195
175
  const parsedPassage = {
196
- original: passage.book + " " + passage.reference,
197
- book: book,
176
+ original: `${passage.book} ${passage.reference}`,
177
+ book,
198
178
  chapter: null,
199
179
  verses: [],
200
180
  type: passage.type,
201
- testament: testament,
181
+ testament,
202
182
  index: passage.index,
203
183
  version: this._handleVersion(passage.version, testament),
184
+ passages: [],
185
+ scripture: null,
186
+ valid: true,
187
+ start: null,
188
+ end: null,
204
189
  }
205
190
 
206
- // Split reference into parts (e.g., "2 John 1", "2 John 2", "2 John 1:1-3,5")
207
- const parts = passage.reference.split(",")
208
-
209
- parts.forEach((part, partIndex) => {
210
- part = part.trim()
211
-
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
- }
218
-
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 3-5" → "2 John 1:3-5"
235
- parsedPassage.chapter = 1
236
- parsedPassage.verses.push(part) // e.g., "3-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., "Isaiah 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
- } else {
270
- parsedPassage.verses.push(Number(part))
271
- parsedPassage.type = "comma_separated_verses"
272
- }
273
- }
274
- })
275
-
276
- // Populate passages and scripture after processing all parts
191
+ this.parseReferenceParts(parsedPassage, passage.reference.split(","))
277
192
  parsedPassage.passages = this.populate(parsedPassage)
278
193
  parsedPassage.scripture = this.scripturize(parsedPassage)
279
194
  parsedPassage.valid = this._isValid(parsedPassage, passage.reference)
280
195
 
281
- // Handle multi-chapter range if applicable
282
- if (
283
- parsedPassage.type === "multi_chapter_verse_range" &&
284
- parts.some((p) => p.includes(":") && p.split(":")[0] !== String(parsedPassage.chapter))
285
- ) {
286
- const lastPart = parts[parts.length - 1]
287
- const [endChapter, endVerse] = lastPart.split(":")
288
- parsedPassage.to = {
289
- book: book,
290
- chapter: Number(endChapter),
291
- verses: endVerse.includes("-") ? [endVerse] : [Number(endVerse)],
292
- }
196
+ if (parsedPassage.type === this.MULTI_CHAPTER_RANGE) {
197
+ this.handleMultiChapterRange(parsedPassage, passage.reference)
293
198
  } else {
294
- delete parsedPassage.to // Remove erroneous 'to' property
199
+ delete parsedPassage.to
200
+ }
201
+
202
+ if (parsedPassage.passages.length > 0) {
203
+ const sortedPassages = parsedPassage.passages.slice().sort((a, b) => {
204
+ if (a.chapter !== b.chapter) return a.chapter - b.chapter
205
+ return a.verse - b.verse
206
+ })
207
+ const firstPassage = sortedPassages[0]
208
+ const lastPassage = sortedPassages[sortedPassages.length - 1]
209
+ parsedPassage.start = {
210
+ book: firstPassage.book,
211
+ chapter: firstPassage.chapter,
212
+ verse: firstPassage.verse,
213
+ }
214
+ parsedPassage.end = {
215
+ book: lastPassage.book,
216
+ chapter: lastPassage.chapter,
217
+ verse: lastPassage.verse,
218
+ }
219
+ }
220
+
221
+ if (!parsedPassage.version) {
222
+ parsedPassage.version = {
223
+ name: "English",
224
+ value: "ENG",
225
+ abbreviation: "eng",
226
+ }
227
+ }
228
+
229
+ // Attach the reference method to this individual passage object
230
+ parsedPassage.reference = function () {
231
+ return this.scripture.passage
295
232
  }
296
233
 
297
234
  return parsedPassage
@@ -300,9 +237,102 @@ class CodexParser {
300
237
  this.versification()
301
238
  return this
302
239
  }
303
- /**
304
- * Generates an array of numbers representing a range from start to end, inclusive.
305
- */
240
+
241
+ parseReferenceParts(passage, parts) {
242
+ const singleChapterBook = this.singleChapterBook.find((b) => Object.keys(b)[0] === passage.book)
243
+
244
+ parts.forEach((part, index) => {
245
+ part = part.trim()
246
+ const isFirstPart = index === 0
247
+
248
+ if (part.includes(":")) {
249
+ this.parseChapterVerse(passage, part, isFirstPart)
250
+ } else if (singleChapterBook) {
251
+ this.parseSingleChapterBook(passage, part, isFirstPart && parts.length === 1)
252
+ } else if (part.includes("-")) {
253
+ this.parseRange(passage, part, isFirstPart)
254
+ } else {
255
+ this.parseSingleNumber(passage, part, isFirstPart)
256
+ }
257
+ })
258
+ }
259
+
260
+ parseChapterVerse(passage, part, isFirstPart) {
261
+ const [chapter, verse] = part.split(":")
262
+ if (isFirstPart) passage.chapter = Number(chapter)
263
+ passage.type = verse.includes("-") ? this.CHAPTER_VERSE_RANGE : this.CHAPTER_VERSE
264
+ passage.verses.push(verse.includes("-") ? verse : Number(verse))
265
+ }
266
+
267
+ parseSingleChapterBook(passage, part, isWholeChapter) {
268
+ const verseCount = this.getChapterVerses(passage.book, 1).length
269
+ if (part === "1" && isWholeChapter) {
270
+ passage.chapter = 1
271
+ passage.type = this.SINGLE_CHAPTER
272
+ passage.verses = [`1-${verseCount}`]
273
+ } else if (part.includes("-")) {
274
+ passage.chapter = 1
275
+ passage.verses.push(part)
276
+ passage.type = this.CHAPTER_VERSE_RANGE
277
+ } else {
278
+ const num = Number(part)
279
+ if (num > 1 || !isWholeChapter) {
280
+ passage.chapter = 1
281
+ passage.verses.push(num)
282
+ passage.type = this.CHAPTER_VERSE
283
+ }
284
+ }
285
+ }
286
+
287
+ parseRange(passage, part, isFirstPart) {
288
+ if (!passage.chapter && isFirstPart) {
289
+ const [start, end] = part.split("-").map(Number)
290
+ passage.chapter = start
291
+ const startVerses = this.getChapterVerses(passage.book, start)
292
+ passage.verses = [`${startVerses[0]}-${startVerses[startVerses.length - 1]}`]
293
+ passage.to = {
294
+ book: passage.book,
295
+ chapter: end,
296
+ verses: [
297
+ `${this.getChapterVerses(passage.book, end)[0]}-${
298
+ this.getChapterVerses(passage.book, end).slice(-1)[0]
299
+ }`,
300
+ ],
301
+ }
302
+ passage.type = this.CHAPTER_RANGE
303
+ } else {
304
+ passage.verses.push(part)
305
+ passage.type = this.CHAPTER_VERSE_RANGE
306
+ }
307
+ }
308
+
309
+ parseSingleNumber(passage, part, isFirstPart) {
310
+ if (isFirstPart && !passage.chapter) {
311
+ passage.chapter = Number(part)
312
+ passage.type = this.SINGLE_CHAPTER
313
+ const chapterVerses = this.getChapterVerses(passage.book, passage.chapter)
314
+ if (chapterVerses.length) {
315
+ passage.verses = [`${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`]
316
+ }
317
+ } else {
318
+ passage.verses.push(Number(part))
319
+ passage.type = this.COMMA_SEPARATED
320
+ }
321
+ }
322
+
323
+ handleMultiChapterRange(passage, reference) {
324
+ const parts = reference.split(",")
325
+ const lastPart = parts[parts.length - 1]
326
+ const [endChapter, endVerse] = lastPart.split(":")
327
+ if (endChapter !== String(passage.chapter)) {
328
+ passage.to = {
329
+ book: passage.book,
330
+ chapter: Number(endChapter),
331
+ verses: endVerse.includes("-") ? [endVerse] : [Number(endVerse)],
332
+ }
333
+ }
334
+ }
335
+
306
336
  _generateRange(start, end) {
307
337
  const range = []
308
338
  for (let i = start; i <= end; i++) {
@@ -315,25 +345,20 @@ class CodexParser {
315
345
  version = version.toLowerCase()
316
346
  if (!this.chapterVerses[book][chapter]) return
317
347
  if (!this.versificationDifferences[book]) return
318
- // Loop through each key-value pair in the dictionary
319
348
  for (const [key, value] of Object.entries(this.versificationDifferences[book])) {
320
- // Check if the key starts with the desired chapter
321
349
  if (value[version].startsWith(`${chapter}:`)) {
322
- // Ensure the version exists in the value object
323
350
  if (value[version]) {
324
- // Extract the verse number from the value
325
351
  const verse = value[version].split(":")[1]
326
352
  this.chapterVerses[book][chapter].push(Number(verse))
327
353
  }
328
354
  }
329
355
  }
330
356
  this.chapterVerses[book][chapter] = Array.from(this.chapterVerses[book][chapter])
331
- return this.chapterVerses // Return the array of verses
357
+ return this.chapterVerses
332
358
  }
333
359
 
334
360
  _setVersion(book, chapter, version) {
335
361
  this.version = version ? version : "eng"
336
-
337
362
  if (this.version !== "eng") {
338
363
  this._searchVersificationDifferences(book, chapter, version)
339
364
  }
@@ -343,31 +368,25 @@ class CodexParser {
343
368
  this.passages.forEach((passage) => {
344
369
  const hasVersification = this.versificationDifferences[passage.book]
345
370
  passage.passages.forEach((subPassage) => {
346
- // Apply general versification differences
347
371
  if (hasVersification) {
348
372
  const key = `${subPassage.chapter}:${subPassage.verse}`
349
373
  if (this.versificationDifferences[passage.book][key]) {
350
374
  subPassage.versification = this.versificationDifferences[passage.book][key]
351
375
  }
352
376
  }
353
-
354
- // Handle specific version adjustments for "lxx" or "mt"
355
377
  if (passage.version) {
356
378
  const versionAbbreviation = passage.version.abbreviation
357
379
  const versionType =
358
380
  versionAbbreviation === "lxx" ? "lxx" : versionAbbreviation === "mt" ? "mt" : null
359
-
360
381
  if (versionType) {
361
382
  const versionReference = `${subPassage.chapter}:${subPassage.verse}`
362
-
363
- // Look for matching versification based on the version type (lxx or mt)
364
383
  for (const versification in this.versificationDifferences[passage.book]) {
365
384
  if (
366
385
  this.versificationDifferences[passage.book][versification][versionType] ===
367
386
  versionReference
368
387
  ) {
369
388
  subPassage.versification = this.versificationDifferences[passage.book][versification]
370
- break // Break once a match is found
389
+ break
371
390
  }
372
391
  }
373
392
  }
@@ -376,96 +395,65 @@ class CodexParser {
376
395
  })
377
396
  }
378
397
 
379
- /**
380
- * Populate all verses from a parsed passage, including all verses in ranges or chapters.
381
- *
382
- * @param {Object} parsedPassage - The parsed passage object containing book, chapter, and verses information.
383
- * @return {Array} An array of passage objects with individual verses.
384
- */
385
- populate(parsedPassage) {
386
- const passages = []
387
- const { book, chapter, verses, type, to } = parsedPassage
388
- const version = parsedPassage.version ? parsedPassage.version.abbreviation : "eng"
389
- this._setVersion(book, chapter, version) // Set version data if needed
398
+ populate(passage) {
399
+ const { book, chapter, verses, type, to } = passage
400
+ const version = passage.version?.abbreviation || "eng"
401
+ this._setVersion(book, chapter, version)
390
402
 
391
- const singleChapterBook = this.singleChapterBook.find((b) => Object.keys(b)[0] === book)
403
+ if (type === this.SINGLE_CHAPTER) {
404
+ const chapterVerses = this.getChapterVerses(book, chapter)
405
+ return this.expandVerses(book, chapter, [`${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`])
406
+ }
392
407
 
393
- if (type === "single_chapter") {
394
- // Handle entire chapter references (e.g., "Isaiah 40" or "2 John 1")
395
- if (singleChapterBook) {
396
- // Single-chapter book: populate all verses from singleChapterBook
397
- const verseCount = singleChapterBook[book][1].length
398
- for (let verse = 1; verse <= verseCount; verse++) {
399
- passages.push({ book, chapter: 1, verse })
400
- }
401
- } else if (this.chapterVerses[book] && this.chapterVerses[book][chapter]) {
402
- // Multi-chapter book: populate all verses in the chapter
403
- this.chapterVerses[book][chapter].forEach((verse) => {
404
- passages.push({ book, chapter: Number(chapter), verse: Number(verse) })
405
- })
406
- }
407
- } else if (type === "comma_separated_verses" || type === "chapter_verse_range") {
408
- // Handle comma-separated verses or single-chapter verse ranges (e.g., "Isaiah 40:3-5,8-9" or "2 John 1:1-3")
409
- verses.forEach((verse) => {
410
- if (typeof verse === "string" && verse.includes("-")) {
411
- const [start, end] = verse.split("-").map(Number)
412
- for (let i = start; i <= end; i++) {
413
- passages.push({ book, chapter: Number(chapter), verse: i })
414
- }
415
- } else {
416
- passages.push({ book, chapter: Number(chapter), verse: Number(verse) })
417
- }
418
- })
419
- } else if (type === "chapter_range") {
420
- // Handle ranges of chapters (e.g., "Isaiah 3-5")
421
- for (let currentChapter = chapter; currentChapter <= to.chapter; currentChapter++) {
422
- if (this.chapterVerses[book] && this.chapterVerses[book][currentChapter]) {
423
- this.chapterVerses[book][currentChapter].forEach((verse) => {
424
- passages.push({ book, chapter: Number(currentChapter), verse: Number(verse) })
425
- })
426
- }
408
+ if (type === this.CHAPTER_VERSE || type === this.COMMA_SEPARATED || type === this.CHAPTER_VERSE_RANGE) {
409
+ return this.expandVerses(book, chapter, verses)
410
+ }
411
+
412
+ if (type === this.CHAPTER_RANGE) {
413
+ const passages = []
414
+ for (let ch = chapter; ch <= to.chapter; ch++) {
415
+ const chapterVerses = this.getChapterVerses(book, ch)
416
+ passages.push(
417
+ ...this.expandVerses(book, ch, [`${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`])
418
+ )
427
419
  }
428
- } else if (type === "multi_chapter_verse_range") {
429
- // Handle multi-chapter verse ranges (e.g., "Isaiah 3:1-5:6")
430
- const startChapter = chapter
420
+ return passages
421
+ }
422
+
423
+ if (type === this.MULTI_CHAPTER_RANGE) {
424
+ const passages = []
431
425
  const startVerse = verses[0].includes("-") ? Number(verses[0].split("-")[0]) : Number(verses[0])
432
- const endChapter = to.chapter
433
426
  const endVerse = to.verses[0].includes("-") ? Number(to.verses[0].split("-")[1]) : Number(to.verses[0])
434
427
 
435
- for (let currentChapter = startChapter; currentChapter <= endChapter; currentChapter++) {
436
- const chapterVerses = this.chapterVerses[book][currentChapter]
437
- if (!chapterVerses) continue
428
+ for (let ch = chapter; ch <= to.chapter; ch++) {
429
+ const chapterVerses = this.getChapterVerses(book, ch)
430
+ const from = ch === chapter ? startVerse : chapterVerses[0]
431
+ const toVerse = ch === to.chapter ? endVerse : chapterVerses[chapterVerses.length - 1]
432
+ passages.push(...this.expandVerses(book, ch, [`${from}-${toVerse}`]))
433
+ }
434
+ return passages
435
+ }
438
436
 
439
- const chapterStartVerse = currentChapter === startChapter ? startVerse : chapterVerses[0]
440
- const chapterEndVerse =
441
- currentChapter === endChapter ? endVerse : chapterVerses[chapterVerses.length - 1]
437
+ return []
438
+ }
442
439
 
443
- for (let verse = chapterStartVerse; verse <= chapterEndVerse; verse++) {
444
- passages.push({ book, chapter: currentChapter, verse })
440
+ expandVerses(book, chapter, verses) {
441
+ const passages = []
442
+ const chapterVerses = this.getChapterVerses(book, chapter)
443
+
444
+ verses.forEach((verse) => {
445
+ if (typeof verse === "string" && verse.includes("-")) {
446
+ const [start, end] = verse.split("-").map(Number)
447
+ for (let i = start; i <= end && i <= chapterVerses[chapterVerses.length - 1]; i++) {
448
+ passages.push({ book, chapter, verse: i })
445
449
  }
450
+ } else {
451
+ passages.push({ book, chapter, verse: Number(verse) })
446
452
  }
447
- } else if (type === "chapter_verse") {
448
- // Handle single chapter:verse references (e.g., "2 John 1:1")
449
- verses.forEach((verse) => {
450
- passages.push({ book, chapter: Number(chapter), verse: Number(verse) })
451
- })
452
- } else if (type === "single_chapter_book_verse_range") {
453
- // Handle ranges in single-chapter books (e.g., "Jude 5-7")
454
- const [startVerse, endVerse] = verses[0].split("-").map(Number)
455
- for (let i = startVerse; i <= endVerse; i++) {
456
- passages.push({ book, chapter: 1, verse: i })
457
- }
458
- }
459
-
453
+ })
460
454
  return passages
461
455
  }
462
456
 
463
- /**
464
- * Converts a book name to its corresponding full name from the bible.
465
- *
466
- * @param {string} book - The abbreviated or partial name of the book.
467
- * @return {string|undefined} The full name of the book if found, otherwise undefined.
468
- */
469
457
  bookify(book) {
470
458
  if (typeof book !== "string") {
471
459
  book = book[0]
@@ -487,78 +475,105 @@ class CodexParser {
487
475
  return bookified
488
476
  }
489
477
 
490
- /**
491
- * Returns the passages stored in the object.
492
- *
493
- * @return {array} The passages stored in the object.
494
- */
495
478
  getPassages() {
496
- // Return the array of passages and add a custom first() method to it
497
- const passagesArray = [...this.passages] // Clone the array to avoid mutation
479
+ const passagesArray = [...this.passages]
498
480
 
499
- // Add first() method directly to the array
500
481
  passagesArray.first = function () {
501
482
  return this.length > 0 ? this[0] : null
502
483
  }
503
484
 
485
+ passagesArray.oldTestament = function () {
486
+ return this.filter((passage) => passage.testament === "old")
487
+ }
488
+
489
+ passagesArray.newTestament = function () {
490
+ return this.filter((passage) => passage.testament === "new")
491
+ }
492
+
493
+ passagesArray.combine = function (options = {}) {
494
+ const { book = true, chapter = true } = options
495
+
496
+ if (!book) {
497
+ return [...this]
498
+ }
499
+
500
+ const parser = new CodexParser()
501
+ const groupedByBook = new Map()
502
+
503
+ this.forEach((passage) => {
504
+ const bookKey = passage.book
505
+ if (!groupedByBook.has(bookKey)) {
506
+ groupedByBook.set(bookKey, [])
507
+ }
508
+ groupedByBook.get(bookKey).push(passage)
509
+ })
510
+
511
+ const combinedPassages = []
512
+
513
+ for (const [book, bookPassages] of groupedByBook) {
514
+ if (chapter) {
515
+ const groupedByChapter = new Map()
516
+ bookPassages.forEach((passage) => {
517
+ const chapterKey = `${passage.book}-${passage.chapter}`
518
+ if (!groupedByChapter.has(chapterKey)) {
519
+ groupedByChapter.set(chapterKey, [])
520
+ }
521
+ groupedByChapter.get(chapterKey).push(passage)
522
+ })
523
+
524
+ for (const passages of groupedByChapter.values()) {
525
+ if (passages.length === 1) {
526
+ combinedPassages.push({ ...passages[0] })
527
+ } else {
528
+ const combined = parser.combine(passages)
529
+ combinedPassages.push(combined)
530
+ }
531
+ }
532
+ } else {
533
+ const combined = parser.combine(bookPassages)
534
+ combinedPassages.push(combined)
535
+ }
536
+ }
537
+
538
+ return combinedPassages
539
+ }
540
+
504
541
  return passagesArray
505
542
  }
506
543
 
507
- // New first() method that can be chained after getPassages()
508
544
  first() {
509
545
  return this.passages.length > 0 ? this.passages[0] : null
510
546
  }
511
547
 
512
- /**
513
- * Converts a passage object into a scripturize object with human-readable name, chapter and verses and a hash.
514
- *
515
- * @param {object} passage - The passage object to scripturize.
516
- * @return {object} The object with the human-readable name, chapter and verses and a hash.
517
- */
518
548
  scripturize(passage) {
519
- // Helper function to format a single chapter:verse combination
520
549
  const formatChapterVerse = (chapter, verses) => {
521
550
  if (!chapter || !verses || verses.length === 0) return ""
522
551
  if (verses.length === 1) {
523
552
  return `${chapter}:${verses[0]}`
524
553
  }
525
-
526
- // Check if verses are continuous (e.g., [1, 2, 3, 4, 5] -> "1-5")
527
554
  const isRange = verses.every((v, i, arr) => i === 0 || v === arr[i - 1] + 1)
528
-
529
555
  if (isRange) {
530
556
  return `${chapter}:${verses[0]}-${verses[verses.length - 1]}`
531
557
  }
532
-
533
- // Comma-separated (e.g., [1, 3, 5] -> "1,3,5")
534
558
  return `${chapter}:${verses.join(",")}`
535
559
  }
536
560
 
537
- // Start constructing the passage string
538
561
  let combined = `${passage.book}`
539
-
540
562
  if (passage.type === "multi_chapter_verse_range" && passage.to) {
541
- // Multi-chapter verse range
542
-
543
563
  combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}-${formatChapterVerse(
544
564
  passage.to.chapter,
545
565
  passage.to.verses
546
566
  )}`
547
567
  } else if (passage.type === "chapter_verse_range") {
548
- // Single-chapter verse range
549
568
  combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
550
569
  } else if (passage.type === "comma_separated_verses") {
551
- // Comma-separated verses
552
570
  combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
553
571
  } else if (passage.type === "chapter_range" && passage.to) {
554
- // Chapter range
555
572
  combined += ` ${passage.chapter}-${passage.to.chapter}`
556
573
  } else {
557
- // Single chapter or single verse
558
574
  combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
559
575
  }
560
576
 
561
- // Generate the chapter:verse (cv) string
562
577
  const cv = passage.to
563
578
  ? `${formatChapterVerse(passage.chapter, passage.verses)}-${formatChapterVerse(
564
579
  passage.to.chapter,
@@ -566,10 +581,8 @@ class CodexParser {
566
581
  )}`
567
582
  : formatChapterVerse(passage.chapter, passage.verses)
568
583
 
569
- // Generate the hash
570
584
  const hash = `${passage.book.toLowerCase()}_${cv.replace(/:/g, ".").replace(/-/g, ".")}`
571
585
 
572
- // Return the final scripture object
573
586
  return {
574
587
  passage: combined,
575
588
  cv: cv,
@@ -577,27 +590,16 @@ class CodexParser {
577
590
  }
578
591
  }
579
592
 
580
- /**
581
- * Combine multiple passages into one. The method checks for duplicates, merges overlapping or adjacent ranges,
582
- * and builds the original and scripture properties.
583
- * **This method will always combine based on English versification. LXX and MT versifications will be reflected in the combined passage.passages.versification.**
584
- * This method will fail if the passages are not to the same book and chapter.
585
- * TODO: Add support for MT and LXX
586
- * @param {array} passages - An array of passage objects to combine.
587
- * @return {object} The combined passage object.
588
- */
589
593
  combine(passages) {
590
594
  if (!passages || passages.length === 0) {
591
595
  throw new Error("No passages provided to join.")
592
596
  }
593
597
 
594
- // Ensure all passages are from the same book
595
598
  const uniqueBooks = [...new Set(passages.map((p) => p.book))]
596
599
  if (uniqueBooks.length > 1) {
597
600
  throw new Error("Passages must be from the same book to join.")
598
601
  }
599
602
 
600
- // Start with the base object
601
603
  const combined = {
602
604
  ...passages[0],
603
605
  verses: [],
@@ -605,36 +607,45 @@ class CodexParser {
605
607
  to: null,
606
608
  scripture: {},
607
609
  type: null,
610
+ start: null,
611
+ end: null,
608
612
  }
609
613
 
610
614
  const chapterVerses = {}
611
615
  let firstChapter = null
612
616
  let lastChapter = null
617
+ let firstVerse = null
618
+ let lastVerse = null
613
619
 
614
- // Collect all verses and passages, grouped by chapter
615
620
  passages.forEach((passage) => {
616
621
  passage.passages.forEach((p) => {
617
622
  if (!chapterVerses[p.chapter]) {
618
623
  chapterVerses[p.chapter] = new Set()
619
624
  }
620
625
  chapterVerses[p.chapter].add(p.verse)
621
- combined.passages.push(p) // Add individual passage
626
+ combined.passages.push(p)
627
+
628
+ if (firstChapter === null || p.chapter < firstChapter) {
629
+ firstChapter = p.chapter
630
+ firstVerse = p.verse
631
+ } else if (p.chapter === firstChapter && (firstVerse === null || p.verse < firstVerse)) {
632
+ firstVerse = p.verse
633
+ }
634
+ if (lastChapter === null || p.chapter > lastChapter) {
635
+ lastChapter = p.chapter
636
+ lastVerse = p.verse
637
+ } else if (p.chapter === lastChapter && (lastVerse === null || p.verse > lastVerse)) {
638
+ lastVerse = p.verse
639
+ }
622
640
  })
623
641
 
624
- // Track first and last chapters
625
642
  const chapters = passage.passages.map((p) => p.chapter)
626
- if (!firstChapter || Math.min(...chapters) < firstChapter) {
627
- firstChapter = Math.min(...chapters)
628
- }
629
- if (!lastChapter || Math.max(...chapters) > lastChapter) {
630
- lastChapter = Math.max(...chapters)
631
- }
643
+ firstChapter = firstChapter === null ? Math.min(...chapters) : Math.min(firstChapter, ...chapters)
644
+ lastChapter = lastChapter === null ? Math.max(...chapters) : Math.max(lastChapter, ...chapters)
632
645
  })
633
646
 
634
- // Ensure unique and sorted passages
635
647
  combined.passages = Array.from(new Set(combined.passages.map(JSON.stringify))).map(JSON.parse)
636
648
 
637
- // Process chapter and verse data
638
649
  const chapterStrings = []
639
650
  const sortedChapters = Object.keys(chapterVerses)
640
651
  .map(Number)
@@ -645,13 +656,12 @@ class CodexParser {
645
656
  const mergedVerses = this.mergeRanges(verses)
646
657
  chapterStrings.push(`${chapter}:${mergedVerses.join(",")}`)
647
658
  if (chapter === firstChapter) {
648
- combined.verses = mergedVerses // First chapter's verses
659
+ combined.verses = mergedVerses
649
660
  }
650
661
  })
651
662
 
652
- // Handle multi-chapter ranges
653
663
  if (firstChapter !== lastChapter) {
654
- combined.type = "multi_chapter_verse_range"
664
+ combined.type = this.MULTI_CHAPTER_RANGE
655
665
  combined.to = {
656
666
  book: combined.book,
657
667
  chapter: lastChapter,
@@ -661,25 +671,37 @@ class CodexParser {
661
671
  ","
662
672
  )}; ${lastChapter}:${combined.to.verses.join(",")}`
663
673
  } else {
664
- // Single-chapter range or comma-separated
665
- if (combined.verses.length > 1) {
666
- combined.type = "chapter_verse_range"
667
- } else {
668
- combined.type = "chapter_verse"
669
- }
674
+ combined.type = combined.verses.length > 1 ? this.CHAPTER_VERSE_RANGE : this.CHAPTER_VERSE
670
675
  combined.original = `${combined.book} ${firstChapter}:${combined.verses.join(",")}`
671
676
  }
672
677
 
673
- // Build the scripture property
674
678
  const chapterString = chapterStrings.join(";")
675
679
  combined.scripture = {
676
680
  passage: `${combined.book} ${chapterString}`,
677
681
  cv: chapterString,
678
- hash: `${combined.book.toLowerCase()}_${chapterString.replace(/:/g, ".").replace(/,|;/g, ".")}`,
682
+ hash: `${combined.book.toLowerCase()}_${chapterString.replace(/:/g, ".").replace(/[,;]/g, ".")}`,
683
+ }
684
+
685
+ combined.start = {
686
+ book: combined.book,
687
+ chapter: firstChapter,
688
+ verse: firstVerse || Math.min(...Array.from(chapterVerses[firstChapter])),
689
+ }
690
+ combined.end = {
691
+ book: combined.book,
692
+ chapter: lastChapter,
693
+ verse: lastVerse || Math.max(...Array.from(chapterVerses[lastChapter])),
679
694
  }
695
+
696
+ // Reattach the reference method to the combined passage
697
+ combined.reference = function () {
698
+ return this.scripture.passage
699
+ }
700
+
680
701
  if (combined.to === null) {
681
702
  delete combined.to
682
703
  }
704
+
683
705
  return combined
684
706
  }
685
707
 
@@ -693,7 +715,6 @@ class CodexParser {
693
715
  if (sortedVerses[i] === end + 1) {
694
716
  end = sortedVerses[i]
695
717
  } else {
696
- // Push range or single verse
697
718
  if (start === end) {
698
719
  merged.push(`${start}`)
699
720
  } else {
@@ -704,7 +725,6 @@ class CodexParser {
704
725
  }
705
726
  }
706
727
 
707
- // Push the final range or single verse
708
728
  if (start === end) {
709
729
  merged.push(`${start}`)
710
730
  } else {
@@ -715,24 +735,17 @@ class CodexParser {
715
735
  }
716
736
 
717
737
  getToc(version = "ESV") {
718
- // Initialize the table of contents (toc)
719
738
  const toc = {}
720
-
721
- // Add Old Testament books and their chapters/verses to toc
722
739
  this.bible.old.forEach((book) => {
723
740
  if (this.chapterVerses[book]) {
724
741
  toc[book] = this.chapterVerses[book]
725
742
  }
726
743
  })
727
-
728
- // Add New Testament books and their chapters/verses to toc
729
744
  this.bible.new.forEach((book) => {
730
745
  if (this.chapterVerses[book]) {
731
746
  toc[book] = this.chapterVerses[book]
732
747
  }
733
748
  })
734
-
735
- // Merge in single-chapter books if not already in toc
736
749
  this.singleChapterBook.forEach((item) => {
737
750
  Object.keys(item).forEach((book) => {
738
751
  if (!toc[book]) {
@@ -740,8 +753,6 @@ class CodexParser {
740
753
  }
741
754
  })
742
755
  })
743
-
744
- // Sort the keys of toc by canonical order
745
756
  const orderedToc = {}
746
757
  const canonicalOrder = [...this.bible.old, ...this.bible.new]
747
758
  canonicalOrder.forEach((book) => {
@@ -749,162 +760,79 @@ class CodexParser {
749
760
  orderedToc[book] = toc[book]
750
761
  }
751
762
  })
752
-
753
763
  return orderedToc
754
764
  }
755
765
 
756
- /**
757
- * Validates a parsed passage to ensure the chapter and verses exist.
758
- *
759
- * @param {Object} passage - The parsed passage object to validate.
760
- * @param {string} reference - The original reference string for error messaging.
761
- * @return {boolean|Object} True if valid, or an error object if invalid.
762
- */
763
766
  _isValid(passage, reference) {
764
- const singleChapterBook = this.singleChapterBook.find((b) => Object.keys(b)[0] === passage.book)
767
+ const { book, chapter, verses, type } = passage
765
768
 
766
- // Check if verses exist at all
767
- if (!passage.verses || passage.verses.length === 0) {
768
- if (passage.type !== "single_chapter") {
769
- return {
770
- error: true,
771
- code: 101,
772
- message: {
773
- chapter_exists: false,
774
- content: "Possible invalid chapter: " + reference,
775
- },
776
- }
777
- }
769
+ if (!verses.length && type !== this.SINGLE_CHAPTER) {
770
+ return this.validationError(101, `Possible invalid chapter: ${reference}`)
778
771
  }
779
772
 
780
- // Handle single-chapter books
781
- if (singleChapterBook) {
782
- const verseCount = singleChapterBook[passage.book][1].length
783
-
784
- if (passage.chapter !== 1) {
785
- return {
786
- error: true,
787
- code: 103,
788
- message: {
789
- chapter_exists: false,
790
- content: `Chapter ${passage.chapter} does not exist in ${passage.book}`,
791
- },
792
- }
793
- }
773
+ const chapterVerses = this.getChapterVerses(book, chapter)
774
+ if (!chapterVerses.length) {
775
+ return this.validationError(102, `Chapter ${chapter} does not exist in ${book}`)
776
+ }
794
777
 
795
- if (passage.type === "single_chapter") {
796
- // For "2 John 1", validate the full range
797
- const [range] = passage.verses // e.g., "1-13"
798
- if (range) {
799
- const [start, end] = range.split("-").map(Number)
800
- if (start < 1 || end > verseCount) {
801
- return {
802
- error: true,
803
- code: 104,
804
- message: {
805
- verse_exists: false,
806
- content: `Verse range ${start}-${end} exceeds available verses (1-${verseCount}) in ${passage.book} 1`,
807
- },
808
- }
809
- }
810
- }
811
- return true // If no specific verses or range matches, it’s valid
812
- }
813
-
814
- // For specific verses in single-chapter books (e.g., "2 John 1:1-3")
815
- for (let i = 0; i < passage.verses.length; i++) {
816
- const verseRange = String(passage.verses[i])
817
- let versesToCheck = verseRange.includes("-") ? verseRange.split("-").map(Number) : [Number(verseRange)]
818
-
819
- if (versesToCheck.length === 2) {
820
- const [start, end] = versesToCheck
821
- versesToCheck = Array.from({ length: end - start + 1 }, (_, idx) => start + idx)
822
- }
823
-
824
- for (const verse of versesToCheck) {
825
- if (verse < 1 || verse > verseCount) {
826
- return {
827
- error: true,
828
- code: 104,
829
- message: {
830
- verse_exists: false,
831
- content: `Verse number ${verse} does not exist in ${passage.book} 1`,
832
- },
833
- }
834
- }
778
+ if (type === this.SINGLE_CHAPTER) {
779
+ const [range] = verses
780
+ if (range) {
781
+ const [start, end] = range.split("-").map(Number)
782
+ if (start < 1 || end > chapterVerses[chapterVerses.length - 1]) {
783
+ return this.validationError(
784
+ 104,
785
+ `Verse range ${start}-${end} exceeds available verses (1-${
786
+ chapterVerses[chapterVerses.length - 1]
787
+ }) in ${book} ${chapter}`
788
+ )
835
789
  }
836
790
  }
837
791
  return true
838
792
  }
839
793
 
840
- // Handle multi-chapter books
841
- if (!this.chapterVerses[passage.book] || !this.chapterVerses[passage.book][passage.chapter]) {
842
- return {
843
- error: true,
844
- code: 102,
845
- message: {
846
- chapter_exists: false,
847
- content: `Chapter ${passage.chapter} does not exist in ${passage.book}`,
848
- },
849
- }
850
- }
851
-
852
- if (passage.type === "single_chapter") {
853
- return true // For multi-chapter books, whole chapter is valid if it exists
854
- }
855
-
856
- for (let i = 0; i < passage.verses.length; i++) {
857
- const passageVerses = String(passage.verses[i])
858
- let verses = passageVerses.includes("-") ? passageVerses.split("-").map(Number) : [Number(passageVerses)]
859
-
860
- if (verses.length === 2) {
861
- // Expand the range if there are two numbers
862
- verses = Array.from({ length: verses[1] - verses[0] + 1 }, (_, index) => verses[0] + index)
863
- }
794
+ return this.validateVerses(book, chapter, verses, reference)
795
+ }
864
796
 
865
- for (const verse of verses) {
866
- const isValidVerse =
867
- this.chapterVerses[passage.book] &&
868
- this.chapterVerses[passage.book][passage.chapter] &&
869
- this.chapterVerses[passage.book][passage.chapter].includes(verse)
870
-
871
- if (!isValidVerse) {
872
- return {
873
- error: true,
874
- code: 104,
875
- message: {
876
- verse_exists: false,
877
- content: `Verse number ${verse} does not exist in ${passage.book} ${passage.chapter}`,
878
- },
879
- }
797
+ validateVerses(book, chapter, verses, reference) {
798
+ const chapterVerses = this.getChapterVerses(book, chapter)
799
+ for (const verse of verses) {
800
+ const verseRange = String(verse)
801
+ const verseNumbers = verseRange.includes("-")
802
+ ? Array.from(
803
+ { length: Number(verseRange.split("-")[1]) - Number(verseRange.split("-")[0]) + 1 },
804
+ (_, i) => Number(verseRange.split("-")[0]) + i
805
+ )
806
+ : [Number(verseRange)]
807
+
808
+ for (const v of verseNumbers) {
809
+ if (!chapterVerses.includes(v)) {
810
+ return this.validationError(104, `Verse number ${v} does not exist in ${book} ${chapter}`)
880
811
  }
881
812
  }
882
813
  }
883
-
884
814
  return true
885
815
  }
886
- _handleVersion(version, testament) {
887
- if (this.version) {
888
- version = this.version
889
- }
890
- if (!version) {
891
- version = "eng"
892
- }
893
- if (version.toLowerCase() === "lxx" && testament.toLowerCase() === "old") {
894
- return {
895
- name: "Septuagint",
896
- value: "LXX",
897
- abbreviation: "lxx",
898
- }
816
+
817
+ validationError(code, message) {
818
+ return {
819
+ error: true,
820
+ code,
821
+ message: { verse_exists: code === 104, chapter_exists: code !== 104, content: message },
899
822
  }
823
+ }
900
824
 
901
- if (version.toLowerCase() === "mt" && testament.toLowerCase() === "old") {
902
- return {
903
- name: "Masoretic Text",
904
- value: "MT",
905
- abbreviation: "mt",
906
- }
825
+ _handleVersion(version, testament) {
826
+ const effectiveVersion = this.version || version || "eng"
827
+ const lowerVersion = effectiveVersion.toLowerCase()
828
+
829
+ if (lowerVersion === "lxx" && testament === "old") {
830
+ return { name: "Septuagint", value: "LXX", abbreviation: "lxx" }
831
+ }
832
+ if (lowerVersion === "mt" && testament === "old") {
833
+ return { name: "Masoretic Text", value: "MT", abbreviation: "mt" }
907
834
  }
835
+ return { name: "English", value: "ENG", abbreviation: "eng" }
908
836
  }
909
837
  }
910
838
 
package/src/esv.js CHANGED
@@ -1,3 +1,4 @@
1
+ require("dotenv").config()
1
2
  const axios = require("axios")
2
3
 
3
4
  // Get all books
@@ -7,7 +8,7 @@ axios
7
8
  q: "John+3:16",
8
9
  },
9
10
  headers: {
10
- Authorization: "Token c117111babd413bd8a22b09ebfe84342dc792781",
11
+ Authorization: `Token ${process.env.ESV_TOKEN}`,
11
12
  },
12
13
  })
13
14
  .then((response) => {