ephrem
Version:
Ephrem is a light-weight API wrapper for API.Bible, built using NodeJS and Typescript. Ephrem validates bible references and fetches scripture text corresponding to the references.
325 lines • 10.7 kB
JavaScript
import fs from "node:fs";
import {
ABB_TO_ID_MAPPING_PATH,
BIBLES_DATA_PATH,
BOOKS_DATA_PATH,
getPassageFromApi,
NAMES_TO_BIBLES_PATH
} from "./api-bible.js";
import { BOOK_IDs } from "./book-ids.js";
import { BaseEphremError, normalizeBookName } from "./utils.js";
class BookIdNotFoundError extends BaseEphremError {
context;
constructor(bookId, bibleId) {
super("Book ID could not be found in the Bible");
this.name = "BookIdNotFoundError";
this.context = { bibleId, bookId };
}
}
class FallbackBibleNotFoundError extends BaseEphremError {
context;
constructor(input, fallbackBibleAbbreviation) {
super(
"Input does not indicate a bible abbreviation, and no fallback Bible Abbreviation was provided"
);
this.name = "FallbackBibleNotFoundError";
this.context = { fallbackBibleAbbreviation, input };
}
}
class UnknownBibleAbbreviationError extends BaseEphremError {
context;
constructor(bibleAbbreviation, mappingFile) {
super("Bible Abbreviation could not be found in the mapping file");
this.name = "UnknownBibleAbbreviationError";
this.context = { bibleAbbreviation, mappingFile };
}
}
class InvalidReferenceError extends BaseEphremError {
context;
constructor(input) {
super(
"Input reference is invalid. Please check the reference and try again."
);
this.name = "InvalidReferenceError";
this.context = { input };
}
}
class BibleNotAvailableError extends BaseEphremError {
context;
constructor(bibleId) {
super(
"Requested Bible is not available. Please check if the API.Bible key has access to this Bible."
);
this.name = "BibleNotAvailableError";
this.context = { bibleId };
}
}
class BookNotInBibleError extends BaseEphremError {
context;
constructor(input, bookId, availableBookIds, bibleName, bibleNameLocal) {
super(
"Requested Book is not available in this Bible. Please check the reference and try again."
);
this.name = "BookNotInBibleError";
this.context = {
availableBookIds,
bibleName,
bibleNameLocal,
bookId,
input
};
}
}
const getBibleIdFromAbbreviation = async (bibleAbbreviation) => {
const bibleMap = JSON.parse(
await fs.promises.readFile(ABB_TO_ID_MAPPING_PATH, "utf-8")
);
return bibleMap[bibleAbbreviation];
};
const getBibleDetails = async (bibleId) => {
const biblesData = JSON.parse(
await fs.promises.readFile(BIBLES_DATA_PATH, "utf-8")
);
return biblesData[bibleId];
};
const getBookDetails = async (bookId, bibleId) => {
const books = JSON.parse(
await fs.promises.readFile(BOOKS_DATA_PATH, "utf-8")
);
return books.find((book) => book.id === bookId && book.bibleId === bibleId);
};
const bookIsInBible = (bookId, bibleId, books) => {
return books.some((book) => book.id === bookId && book.bibleId === bibleId);
};
const getBookIdInBible = (normalizedBookName, bibleId, bookNamesToBibles) => {
if (!(normalizedBookName in bookNamesToBibles)) {
return void 0;
}
return bookNamesToBibles[normalizedBookName][bibleId];
};
const getKeyOfMaxValue = (obj) => {
return Object.keys(obj).reduce(
(a, b) => obj[a] > obj[b] ? a : b,
Object.keys(obj)[0]
);
};
const getBookIdByMajority = (normalizedBookName, bibleMap, bookNamesToBibles) => {
if (!(normalizedBookName in bookNamesToBibles)) {
return void 0;
}
const biblesWithBook = bookNamesToBibles[normalizedBookName];
const biblesToConsider = Object.values(bibleMap);
const voteTally = {};
const alreadyConsideredBibleIds = [];
for (const [bibleId, bookId] of Object.entries(biblesWithBook)) {
if (alreadyConsideredBibleIds.includes(bibleId) || !biblesToConsider.includes(bibleId)) {
continue;
}
voteTally[bookId] ||= 0;
voteTally[bookId] += 1;
alreadyConsideredBibleIds.push(bibleId);
}
return getKeyOfMaxValue(voteTally);
};
const getBookId = async (bookName, bibleId) => {
const bookNamesToBibles = JSON.parse(
await fs.promises.readFile(NAMES_TO_BIBLES_PATH, "utf-8")
);
const normalizedBookName = normalizeBookName(bookName);
let bookId = void 0;
if (bibleId !== void 0) {
bookId = getBookIdInBible(normalizedBookName, bibleId, bookNamesToBibles);
}
if (bookId === void 0) {
const bibleMap = JSON.parse(
await fs.promises.readFile(ABB_TO_ID_MAPPING_PATH, "utf-8")
);
bookId = getBookIdByMajority(
normalizedBookName,
bibleMap,
bookNamesToBibles
);
}
if (bookId && bookId in BOOK_IDs) {
return bookId;
}
return void 0;
};
const extractTranslationAndBookChapterVerse = (input) => {
const bibleAbbreviation = /\(([^)]+)\)/.exec(input)?.[1];
const bookChapterVerse = bibleAbbreviation ? input.replace(`(${bibleAbbreviation})`, "") : input;
if (bookChapterVerse.includes("-") && bookChapterVerse.includes(":")) {
const hyphenIndex = bookChapterVerse.indexOf("-");
const colonIndex = bookChapterVerse.indexOf(":");
if (hyphenIndex < colonIndex) {
throw new InvalidReferenceError(input);
}
}
return { bibleAbbreviation, bookChapterVerse };
};
const splitChapterAndVerse = (chapterVerse) => {
const chapterVerseParts = chapterVerse.split("-").map((trimPart) => trimPart.trim());
let chapterEnd, chapterStart, verseEnd, verseStart;
if (chapterVerseParts[0]?.includes(":")) {
[chapterStart, verseStart] = chapterVerseParts[0].split(":");
if (chapterVerseParts[1]) {
chapterEnd = chapterVerseParts[1].includes(":") ? chapterVerseParts[1].split(":")[0] : void 0;
verseEnd = chapterVerseParts[1].includes(":") ? chapterVerseParts[1].split(":")[1] : chapterVerseParts[1];
}
} else {
[chapterStart, chapterEnd] = chapterVerseParts;
}
return { chapterEnd, chapterStart, verseEnd, verseStart };
};
const parseReference = async (input, fallbackBibleAbbreviation) => {
const { bibleAbbreviation, bookChapterVerse } = extractTranslationAndBookChapterVerse(input);
let [bookName, chapterVerse] = bookChapterVerse.split(/\s+(?=\d)/);
if (!bookName || !chapterVerse) {
throw new InvalidReferenceError(input);
}
let targetBibleAbbreviation;
if (bibleAbbreviation !== void 0) {
targetBibleAbbreviation = bibleAbbreviation;
} else {
if (!fallbackBibleAbbreviation) {
throw new FallbackBibleNotFoundError(input, fallbackBibleAbbreviation);
}
targetBibleAbbreviation = fallbackBibleAbbreviation;
}
const bibleId = await getBibleIdFromAbbreviation(targetBibleAbbreviation);
if (bibleId === void 0) {
throw new UnknownBibleAbbreviationError(
targetBibleAbbreviation,
ABB_TO_ID_MAPPING_PATH
);
}
const bookId = await getBookId(bookName.trim(), bibleId);
if (!bookId) {
return void 0;
}
const booksData = JSON.parse(
await fs.promises.readFile(BOOKS_DATA_PATH, "utf-8")
);
const biblesData = JSON.parse(
await fs.promises.readFile(BIBLES_DATA_PATH, "utf-8")
);
if (!bookIsInBible(bookId, bibleId, booksData)) {
throw new BookNotInBibleError(
input,
bookId,
booksData.filter((book) => book.bibleId === bibleId).map((book) => book.id),
biblesData[bibleId].name,
biblesData[bibleId].nameLocal
);
}
const { chapterEnd, chapterStart, verseEnd, verseStart } = splitChapterAndVerse(chapterVerse);
return {
bibleId,
bookId,
chapterEnd,
chapterStart,
verseEnd,
verseStart
};
};
const createPassageBoundary = (bookId, chapter, verse) => {
return verse ? `${bookId}.${chapter}.${verse}` : `${bookId}.${chapter}`;
};
const getPassageId = (reference) => {
const sections = [];
const requiredSection = createPassageBoundary(
reference.bookId,
reference.chapterStart,
reference.verseStart
);
let optionalSection = void 0;
if (reference.chapterEnd !== void 0 && reference.verseEnd !== void 0) {
optionalSection = createPassageBoundary(
reference.bookId,
reference.chapterEnd,
reference.verseEnd
);
} else if (reference.chapterEnd !== void 0 && reference.chapterEnd !== reference.chapterStart) {
optionalSection = createPassageBoundary(
reference.bookId,
reference.chapterEnd
);
} else if (reference.verseEnd !== void 0 && reference.verseStart !== reference.verseEnd) {
optionalSection = createPassageBoundary(
reference.bookId,
reference.chapterStart,
reference.verseEnd
);
}
sections.push(requiredSection);
if (optionalSection) {
sections.push(optionalSection);
}
return sections.join("-").replace(/\s+/g, "");
};
const getPassageFromReference = async (reference, passageOptions) => {
const passageId = getPassageId(reference);
return await getPassageFromApi(passageId, reference.bibleId, passageOptions);
};
const getPassage = async (input, passageOptions, fallbackBibleAbbreviation) => {
const reference = await parseReference(input, fallbackBibleAbbreviation);
if (!reference) {
throw new InvalidReferenceError(input);
}
return await getPassageFromReference(reference, passageOptions);
};
const _getPassageWithDetails = async (passageAndFums, reference) => {
const bible = await getBibleDetails(reference.bibleId);
if (bible === void 0) {
throw new BibleNotAvailableError(reference.bibleId);
}
const book = await getBookDetails(reference.bookId, reference.bibleId);
if (book === void 0) {
throw new BookIdNotFoundError(reference.bookId, reference.bibleId);
}
return {
bible,
book,
fums: passageAndFums.meta,
passage: passageAndFums.data
};
};
const getPassageWithDetails = async (input, passageOptions, fallbackBibleAbbreviation) => {
const passageAndFums = await getPassage(
input,
passageOptions,
fallbackBibleAbbreviation
);
const reference = await parseReference(input, fallbackBibleAbbreviation);
if (!reference) {
throw new InvalidReferenceError(input);
}
return await _getPassageWithDetails(passageAndFums, reference);
};
const getPassageWithDetailsFromReference = async (reference, passageOptions) => {
const passageAndFums = await getPassageFromReference(
reference,
passageOptions
);
return await _getPassageWithDetails(passageAndFums, reference);
};
export {
BibleNotAvailableError,
BookIdNotFoundError,
BookNotInBibleError,
FallbackBibleNotFoundError,
InvalidReferenceError,
UnknownBibleAbbreviationError,
_getPassageWithDetails,
getBibleDetails,
getBibleIdFromAbbreviation,
getBookDetails,
getBookId,
getPassage,
getPassageFromReference,
getPassageId,
getPassageWithDetails,
getPassageWithDetailsFromReference,
parseReference
};
//# sourceMappingURL=reference.js.map