codexparser 0.1.78 → 0.1.80
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/configs/.markdownlint.yaml +2 -0
- package/.trunk/trunk.yaml +32 -0
- package/package.json +1 -1
- package/src/CodexParser.js +239 -156
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# This file controls the behavior of Trunk: https://docs.trunk.io/cli
|
|
2
|
+
# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
|
|
3
|
+
version: 0.1
|
|
4
|
+
cli:
|
|
5
|
+
version: 1.24.0
|
|
6
|
+
# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
|
|
7
|
+
plugins:
|
|
8
|
+
sources:
|
|
9
|
+
- id: trunk
|
|
10
|
+
ref: v1.7.0
|
|
11
|
+
uri: https://github.com/trunk-io/plugins
|
|
12
|
+
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
|
|
13
|
+
runtimes:
|
|
14
|
+
enabled:
|
|
15
|
+
- node@22.16.0
|
|
16
|
+
- python@3.10.8
|
|
17
|
+
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
|
|
18
|
+
lint:
|
|
19
|
+
enabled:
|
|
20
|
+
- checkov@3.2.443
|
|
21
|
+
- git-diff-check
|
|
22
|
+
- markdownlint@0.45.0
|
|
23
|
+
- osv-scanner@2.0.3
|
|
24
|
+
- prettier@3.5.3
|
|
25
|
+
- trufflehog@3.89.2
|
|
26
|
+
actions:
|
|
27
|
+
disabled:
|
|
28
|
+
- trunk-announce
|
|
29
|
+
- trunk-check-pre-push
|
|
30
|
+
- trunk-fmt-pre-commit
|
|
31
|
+
enabled:
|
|
32
|
+
- trunk-upgrade-available
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codexparser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.80",
|
|
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": {
|
package/src/CodexParser.js
CHANGED
|
@@ -23,7 +23,7 @@ class CodexParser {
|
|
|
23
23
|
/**
|
|
24
24
|
* Initializes the parser with default properties and data.
|
|
25
25
|
*/
|
|
26
|
-
constructor() {
|
|
26
|
+
constructor(config = {}) {
|
|
27
27
|
this.found = [] // Array to store detected references
|
|
28
28
|
this.passages = [] // Array to store parsed passages
|
|
29
29
|
this.bible = bible // Bible data (Old/New Testament books)
|
|
@@ -53,9 +53,12 @@ class CodexParser {
|
|
|
53
53
|
this.CHAPTER_RANGE = "chapter_range"
|
|
54
54
|
this.MULTI_CHAPTER_RANGE = "multi_chapter_verse_range"
|
|
55
55
|
this.config = {
|
|
56
|
-
booksOnly: false,
|
|
56
|
+
booksOnly: config.booksOnly ?? false,
|
|
57
|
+
invalid_sequence_strategy: config.invalid_sequence_strategy ?? "include",
|
|
58
|
+
invalid_passage_strategy: config.invalid_passage_strategy ?? "include",
|
|
57
59
|
}
|
|
58
60
|
}
|
|
61
|
+
|
|
59
62
|
/**
|
|
60
63
|
* Sets configuration options for the parser.
|
|
61
64
|
* @param {Object} config - Configuration options.
|
|
@@ -65,6 +68,8 @@ class CodexParser {
|
|
|
65
68
|
options(config) {
|
|
66
69
|
this.config = {
|
|
67
70
|
booksOnly: config.booksOnly ?? false,
|
|
71
|
+
invalid_sequence_strategy: config.invalid_sequence_strategy ?? this.config.invalid_sequence_strategy,
|
|
72
|
+
invalid_passage_strategy: config.invalid_passage_strategy ?? this.config.invalid_passage_strategy,
|
|
68
73
|
}
|
|
69
74
|
return this
|
|
70
75
|
}
|
|
@@ -89,138 +94,173 @@ class CodexParser {
|
|
|
89
94
|
const fullNames = [...this.bible.old, ...this.bible.new]
|
|
90
95
|
const abbreviations = Object.keys(this.abbreviations)
|
|
91
96
|
this.found = []
|
|
92
|
-
|
|
93
|
-
// Normalize text: remove curly quotes, replace periods before numbers with colons
|
|
97
|
+
// Normalize text for parsing but keep original for originalText
|
|
94
98
|
let normalizedText = text
|
|
95
99
|
.replace(/[“”]/g, "") // Remove curly quotes
|
|
96
|
-
.replace(/\.(?=\d)/g, ":")
|
|
100
|
+
.replace(/\.(?=\d)/g, ":") // Replace periods before digits with colons (e.g., "Re13.8" -> "Re13:8")
|
|
101
|
+
.replace(/\s+/g, " ") // Normalize multiple spaces to single
|
|
102
|
+
const lowercaseBibleFullNames = fullNames.map((book) => book.toLowerCase())
|
|
103
|
+
const lowercaseBibleAbbreviations = abbreviations.map((abbr) => abbr.toLowerCase())
|
|
97
104
|
const lowerCaseText = normalizedText.toLowerCase()
|
|
98
105
|
let i = 0
|
|
99
106
|
|
|
107
|
+
console.log("[Scan] Input text:", text)
|
|
108
|
+
console.log("[Scan] Normalized text:", normalizedText)
|
|
109
|
+
|
|
110
|
+
const isValidChapterVerseChar = (char) => /[^A-Za-z]/.test(char) // Non-letter characters
|
|
111
|
+
const isNextBibleBook = (startIndex) => {
|
|
112
|
+
const textAfterCurrentPosition = lowerCaseText.substring(startIndex).trim()
|
|
113
|
+
// Check if the text starts with a book name or abbreviation followed by a digit
|
|
114
|
+
return (
|
|
115
|
+
lowercaseBibleFullNames.some((book) => {
|
|
116
|
+
if (textAfterCurrentPosition.startsWith(book)) {
|
|
117
|
+
const nextIndex = startIndex + book.length
|
|
118
|
+
const nextChar = lowerCaseText[nextIndex]
|
|
119
|
+
return nextChar && /\d/.test(nextChar)
|
|
120
|
+
}
|
|
121
|
+
return false
|
|
122
|
+
}) ||
|
|
123
|
+
lowercaseBibleAbbreviations.some((abbr) => {
|
|
124
|
+
if (textAfterCurrentPosition.startsWith(abbr)) {
|
|
125
|
+
const nextIndex = startIndex + abbr.length
|
|
126
|
+
const nextChar = lowerCaseText[nextIndex]
|
|
127
|
+
return nextChar && (/\d/.test(nextChar) || /\./.test(nextChar))
|
|
128
|
+
}
|
|
129
|
+
return false
|
|
130
|
+
})
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
const detectSuffix = (startIndex, inputText) => {
|
|
134
|
+
const suffixMatch = inputText.substring(startIndex).match(/\b(LXX|MT)\b/i)
|
|
135
|
+
return suffixMatch ? { version: suffixMatch[0].toUpperCase(), length: suffixMatch[0].length } : null
|
|
136
|
+
}
|
|
137
|
+
|
|
100
138
|
while (i < lowerCaseText.length) {
|
|
101
139
|
let foundBook = null
|
|
102
|
-
let startIndex = -1
|
|
103
140
|
let matchedLength = 0
|
|
104
|
-
let
|
|
105
|
-
let
|
|
106
|
-
|
|
107
|
-
// Skip whitespace
|
|
108
|
-
while (i < lowerCaseText.length && /\s/.test(lowerCaseText[i])) {
|
|
109
|
-
i++
|
|
110
|
-
}
|
|
111
|
-
if (i >= lowerCaseText.length) break
|
|
112
|
-
|
|
113
|
-
// Check for opening parenthesis
|
|
114
|
-
if (i < lowerCaseText.length && lowerCaseText[i] === "(") {
|
|
115
|
-
hasOpeningParen = true
|
|
116
|
-
parenStartIndex = i
|
|
117
|
-
i++
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Record potential start of reference
|
|
121
|
-
startIndex = i
|
|
141
|
+
let originalBookText = ""
|
|
142
|
+
let startIndex = i
|
|
122
143
|
|
|
123
|
-
// Check
|
|
124
|
-
for (let
|
|
144
|
+
// Check full book names
|
|
145
|
+
for (let j = 0; j < lowercaseBibleFullNames.length; j++) {
|
|
146
|
+
const book = lowercaseBibleFullNames[j]
|
|
125
147
|
if (
|
|
126
|
-
lowerCaseText.startsWith(book
|
|
127
|
-
(i + book.length >= lowerCaseText.length ||
|
|
148
|
+
lowerCaseText.startsWith(book, i) &&
|
|
149
|
+
(i + book.length >= lowerCaseText.length || /\d/.test(lowerCaseText[i + book.length]))
|
|
128
150
|
) {
|
|
129
|
-
foundBook =
|
|
151
|
+
foundBook = fullNames[j]
|
|
130
152
|
matchedLength = book.length
|
|
131
|
-
|
|
153
|
+
originalBookText = text.slice(i, i + book.length)
|
|
154
|
+
console.log(`[Scan] Matched full book name: "${foundBook}" at index ${i}`)
|
|
132
155
|
}
|
|
133
156
|
}
|
|
157
|
+
|
|
158
|
+
// Check abbreviations
|
|
134
159
|
if (!foundBook) {
|
|
135
|
-
for (let
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
160
|
+
for (let k = 0; k < lowercaseBibleAbbreviations.length; k++) {
|
|
161
|
+
const abbreviation = lowercaseBibleAbbreviations[k]
|
|
162
|
+
const abbrPattern = abbreviation.replace(/\./g, "\\.?")
|
|
163
|
+
const regex = new RegExp(`^${abbrPattern}(\\.?\\s*\\d)`, "i")
|
|
164
|
+
const match = lowerCaseText.slice(i).match(regex)
|
|
165
|
+
if (match) {
|
|
166
|
+
foundBook = this.abbreviations[abbreviations[k]]
|
|
167
|
+
matchedLength = match[0].length - match[1].length // Exclude chapter-verse part
|
|
168
|
+
originalBookText = text.slice(i, i + matchedLength)
|
|
169
|
+
console.log(
|
|
170
|
+
`[Scan] Matched abbreviation: "${abbreviations[k]}" -> "${foundBook}" at index ${i}`
|
|
171
|
+
)
|
|
143
172
|
}
|
|
144
173
|
}
|
|
145
174
|
}
|
|
146
175
|
|
|
147
176
|
if (foundBook) {
|
|
148
|
-
// Check if book is followed by a valid reference or version when booksOnly is false
|
|
149
|
-
let isFollowedByReference = false
|
|
150
|
-
let j = i + matchedLength
|
|
151
|
-
// Skip spaces
|
|
152
|
-
while (j < lowerCaseText.length && /\s/.test(lowerCaseText[j])) {
|
|
153
|
-
j++
|
|
154
|
-
}
|
|
155
|
-
// Check for digit (chapter number) or version suffix (LXX/MT)
|
|
156
|
-
if (
|
|
157
|
-
j < lowerCaseText.length &&
|
|
158
|
-
(/\d/.test(lowerCaseText[j]) || lowerCaseText.substring(j).match(/^(lxx|mt)\b/i))
|
|
159
|
-
) {
|
|
160
|
-
isFollowedByReference = true
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (!this.config.booksOnly && !hasOpeningParen && !isFollowedByReference) {
|
|
164
|
-
i++
|
|
165
|
-
continue
|
|
166
|
-
}
|
|
167
|
-
|
|
168
177
|
i += matchedLength
|
|
169
178
|
let chapterVerse = ""
|
|
170
|
-
let
|
|
171
|
-
|
|
172
|
-
// Capture space after book
|
|
173
|
-
if (i < normalizedText.length && normalizedText[i] === " ") {
|
|
174
|
-
chapterVerse += " "
|
|
175
|
-
i++
|
|
176
|
-
}
|
|
179
|
+
let originalChapterVerseText = ""
|
|
180
|
+
const references = []
|
|
177
181
|
|
|
178
|
-
// Capture chapter-verse
|
|
179
|
-
while (i <
|
|
180
|
-
if (
|
|
182
|
+
// Capture chapter-verse until a letter (potential new book) or semicolon
|
|
183
|
+
while (i < normalizedText.length && isValidChapterVerseChar(normalizedText[i])) {
|
|
184
|
+
if (isNextBibleBook(i)) {
|
|
185
|
+
console.log(`[Scan] Detected next book at index ${i}, breaking`)
|
|
186
|
+
break
|
|
187
|
+
}
|
|
188
|
+
if (normalizedText[i] === ";") {
|
|
189
|
+
const formattedReference = chapterVerse.trim().replace(/[^a-zA-Z0-9:,\-]+$/g, "")
|
|
190
|
+
if (formattedReference) {
|
|
191
|
+
// Find the last digit in the reference
|
|
192
|
+
const lastDigitMatch = formattedReference.match(/\d(?=[^0-9]*$)/)
|
|
193
|
+
let endIndex = i - 1 // Default to position before semicolon
|
|
194
|
+
if (lastDigitMatch) {
|
|
195
|
+
const lastDigitIndex = formattedReference.lastIndexOf(lastDigitMatch[0])
|
|
196
|
+
endIndex = startIndex + matchedLength + lastDigitIndex
|
|
197
|
+
}
|
|
198
|
+
references.push({
|
|
199
|
+
reference: formattedReference,
|
|
200
|
+
originalText: (originalBookText + originalChapterVerseText).trim(),
|
|
201
|
+
startIndex,
|
|
202
|
+
endIndex,
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
chapterVerse = ""
|
|
206
|
+
originalChapterVerseText = ""
|
|
207
|
+
originalBookText = foundBook // Reuse book for semicolon-separated references
|
|
208
|
+
startIndex = i + 1 // Start of next reference
|
|
209
|
+
i++
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
181
212
|
chapterVerse += normalizedText[i]
|
|
213
|
+
originalChapterVerseText += text[i]
|
|
182
214
|
i++
|
|
183
215
|
}
|
|
184
216
|
|
|
185
|
-
//
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
217
|
+
// Add any remaining reference
|
|
218
|
+
if (chapterVerse.trim().length > 0) {
|
|
219
|
+
const formattedReference = chapterVerse.trim().replace(/[^a-zA-Z0-9:,\-]+$/g, "")
|
|
220
|
+
if (formattedReference) {
|
|
221
|
+
// Find the last digit in the reference
|
|
222
|
+
const lastDigitMatch = formattedReference.match(/\d(?=[^0-9]*$)/)
|
|
223
|
+
let endIndex = i - 1 // Default to last character
|
|
224
|
+
if (lastDigitMatch) {
|
|
225
|
+
const lastDigitIndex = formattedReference.lastIndexOf(lastDigitMatch[0])
|
|
226
|
+
endIndex = startIndex + matchedLength + lastDigitIndex
|
|
227
|
+
}
|
|
228
|
+
references.push({
|
|
229
|
+
reference: formattedReference,
|
|
230
|
+
originalText: (originalBookText + originalChapterVerseText).trim(),
|
|
231
|
+
startIndex,
|
|
232
|
+
endIndex,
|
|
233
|
+
})
|
|
199
234
|
}
|
|
235
|
+
}
|
|
200
236
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
237
|
+
// Process each reference
|
|
238
|
+
references.forEach((refObj) => {
|
|
239
|
+
// Detect version suffix (LXX or MT)
|
|
240
|
+
let version = null
|
|
241
|
+
let originalText = refObj.originalText
|
|
242
|
+
const suffix = detectSuffix(i, text)
|
|
243
|
+
if (suffix) {
|
|
244
|
+
version = suffix.version
|
|
245
|
+
originalText += ` ${version}`
|
|
246
|
+
i += suffix.length
|
|
247
|
+
// Update endIndex if version suffix follows a digit
|
|
248
|
+
if (refObj.endIndex === i - suffix.length - 1) {
|
|
249
|
+
refObj.endIndex = i - 1
|
|
250
|
+
}
|
|
205
251
|
}
|
|
206
252
|
|
|
207
|
-
// Use original text for reference only (exclude parentheses)
|
|
208
|
-
const originalText = normalizedText.slice(startIndex, hasOpeningParen ? endIndex - 1 : endIndex)
|
|
209
|
-
|
|
210
|
-
// Determine type
|
|
211
253
|
let type
|
|
212
|
-
|
|
254
|
+
let ref = refObj.reference.replace(/^\.\s*/, "") // Remove leading period and space
|
|
213
255
|
if (this.config.booksOnly && !ref) {
|
|
214
256
|
type = "book_only"
|
|
215
257
|
} else if (ref.includes(":")) {
|
|
216
258
|
if (ref.includes("-")) {
|
|
217
|
-
const [start, end] = ref.split("-")
|
|
218
|
-
const startParts = start.split(":")
|
|
219
|
-
const endParts = end.split(":")
|
|
259
|
+
const [start, end] = ref.split("-").map((s) => s.trim())
|
|
260
|
+
const startParts = start.split(":").map((s) => s.trim())
|
|
261
|
+
const endParts = end.split(":").map((s) => s.trim())
|
|
220
262
|
type =
|
|
221
|
-
startParts.length > 1 &&
|
|
222
|
-
endParts.length > 1 &&
|
|
223
|
-
startParts[0].trim() !== endParts[0].trim()
|
|
263
|
+
startParts.length > 1 && endParts.length > 1 && startParts[0] !== endParts[0]
|
|
224
264
|
? "multi_chapter_verse_range"
|
|
225
265
|
: "chapter_verse_range"
|
|
226
266
|
} else if (ref.includes(",")) {
|
|
@@ -236,23 +276,29 @@ class CodexParser {
|
|
|
236
276
|
type = "book_only"
|
|
237
277
|
}
|
|
238
278
|
|
|
239
|
-
|
|
279
|
+
const referenceObj = {
|
|
240
280
|
book: foundBook,
|
|
241
281
|
reference: ref,
|
|
242
|
-
startIndex: hasOpeningParen ? parenStartIndex : startIndex,
|
|
243
|
-
endIndex,
|
|
244
282
|
version,
|
|
245
283
|
type,
|
|
246
284
|
originalText,
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
285
|
+
startIndex: refObj.startIndex,
|
|
286
|
+
endIndex: refObj.endIndex,
|
|
287
|
+
}
|
|
288
|
+
this.found.push(referenceObj)
|
|
289
|
+
console.log(`[Scan] Stored reference: ${JSON.stringify(referenceObj)}`)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
// Skip any trailing spaces after the reference
|
|
293
|
+
while (i < lowerCaseText.length && /\s/.test(lowerCaseText[i])) {
|
|
294
|
+
i++
|
|
250
295
|
}
|
|
251
296
|
} else {
|
|
252
297
|
i++
|
|
253
298
|
}
|
|
254
299
|
}
|
|
255
300
|
|
|
301
|
+
console.log("[Scan] Final found references:", JSON.stringify(this.found, null, 2))
|
|
256
302
|
return this
|
|
257
303
|
}
|
|
258
304
|
|
|
@@ -300,14 +346,16 @@ class CodexParser {
|
|
|
300
346
|
abbr: null,
|
|
301
347
|
}
|
|
302
348
|
|
|
303
|
-
// Clean reference for parsing
|
|
304
|
-
let cleanReference = passage.reference
|
|
305
|
-
if (
|
|
306
|
-
cleanReference = cleanReference.
|
|
349
|
+
// Clean reference for parsing
|
|
350
|
+
let cleanReference = passage.reference.replace(/\s*(LXX|MT)$/i, "").trim()
|
|
351
|
+
if (cleanReference.endsWith(",")) {
|
|
352
|
+
cleanReference = cleanReference.slice(0, -1).trim()
|
|
307
353
|
}
|
|
308
354
|
|
|
309
|
-
// Handle
|
|
310
|
-
if (!cleanReference
|
|
355
|
+
// Handle book-only or empty references
|
|
356
|
+
if (!cleanReference && this.config.booksOnly) {
|
|
357
|
+
parsedPassage.type = "book_only"
|
|
358
|
+
} else if (!cleanReference || cleanReference.match(/^\d+\s*[:;]?\s*$/)) {
|
|
311
359
|
const chapterMatch = cleanReference.match(/\d+/) || ["1"]
|
|
312
360
|
const chapter = Number(chapterMatch[0])
|
|
313
361
|
parsedPassage.chapter = chapter
|
|
@@ -319,7 +367,7 @@ class CodexParser {
|
|
|
319
367
|
parsedPassage.verses = [`${startVerse}-${endVerse}`]
|
|
320
368
|
}
|
|
321
369
|
} else {
|
|
322
|
-
this.parseReferenceParts(parsedPassage, cleanReference
|
|
370
|
+
this.parseReferenceParts(parsedPassage, cleanReference)
|
|
323
371
|
}
|
|
324
372
|
|
|
325
373
|
parsedPassage.passages = this.populate(parsedPassage)
|
|
@@ -384,20 +432,40 @@ class CodexParser {
|
|
|
384
432
|
/**
|
|
385
433
|
* Parses reference parts into chapter and verse components.
|
|
386
434
|
* @param {Object} passage - The passage object to populate.
|
|
387
|
-
* @param {string
|
|
435
|
+
* @param {string} reference - The reference string.
|
|
388
436
|
* @private
|
|
389
437
|
*/
|
|
390
|
-
parseReferenceParts(passage,
|
|
438
|
+
parseReferenceParts(passage, reference) {
|
|
391
439
|
const singleChapterBook = this.singleChapterBook.find((b) => Object.keys(b)[0] === passage.book)
|
|
440
|
+
const parts = reference
|
|
441
|
+
.split(",")
|
|
442
|
+
.map((part) => part.trim())
|
|
443
|
+
.filter((part) => part)
|
|
392
444
|
|
|
393
445
|
parts.forEach((part, index) => {
|
|
394
|
-
part = part.trim()
|
|
395
|
-
if (!part) return // Skip empty parts from trailing commas
|
|
396
446
|
const isFirstPart = index === 0
|
|
397
447
|
|
|
398
|
-
// Handle chapter
|
|
448
|
+
// Handle multi-chapter ranges (e.g., "2:1-3:19")
|
|
449
|
+
if (part.includes("-") && part.includes(":")) {
|
|
450
|
+
const [start, end] = part.split("-").map((s) => s.trim())
|
|
451
|
+
const startParts = start.split(/[:.]/).map((s) => s.trim())
|
|
452
|
+
const endParts = end.split(/[:.]/).map((s) => s.trim())
|
|
453
|
+
if (startParts.length > 1 && endParts.length > 1 && startParts[0] !== endParts[0]) {
|
|
454
|
+
passage.type = this.MULTI_CHAPTER_RANGE
|
|
455
|
+
passage.chapter = Number(startParts[0])
|
|
456
|
+
passage.verses = [startParts[1] || "1"]
|
|
457
|
+
passage.to = {
|
|
458
|
+
book: passage.book,
|
|
459
|
+
chapter: Number(endParts[0]),
|
|
460
|
+
verses: [endParts[1] || "1"],
|
|
461
|
+
}
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Handle chapter-only references (e.g., "3")
|
|
399
467
|
if (!part.includes(":") && !part.includes("-") && !singleChapterBook) {
|
|
400
|
-
const chapter = Number(part.replace(/[^0-9]/g, ""))
|
|
468
|
+
const chapter = Number(part.replace(/[^0-9]/g, ""))
|
|
401
469
|
if (chapter > 0) {
|
|
402
470
|
passage.chapter = chapter
|
|
403
471
|
passage.type = this.SINGLE_CHAPTER
|
|
@@ -422,6 +490,7 @@ class CodexParser {
|
|
|
422
490
|
}
|
|
423
491
|
})
|
|
424
492
|
}
|
|
493
|
+
|
|
425
494
|
/**
|
|
426
495
|
* Parses chapter-verse references (e.g., "3:16").
|
|
427
496
|
* @param {Object} passage - The passage object.
|
|
@@ -430,10 +499,18 @@ class CodexParser {
|
|
|
430
499
|
* @private
|
|
431
500
|
*/
|
|
432
501
|
parseChapterVerse(passage, part, isFirstPart) {
|
|
433
|
-
const [chapter, verse] = part.split(
|
|
502
|
+
const [chapter, verse] = part.split(/[:.]/).map((s) => s.trim())
|
|
434
503
|
if (isFirstPart) passage.chapter = Number(chapter)
|
|
435
|
-
passage.type = verse.includes("-")
|
|
436
|
-
|
|
504
|
+
passage.type = verse.includes("-")
|
|
505
|
+
? this.CHAPTER_VERSE_RANGE
|
|
506
|
+
: verse.includes(",")
|
|
507
|
+
? this.COMMA_SEPARATED
|
|
508
|
+
: this.CHAPTER_VERSE
|
|
509
|
+
if (verse.includes(",")) {
|
|
510
|
+
passage.verses.push(...verse.split(",").map((v) => v.trim()))
|
|
511
|
+
} else {
|
|
512
|
+
passage.verses.push(verse)
|
|
513
|
+
}
|
|
437
514
|
}
|
|
438
515
|
|
|
439
516
|
/**
|
|
@@ -453,9 +530,13 @@ class CodexParser {
|
|
|
453
530
|
passage.chapter = 1
|
|
454
531
|
passage.verses.push(part)
|
|
455
532
|
passage.type = this.CHAPTER_VERSE_RANGE
|
|
533
|
+
} else if (part.includes(",")) {
|
|
534
|
+
passage.chapter = 1
|
|
535
|
+
passage.verses.push(...part.split(",").map((v) => v.trim()))
|
|
536
|
+
passage.type = this.COMMA_SEPARATED
|
|
456
537
|
} else {
|
|
457
538
|
const num = Number(part)
|
|
458
|
-
if (num >
|
|
539
|
+
if (num > 0) {
|
|
459
540
|
passage.chapter = 1
|
|
460
541
|
passage.verses.push(num)
|
|
461
542
|
passage.type = this.CHAPTER_VERSE
|
|
@@ -521,13 +602,13 @@ class CodexParser {
|
|
|
521
602
|
*/
|
|
522
603
|
handleMultiChapterRange(passage, reference) {
|
|
523
604
|
const parts = reference.split(",")
|
|
524
|
-
const lastPart = parts[parts.length - 1]
|
|
525
|
-
const [endChapter, endVerse] = lastPart.split(
|
|
605
|
+
const lastPart = parts[parts.length - 1].trim()
|
|
606
|
+
const [endChapter, endVerse] = lastPart.split(/[:.]/).map((s) => s.trim())
|
|
526
607
|
if (endChapter !== String(passage.chapter)) {
|
|
527
608
|
passage.to = {
|
|
528
609
|
book: passage.book,
|
|
529
610
|
chapter: Number(endChapter),
|
|
530
|
-
verses: endVerse
|
|
611
|
+
verses: endVerse ? [endVerse] : ["1"],
|
|
531
612
|
}
|
|
532
613
|
}
|
|
533
614
|
}
|
|
@@ -576,17 +657,16 @@ class CodexParser {
|
|
|
576
657
|
}
|
|
577
658
|
}
|
|
578
659
|
}
|
|
579
|
-
// Ensure unique verses and update the singleChapterBook entry
|
|
580
660
|
singleChapterBook[book][chapter] = Array.from(new Set(singleChapterBook[book][chapter]))
|
|
581
|
-
return singleChapterBook[book]
|
|
661
|
+
return singleChapterBook[book]
|
|
582
662
|
}
|
|
583
663
|
|
|
584
664
|
// Handle all other books using chapterVerses
|
|
585
665
|
if (!this.chapterVerses[book][chapter]) {
|
|
586
|
-
return
|
|
666
|
+
return
|
|
587
667
|
}
|
|
588
668
|
if (!this.versificationDifferences[book]) {
|
|
589
|
-
return
|
|
669
|
+
return
|
|
590
670
|
}
|
|
591
671
|
for (const [key, value] of Object.entries(this.versificationDifferences[book])) {
|
|
592
672
|
if (value[version].startsWith(`${chapter}:`)) {
|
|
@@ -596,8 +676,8 @@ class CodexParser {
|
|
|
596
676
|
}
|
|
597
677
|
}
|
|
598
678
|
}
|
|
599
|
-
this.chapterVerses[book][chapter] = Array.from(this.chapterVerses[book][chapter])
|
|
600
|
-
return this.chapterVerses
|
|
679
|
+
this.chapterVerses[book][chapter] = Array.from(new Set(this.chapterVerses[book][chapter]))
|
|
680
|
+
return this.chapterVerses
|
|
601
681
|
}
|
|
602
682
|
|
|
603
683
|
/**
|
|
@@ -669,24 +749,31 @@ class CodexParser {
|
|
|
669
749
|
|
|
670
750
|
if (type === this.CHAPTER_RANGE) {
|
|
671
751
|
const passages = []
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
752
|
+
if (to && to.chapter) {
|
|
753
|
+
for (let ch = chapter; ch <= to.chapter; ch++) {
|
|
754
|
+
const chapterVerses = this.getChapterVerses(book, ch)
|
|
755
|
+
passages.push(
|
|
756
|
+
...this.expandVerses(book, ch, [
|
|
757
|
+
`${chapterVerses[0]}-${chapterVerses[chapterVerses.length - 1]}`,
|
|
758
|
+
])
|
|
759
|
+
)
|
|
760
|
+
}
|
|
677
761
|
}
|
|
678
762
|
return passages
|
|
679
763
|
}
|
|
680
764
|
|
|
681
765
|
if (type === this.MULTI_CHAPTER_RANGE) {
|
|
682
766
|
const passages = []
|
|
683
|
-
const startVerse = verses[0]
|
|
684
|
-
const endVerse = to
|
|
767
|
+
const startVerse = verses[0]?.includes("-") ? Number(verses[0].split("-")[0]) : Number(verses[0]) || 1
|
|
768
|
+
const endVerse = to?.verses?.[0]?.includes("-")
|
|
769
|
+
? Number(to.verses[0].split("-")[1])
|
|
770
|
+
: Number(to?.verses?.[0]) || 1
|
|
771
|
+
const endChapter = to?.chapter || chapter
|
|
685
772
|
|
|
686
|
-
for (let ch = chapter; ch <=
|
|
773
|
+
for (let ch = chapter; ch <= endChapter; ch++) {
|
|
687
774
|
const chapterVerses = this.getChapterVerses(book, ch)
|
|
688
775
|
const from = ch === chapter ? startVerse : chapterVerses[0]
|
|
689
|
-
const toVerse = ch ===
|
|
776
|
+
const toVerse = ch === endChapter ? endVerse : chapterVerses[chapterVerses.length - 1]
|
|
690
777
|
passages.push(...this.expandVerses(book, ch, [`${from}-${toVerse}`]))
|
|
691
778
|
}
|
|
692
779
|
return passages
|
|
@@ -713,7 +800,10 @@ class CodexParser {
|
|
|
713
800
|
passages.push({ book, chapter, verse: i })
|
|
714
801
|
}
|
|
715
802
|
} else {
|
|
716
|
-
|
|
803
|
+
const verseNum = Number(verse)
|
|
804
|
+
if (!isNaN(verseNum) && verseNum > 0) {
|
|
805
|
+
passages.push({ book, chapter, verse: verseNum })
|
|
806
|
+
}
|
|
717
807
|
}
|
|
718
808
|
})
|
|
719
809
|
return passages
|
|
@@ -729,15 +819,13 @@ class CodexParser {
|
|
|
729
819
|
book = book[0]
|
|
730
820
|
}
|
|
731
821
|
book = book.toLowerCase()
|
|
732
|
-
// Check if book is an abbreviation
|
|
733
822
|
let bookified = this.abbreviations[Object.keys(this.abbreviations).find((abbr) => abbr.toLowerCase() === book)]
|
|
734
823
|
if (bookified) {
|
|
735
824
|
return bookified
|
|
736
825
|
}
|
|
737
|
-
// Check if book is a full name
|
|
738
826
|
bookified =
|
|
739
827
|
this.bible.new.find((b) => b.toLowerCase() === book) || this.bible.old.find((b) => b.toLowerCase() === book)
|
|
740
|
-
return bookified || book
|
|
828
|
+
return bookified || book
|
|
741
829
|
}
|
|
742
830
|
|
|
743
831
|
/**
|
|
@@ -829,7 +917,7 @@ class CodexParser {
|
|
|
829
917
|
if (verses.length === 1) {
|
|
830
918
|
return `${chapter}:${verses[0]}`
|
|
831
919
|
}
|
|
832
|
-
const isRange = verses.every((v, i, arr) => i === 0 || v === arr[i - 1] + 1)
|
|
920
|
+
const isRange = verses.every((v, i, arr) => i === 0 || Number(v) === Number(arr[i - 1]) + 1)
|
|
833
921
|
if (isRange) {
|
|
834
922
|
return `${chapter}:${verses[0]}-${verses[verses.length - 1]}`
|
|
835
923
|
}
|
|
@@ -842,9 +930,7 @@ class CodexParser {
|
|
|
842
930
|
passage.to.chapter,
|
|
843
931
|
passage.to.verses
|
|
844
932
|
)}`
|
|
845
|
-
} else if (passage.type === "chapter_verse_range") {
|
|
846
|
-
combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
|
|
847
|
-
} else if (passage.type === "comma_separated_verses") {
|
|
933
|
+
} else if (passage.type === "chapter_verse_range" || passage.type === "comma_separated_verses") {
|
|
848
934
|
combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
|
|
849
935
|
} else if (passage.type === "chapter_range" && passage.to) {
|
|
850
936
|
combined += ` ${passage.chapter}-${passage.to.chapter}`
|
|
@@ -870,7 +956,7 @@ class CodexParser {
|
|
|
870
956
|
|
|
871
957
|
/**
|
|
872
958
|
* Combines multiple passages into a single reference.
|
|
873
|
-
* @param {Object[]} [passages=this.passages] - Array of passages to combine
|
|
959
|
+
* @param {Object[]} [passages=this.passages] - Array of passages to combine.
|
|
874
960
|
* @returns {Object} Combined passage object.
|
|
875
961
|
*/
|
|
876
962
|
combine(passages = this.passages) {
|
|
@@ -933,7 +1019,7 @@ class CodexParser {
|
|
|
933
1019
|
sortedChapters.forEach((chapter) => {
|
|
934
1020
|
const verses = Array.from(chapterVerses[chapter])
|
|
935
1021
|
.map(Number)
|
|
936
|
-
.filter((verse) => verse > 0)
|
|
1022
|
+
.filter((verse) => verse > 0)
|
|
937
1023
|
.sort((a, b) => a - b)
|
|
938
1024
|
if (verses.length > 0) {
|
|
939
1025
|
const mergedVerses = this.mergeRanges(verses)
|
|
@@ -985,7 +1071,6 @@ class CodexParser {
|
|
|
985
1071
|
: Math.max(...Array.from(chapterVerses[lastChapter]).filter((verse) => verse > 0)),
|
|
986
1072
|
}
|
|
987
1073
|
|
|
988
|
-
// Reattach the reference method to the combined passage
|
|
989
1074
|
combined.reference = function () {
|
|
990
1075
|
return this.scripture.passage
|
|
991
1076
|
}
|
|
@@ -1124,7 +1209,7 @@ class CodexParser {
|
|
|
1124
1209
|
: [Number(verseRange)]
|
|
1125
1210
|
|
|
1126
1211
|
for (const v of verseNumbers) {
|
|
1127
|
-
if (!chapterVerses.includes(v)) {
|
|
1212
|
+
if (isNaN(v) || v <= 0 || !chapterVerses.includes(v)) {
|
|
1128
1213
|
return this.validationError(104, `Verse number ${v} does not exist in ${book} ${chapter}`)
|
|
1129
1214
|
}
|
|
1130
1215
|
}
|
|
@@ -1184,21 +1269,18 @@ class CodexParser {
|
|
|
1184
1269
|
const { originalText, abbr, original } = passage
|
|
1185
1270
|
const newReference = useAbbreviations ? abbr : original
|
|
1186
1271
|
|
|
1187
|
-
const regex = new RegExp(`${originalText}`, "g")
|
|
1272
|
+
const regex = new RegExp(`${originalText.replace(/([.*+?^${}()|[\]\\])/g, "\\$1")}`, "g")
|
|
1188
1273
|
|
|
1189
|
-
// Find all matches
|
|
1190
1274
|
const matches = [...result.matchAll(regex)]
|
|
1191
1275
|
if (matches.length > 0) {
|
|
1192
|
-
// Process matches in reverse to avoid index shifting
|
|
1193
1276
|
for (let j = matches.length - 1; j >= 0; j--) {
|
|
1194
1277
|
const match = matches[j]
|
|
1195
1278
|
const startIndex = match.index
|
|
1196
1279
|
const endIndex = startIndex + match[0].length
|
|
1197
|
-
const leadingSpace = match[1] || ""
|
|
1280
|
+
const leadingSpace = match[1] || ""
|
|
1198
1281
|
const hasOpeningParen = match[2] === "("
|
|
1199
1282
|
const hasClosingParen = match[3] === ")"
|
|
1200
|
-
const trailingSpace = match[4] || " "
|
|
1201
|
-
// Preserve parentheses if present
|
|
1283
|
+
const trailingSpace = match[4] || " "
|
|
1202
1284
|
const replacement =
|
|
1203
1285
|
hasOpeningParen && hasClosingParen
|
|
1204
1286
|
? `${leadingSpace}(${newReference})${trailingSpace}`
|
|
@@ -1210,6 +1292,7 @@ class CodexParser {
|
|
|
1210
1292
|
|
|
1211
1293
|
return result
|
|
1212
1294
|
}
|
|
1295
|
+
|
|
1213
1296
|
/**
|
|
1214
1297
|
* Checks if all references in the passages array are from the same book.
|
|
1215
1298
|
* @returns {boolean} True if all passages are from the same book, false otherwise.
|