versification
Version:
A library for parsing Paratext's vrs files.
373 lines (295 loc) • 11 kB
text/typescript
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}