codexparser 0.1.97 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.trunk/trunk.yaml +3 -3
- package/CHANGELOG.md +18 -0
- package/README.md +76 -0
- package/REFACTORING.md +214 -0
- package/RELEASE_NOTES_v0.2.0.md +5 -0
- package/index.js +1 -1
- package/package.json +4 -24
- package/src/{CodexParser.js → CodexParser.js.backup} +303 -247
- package/src/core/CodexParser.js +353 -0
- package/src/core/PassageCollection.js +404 -0
- package/src/core/PassageValidator.js +107 -0
- package/src/core/ReferenceParser.js +638 -0
- package/src/core/ScriptureScanner.js +235 -0
- package/src/core/VersificationHandler.js +143 -0
- package/src/core/VersionHandler.js +77 -0
- package/src/data/toc.js +7 -0
- package/src/format/osis.js +210 -0
- package/src/index.js +12 -0
- package/src/utils/PassageUtils.js +117 -0
- package/src/utils/chapterVerseCombine.js +65 -0
- package/.babelrc +0 -5
- package/src/chapterVerseCombine.js +0 -63
- package/src/toc.js +0 -2
- package/webpack.config.js +0 -38
- /package/src/{abbr → data/abbr}/sbl.js +0 -0
- /package/src/{bible.js → data/bible.js} +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/1chronicles.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/1corinthians.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/1john.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/1kings.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/1peter.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/1samuel.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/1thessalonians.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/1timothy.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/2chronicles.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/2corinthians.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/2kings.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/2peter.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/2samuel.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/2thessalonians.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/2timothy.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/acts.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/amos.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/colossians.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/daniel.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/deuteronomy.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/ecclesiastes.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/ephesians.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/esther.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/exodus.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/ezekiel.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/ezra.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/galatians.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/genesis.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/habakkuk.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/haggai.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/hebrews.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/hosea.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/isaiah.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/james.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/jeremiah.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/job.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/joel.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/john.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/jonah.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/joshua.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/judges.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/lamentations.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/leviticus.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/luke.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/malachi.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/mark.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/matthew.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/micah.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/nahum.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/nehemiah.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/numbers.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/philippians.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/proverbs.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/psalms.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/revelation.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/romans.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/ruth.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/songs.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/titus.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/zechariah.js +0 -0
- /package/src/{chapter_verses → data/chapter_verses}/zephaniah.js +0 -0
- /package/src/{esv.js → data/esv.js} +0 -0
- /package/src/{versifications → data/versifications}/1chronicles.js +0 -0
- /package/src/{versifications → data/versifications}/1kings.js +0 -0
- /package/src/{versifications → data/versifications}/1samuel.js +0 -0
- /package/src/{versifications → data/versifications}/2chronicles.js +0 -0
- /package/src/{versifications → data/versifications}/2samuel.js +0 -0
- /package/src/{versifications → data/versifications}/daniel.js +0 -0
- /package/src/{versifications → data/versifications}/deuteronomy.js +0 -0
- /package/src/{versifications → data/versifications}/ecclesiastes.js +0 -0
- /package/src/{versifications → data/versifications}/exodus.js +0 -0
- /package/src/{versifications → data/versifications}/ezekiel.js +0 -0
- /package/src/{versifications → data/versifications}/genesis.js +0 -0
- /package/src/{versifications → data/versifications}/hosea.js +0 -0
- /package/src/{versifications → data/versifications}/isaiah.js +0 -0
- /package/src/{versifications → data/versifications}/jeremiah.js +0 -0
- /package/src/{versifications → data/versifications}/job.js +0 -0
- /package/src/{versifications → data/versifications}/joel.js +0 -0
- /package/src/{versifications → data/versifications}/jonah.js +0 -0
- /package/src/{versifications → data/versifications}/joshua.js +0 -0
- /package/src/{versifications → data/versifications}/leviticus.js +0 -0
- /package/src/{versifications → data/versifications}/malachi.js +0 -0
- /package/src/{versifications → data/versifications}/micah.js +0 -0
- /package/src/{versifications → data/versifications}/nahum.js +0 -0
- /package/src/{versifications → data/versifications}/nehemiah.js +0 -0
- /package/src/{versifications → data/versifications}/numbers.js +0 -0
- /package/src/{versifications → data/versifications}/proverbs.js +0 -0
- /package/src/{versifications → data/versifications}/psalms.js +0 -0
- /package/src/{versifications → data/versifications}/song.js +0 -0
- /package/src/{versifications → data/versifications}/zechariah.js +0 -0
- /package/src/{versified.js → data/versified.js} +0 -0
- /package/src/{abbr.js → format/abbr.js} +0 -0
- /package/src/{functions.js → utils/functions.js} +0 -0
- /package/src/{regex.js → utils/regex.js} +0 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReferenceParser.js
|
|
3
|
+
* Parses scripture references into structured passage objects
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const bible = require("../data/bible")
|
|
7
|
+
const abbreviations = require("../format/abbr")
|
|
8
|
+
const sblAbbreviations = require("../data/abbr/sbl")
|
|
9
|
+
const PassageUtils = require("../utils/PassageUtils")
|
|
10
|
+
const PassageValidator = require("./PassageValidator")
|
|
11
|
+
const VersionHandler = require("./VersionHandler")
|
|
12
|
+
const { formatOsis, formatOsisNumeric } = require("../format/osis")
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parses scripture references
|
|
16
|
+
*/
|
|
17
|
+
class ReferenceParser {
|
|
18
|
+
static REFERENCE_TYPES = {
|
|
19
|
+
SINGLE_CHAPTER: "single_chapter",
|
|
20
|
+
CHAPTER_VERSE: "chapter_verse",
|
|
21
|
+
CHAPTER_VERSE_RANGE: "chapter_verse_range",
|
|
22
|
+
COMMA_SEPARATED: "comma_separated_verses",
|
|
23
|
+
CHAPTER_RANGE: "chapter_range",
|
|
24
|
+
MULTI_CHAPTER_RANGE: "multi_chapter_verse_range",
|
|
25
|
+
BOOK_ONLY: "book_only",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#config
|
|
29
|
+
|
|
30
|
+
constructor(config = {}) {
|
|
31
|
+
this.#config = config
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parses found references into structured passage objects
|
|
36
|
+
* @param {Array} foundReferences - Array of found references from scanner
|
|
37
|
+
* @param {string} currentVersion - Current Bible version
|
|
38
|
+
* @returns {Array} Array of parsed passage objects
|
|
39
|
+
*/
|
|
40
|
+
parse(foundReferences, currentVersion = null) {
|
|
41
|
+
return foundReferences.map((reference) => {
|
|
42
|
+
const book = this.#normalizeBookName(reference.book)
|
|
43
|
+
const testament = bible.old.includes(book) ? "old" : "new"
|
|
44
|
+
|
|
45
|
+
const parsedPassage = {
|
|
46
|
+
original: `${reference.book} ${reference.reference}`,
|
|
47
|
+
book,
|
|
48
|
+
chapter: null,
|
|
49
|
+
verses: [],
|
|
50
|
+
type: reference.type,
|
|
51
|
+
testament,
|
|
52
|
+
startIndex: reference.startIndex,
|
|
53
|
+
endIndex: reference.endIndex,
|
|
54
|
+
originalText: reference.originalText,
|
|
55
|
+
version: VersionHandler.getVersion(reference.version || currentVersion, testament),
|
|
56
|
+
passages: [],
|
|
57
|
+
scripture: null,
|
|
58
|
+
valid: true,
|
|
59
|
+
start: null,
|
|
60
|
+
end: null,
|
|
61
|
+
abbr: null,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Clean reference for parsing
|
|
65
|
+
let cleanReference = reference.reference.replace(/\s*(LXX|MT)$/i, "").trim()
|
|
66
|
+
if (cleanReference.endsWith(",")) {
|
|
67
|
+
cleanReference = cleanReference.slice(0, -1).trim()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Handle book-only or empty references
|
|
71
|
+
if (!cleanReference && this.#config.booksOnly) {
|
|
72
|
+
parsedPassage.type = ReferenceParser.REFERENCE_TYPES.BOOK_ONLY
|
|
73
|
+
} else if (!cleanReference || cleanReference.match(/^\d+\s*[:;]?\s*$/)) {
|
|
74
|
+
this.#handleEmptyReference(parsedPassage, cleanReference)
|
|
75
|
+
} else if (reference.type === ReferenceParser.REFERENCE_TYPES.COMMA_SEPARATED) {
|
|
76
|
+
this.#handleCommaSeparated(parsedPassage, cleanReference)
|
|
77
|
+
} else {
|
|
78
|
+
this.#parseReferenceParts(parsedPassage, cleanReference)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Populate passages and scripture
|
|
82
|
+
parsedPassage.passages = this.#populatePassage(parsedPassage)
|
|
83
|
+
parsedPassage.scripture = this.#formatScripture(parsedPassage)
|
|
84
|
+
parsedPassage.valid = PassageValidator.validate(parsedPassage, cleanReference)
|
|
85
|
+
// Set abbreviation
|
|
86
|
+
this.#setAbbreviation(parsedPassage, reference.reference)
|
|
87
|
+
|
|
88
|
+
// Handle multi-chapter range
|
|
89
|
+
if (parsedPassage.type === ReferenceParser.REFERENCE_TYPES.MULTI_CHAPTER_RANGE) {
|
|
90
|
+
this.#handleMultiChapterRange(parsedPassage, cleanReference)
|
|
91
|
+
} else {
|
|
92
|
+
delete parsedPassage.to
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Calculate start and end
|
|
96
|
+
this.#calculateStartEnd(parsedPassage)
|
|
97
|
+
|
|
98
|
+
// Attach per-passage version helpers for backward compatibility
|
|
99
|
+
this.#attachVersionHelpers(parsedPassage)
|
|
100
|
+
|
|
101
|
+
return parsedPassage
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Normalizes book names using abbreviations or full names
|
|
107
|
+
* @private
|
|
108
|
+
*/
|
|
109
|
+
#normalizeBookName(book) {
|
|
110
|
+
if (typeof book !== "string") {
|
|
111
|
+
book = book[0]
|
|
112
|
+
}
|
|
113
|
+
book = book.toLowerCase()
|
|
114
|
+
|
|
115
|
+
const bookified = abbreviations[Object.keys(abbreviations).find((abbr) => abbr.toLowerCase() === book)]
|
|
116
|
+
if (bookified) {
|
|
117
|
+
return bookified
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
bible.new.find((b) => b.toLowerCase() === book) || bible.old.find((b) => b.toLowerCase() === book) || book
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Attaches version helper methods to a single passage object
|
|
127
|
+
* @private
|
|
128
|
+
*/
|
|
129
|
+
#attachVersionHelpers(passage) {
|
|
130
|
+
const self = this
|
|
131
|
+
const computeConverted = (srcPassage, targetAbbr) => {
|
|
132
|
+
const versionObj = VersionHandler.getVersionObject(targetAbbr)
|
|
133
|
+
const cloned = JSON.parse(JSON.stringify(srcPassage))
|
|
134
|
+
cloned.version = versionObj
|
|
135
|
+
|
|
136
|
+
// Remap chapters/verses according to versification, if present
|
|
137
|
+
cloned.passages.forEach((sub) => {
|
|
138
|
+
if (sub.versification && sub.versification[targetAbbr]) {
|
|
139
|
+
const [ch, v] = sub.versification[targetAbbr].split(":").map(Number)
|
|
140
|
+
sub.chapter = ch
|
|
141
|
+
sub.verse = v
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Sort and recompute summary fields
|
|
146
|
+
cloned.passages.sort((a, b) => a.chapter - b.chapter || a.verse - b.verse)
|
|
147
|
+
|
|
148
|
+
if (cloned.passages.length > 0) {
|
|
149
|
+
cloned.start = {
|
|
150
|
+
book: cloned.book,
|
|
151
|
+
chapter: cloned.passages[0].chapter,
|
|
152
|
+
verse: cloned.passages[0].verse,
|
|
153
|
+
}
|
|
154
|
+
cloned.end = {
|
|
155
|
+
book: cloned.book,
|
|
156
|
+
chapter: cloned.passages[cloned.passages.length - 1].chapter,
|
|
157
|
+
verse: cloned.passages[cloned.passages.length - 1].verse,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const chapterVersesMap = {}
|
|
162
|
+
cloned.passages.forEach((p) => {
|
|
163
|
+
if (!chapterVersesMap[p.chapter]) chapterVersesMap[p.chapter] = new Set()
|
|
164
|
+
chapterVersesMap[p.chapter].add(p.verse)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const sortedChs = Object.keys(chapterVersesMap)
|
|
168
|
+
.map(Number)
|
|
169
|
+
.sort((a, b) => a - b)
|
|
170
|
+
const chapterStrs = []
|
|
171
|
+
|
|
172
|
+
const mergeRanges = (verses) => {
|
|
173
|
+
const sorted = [...verses].sort((a, b) => a - b)
|
|
174
|
+
const merged = []
|
|
175
|
+
if (sorted.length === 0) return merged
|
|
176
|
+
let start = sorted[0]
|
|
177
|
+
let end = sorted[0]
|
|
178
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
179
|
+
if (sorted[i] === end + 1) {
|
|
180
|
+
end = sorted[i]
|
|
181
|
+
} else {
|
|
182
|
+
merged.push(start === end ? `${start}` : `${start}-${end}`)
|
|
183
|
+
start = end = sorted[i]
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
merged.push(start === end ? `${start}` : `${start}-${end}`)
|
|
187
|
+
return merged
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
sortedChs.forEach((ch) => {
|
|
191
|
+
const vs = Array.from(chapterVersesMap[ch])
|
|
192
|
+
.filter((v) => v > 0)
|
|
193
|
+
.sort((a, b) => a - b)
|
|
194
|
+
if (vs.length > 0) {
|
|
195
|
+
const merged = mergeRanges(vs)
|
|
196
|
+
chapterStrs.push(`${ch}:${merged.join(",")}`)
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
if (chapterStrs.length === 0) {
|
|
201
|
+
return cloned // no verses, return as-is
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const firstCh = sortedChs[0]
|
|
205
|
+
const lastCh = sortedChs[sortedChs.length - 1]
|
|
206
|
+
cloned.chapter = firstCh
|
|
207
|
+
|
|
208
|
+
const mergedFirst = mergeRanges(chapterVersesMap[firstCh] || new Set())
|
|
209
|
+
cloned.verses = mergedFirst
|
|
210
|
+
|
|
211
|
+
if (firstCh !== lastCh) {
|
|
212
|
+
cloned.type = ReferenceParser.REFERENCE_TYPES.MULTI_CHAPTER_RANGE
|
|
213
|
+
cloned.to = {
|
|
214
|
+
book: cloned.book,
|
|
215
|
+
chapter: lastCh,
|
|
216
|
+
verses: mergeRanges(chapterVersesMap[lastCh] || new Set()),
|
|
217
|
+
}
|
|
218
|
+
cloned.original = `${cloned.book} ${chapterStrs.join("; ")}`
|
|
219
|
+
} else {
|
|
220
|
+
const hasRangeOrMultiple =
|
|
221
|
+
mergedFirst.length > 1 || (mergedFirst.length === 1 && mergedFirst[0].includes("-"))
|
|
222
|
+
cloned.type = hasRangeOrMultiple
|
|
223
|
+
? ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE_RANGE
|
|
224
|
+
: ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE
|
|
225
|
+
if (cloned.to) delete cloned.to
|
|
226
|
+
cloned.original = `${cloned.book} ${chapterStrs[0]}`
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const chString = chapterStrs.join("; ")
|
|
230
|
+
cloned.scripture = {
|
|
231
|
+
passage: `${cloned.book} ${chString}`,
|
|
232
|
+
cv: chString,
|
|
233
|
+
hash: formatOsis(cloned),
|
|
234
|
+
osisNumeric: formatOsisNumeric(cloned),
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Set abbr
|
|
238
|
+
const sblEntry = Object.entries(sblAbbreviations).find(
|
|
239
|
+
([key]) => key.toLowerCase() === cloned.book.toLowerCase()
|
|
240
|
+
)
|
|
241
|
+
const suffix = versionObj.abbreviation === "eng" ? "" : ` ${versionObj.value}`
|
|
242
|
+
if (sblEntry) {
|
|
243
|
+
const { value, abbr } = sblEntry[1]
|
|
244
|
+
cloned.abbr = abbr
|
|
245
|
+
? `${value}. ${cloned.scripture.cv}${suffix}`
|
|
246
|
+
: `${value} ${cloned.scripture.cv}${suffix}`
|
|
247
|
+
} else {
|
|
248
|
+
cloned.abbr = `${cloned.book} ${cloned.scripture.cv}${suffix}`
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return cloned
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
passage.getVersion = function (targetVersion) {
|
|
255
|
+
const targetAbbr = targetVersion.toLowerCase() === "bhs" ? "mt" : targetVersion.toLowerCase()
|
|
256
|
+
return computeConverted(this, targetAbbr)
|
|
257
|
+
}
|
|
258
|
+
passage.getLXX = function () {
|
|
259
|
+
return this.getVersion("lxx")
|
|
260
|
+
}
|
|
261
|
+
passage.getMT = function () {
|
|
262
|
+
return this.getVersion("mt")
|
|
263
|
+
}
|
|
264
|
+
passage.getBHS = function () {
|
|
265
|
+
return this.getVersion("mt")
|
|
266
|
+
}
|
|
267
|
+
passage.getEnglish = function () {
|
|
268
|
+
return this.getVersion("eng")
|
|
269
|
+
}
|
|
270
|
+
passage.convertVersion = function (targetVersion) {
|
|
271
|
+
const targetAbbr = targetVersion.toLowerCase() === "bhs" ? "mt" : targetVersion.toLowerCase()
|
|
272
|
+
|
|
273
|
+
// Check if any passages have versification data
|
|
274
|
+
const hasVersification = this.passages.some((p) => p.versification)
|
|
275
|
+
|
|
276
|
+
if (!hasVersification) {
|
|
277
|
+
// No versification exists, return a clone with updated version info only
|
|
278
|
+
const cloned = JSON.parse(JSON.stringify(this))
|
|
279
|
+
cloned.version = VersionHandler.getVersionObject(targetAbbr)
|
|
280
|
+
return cloned
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Has versification, use the full conversion
|
|
284
|
+
return computeConverted(this, targetAbbr)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Handles empty or chapter-only references
|
|
290
|
+
* @private
|
|
291
|
+
*/
|
|
292
|
+
#handleEmptyReference(passage, cleanReference) {
|
|
293
|
+
const chapterMatch = cleanReference.match(/\d+/) || ["1"]
|
|
294
|
+
const chapter = Number(chapterMatch[0])
|
|
295
|
+
passage.chapter = chapter
|
|
296
|
+
passage.type = ReferenceParser.REFERENCE_TYPES.SINGLE_CHAPTER
|
|
297
|
+
|
|
298
|
+
const chapterVerses = PassageUtils.getChapterVerses(passage.book, chapter)
|
|
299
|
+
if (chapterVerses.length) {
|
|
300
|
+
const startVerse = chapterVerses[0]
|
|
301
|
+
const endVerse = chapterVerses[chapterVerses.length - 1]
|
|
302
|
+
passage.verses = [`${startVerse}-${endVerse}`]
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Handles comma-separated verses
|
|
308
|
+
* @private
|
|
309
|
+
*/
|
|
310
|
+
#handleCommaSeparated(passage, cleanReference) {
|
|
311
|
+
const [chapter, verses] = cleanReference.split(":")
|
|
312
|
+
passage.chapter = Number(chapter)
|
|
313
|
+
passage.verses = verses.split(",").map((v) => v.trim())
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Parses reference parts into chapter and verse components
|
|
318
|
+
* @private
|
|
319
|
+
*/
|
|
320
|
+
#parseReferenceParts(passage, reference) {
|
|
321
|
+
const singleChapterBook = PassageUtils.SINGLE_CHAPTER_BOOKS.find((b) => Object.keys(b)[0] === passage.book)
|
|
322
|
+
const parts = reference
|
|
323
|
+
.split(",")
|
|
324
|
+
.map((part) => part.trim())
|
|
325
|
+
.filter((part) => part)
|
|
326
|
+
|
|
327
|
+
parts.forEach((part, index) => {
|
|
328
|
+
const isFirstPart = index === 0
|
|
329
|
+
|
|
330
|
+
// Handle multi-chapter ranges (e.g., "2:1-3:19")
|
|
331
|
+
if (part.includes("-") && part.includes(":")) {
|
|
332
|
+
const [start, end] = part.split("-").map((s) => s.trim())
|
|
333
|
+
const startParts = start.split(/[:.]/).map((s) => s.trim())
|
|
334
|
+
const endParts = end.split(/[:.]/).map((s) => s.trim())
|
|
335
|
+
|
|
336
|
+
if (startParts.length > 1 && endParts.length > 1 && startParts[0] !== endParts[0]) {
|
|
337
|
+
passage.type = ReferenceParser.REFERENCE_TYPES.MULTI_CHAPTER_RANGE
|
|
338
|
+
passage.chapter = Number(startParts[0])
|
|
339
|
+
passage.verses = [startParts[1] || "1"]
|
|
340
|
+
passage.to = {
|
|
341
|
+
book: passage.book,
|
|
342
|
+
chapter: Number(endParts[0]),
|
|
343
|
+
verses: [endParts[1] || "1"],
|
|
344
|
+
}
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Handle chapter-only references
|
|
350
|
+
if (!part.includes(":") && !part.includes("-") && !singleChapterBook) {
|
|
351
|
+
this.#parseChapterOnly(passage, part)
|
|
352
|
+
} else if (part.includes(":")) {
|
|
353
|
+
this.#parseChapterVerse(passage, part, isFirstPart)
|
|
354
|
+
} else if (singleChapterBook) {
|
|
355
|
+
this.#parseSingleChapterBook(passage, part, isFirstPart && parts.length === 1)
|
|
356
|
+
} else if (part.includes("-")) {
|
|
357
|
+
this.#parseRange(passage, part, isFirstPart)
|
|
358
|
+
} else {
|
|
359
|
+
this.#parseSingleNumber(passage, part, isFirstPart)
|
|
360
|
+
}
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Parses chapter-only reference
|
|
366
|
+
* @private
|
|
367
|
+
*/
|
|
368
|
+
#parseChapterOnly(passage, part) {
|
|
369
|
+
const chapter = Number(part.replace(/[^0-9]/g, ""))
|
|
370
|
+
if (chapter > 0) {
|
|
371
|
+
passage.chapter = chapter
|
|
372
|
+
passage.type = ReferenceParser.REFERENCE_TYPES.SINGLE_CHAPTER
|
|
373
|
+
const chapterVerses = PassageUtils.getChapterVerses(passage.book, chapter)
|
|
374
|
+
if (chapterVerses.length) {
|
|
375
|
+
const startVerse = chapterVerses[0]
|
|
376
|
+
const endVerse = chapterVerses[chapterVerses.length - 1]
|
|
377
|
+
passage.verses = [`${startVerse}-${endVerse}`]
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Parses chapter:verse references
|
|
384
|
+
* @private
|
|
385
|
+
*/
|
|
386
|
+
#parseChapterVerse(passage, part, isFirstPart) {
|
|
387
|
+
const [chapter, verse] = part.split(/[:.]/).map((s) => s.trim())
|
|
388
|
+
if (isFirstPart) passage.chapter = Number(chapter)
|
|
389
|
+
|
|
390
|
+
passage.type = verse.includes("-")
|
|
391
|
+
? ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE_RANGE
|
|
392
|
+
: verse.includes(",")
|
|
393
|
+
? ReferenceParser.REFERENCE_TYPES.COMMA_SEPARATED
|
|
394
|
+
: ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE
|
|
395
|
+
|
|
396
|
+
if (verse.includes(",")) {
|
|
397
|
+
passage.verses.push(...verse.split(",").map((v) => v.trim()))
|
|
398
|
+
} else {
|
|
399
|
+
passage.verses.push(verse)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Parses references for single-chapter books
|
|
405
|
+
* @private
|
|
406
|
+
*/
|
|
407
|
+
#parseSingleChapterBook(passage, part, isWholeChapter) {
|
|
408
|
+
const verseCount = PassageUtils.getChapterVerses(passage.book, 1).length
|
|
409
|
+
|
|
410
|
+
if (part === "1" && isWholeChapter) {
|
|
411
|
+
passage.chapter = 1
|
|
412
|
+
passage.type = ReferenceParser.REFERENCE_TYPES.SINGLE_CHAPTER
|
|
413
|
+
passage.verses = [`1-${verseCount}`]
|
|
414
|
+
} else if (part.includes("-")) {
|
|
415
|
+
passage.chapter = 1
|
|
416
|
+
passage.verses.push(part)
|
|
417
|
+
passage.type = ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE_RANGE
|
|
418
|
+
} else if (part.includes(",")) {
|
|
419
|
+
passage.chapter = 1
|
|
420
|
+
passage.verses.push(...part.split(",").map((v) => v.trim()))
|
|
421
|
+
passage.type = ReferenceParser.REFERENCE_TYPES.COMMA_SEPARATED
|
|
422
|
+
} else {
|
|
423
|
+
const num = Number(part)
|
|
424
|
+
if (num > 0) {
|
|
425
|
+
passage.chapter = 1
|
|
426
|
+
passage.verses.push(num)
|
|
427
|
+
passage.type = ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Parses range references
|
|
434
|
+
* @private
|
|
435
|
+
*/
|
|
436
|
+
#parseRange(passage, part, isFirstPart) {
|
|
437
|
+
if (!passage.chapter && isFirstPart) {
|
|
438
|
+
const [start, end] = part.split("-").map(Number)
|
|
439
|
+
passage.chapter = start
|
|
440
|
+
const startVerses = PassageUtils.getChapterVerses(passage.book, start)
|
|
441
|
+
passage.verses = [`${startVerses[0]}-${startVerses[startVerses.length - 1]}`]
|
|
442
|
+
passage.to = {
|
|
443
|
+
book: passage.book,
|
|
444
|
+
chapter: end,
|
|
445
|
+
verses: [
|
|
446
|
+
`${PassageUtils.getChapterVerses(passage.book, end)[0]}-${
|
|
447
|
+
PassageUtils.getChapterVerses(passage.book, end).slice(-1)[0]
|
|
448
|
+
}`,
|
|
449
|
+
],
|
|
450
|
+
}
|
|
451
|
+
passage.type = ReferenceParser.REFERENCE_TYPES.CHAPTER_RANGE
|
|
452
|
+
} else {
|
|
453
|
+
passage.verses.push(part)
|
|
454
|
+
passage.type = ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE_RANGE
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Parses single number references
|
|
460
|
+
* @private
|
|
461
|
+
*/
|
|
462
|
+
#parseSingleNumber(passage, part, isFirstPart) {
|
|
463
|
+
if (isFirstPart && !passage.chapter) {
|
|
464
|
+
passage.chapter = Number(part)
|
|
465
|
+
passage.type = ReferenceParser.REFERENCE_TYPES.SINGLE_CHAPTER
|
|
466
|
+
const chapterVerses = PassageUtils.getChapterVerses(passage.book, passage.chapter)
|
|
467
|
+
if (chapterVerses.length) {
|
|
468
|
+
passage.verses = [`${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`]
|
|
469
|
+
}
|
|
470
|
+
} else {
|
|
471
|
+
passage.verses.push(Number(part))
|
|
472
|
+
passage.type = ReferenceParser.REFERENCE_TYPES.COMMA_SEPARATED
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Handles multi-chapter range references
|
|
478
|
+
* @private
|
|
479
|
+
*/
|
|
480
|
+
#handleMultiChapterRange(passage, reference) {
|
|
481
|
+
const parts = reference.split(",")
|
|
482
|
+
const lastPart = parts[parts.length - 1].trim()
|
|
483
|
+
const [endChapter, endVerse] = lastPart.split(/[:.]/).map((s) => s.trim())
|
|
484
|
+
|
|
485
|
+
if (endChapter !== String(passage.chapter)) {
|
|
486
|
+
passage.to = {
|
|
487
|
+
book: passage.book,
|
|
488
|
+
chapter: Number(endChapter),
|
|
489
|
+
verses: endVerse ? [endVerse] : ["1"],
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Populates passage with expanded verse objects
|
|
496
|
+
* @private
|
|
497
|
+
*/
|
|
498
|
+
#populatePassage(passage) {
|
|
499
|
+
const { book, chapter, verses, type, to } = passage
|
|
500
|
+
|
|
501
|
+
if (type === ReferenceParser.REFERENCE_TYPES.SINGLE_CHAPTER) {
|
|
502
|
+
const chapterVerses = PassageUtils.getChapterVerses(book, chapter)
|
|
503
|
+
return PassageUtils.expandVerses(book, chapter, [
|
|
504
|
+
`${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`,
|
|
505
|
+
])
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (
|
|
509
|
+
type === ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE ||
|
|
510
|
+
type === ReferenceParser.REFERENCE_TYPES.COMMA_SEPARATED ||
|
|
511
|
+
type === ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE_RANGE
|
|
512
|
+
) {
|
|
513
|
+
return PassageUtils.expandVerses(book, chapter, verses)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (type === ReferenceParser.REFERENCE_TYPES.CHAPTER_RANGE) {
|
|
517
|
+
const passages = []
|
|
518
|
+
if (to && to.chapter) {
|
|
519
|
+
for (let ch = chapter; ch <= to.chapter; ch++) {
|
|
520
|
+
const chapterVerses = PassageUtils.getChapterVerses(book, ch)
|
|
521
|
+
passages.push(
|
|
522
|
+
...PassageUtils.expandVerses(book, ch, [
|
|
523
|
+
`${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`,
|
|
524
|
+
])
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return passages
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (type === ReferenceParser.REFERENCE_TYPES.MULTI_CHAPTER_RANGE) {
|
|
532
|
+
const passages = []
|
|
533
|
+
const startVerse = verses[0]?.includes("-") ? Number(verses[0].split("-")[0]) : Number(verses[0]) || 1
|
|
534
|
+
const endVerse = to?.verses?.[0]?.includes("-")
|
|
535
|
+
? Number(to.verses[0].split("-")[1])
|
|
536
|
+
: Number(to?.verses?.[0]) || 1
|
|
537
|
+
const endChapter = to?.chapter || chapter
|
|
538
|
+
|
|
539
|
+
for (let ch = chapter; ch <= endChapter; ch++) {
|
|
540
|
+
const chapterVerses = PassageUtils.getChapterVerses(book, ch)
|
|
541
|
+
const from = ch === chapter ? startVerse : chapterVerses[0]
|
|
542
|
+
const toVerse = ch === endChapter ? endVerse : chapterVerses[chapterVerses.length - 1]
|
|
543
|
+
passages.push(...PassageUtils.expandVerses(book, ch, [`${from}-${toVerse}`]))
|
|
544
|
+
}
|
|
545
|
+
return passages
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return []
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Formats a passage into a human-readable reference
|
|
553
|
+
* @private
|
|
554
|
+
*/
|
|
555
|
+
#formatScripture(passage) {
|
|
556
|
+
let combined = `${passage.book}`
|
|
557
|
+
|
|
558
|
+
if (passage.type === ReferenceParser.REFERENCE_TYPES.MULTI_CHAPTER_RANGE && passage.to) {
|
|
559
|
+
combined += ` ${PassageUtils.formatChapterVerse(
|
|
560
|
+
passage.chapter,
|
|
561
|
+
passage.verses
|
|
562
|
+
)}-${PassageUtils.formatChapterVerse(passage.to.chapter, passage.to.verses)}`
|
|
563
|
+
} else if (
|
|
564
|
+
passage.type === ReferenceParser.REFERENCE_TYPES.CHAPTER_VERSE_RANGE ||
|
|
565
|
+
passage.type === ReferenceParser.REFERENCE_TYPES.COMMA_SEPARATED
|
|
566
|
+
) {
|
|
567
|
+
combined += ` ${PassageUtils.formatChapterVerse(passage.chapter, passage.verses)}`
|
|
568
|
+
} else if (passage.type === ReferenceParser.REFERENCE_TYPES.CHAPTER_RANGE && passage.to) {
|
|
569
|
+
combined += ` ${passage.chapter}-${passage.to.chapter}`
|
|
570
|
+
} else {
|
|
571
|
+
combined += ` ${PassageUtils.formatChapterVerse(passage.chapter, passage.verses)}`
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const cv = passage.to
|
|
575
|
+
? `${PassageUtils.formatChapterVerse(passage.chapter, passage.verses)}-${PassageUtils.formatChapterVerse(
|
|
576
|
+
passage.to.chapter,
|
|
577
|
+
passage.to.verses
|
|
578
|
+
)}`
|
|
579
|
+
: PassageUtils.formatChapterVerse(passage.chapter, passage.verses)
|
|
580
|
+
|
|
581
|
+
const hash = formatOsis(passage)
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
passage: combined,
|
|
585
|
+
cv: cv,
|
|
586
|
+
hash: hash,
|
|
587
|
+
osisNumeric: formatOsisNumeric(passage),
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Sets the SBL abbreviation for a passage
|
|
593
|
+
* @private
|
|
594
|
+
*/
|
|
595
|
+
#setAbbreviation(passage, reference) {
|
|
596
|
+
const sblEntry = Object.entries(sblAbbreviations).find(
|
|
597
|
+
([key]) => key.toLowerCase() === passage.book.toLowerCase()
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
if (sblEntry) {
|
|
601
|
+
const { value, abbr } = sblEntry[1]
|
|
602
|
+
const ref = reference.replace(/\s*(LXX|MT)$/i, "").trim()
|
|
603
|
+
const suffix = passage.version.value !== "ENG" ? " " + passage.version.value : ""
|
|
604
|
+
passage.abbr = abbr ? `${value}. ${ref}${suffix}` : `${value} ${ref}${suffix}`
|
|
605
|
+
} else {
|
|
606
|
+
passage.abbr = passage.original
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Calculates start and end based on passages array
|
|
612
|
+
* @private
|
|
613
|
+
*/
|
|
614
|
+
#calculateStartEnd(passage) {
|
|
615
|
+
if (passage.passages.length > 0) {
|
|
616
|
+
const sortedPassages = passage.passages.slice().sort((a, b) => {
|
|
617
|
+
if (a.chapter !== b.chapter) return a.chapter - b.chapter
|
|
618
|
+
return a.verse - b.verse
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
const firstPassage = sortedPassages[0]
|
|
622
|
+
const lastPassage = sortedPassages[sortedPassages.length - 1]
|
|
623
|
+
|
|
624
|
+
passage.start = {
|
|
625
|
+
book: firstPassage.book,
|
|
626
|
+
chapter: firstPassage.chapter,
|
|
627
|
+
verse: firstPassage.verse,
|
|
628
|
+
}
|
|
629
|
+
passage.end = {
|
|
630
|
+
book: lastPassage.book,
|
|
631
|
+
chapter: lastPassage.chapter,
|
|
632
|
+
verse: lastPassage.verse,
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
module.exports = ReferenceParser
|