@gracious.tech/fetch-client
Version:
Interact with a fetch(bible) collection in an API-like way
522 lines (521 loc) • 19.9 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
import {
book_names_english,
books_ordered,
PassageReference,
detect_references,
english_abbrev_include,
english_abbrev_exclude,
book_abbrev_english
} from "@gracious.tech/bible-references";
import { BibleBookHtml, BibleBookUsx, BibleBookUsfm, BibleBookTxt } from "./book.js";
import { filter_licenses } from "./licenses.js";
import { deep_copy, fuzzy_search, request } from "./utils.js";
import { TranslationExtra } from "./translation.js";
class BibleCollection {
// @internal
constructor(usage, remember_fetches, manifests) {
// @internal
__publicField(this, "_usage");
// @internal
__publicField(this, "_remember_fetches");
// @internal
__publicField(this, "_fetch_book_cache", {});
// @internal
__publicField(this, "_fetch_extras_cache", {});
// @internal
__publicField(this, "_local_book_names", {});
// @internal
__publicField(this, "_manifest");
// @internal
__publicField(this, "_endpoints", {});
// Map translation ids to endpoints
// @internal
__publicField(this, "_modern_year", (/* @__PURE__ */ new Date()).getFullYear() - 70);
this._usage = usage;
this._remember_fetches = remember_fetches;
this._manifest = {
licenses: manifests[0][1].licenses,
// Still useful even if resolved within transl.s
languages: {},
language2to3: {},
// NOTE If first manifest is sparse then may not include all possible codes
// But this is a non-essential array just used to slightly improve sorting
languages_most_spoken: manifests[0][1].languages_most_spoken,
translations: {}
};
const languages = /* @__PURE__ */ new Set();
for (const [endpoint, manifest] of manifests.reverse()) {
for (const [trans, trans_data] of Object.entries(manifest.translations)) {
let licenses = trans_data.copyright.licenses.map((item) => {
if (typeof item.license === "string") {
return {
id: item.license,
name: manifest.licenses[item.license].name,
restrictions: manifest.licenses[item.license].restrictions,
url: item.url
};
} else {
return {
id: null,
name: "Custom license",
restrictions: item.license,
url: item.url
};
}
});
licenses = filter_licenses(licenses, this._usage);
if (!licenses.length) {
continue;
}
languages.add(trans.slice(0, 3));
const ot = trans_data.books_ot === true ? books_ordered.slice(0, 39) : trans_data.books_ot;
const nt = trans_data.books_nt === true ? books_ordered.slice(39) : trans_data.books_nt;
this._manifest.translations[trans] = {
...trans_data,
books_ot_list: ot,
books_nt_list: nt,
copyright: {
...trans_data.copyright,
licenses
}
};
this._endpoints[trans] = endpoint;
}
for (const lang in manifest.languages) {
if (languages.has(lang)) {
this._manifest.languages[lang] = manifest.languages[lang];
}
}
for (const [lang2, lang3] of Object.entries(manifest.language2to3)) {
if (languages.has(lang3)) {
this._manifest.language2to3[lang2] = manifest.language2to3[lang2];
}
}
}
}
// @internal
_ensure_trans_exists(translation) {
if (!this.has_translation(translation)) {
throw new Error(`Translation with id "${translation}" does not exist in collection(s)`);
}
}
// @internal
_ensure_book_exists(translation, book) {
this._ensure_trans_exists(translation);
if (!books_ordered.includes(book)) {
throw new Error(`Book id "${book}" is not valid (should be 3 letters lowercase)`);
}
if (!this.has_book(translation, book)) {
throw new Error(`Translation "${translation}" does not have book "${book}"`);
}
}
// Check if a language exists (must be 3 character id)
has_language(language) {
return language in this._manifest.languages;
}
// Check if a translation exists
has_translation(translation) {
return translation in this._manifest.translations;
}
// Check if a book exists within a translation
has_book(translation, book) {
this._ensure_trans_exists(translation);
const trans_meta = this._manifest.translations[translation];
return trans_meta.books_ot_list.includes(book) || trans_meta.books_nt_list.includes(book);
}
// Get a language's metadata
get_language(code) {
const data = this._manifest.languages[code];
if (!data) {
return void 0;
}
return {
code,
name_local: data.local,
name_english: data.english,
name_bilingual: data.english === data.local ? data.local : `${data.local} (${data.english})`,
population: data.pop
};
}
get_languages({ object, exclude_old, sort_by, search } = {}) {
let list = Object.keys(this._manifest.languages).map((code) => this.get_language(code));
if (exclude_old) {
list = list.filter((item) => item.population !== null);
}
if (search !== void 0) {
list = fuzzy_search(search, list, (c) => c.name_local + " " + c.name_english);
}
if (object) {
return Object.fromEntries(list.map((item) => [item.code, item]));
}
if (!search) {
if (sort_by === "population_L1") {
list.sort((a, b) => {
var _a, _b;
return ((_a = b.population) != null ? _a : -1) - ((_b = a.population) != null ? _b : -1);
});
} else if (sort_by === "population") {
const item_to_pop = (item) => {
var _a;
const most_spoken_i = this._manifest.languages_most_spoken.indexOf(item.code);
if (most_spoken_i !== -1) {
const list_len = this._manifest.languages_most_spoken.length;
return (list_len - most_spoken_i) * 9999999999;
}
return (_a = item.population) != null ? _a : -1;
};
list.sort((a, b) => {
return item_to_pop(b) - item_to_pop(a);
});
} else {
list.sort((a, b) => {
const name_key = sort_by === "english" ? "name_english" : "name_local";
return a[name_key].localeCompare(b[name_key]);
});
}
}
return list;
}
// Get the user's preferred available language (no arg required when used in browser)
get_preferred_language(preferences = []) {
return this.get_language(this._get_preferred_language_code(preferences));
}
// @internal Get preferred language code
_get_preferred_language_code(preferences = []) {
var _a, _b, _c;
if (preferences.length === 0 && typeof self !== "undefined") {
preferences = [...(_b = self.navigator.languages) != null ? _b : [(_a = self.navigator.language) != null ? _a : "eng"]];
}
for (let code of preferences) {
code = (_c = code.toLowerCase().split("-")[0]) != null ? _c : "";
if (code in this._manifest.languages) {
return code;
}
if (code in this._manifest.language2to3) {
return this._manifest.language2to3[code];
}
}
return "eng" in this._manifest.languages ? "eng" : Object.keys(this._manifest.languages)[0];
}
// Get a translation's metadata
get_translation(id) {
return this._get_translation(id);
}
// @internal Version that takes a `usage` arg which is only useful for `get_translations()`
_get_translation(id, usage) {
const trans = this._manifest.translations[id];
if (!trans) {
return void 0;
}
let bilingual = trans.name.local || trans.name.english;
if (trans.name.local && trans.name.english && trans.name.local.toLowerCase() !== trans.name.english.toLowerCase()) {
bilingual = `${trans.name.local} (${trans.name.english})`;
}
let bilingual_abbrev = trans.name.local_abbrev || trans.name.english_abbrev;
if (trans.name.local_abbrev && trans.name.english_abbrev && trans.name.local_abbrev !== trans.name.english_abbrev) {
bilingual_abbrev = `${trans.name.local_abbrev} (${trans.name.english_abbrev})`;
}
return {
id,
language: id.slice(0, 3),
direction: trans.direction,
year: trans.year,
name: trans.name.local || trans.name.english,
name_abbrev: trans.name.local_abbrev || trans.name.english_abbrev,
name_english: trans.name.english,
name_english_abbrev: trans.name.english_abbrev,
name_local: trans.name.local,
name_local_abbrev: trans.name.local_abbrev,
name_bilingual: bilingual,
name_bilingual_abbrev: bilingual_abbrev,
attribution: trans.copyright.attribution,
attribution_url: trans.copyright.attribution_url,
licenses: deep_copy(
filter_licenses(trans.copyright.licenses, { ...this._usage, ...usage })
),
liternalness: trans.literalness,
tags: [...trans.tags]
};
}
get_translations({ language, object, sort_by_year, usage, exclude_obsolete, exclude_incomplete } = {}) {
let list = Object.keys(this._manifest.translations).map((id) => {
return this._get_translation(id, usage);
}).filter((trans) => trans.licenses.length);
if (language) {
list = list.filter((item) => item.language === language);
}
if (exclude_obsolete) {
const modern = list.filter((item) => item.year >= this._modern_year);
if (modern.length) {
list = modern;
}
for (const tag of ["archaic", "questionable", "niche"]) {
const reduced_list = list.filter((item) => !item.tags.includes(tag));
if (reduced_list.length) {
list = reduced_list;
}
}
}
if (exclude_incomplete) {
list = list.filter((item) => {
const trans_meta = this._manifest.translations[item.id];
return trans_meta.books_ot === true && trans_meta.books_nt === true;
});
}
if (object) {
return Object.fromEntries(list.map((item) => [item.id, item]));
}
list.sort((a, b) => {
return sort_by_year ? b.year - a.year : a.name_local.localeCompare(b.name_local);
});
return list;
}
// Get user's preferred available translation (provide language preferences if not in browser)
get_preferred_translation(languages = []) {
return this.get_translation(this._get_preferred_translation_id(languages));
}
// @internal Get preferred translation id
_get_preferred_translation_id(languages = []) {
const language = this._get_preferred_language_code(languages);
let candidate = null;
let candidate_full = false;
let candidate_year = -9999;
for (const [id, data] of Object.entries(this._manifest.translations)) {
if (id.slice(0, 3) === language) {
if (data.tags.includes("recommended")) {
return id;
}
const full = data.books_ot === true && data.books_nt === true;
if (!candidate || !candidate_full && full || full && data.year > candidate_year) {
candidate = id;
candidate_year = data.year;
candidate_full = full;
}
}
}
return candidate != null ? candidate : Object.keys(this._manifest.translations)[0];
}
get_books(translation, { object, sort_by_name, testament, whole } = {}) {
let available = books_ordered;
if (translation) {
this._ensure_trans_exists(translation);
const trans_meta = this._manifest.translations[translation];
available = [...trans_meta.books_ot_list, ...trans_meta.books_nt_list];
}
let local = {};
if (translation && translation in this._local_book_names) {
local = this._local_book_names[translation];
}
const slice = testament ? testament === "ot" ? [0, 39] : [39] : [];
const list = books_ordered.slice(...slice).filter((id) => whole || available.includes(id)).map((id) => {
var _a, _b, _c, _d;
const ot = books_ordered.indexOf(id) < 39;
const local_name = (_a = local[id]) == null ? void 0 : _a.normal;
let bilingual = book_names_english[id];
if (local_name && bilingual.toLowerCase() !== local_name.toLowerCase()) {
bilingual = `${local_name} (${bilingual})`;
}
const local_abbrev = (_b = local[id]) == null ? void 0 : _b.abbrev;
let bilingual_abbrev = book_abbrev_english[id];
if (local_abbrev && bilingual_abbrev.toLowerCase() !== local_abbrev.toLowerCase()) {
bilingual_abbrev = `${local_abbrev} (${bilingual_abbrev})`;
}
return {
id,
name: local_name || book_names_english[id],
name_abbrev: local_abbrev || book_abbrev_english[id],
name_english: book_names_english[id],
name_english_abbrev: book_abbrev_english[id],
name_local: local_name != null ? local_name : "",
name_local_abbrev: local_abbrev != null ? local_abbrev : "",
name_local_long: (_d = (_c = local[id]) == null ? void 0 : _c.long) != null ? _d : "",
name_bilingual: bilingual,
name_bilingual_abbrev: bilingual_abbrev,
ot,
nt: !ot,
available: !!translation && available.includes(id)
};
});
if (object) {
return Object.fromEntries(list.map((item) => [item.id, item]));
}
if (sort_by_name) {
list.sort((a, b) => a.name_english.localeCompare(b.name_english));
}
return list;
}
// Get the URL for a book's content (useful for caching and manual retrieval)
get_book_url(translation, book, format = "html") {
const ext = ["html", "txt"].includes(format) ? "json" : format;
return `${this._endpoints[translation]}bibles/${translation}/${format}/${book}.${ext}`;
}
// Get book ids that are available/missing for a translation for each testament
get_completion(translation) {
this._ensure_trans_exists(translation);
const data = {
nt: { available: [], missing: [] },
ot: { available: [], missing: [] }
};
const trans_meta = this._manifest.translations[translation];
const trans_books = [...trans_meta.books_ot_list, ...trans_meta.books_nt_list];
let testament = "ot";
for (const book of books_ordered) {
if (book === "mat") {
testament = "nt";
}
const status = trans_books.includes(book) ? "available" : "missing";
data[testament][status].push(book);
}
return data;
}
async fetch_book(translation, book, format = "html") {
this._ensure_book_exists(translation, book);
const key = `${translation} ${book} ${format}`;
if (key in this._fetch_book_cache) {
return this._fetch_book_cache[key];
}
const promise = request(this.get_book_url(translation, book, format)).then((contents) => {
const format_class = {
html: BibleBookHtml,
usx: BibleBookUsx,
usfm: BibleBookUsfm,
txt: BibleBookTxt
}[format];
return new format_class(contents, this._manifest.translations[translation].copyright);
});
if (this._remember_fetches) {
this._fetch_book_cache[key] = promise;
promise.catch(() => {
delete this._fetch_book_cache[key];
});
}
return promise;
}
// Make request for extra metadata for a translation (such as book names and section headings).
// This will also auto-provide local book names for future calls of `get_books()`.
async fetch_translation_extras(translation) {
this._ensure_trans_exists(translation);
if (translation in this._fetch_extras_cache) {
return this._fetch_extras_cache[translation];
}
const url = `${this._endpoints[translation]}bibles/${translation}/extra.json`;
const promise = request(url).then((contents) => {
const data = JSON.parse(contents);
this._local_book_names[translation] = data.book_names;
return new TranslationExtra(data);
});
if (this._remember_fetches) {
this._fetch_extras_cache[translation] = promise;
promise.catch(() => {
delete this._fetch_book_cache[translation];
});
}
return promise;
}
// @internal Auto-prepare args for from_string/detect_references based on translation
_from_string_args(translation = [], always_detect_english = true) {
var _a, _b, _c;
const book_names = [];
const translations = typeof translation === "string" ? [translation] : translation;
for (const trans of translations) {
for (const [code, name_types] of Object.entries((_a = this._local_book_names[trans]) != null ? _a : {})) {
if (name_types.normal) {
book_names.push([code, name_types.normal]);
}
if (name_types.abbrev) {
book_names.push([code, name_types.abbrev]);
}
}
}
if (always_detect_english) {
for (const [code, name] of Object.entries(book_names_english)) {
book_names.push([code, name]);
}
for (const [code, name] of english_abbrev_include) {
book_names.push([code, name]);
}
}
const chinese_like = [
"zho",
// Chinese macrolanguage group
"lzh",
"gan",
"hak",
"czh",
"cjy",
"cmn",
"mnp",
"cdo",
// Part of 'zho' group
"nan",
"czo",
"cnp",
"cpx",
"csp",
"wuu",
"hsn",
"yue",
// Part of 'zho' group
"jpn",
// Japanese
"kor"
// Korean
];
const lang = (_c = (_b = translations[0]) == null ? void 0 : _b.split("_")[0]) != null ? _c : "eng";
const exclude_book_names = lang === "eng" ? [...english_abbrev_exclude] : [];
const min_chars = chinese_like.includes(lang) ? 1 : 2;
const match_from_start = !chinese_like.includes(lang);
return [
book_names,
exclude_book_names,
min_chars,
match_from_start
];
}
// Detect bible references in a block of text using book names of given translation(s).
// A generator is returned and can be passed updated text each time it yields a result.
// You must have first awaited a call to `fetch_translation_extras()` to be able to parse
// non-English references.
detect_references(text, translation = [], always_detect_english = true) {
return detect_references(
text,
...this._from_string_args(translation, always_detect_english)
);
}
// Parse a single bible reference string into a PassageReference object (validating it).
// Supports only single passages (for e.g. Matt 10:6,8 use `detect_references`).
// You must have first awaited a call to `fetch_translation_extras()` to be able to parse
// non-English references.
string_to_reference(text, translation = [], always_detect_english = true) {
return PassageReference.from_string(
text,
...this._from_string_args(translation, always_detect_english)
);
}
// Render a PassageReference object as a string using the given translation's book names.
// You must have first awaited a call to `fetch_translation_extras()` for the translation,
// or English will be used by default.
reference_to_string(reference, translation, abbreviate) {
var _a;
const book_names = { ...abbreviate ? book_abbrev_english : book_names_english };
if (translation) {
const name_prop = abbreviate ? "abbrev" : "normal";
for (const [book, props] of Object.entries((_a = this._local_book_names[translation]) != null ? _a : {})) {
if (props[name_prop]) {
book_names[book] = props[name_prop];
}
}
}
return reference.toString(book_names);
}
}
export {
BibleCollection
};
//# sourceMappingURL=collection.js.map