codexparser 0.1.55 → 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.55",
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,129 +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., "Matthew 1", "2 John 2", "Matthew 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 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
- }
282
- }
283
- })
284
-
285
- // Populate passages and scripture after processing all parts
191
+ this.parseReferenceParts(parsedPassage, passage.reference.split(","))
286
192
  parsedPassage.passages = this.populate(parsedPassage)
287
193
  parsedPassage.scripture = this.scripturize(parsedPassage)
288
194
  parsedPassage.valid = this._isValid(parsedPassage, passage.reference)
289
195
 
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)],
301
- }
196
+ if (parsedPassage.type === this.MULTI_CHAPTER_RANGE) {
197
+ this.handleMultiChapterRange(parsedPassage, passage.reference)
302
198
  } else {
303
- 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
304
232
  }
305
233
 
306
234
  return parsedPassage
@@ -309,9 +237,102 @@ class CodexParser {
309
237
  this.versification()
310
238
  return this
311
239
  }
312
- /**
313
- * Generates an array of numbers representing a range from start to end, inclusive.
314
- */
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
+
315
336
  _generateRange(start, end) {
316
337
  const range = []
317
338
  for (let i = start; i <= end; i++) {
@@ -324,25 +345,20 @@ class CodexParser {
324
345
  version = version.toLowerCase()
325
346
  if (!this.chapterVerses[book][chapter]) return
326
347
  if (!this.versificationDifferences[book]) return
327
- // Loop through each key-value pair in the dictionary
328
348
  for (const [key, value] of Object.entries(this.versificationDifferences[book])) {
329
- // Check if the key starts with the desired chapter
330
349
  if (value[version].startsWith(`${chapter}:`)) {
331
- // Ensure the version exists in the value object
332
350
  if (value[version]) {
333
- // Extract the verse number from the value
334
351
  const verse = value[version].split(":")[1]
335
352
  this.chapterVerses[book][chapter].push(Number(verse))
336
353
  }
337
354
  }
338
355
  }
339
356
  this.chapterVerses[book][chapter] = Array.from(this.chapterVerses[book][chapter])
340
- return this.chapterVerses // Return the array of verses
357
+ return this.chapterVerses
341
358
  }
342
359
 
343
360
  _setVersion(book, chapter, version) {
344
361
  this.version = version ? version : "eng"
345
-
346
362
  if (this.version !== "eng") {
347
363
  this._searchVersificationDifferences(book, chapter, version)
348
364
  }
@@ -352,31 +368,25 @@ class CodexParser {
352
368
  this.passages.forEach((passage) => {
353
369
  const hasVersification = this.versificationDifferences[passage.book]
354
370
  passage.passages.forEach((subPassage) => {
355
- // Apply general versification differences
356
371
  if (hasVersification) {
357
372
  const key = `${subPassage.chapter}:${subPassage.verse}`
358
373
  if (this.versificationDifferences[passage.book][key]) {
359
374
  subPassage.versification = this.versificationDifferences[passage.book][key]
360
375
  }
361
376
  }
362
-
363
- // Handle specific version adjustments for "lxx" or "mt"
364
377
  if (passage.version) {
365
378
  const versionAbbreviation = passage.version.abbreviation
366
379
  const versionType =
367
380
  versionAbbreviation === "lxx" ? "lxx" : versionAbbreviation === "mt" ? "mt" : null
368
-
369
381
  if (versionType) {
370
382
  const versionReference = `${subPassage.chapter}:${subPassage.verse}`
371
-
372
- // Look for matching versification based on the version type (lxx or mt)
373
383
  for (const versification in this.versificationDifferences[passage.book]) {
374
384
  if (
375
385
  this.versificationDifferences[passage.book][versification][versionType] ===
376
386
  versionReference
377
387
  ) {
378
388
  subPassage.versification = this.versificationDifferences[passage.book][versification]
379
- break // Break once a match is found
389
+ break
380
390
  }
381
391
  }
382
392
  }
@@ -385,96 +395,65 @@ class CodexParser {
385
395
  })
386
396
  }
387
397
 
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
398
+ populate(passage) {
399
+ const { book, chapter, verses, type, to } = passage
400
+ const version = passage.version?.abbreviation || "eng"
401
+ this._setVersion(book, chapter, version)
399
402
 
400
- 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
+ }
401
407
 
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
- }
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
+ )
436
419
  }
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
420
+ return passages
421
+ }
422
+
423
+ if (type === this.MULTI_CHAPTER_RANGE) {
424
+ const passages = []
440
425
  const startVerse = verses[0].includes("-") ? Number(verses[0].split("-")[0]) : Number(verses[0])
441
- const endChapter = to.chapter
442
426
  const endVerse = to.verses[0].includes("-") ? Number(to.verses[0].split("-")[1]) : Number(to.verses[0])
443
427
 
444
- for (let currentChapter = startChapter; currentChapter <= endChapter; currentChapter++) {
445
- const chapterVerses = this.chapterVerses[book][currentChapter]
446
- 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
+ }
436
+
437
+ return []
438
+ }
447
439
 
448
- const chapterStartVerse = currentChapter === startChapter ? startVerse : chapterVerses[0]
449
- const chapterEndVerse =
450
- currentChapter === endChapter ? endVerse : chapterVerses[chapterVerses.length - 1]
440
+ expandVerses(book, chapter, verses) {
441
+ const passages = []
442
+ const chapterVerses = this.getChapterVerses(book, chapter)
451
443
 
452
- for (let verse = chapterStartVerse; verse <= chapterEndVerse; verse++) {
453
- passages.push({ book, chapter: currentChapter, verse })
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 })
454
449
  }
450
+ } else {
451
+ passages.push({ book, chapter, verse: Number(verse) })
455
452
  }
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
-
453
+ })
469
454
  return passages
470
455
  }
471
456
 
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
457
  bookify(book) {
479
458
  if (typeof book !== "string") {
480
459
  book = book[0]
@@ -496,78 +475,105 @@ class CodexParser {
496
475
  return bookified
497
476
  }
498
477
 
499
- /**
500
- * Returns the passages stored in the object.
501
- *
502
- * @return {array} The passages stored in the object.
503
- */
504
478
  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
479
+ const passagesArray = [...this.passages]
507
480
 
508
- // Add first() method directly to the array
509
481
  passagesArray.first = function () {
510
482
  return this.length > 0 ? this[0] : null
511
483
  }
512
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
+
513
541
  return passagesArray
514
542
  }
515
543
 
516
- // New first() method that can be chained after getPassages()
517
544
  first() {
518
545
  return this.passages.length > 0 ? this.passages[0] : null
519
546
  }
520
547
 
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
548
  scripturize(passage) {
528
- // Helper function to format a single chapter:verse combination
529
549
  const formatChapterVerse = (chapter, verses) => {
530
550
  if (!chapter || !verses || verses.length === 0) return ""
531
551
  if (verses.length === 1) {
532
552
  return `${chapter}:${verses[0]}`
533
553
  }
534
-
535
- // Check if verses are continuous (e.g., [1, 2, 3, 4, 5] -> "1-5")
536
554
  const isRange = verses.every((v, i, arr) => i === 0 || v === arr[i - 1] + 1)
537
-
538
555
  if (isRange) {
539
556
  return `${chapter}:${verses[0]}-${verses[verses.length - 1]}`
540
557
  }
541
-
542
- // Comma-separated (e.g., [1, 3, 5] -> "1,3,5")
543
558
  return `${chapter}:${verses.join(",")}`
544
559
  }
545
560
 
546
- // Start constructing the passage string
547
561
  let combined = `${passage.book}`
548
-
549
562
  if (passage.type === "multi_chapter_verse_range" && passage.to) {
550
- // Multi-chapter verse range
551
-
552
563
  combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}-${formatChapterVerse(
553
564
  passage.to.chapter,
554
565
  passage.to.verses
555
566
  )}`
556
567
  } else if (passage.type === "chapter_verse_range") {
557
- // Single-chapter verse range
558
568
  combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
559
569
  } else if (passage.type === "comma_separated_verses") {
560
- // Comma-separated verses
561
570
  combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
562
571
  } else if (passage.type === "chapter_range" && passage.to) {
563
- // Chapter range
564
572
  combined += ` ${passage.chapter}-${passage.to.chapter}`
565
573
  } else {
566
- // Single chapter or single verse
567
574
  combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
568
575
  }
569
576
 
570
- // Generate the chapter:verse (cv) string
571
577
  const cv = passage.to
572
578
  ? `${formatChapterVerse(passage.chapter, passage.verses)}-${formatChapterVerse(
573
579
  passage.to.chapter,
@@ -575,10 +581,8 @@ class CodexParser {
575
581
  )}`
576
582
  : formatChapterVerse(passage.chapter, passage.verses)
577
583
 
578
- // Generate the hash
579
584
  const hash = `${passage.book.toLowerCase()}_${cv.replace(/:/g, ".").replace(/-/g, ".")}`
580
585
 
581
- // Return the final scripture object
582
586
  return {
583
587
  passage: combined,
584
588
  cv: cv,
@@ -586,27 +590,16 @@ class CodexParser {
586
590
  }
587
591
  }
588
592
 
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
593
  combine(passages) {
599
594
  if (!passages || passages.length === 0) {
600
595
  throw new Error("No passages provided to join.")
601
596
  }
602
597
 
603
- // Ensure all passages are from the same book
604
598
  const uniqueBooks = [...new Set(passages.map((p) => p.book))]
605
599
  if (uniqueBooks.length > 1) {
606
600
  throw new Error("Passages must be from the same book to join.")
607
601
  }
608
602
 
609
- // Start with the base object
610
603
  const combined = {
611
604
  ...passages[0],
612
605
  verses: [],
@@ -614,36 +607,45 @@ class CodexParser {
614
607
  to: null,
615
608
  scripture: {},
616
609
  type: null,
610
+ start: null,
611
+ end: null,
617
612
  }
618
613
 
619
614
  const chapterVerses = {}
620
615
  let firstChapter = null
621
616
  let lastChapter = null
617
+ let firstVerse = null
618
+ let lastVerse = null
622
619
 
623
- // Collect all verses and passages, grouped by chapter
624
620
  passages.forEach((passage) => {
625
621
  passage.passages.forEach((p) => {
626
622
  if (!chapterVerses[p.chapter]) {
627
623
  chapterVerses[p.chapter] = new Set()
628
624
  }
629
625
  chapterVerses[p.chapter].add(p.verse)
630
- 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
+ }
631
640
  })
632
641
 
633
- // Track first and last chapters
634
642
  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
- }
643
+ firstChapter = firstChapter === null ? Math.min(...chapters) : Math.min(firstChapter, ...chapters)
644
+ lastChapter = lastChapter === null ? Math.max(...chapters) : Math.max(lastChapter, ...chapters)
641
645
  })
642
646
 
643
- // Ensure unique and sorted passages
644
647
  combined.passages = Array.from(new Set(combined.passages.map(JSON.stringify))).map(JSON.parse)
645
648
 
646
- // Process chapter and verse data
647
649
  const chapterStrings = []
648
650
  const sortedChapters = Object.keys(chapterVerses)
649
651
  .map(Number)
@@ -654,13 +656,12 @@ class CodexParser {
654
656
  const mergedVerses = this.mergeRanges(verses)
655
657
  chapterStrings.push(`${chapter}:${mergedVerses.join(",")}`)
656
658
  if (chapter === firstChapter) {
657
- combined.verses = mergedVerses // First chapter's verses
659
+ combined.verses = mergedVerses
658
660
  }
659
661
  })
660
662
 
661
- // Handle multi-chapter ranges
662
663
  if (firstChapter !== lastChapter) {
663
- combined.type = "multi_chapter_verse_range"
664
+ combined.type = this.MULTI_CHAPTER_RANGE
664
665
  combined.to = {
665
666
  book: combined.book,
666
667
  chapter: lastChapter,
@@ -670,25 +671,37 @@ class CodexParser {
670
671
  ","
671
672
  )}; ${lastChapter}:${combined.to.verses.join(",")}`
672
673
  } 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
- }
674
+ combined.type = combined.verses.length > 1 ? this.CHAPTER_VERSE_RANGE : this.CHAPTER_VERSE
679
675
  combined.original = `${combined.book} ${firstChapter}:${combined.verses.join(",")}`
680
676
  }
681
677
 
682
- // Build the scripture property
683
678
  const chapterString = chapterStrings.join(";")
684
679
  combined.scripture = {
685
680
  passage: `${combined.book} ${chapterString}`,
686
681
  cv: chapterString,
687
- 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])),
688
694
  }
695
+
696
+ // Reattach the reference method to the combined passage
697
+ combined.reference = function () {
698
+ return this.scripture.passage
699
+ }
700
+
689
701
  if (combined.to === null) {
690
702
  delete combined.to
691
703
  }
704
+
692
705
  return combined
693
706
  }
694
707
 
@@ -702,7 +715,6 @@ class CodexParser {
702
715
  if (sortedVerses[i] === end + 1) {
703
716
  end = sortedVerses[i]
704
717
  } else {
705
- // Push range or single verse
706
718
  if (start === end) {
707
719
  merged.push(`${start}`)
708
720
  } else {
@@ -713,7 +725,6 @@ class CodexParser {
713
725
  }
714
726
  }
715
727
 
716
- // Push the final range or single verse
717
728
  if (start === end) {
718
729
  merged.push(`${start}`)
719
730
  } else {
@@ -724,24 +735,17 @@ class CodexParser {
724
735
  }
725
736
 
726
737
  getToc(version = "ESV") {
727
- // Initialize the table of contents (toc)
728
738
  const toc = {}
729
-
730
- // Add Old Testament books and their chapters/verses to toc
731
739
  this.bible.old.forEach((book) => {
732
740
  if (this.chapterVerses[book]) {
733
741
  toc[book] = this.chapterVerses[book]
734
742
  }
735
743
  })
736
-
737
- // Add New Testament books and their chapters/verses to toc
738
744
  this.bible.new.forEach((book) => {
739
745
  if (this.chapterVerses[book]) {
740
746
  toc[book] = this.chapterVerses[book]
741
747
  }
742
748
  })
743
-
744
- // Merge in single-chapter books if not already in toc
745
749
  this.singleChapterBook.forEach((item) => {
746
750
  Object.keys(item).forEach((book) => {
747
751
  if (!toc[book]) {
@@ -749,8 +753,6 @@ class CodexParser {
749
753
  }
750
754
  })
751
755
  })
752
-
753
- // Sort the keys of toc by canonical order
754
756
  const orderedToc = {}
755
757
  const canonicalOrder = [...this.bible.old, ...this.bible.new]
756
758
  canonicalOrder.forEach((book) => {
@@ -758,162 +760,79 @@ class CodexParser {
758
760
  orderedToc[book] = toc[book]
759
761
  }
760
762
  })
761
-
762
763
  return orderedToc
763
764
  }
764
765
 
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
766
  _isValid(passage, reference) {
773
- const singleChapterBook = this.singleChapterBook.find((b) => Object.keys(b)[0] === passage.book)
767
+ const { book, chapter, verses, type } = passage
774
768
 
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
- }
769
+ if (!verses.length && type !== this.SINGLE_CHAPTER) {
770
+ return this.validationError(101, `Possible invalid chapter: ${reference}`)
787
771
  }
788
772
 
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
- }
773
+ const chapterVerses = this.getChapterVerses(book, chapter)
774
+ if (!chapterVerses.length) {
775
+ return this.validationError(102, `Chapter ${chapter} does not exist in ${book}`)
776
+ }
803
777
 
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
- }
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
+ )
844
789
  }
845
790
  }
846
791
  return true
847
792
  }
848
793
 
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
- }
794
+ return this.validateVerses(book, chapter, verses, reference)
795
+ }
873
796
 
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
- }
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}`)
889
811
  }
890
812
  }
891
813
  }
892
-
893
814
  return true
894
815
  }
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
- }
816
+
817
+ validationError(code, message) {
818
+ return {
819
+ error: true,
820
+ code,
821
+ message: { verse_exists: code === 104, chapter_exists: code !== 104, content: message },
908
822
  }
823
+ }
909
824
 
910
- if (version.toLowerCase() === "mt" && testament.toLowerCase() === "old") {
911
- return {
912
- name: "Masoretic Text",
913
- value: "MT",
914
- abbreviation: "mt",
915
- }
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" }
916
834
  }
835
+ return { name: "English", value: "ENG", abbreviation: "eng" }
917
836
  }
918
837
  }
919
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) => {