UNPKG

versification

Version:

A library for parsing Paratext's vrs files.

373 lines (295 loc) 11 kB
import {bookIdToNumber} from './Canon' import VerseRef from './VerseRef'; enum LineType { Comment, ChapterVerse, StandardMapping, OneToManyMapping, ExcludedVerse, VerseSegments, } export default class Versification { public name: string; private _books: number[][]; private _verseMappings: [VerseRef, VerseRef][]; private _excludedVerses: Set<number>; private _verseSegments: Record<number, string[]>; public constructor(vrsData: string) { this.name = ''; this._books = []; this._verseMappings = []; this._excludedVerses = new Set(); this._verseSegments = {}; this.parseData(vrsData); } public static nameToFileName(versificationName: string) { if (!versificationName) return null; if (versificationName.includes('-')) [versificationName] = versificationName.split('-'); const lowerCaseVersificationNameWithoutSpaces = versificationName.toLowerCase()?.replace(/\s/g, ''); switch (lowerCaseVersificationNameWithoutSpaces) { case 'english': return 'eng'; case 'original': return 'org'; case 'septuagint': return 'lxx'; case 'russianprotestant': return 'rsc'; case 'russianorthodox': return 'rso'; case 'vulgate': return 'vul'; } return null; } public static bookIdToNumber(bookId: string): number | undefined { return bookIdToNumber(bookId); } private parseData(vrsData: string) { vrsData.split('\n').forEach(line => this.praseLine(line)); } private lineType(line: string): LineType { const commentLine = line[0] === '#'; if (commentLine || line.length < 2) return LineType.Comment; if (line.includes('=')) { if (line.includes('&')) return LineType.OneToManyMapping; return LineType.StandardMapping; } if (line[0] === '-') return LineType.ExcludedVerse; if (line[0] === '*') return LineType.VerseSegments; return LineType.ChapterVerse; } private praseLine(line: string) { line = line.trimStart(); if (line.startsWith('#!')) line = line.substring(2); line = line.trimStart(); const lineType = this.lineType(line); switch (lineType) { case LineType.Comment: if (!this.name) this.parseCommentLine(line); break; case LineType.ChapterVerse: this.parseChapterVerseLine(line); break; case LineType.StandardMapping: this.parseStandardMapping(line); break; case LineType.OneToManyMapping: this.parseManyToOneMapping(line); break; case LineType.ExcludedVerse: this.parseExcludedVerse(line); break; case LineType.VerseSegments: this.parseVerseSegments(line); break; } } private parseCommentLine(line: string) { if (!line.includes('Versification')) return; const results = line.match(/Versification\s*"(.*)"/) this.name = results?.[1] || ''; } private parseChapterVerseLine(line: string) { const parts = line.split(' '); const [book, ...rest] = parts; const bookNumber = bookIdToNumber(book); if (bookNumber === undefined) return; rest.forEach((chapterVerse) => { const [chapter, numberOfVerses] = chapterVerse.split(':'); if (!this._books[bookNumber]) this._books[bookNumber] = [] this._books[bookNumber][parseInt(chapter)] = parseInt(numberOfVerses); }); } private parseVerseRange(verseRefRange: string): VerseRef[] { const [bookChapter, verse] = verseRefRange.trim().split(':'); const [start, end] = verse.split('-').map(x => parseInt(x)); if (!end) { const verseRef = VerseRef.parse(verseRefRange.trim()); if (!verseRef) return []; return [verseRef]; } const verseNumbers = end < start ? [start] : Array.apply(0, Array(end - start + 1)).map((_, i) => i + start); const ret: VerseRef[] = []; verseNumbers.forEach((verseNumber) => { const fromVerseRef = VerseRef.parse(`${bookChapter}:${verseNumber}`); if (fromVerseRef) ret.push(fromVerseRef); }) return ret; } private parseStandardMapping(line: string) { const [from, to] = line.split('='); if (!from.includes('-')) { const fromVerseRef = VerseRef.parse(from.trim()); const toVerseRef = VerseRef.parse(to.trim()); if (!toVerseRef || !fromVerseRef) return; this._verseMappings.push([fromVerseRef, toVerseRef]); } else { const fromVerseRefs = this.parseVerseRange(from); const toVerseRefs = this.parseVerseRange(to); fromVerseRefs.forEach((fromVerseRef, index) => { // If toVerseRefs has less the fromVerseRefs repeat the final verseref const toVerse = toVerseRefs[index] || toVerseRefs[toVerseRefs.length - 1]; this._verseMappings.push([fromVerseRef, toVerse]); }); } } private removeManyToOneMarker(verseRefString: string): string { return this.removeLeadingChar(verseRefString, '&'); } private removeVerseSegmentMarker(verseRefString: string): string { return this.removeLeadingChar(verseRefString, '*'); } private removeLeadingChar(str: string, leadingChar: string): string { if (str.startsWith(leadingChar)) return str.substring(1); return str; } private parseManyToOneMapping(line: string) { const [from, to] = line.split('=').map(x => x.trim()); const fromVerseRefs = this.parseVerseRange(this.removeManyToOneMarker(from)); const toVerseRefs = this.parseVerseRange(to); fromVerseRefs.forEach((fromVerseRef) => { toVerseRefs.forEach((toVerseRefs) => { this._verseMappings.push([fromVerseRef, toVerseRefs]); }); }); } private parseExcludedVerse(line: string) { const verses = this.parseVerseRange(line.substring(1)); if (!verses[0]) return; this._excludedVerses.add(verses[0].bbbcccvvv()); } private parseVerseSegments(line: string) { const [verseRefStr, ...parts] = line.trim().split(','); const [verseRef] = this.parseVerseRange(this.removeVerseSegmentMarker(verseRefStr)); if (!verseRef) return; const segments: string[] = []; parts.forEach((part) => { segments.push(part === '-' ? '' : part); }); this._verseSegments[verseRef.bbbcccvvv()] = segments; } // Returns collection of 1 based index arrays (where GEN is 1), describing number of verses per chapter. public books(): readonly (number[])[] { return this._books; } public mappings(): ([VerseRef, VerseRef][]) { return this._verseMappings; } public excludedVerses(book?: number | undefined, chapter?: number | undefined): Set<number> { if (!book) return this._excludedVerses; if (this._excludedVerses.size === 0) return this._excludedVerses; const ret = new Set<number>(); this._excludedVerses.forEach(bbbcccvvv => { if (VerseRef.toBook(bbbcccvvv) !== book) return; if (!chapter || (VerseRef.toChapter(bbbcccvvv)) === chapter) ret.add(bbbcccvvv); }); return ret; } public verseSegments(book?: number | undefined, chapter?: number | undefined): Record<number, string[]> { if (!book) return this._verseSegments; if (Object.keys(this._verseSegments).length === 0) return this._verseSegments; const ret: Record<number, string[]> = {}; Object.keys(this._verseSegments).map(Number).forEach(bbbcccvvv => { if (VerseRef.toBook(bbbcccvvv) !== book) return; if (!chapter || (VerseRef.toChapter(bbbcccvvv) === chapter)) ret[bbbcccvvv] = this._verseSegments[bbbcccvvv]; }); return ret; } // verseRef : The verseRef to find in the verse mappings. (if range will compare first verse in range) // reverse : if true perform a reverse lookup in the verse mappings. private lookupMapping(verseRef: VerseRef, reverse?: boolean): VerseRef | undefined { const searchIndex = reverse ? 1 : 0; const resultIndex = reverse ? 0 : 1; const foundMap = this._verseMappings.find(x => x[searchIndex]?.bbbcccvvv() === verseRef.bbbcccvvv()); if (!foundMap) return undefined; const result = foundMap[resultIndex]; if (!result) return undefined; const diff = result.verseNum() - verseRef.verseNum(); const verseNumEnd = verseRef.verseNumEnd(); return new VerseRef(result.bbbcccvvv(), verseNumEnd !== undefined ? verseNumEnd + diff : undefined, verseRef.segment(), this); } public changeVersification(verseRef: VerseRef): VerseRef { if (!verseRef) return verseRef; const sourceVersification = verseRef.versification(); if (!sourceVersification) return new VerseRef(verseRef.bbbcccvvv(), verseRef.verseNumEnd(), verseRef.segment(), this); if (sourceVersification.equals(this)) return verseRef; verseRef = sourceVersification.lookupMapping(verseRef) || verseRef; verseRef = this.lookupMapping(verseRef, true) || verseRef; return new VerseRef(verseRef.bbbcccvvv(), verseRef.verseNumEnd(), verseRef.segment(), this); } public equals(versification: Versification) { return this.name === versification.name; } } export {VerseRef}