codexparser 0.1.79 → 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 +265 -169
|
@@ -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
|
}
|
|
@@ -87,159 +92,213 @@ class CodexParser {
|
|
|
87
92
|
*/
|
|
88
93
|
scan(text) {
|
|
89
94
|
const fullNames = [...this.bible.old, ...this.bible.new]
|
|
90
|
-
const abbreviations = Object.keys(this.abbreviations)
|
|
95
|
+
const abbreviations = Object.keys(this.abbreviations)
|
|
91
96
|
this.found = []
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
// Normalize text for parsing but keep original for originalText
|
|
98
|
+
let normalizedText = text
|
|
99
|
+
.replace(/[“”]/g, "") // Remove curly quotes
|
|
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())
|
|
95
104
|
const lowerCaseText = normalizedText.toLowerCase()
|
|
96
105
|
let i = 0
|
|
97
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
|
+
|
|
98
138
|
while (i < lowerCaseText.length) {
|
|
99
139
|
let foundBook = null
|
|
100
|
-
let startIndex = i
|
|
101
140
|
let matchedLength = 0
|
|
141
|
+
let originalBookText = ""
|
|
142
|
+
let startIndex = i
|
|
102
143
|
|
|
103
|
-
// Check
|
|
104
|
-
for (let
|
|
144
|
+
// Check full book names
|
|
145
|
+
for (let j = 0; j < lowercaseBibleFullNames.length; j++) {
|
|
146
|
+
const book = lowercaseBibleFullNames[j]
|
|
105
147
|
if (
|
|
106
|
-
lowerCaseText.startsWith(book
|
|
107
|
-
(i + book.length >= lowerCaseText.length ||
|
|
148
|
+
lowerCaseText.startsWith(book, i) &&
|
|
149
|
+
(i + book.length >= lowerCaseText.length || /\d/.test(lowerCaseText[i + book.length]))
|
|
108
150
|
) {
|
|
109
|
-
foundBook =
|
|
151
|
+
foundBook = fullNames[j]
|
|
110
152
|
matchedLength = book.length
|
|
111
|
-
|
|
153
|
+
originalBookText = text.slice(i, i + book.length)
|
|
154
|
+
console.log(`[Scan] Matched full book name: "${foundBook}" at index ${i}`)
|
|
112
155
|
}
|
|
113
156
|
}
|
|
157
|
+
|
|
158
|
+
// Check abbreviations
|
|
114
159
|
if (!foundBook) {
|
|
115
|
-
for (let
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
+
)
|
|
123
172
|
}
|
|
124
173
|
}
|
|
125
174
|
}
|
|
126
175
|
|
|
127
176
|
if (foundBook) {
|
|
128
|
-
|
|
129
|
-
let
|
|
130
|
-
let
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// Skip spaces
|
|
140
|
-
while (j < lowerCaseText.length && /\s/.test(lowerCaseText[j])) {
|
|
141
|
-
chapterVerse += normalizedText[j]
|
|
142
|
-
j++
|
|
177
|
+
i += matchedLength
|
|
178
|
+
let chapterVerse = ""
|
|
179
|
+
let originalChapterVerseText = ""
|
|
180
|
+
const references = []
|
|
181
|
+
|
|
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
|
|
143
187
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
+
})
|
|
152
204
|
}
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
155
211
|
}
|
|
212
|
+
chapterVerse += normalizedText[i]
|
|
213
|
+
originalChapterVerseText += text[i]
|
|
214
|
+
i++
|
|
215
|
+
}
|
|
156
216
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
})
|
|
170
234
|
}
|
|
235
|
+
}
|
|
171
236
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
+
}
|
|
179
251
|
}
|
|
180
252
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
? "multi_chapter_verse_range"
|
|
197
|
-
: "chapter_verse_range"
|
|
198
|
-
} else if (ref.includes(",")) {
|
|
199
|
-
type = "comma_separated_verses"
|
|
200
|
-
} else {
|
|
201
|
-
type = "chapter_verse"
|
|
202
|
-
}
|
|
203
|
-
} else if (ref.includes("-")) {
|
|
204
|
-
type = "chapter_range"
|
|
205
|
-
} else if (/\d/.test(ref)) {
|
|
206
|
-
type = "single_chapter"
|
|
253
|
+
let type
|
|
254
|
+
let ref = refObj.reference.replace(/^\.\s*/, "") // Remove leading period and space
|
|
255
|
+
if (this.config.booksOnly && !ref) {
|
|
256
|
+
type = "book_only"
|
|
257
|
+
} else if (ref.includes(":")) {
|
|
258
|
+
if (ref.includes("-")) {
|
|
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())
|
|
262
|
+
type =
|
|
263
|
+
startParts.length > 1 && endParts.length > 1 && startParts[0] !== endParts[0]
|
|
264
|
+
? "multi_chapter_verse_range"
|
|
265
|
+
: "chapter_verse_range"
|
|
266
|
+
} else if (ref.includes(",")) {
|
|
267
|
+
type = "comma_separated_verses"
|
|
207
268
|
} else {
|
|
208
|
-
type = "
|
|
269
|
+
type = "chapter_verse"
|
|
209
270
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
version,
|
|
217
|
-
type,
|
|
218
|
-
originalText: normalizedText.slice(currentStartIndex, endIndex),
|
|
219
|
-
})
|
|
271
|
+
} else if (ref.includes("-")) {
|
|
272
|
+
type = "chapter_range"
|
|
273
|
+
} else if (/\d/.test(ref)) {
|
|
274
|
+
type = "single_chapter"
|
|
275
|
+
} else {
|
|
276
|
+
type = "book_only"
|
|
220
277
|
}
|
|
221
278
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
continue // Process next reference
|
|
279
|
+
const referenceObj = {
|
|
280
|
+
book: foundBook,
|
|
281
|
+
reference: ref,
|
|
282
|
+
version,
|
|
283
|
+
type,
|
|
284
|
+
originalText,
|
|
285
|
+
startIndex: refObj.startIndex,
|
|
286
|
+
endIndex: refObj.endIndex,
|
|
231
287
|
}
|
|
288
|
+
this.found.push(referenceObj)
|
|
289
|
+
console.log(`[Scan] Stored reference: ${JSON.stringify(referenceObj)}`)
|
|
290
|
+
})
|
|
232
291
|
|
|
233
|
-
|
|
234
|
-
|
|
292
|
+
// Skip any trailing spaces after the reference
|
|
293
|
+
while (i < lowerCaseText.length && /\s/.test(lowerCaseText[i])) {
|
|
294
|
+
i++
|
|
235
295
|
}
|
|
236
|
-
|
|
237
|
-
i = j
|
|
238
296
|
} else {
|
|
239
297
|
i++
|
|
240
298
|
}
|
|
241
299
|
}
|
|
242
300
|
|
|
301
|
+
console.log("[Scan] Final found references:", JSON.stringify(this.found, null, 2))
|
|
243
302
|
return this
|
|
244
303
|
}
|
|
245
304
|
|
|
@@ -287,14 +346,16 @@ class CodexParser {
|
|
|
287
346
|
abbr: null,
|
|
288
347
|
}
|
|
289
348
|
|
|
290
|
-
// Clean reference for parsing
|
|
291
|
-
let cleanReference = passage.reference
|
|
292
|
-
if (
|
|
293
|
-
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()
|
|
294
353
|
}
|
|
295
354
|
|
|
296
|
-
// Handle
|
|
297
|
-
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*$/)) {
|
|
298
359
|
const chapterMatch = cleanReference.match(/\d+/) || ["1"]
|
|
299
360
|
const chapter = Number(chapterMatch[0])
|
|
300
361
|
parsedPassage.chapter = chapter
|
|
@@ -306,7 +367,7 @@ class CodexParser {
|
|
|
306
367
|
parsedPassage.verses = [`${startVerse}-${endVerse}`]
|
|
307
368
|
}
|
|
308
369
|
} else {
|
|
309
|
-
this.parseReferenceParts(parsedPassage, cleanReference
|
|
370
|
+
this.parseReferenceParts(parsedPassage, cleanReference)
|
|
310
371
|
}
|
|
311
372
|
|
|
312
373
|
parsedPassage.passages = this.populate(parsedPassage)
|
|
@@ -371,20 +432,40 @@ class CodexParser {
|
|
|
371
432
|
/**
|
|
372
433
|
* Parses reference parts into chapter and verse components.
|
|
373
434
|
* @param {Object} passage - The passage object to populate.
|
|
374
|
-
* @param {string
|
|
435
|
+
* @param {string} reference - The reference string.
|
|
375
436
|
* @private
|
|
376
437
|
*/
|
|
377
|
-
parseReferenceParts(passage,
|
|
438
|
+
parseReferenceParts(passage, reference) {
|
|
378
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)
|
|
379
444
|
|
|
380
445
|
parts.forEach((part, index) => {
|
|
381
|
-
part = part.trim()
|
|
382
|
-
if (!part) return // Skip empty parts from trailing commas
|
|
383
446
|
const isFirstPart = index === 0
|
|
384
447
|
|
|
385
|
-
// 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")
|
|
386
467
|
if (!part.includes(":") && !part.includes("-") && !singleChapterBook) {
|
|
387
|
-
const chapter = Number(part.replace(/[^0-9]/g, ""))
|
|
468
|
+
const chapter = Number(part.replace(/[^0-9]/g, ""))
|
|
388
469
|
if (chapter > 0) {
|
|
389
470
|
passage.chapter = chapter
|
|
390
471
|
passage.type = this.SINGLE_CHAPTER
|
|
@@ -409,6 +490,7 @@ class CodexParser {
|
|
|
409
490
|
}
|
|
410
491
|
})
|
|
411
492
|
}
|
|
493
|
+
|
|
412
494
|
/**
|
|
413
495
|
* Parses chapter-verse references (e.g., "3:16").
|
|
414
496
|
* @param {Object} passage - The passage object.
|
|
@@ -417,10 +499,18 @@ class CodexParser {
|
|
|
417
499
|
* @private
|
|
418
500
|
*/
|
|
419
501
|
parseChapterVerse(passage, part, isFirstPart) {
|
|
420
|
-
const [chapter, verse] = part.split(
|
|
502
|
+
const [chapter, verse] = part.split(/[:.]/).map((s) => s.trim())
|
|
421
503
|
if (isFirstPart) passage.chapter = Number(chapter)
|
|
422
|
-
passage.type = verse.includes("-")
|
|
423
|
-
|
|
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
|
+
}
|
|
424
514
|
}
|
|
425
515
|
|
|
426
516
|
/**
|
|
@@ -440,9 +530,13 @@ class CodexParser {
|
|
|
440
530
|
passage.chapter = 1
|
|
441
531
|
passage.verses.push(part)
|
|
442
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
|
|
443
537
|
} else {
|
|
444
538
|
const num = Number(part)
|
|
445
|
-
if (num >
|
|
539
|
+
if (num > 0) {
|
|
446
540
|
passage.chapter = 1
|
|
447
541
|
passage.verses.push(num)
|
|
448
542
|
passage.type = this.CHAPTER_VERSE
|
|
@@ -508,13 +602,13 @@ class CodexParser {
|
|
|
508
602
|
*/
|
|
509
603
|
handleMultiChapterRange(passage, reference) {
|
|
510
604
|
const parts = reference.split(",")
|
|
511
|
-
const lastPart = parts[parts.length - 1]
|
|
512
|
-
const [endChapter, endVerse] = lastPart.split(
|
|
605
|
+
const lastPart = parts[parts.length - 1].trim()
|
|
606
|
+
const [endChapter, endVerse] = lastPart.split(/[:.]/).map((s) => s.trim())
|
|
513
607
|
if (endChapter !== String(passage.chapter)) {
|
|
514
608
|
passage.to = {
|
|
515
609
|
book: passage.book,
|
|
516
610
|
chapter: Number(endChapter),
|
|
517
|
-
verses: endVerse
|
|
611
|
+
verses: endVerse ? [endVerse] : ["1"],
|
|
518
612
|
}
|
|
519
613
|
}
|
|
520
614
|
}
|
|
@@ -563,17 +657,16 @@ class CodexParser {
|
|
|
563
657
|
}
|
|
564
658
|
}
|
|
565
659
|
}
|
|
566
|
-
// Ensure unique verses and update the singleChapterBook entry
|
|
567
660
|
singleChapterBook[book][chapter] = Array.from(new Set(singleChapterBook[book][chapter]))
|
|
568
|
-
return singleChapterBook[book]
|
|
661
|
+
return singleChapterBook[book]
|
|
569
662
|
}
|
|
570
663
|
|
|
571
664
|
// Handle all other books using chapterVerses
|
|
572
665
|
if (!this.chapterVerses[book][chapter]) {
|
|
573
|
-
return
|
|
666
|
+
return
|
|
574
667
|
}
|
|
575
668
|
if (!this.versificationDifferences[book]) {
|
|
576
|
-
return
|
|
669
|
+
return
|
|
577
670
|
}
|
|
578
671
|
for (const [key, value] of Object.entries(this.versificationDifferences[book])) {
|
|
579
672
|
if (value[version].startsWith(`${chapter}:`)) {
|
|
@@ -583,8 +676,8 @@ class CodexParser {
|
|
|
583
676
|
}
|
|
584
677
|
}
|
|
585
678
|
}
|
|
586
|
-
this.chapterVerses[book][chapter] = Array.from(this.chapterVerses[book][chapter])
|
|
587
|
-
return this.chapterVerses
|
|
679
|
+
this.chapterVerses[book][chapter] = Array.from(new Set(this.chapterVerses[book][chapter]))
|
|
680
|
+
return this.chapterVerses
|
|
588
681
|
}
|
|
589
682
|
|
|
590
683
|
/**
|
|
@@ -656,24 +749,31 @@ class CodexParser {
|
|
|
656
749
|
|
|
657
750
|
if (type === this.CHAPTER_RANGE) {
|
|
658
751
|
const passages = []
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
+
}
|
|
664
761
|
}
|
|
665
762
|
return passages
|
|
666
763
|
}
|
|
667
764
|
|
|
668
765
|
if (type === this.MULTI_CHAPTER_RANGE) {
|
|
669
766
|
const passages = []
|
|
670
|
-
const startVerse = verses[0]
|
|
671
|
-
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
|
|
672
772
|
|
|
673
|
-
for (let ch = chapter; ch <=
|
|
773
|
+
for (let ch = chapter; ch <= endChapter; ch++) {
|
|
674
774
|
const chapterVerses = this.getChapterVerses(book, ch)
|
|
675
775
|
const from = ch === chapter ? startVerse : chapterVerses[0]
|
|
676
|
-
const toVerse = ch ===
|
|
776
|
+
const toVerse = ch === endChapter ? endVerse : chapterVerses[chapterVerses.length - 1]
|
|
677
777
|
passages.push(...this.expandVerses(book, ch, [`${from}-${toVerse}`]))
|
|
678
778
|
}
|
|
679
779
|
return passages
|
|
@@ -700,7 +800,10 @@ class CodexParser {
|
|
|
700
800
|
passages.push({ book, chapter, verse: i })
|
|
701
801
|
}
|
|
702
802
|
} else {
|
|
703
|
-
|
|
803
|
+
const verseNum = Number(verse)
|
|
804
|
+
if (!isNaN(verseNum) && verseNum > 0) {
|
|
805
|
+
passages.push({ book, chapter, verse: verseNum })
|
|
806
|
+
}
|
|
704
807
|
}
|
|
705
808
|
})
|
|
706
809
|
return passages
|
|
@@ -716,15 +819,13 @@ class CodexParser {
|
|
|
716
819
|
book = book[0]
|
|
717
820
|
}
|
|
718
821
|
book = book.toLowerCase()
|
|
719
|
-
// Check if book is an abbreviation
|
|
720
822
|
let bookified = this.abbreviations[Object.keys(this.abbreviations).find((abbr) => abbr.toLowerCase() === book)]
|
|
721
823
|
if (bookified) {
|
|
722
824
|
return bookified
|
|
723
825
|
}
|
|
724
|
-
// Check if book is a full name
|
|
725
826
|
bookified =
|
|
726
827
|
this.bible.new.find((b) => b.toLowerCase() === book) || this.bible.old.find((b) => b.toLowerCase() === book)
|
|
727
|
-
return bookified || book
|
|
828
|
+
return bookified || book
|
|
728
829
|
}
|
|
729
830
|
|
|
730
831
|
/**
|
|
@@ -816,7 +917,7 @@ class CodexParser {
|
|
|
816
917
|
if (verses.length === 1) {
|
|
817
918
|
return `${chapter}:${verses[0]}`
|
|
818
919
|
}
|
|
819
|
-
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)
|
|
820
921
|
if (isRange) {
|
|
821
922
|
return `${chapter}:${verses[0]}-${verses[verses.length - 1]}`
|
|
822
923
|
}
|
|
@@ -829,9 +930,7 @@ class CodexParser {
|
|
|
829
930
|
passage.to.chapter,
|
|
830
931
|
passage.to.verses
|
|
831
932
|
)}`
|
|
832
|
-
} else if (passage.type === "chapter_verse_range") {
|
|
833
|
-
combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
|
|
834
|
-
} else if (passage.type === "comma_separated_verses") {
|
|
933
|
+
} else if (passage.type === "chapter_verse_range" || passage.type === "comma_separated_verses") {
|
|
835
934
|
combined += ` ${formatChapterVerse(passage.chapter, passage.verses)}`
|
|
836
935
|
} else if (passage.type === "chapter_range" && passage.to) {
|
|
837
936
|
combined += ` ${passage.chapter}-${passage.to.chapter}`
|
|
@@ -857,7 +956,7 @@ class CodexParser {
|
|
|
857
956
|
|
|
858
957
|
/**
|
|
859
958
|
* Combines multiple passages into a single reference.
|
|
860
|
-
* @param {Object[]} [passages=this.passages] - Array of passages to combine
|
|
959
|
+
* @param {Object[]} [passages=this.passages] - Array of passages to combine.
|
|
861
960
|
* @returns {Object} Combined passage object.
|
|
862
961
|
*/
|
|
863
962
|
combine(passages = this.passages) {
|
|
@@ -920,7 +1019,7 @@ class CodexParser {
|
|
|
920
1019
|
sortedChapters.forEach((chapter) => {
|
|
921
1020
|
const verses = Array.from(chapterVerses[chapter])
|
|
922
1021
|
.map(Number)
|
|
923
|
-
.filter((verse) => verse > 0)
|
|
1022
|
+
.filter((verse) => verse > 0)
|
|
924
1023
|
.sort((a, b) => a - b)
|
|
925
1024
|
if (verses.length > 0) {
|
|
926
1025
|
const mergedVerses = this.mergeRanges(verses)
|
|
@@ -972,7 +1071,6 @@ class CodexParser {
|
|
|
972
1071
|
: Math.max(...Array.from(chapterVerses[lastChapter]).filter((verse) => verse > 0)),
|
|
973
1072
|
}
|
|
974
1073
|
|
|
975
|
-
// Reattach the reference method to the combined passage
|
|
976
1074
|
combined.reference = function () {
|
|
977
1075
|
return this.scripture.passage
|
|
978
1076
|
}
|
|
@@ -1111,7 +1209,7 @@ class CodexParser {
|
|
|
1111
1209
|
: [Number(verseRange)]
|
|
1112
1210
|
|
|
1113
1211
|
for (const v of verseNumbers) {
|
|
1114
|
-
if (!chapterVerses.includes(v)) {
|
|
1212
|
+
if (isNaN(v) || v <= 0 || !chapterVerses.includes(v)) {
|
|
1115
1213
|
return this.validationError(104, `Verse number ${v} does not exist in ${book} ${chapter}`)
|
|
1116
1214
|
}
|
|
1117
1215
|
}
|
|
@@ -1171,21 +1269,18 @@ class CodexParser {
|
|
|
1171
1269
|
const { originalText, abbr, original } = passage
|
|
1172
1270
|
const newReference = useAbbreviations ? abbr : original
|
|
1173
1271
|
|
|
1174
|
-
const regex = new RegExp(`${originalText}`, "g")
|
|
1272
|
+
const regex = new RegExp(`${originalText.replace(/([.*+?^${}()|[\]\\])/g, "\\$1")}`, "g")
|
|
1175
1273
|
|
|
1176
|
-
// Find all matches
|
|
1177
1274
|
const matches = [...result.matchAll(regex)]
|
|
1178
1275
|
if (matches.length > 0) {
|
|
1179
|
-
// Process matches in reverse to avoid index shifting
|
|
1180
1276
|
for (let j = matches.length - 1; j >= 0; j--) {
|
|
1181
1277
|
const match = matches[j]
|
|
1182
1278
|
const startIndex = match.index
|
|
1183
1279
|
const endIndex = startIndex + match[0].length
|
|
1184
|
-
const leadingSpace = match[1] || ""
|
|
1280
|
+
const leadingSpace = match[1] || ""
|
|
1185
1281
|
const hasOpeningParen = match[2] === "("
|
|
1186
1282
|
const hasClosingParen = match[3] === ")"
|
|
1187
|
-
const trailingSpace = match[4] || " "
|
|
1188
|
-
// Preserve parentheses if present
|
|
1283
|
+
const trailingSpace = match[4] || " "
|
|
1189
1284
|
const replacement =
|
|
1190
1285
|
hasOpeningParen && hasClosingParen
|
|
1191
1286
|
? `${leadingSpace}(${newReference})${trailingSpace}`
|
|
@@ -1197,6 +1292,7 @@ class CodexParser {
|
|
|
1197
1292
|
|
|
1198
1293
|
return result
|
|
1199
1294
|
}
|
|
1295
|
+
|
|
1200
1296
|
/**
|
|
1201
1297
|
* Checks if all references in the passages array are from the same book.
|
|
1202
1298
|
* @returns {boolean} True if all passages are from the same book, false otherwise.
|