mirror of
https://github.com/gslogimaker/my-bible-obsidian-plugin.git
synced 2026-07-22 09:20:28 +00:00
3395 lines
91 KiB
TypeScript
3395 lines
91 KiB
TypeScript
import {
|
|
App,
|
|
Modal,
|
|
Notice,
|
|
Plugin,
|
|
PluginSettingTab,
|
|
Setting,
|
|
FuzzySuggestModal,
|
|
TFile,
|
|
normalizePath,
|
|
requestUrl,
|
|
MarkdownRenderer,
|
|
} from 'obsidian'
|
|
|
|
import { E_CANCELED, Mutex } from 'async-mutex'
|
|
import { randomInt } from 'crypto'
|
|
import { BBCodeTag, legacy, parse_mybible } from 'mybible_parser'
|
|
import { mb } from 'api'
|
|
|
|
const BUILD_END_TOAST = "Bible build finished!";
|
|
const SELECTED_TRANSLATION_OPTION = "<Selected reading translation, {0}>"
|
|
const SELECTED_TRANSLATION_OPTION_KEY = "default"
|
|
const REPORT_ISSUE_URL = "https://github.com/GsLogiMaker/my-bible-obsidian-plugin/issues/new/choose"
|
|
enum BookNameStyle { Display, Abbreviated, Full }
|
|
|
|
class Version {
|
|
major:number = 1
|
|
minor:number = 0
|
|
patch:number = 0
|
|
|
|
constructor(ma:number, mi:number, pa:number) {
|
|
this.major = ma
|
|
this.minor = mi
|
|
this.patch = pa
|
|
}
|
|
|
|
static fromString(version:string):Version {
|
|
let parts = version.split(".")
|
|
return new Version(
|
|
Number(parts[0]),
|
|
Number(parts[1]),
|
|
Number(parts[2]),
|
|
)
|
|
}
|
|
}
|
|
|
|
class MyBibleSettings {
|
|
translation: string
|
|
reading_translation: string
|
|
bible_folder: string
|
|
|
|
store_locally: boolean
|
|
|
|
padded_order: boolean
|
|
book_folders_enabled: boolean
|
|
book_name_format: string
|
|
book_name_delimiter: string
|
|
book_name_capitalization: string
|
|
book_name_abbreviated: boolean
|
|
|
|
padded_chapter: boolean
|
|
book_ordering: string
|
|
chapter_name_format: string
|
|
chapter_body_format: string
|
|
|
|
build_with_dynamic_verses: boolean
|
|
verse_body_format: string
|
|
|
|
index_enabled: boolean
|
|
index_name_format: string
|
|
index_format: string
|
|
index_link_format: string
|
|
|
|
chapter_index_enabled: boolean
|
|
chapter_index_name_format: string
|
|
chapter_index_format: string
|
|
chapter_index_link_format: string
|
|
|
|
enable_javascript_execution:boolean
|
|
|
|
_built_translation: string;
|
|
_last_opened_version: Version|undefined
|
|
_plugin:MyBible
|
|
|
|
constructor() {}
|
|
|
|
async set_translation(val: string) {
|
|
if (val === SELECTED_TRANSLATION_OPTION_KEY) {
|
|
this.translation = val
|
|
return
|
|
}
|
|
|
|
let has_translation = false;
|
|
let translations = (await this._plugin.bible_api.get_translations());
|
|
if (!(val in translations)) {
|
|
this.translation = await this._plugin.bible_api.get_default_translation();
|
|
} else {
|
|
this.translation = val;
|
|
}
|
|
}
|
|
|
|
async setLastOpenedVersion(version:Version) {
|
|
this._last_opened_version = version
|
|
this._plugin.saveSettings()
|
|
}
|
|
}
|
|
|
|
const DEFAULT_SETTINGS: MyBibleSettings = {
|
|
translation: SELECTED_TRANSLATION_OPTION_KEY,
|
|
reading_translation: "WEB",
|
|
bible_folder: "/My Bible/",
|
|
book_folders_enabled: true,
|
|
book_name_format: "{order} {book}",
|
|
book_name_delimiter: " ",
|
|
chapter_name_format: "{book} {chapter}",
|
|
book_name_capitalization: "name_case",
|
|
book_name_abbreviated: false,
|
|
padded_order: true,
|
|
padded_chapter: false,
|
|
book_ordering: "christian",
|
|
build_with_dynamic_verses: true,
|
|
verse_body_format: "###### {verse}\n"
|
|
+ "{verse_text}"
|
|
,
|
|
chapter_body_format: "\n"
|
|
+ "##### "
|
|
+ "**[[{last_chapter_name}|⏪ {last_chapter_name}]] | [[{chapter_index}|Chapters]] | [[{next_chapter_name}|{next_chapter_name} ⏩]]**<br>"
|
|
+ "**[[{first_chapter_name}|First ({first_chapter})]] | [[{final_chapter_name}|Last ({final_chapter})]]**<br><br>\n"
|
|
+ "\n"
|
|
+ "{verses}\n"
|
|
+ "\n"
|
|
+ "##### "
|
|
+ "**[[{last_chapter_name}|⏪ {last_chapter_name}]] | [[{chapter_index}|Chapters]] | [[{next_chapter_name}|{next_chapter_name} ⏩]]**<br>"
|
|
+ "**[[{first_chapter_name}|First ({first_chapter})]] | [[{final_chapter_name}|Last ({final_chapter})]]**\n"
|
|
,
|
|
index_enabled: true,
|
|
index_name_format: "-- Bible --",
|
|
index_link_format: "- [[{chapter_index}|{book}]]",
|
|
index_format: ""
|
|
+ "### Old testament\n"
|
|
+ "{old_testament}\n"
|
|
+ "### New testament\n"
|
|
+ "{new_testament}\n"
|
|
+ "### Apocrypha\n"
|
|
+ "{apocrypha}"
|
|
,
|
|
chapter_index_enabled: true,
|
|
chapter_index_name_format: "-- {book} --",
|
|
chapter_index_link_format: "- [[{chapter_name}|{chapter}]]",
|
|
chapter_index_format: ""
|
|
+ "##### *[[{index}|Books]]*\n"
|
|
+ "\n"
|
|
+ "### Chapters\n"
|
|
+ "{chapters}\n"
|
|
,
|
|
store_locally: false,
|
|
enable_javascript_execution: false,
|
|
|
|
_built_translation: "",
|
|
_last_opened_version: undefined,
|
|
} as MyBibleSettings
|
|
|
|
export function getPlugin():MyBible {
|
|
return MyBible.plugin
|
|
}
|
|
|
|
export function httpGet(theUrl: string): Promise<string> {
|
|
const request = requestUrl(theUrl).text;
|
|
|
|
const timeout = new Promise<never>((_, reject) =>
|
|
setTimeout(() => {
|
|
const callSiteErr = new Error(
|
|
`Request timed out after 10 seconds: ${theUrl}`
|
|
);
|
|
callSiteErr.name = "NetworkError";
|
|
reject(callSiteErr)
|
|
}, 10000)
|
|
);
|
|
|
|
try {
|
|
return Promise.race([request, timeout])
|
|
.catch((e) => {
|
|
throw e;
|
|
});
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
export function is_alpha(string: string): boolean {
|
|
for (let char_str of string) {
|
|
let char = char_str.charCodeAt(0);
|
|
if ((char > 64 && char < 91) || (char > 96 && char < 123) || (char > 39 && char < 42)) {
|
|
// Character is a capital or lowercase letter, continue
|
|
continue;
|
|
}
|
|
// Character is not a capital or lowercase letter, return false
|
|
return false;
|
|
}
|
|
// All characters were uppercase or lowercase letters, return true
|
|
return true;
|
|
}
|
|
|
|
export function is_alphanumeric(string: string): boolean {
|
|
for (let char_str of string) {
|
|
let char = char_str.charCodeAt(0);
|
|
if ((char > 64 && char < 91) || (char > 96 && char < 123) || (char > 47 && char < 58)) {
|
|
// Character is a capital or lowercase letter, continue
|
|
continue;
|
|
}
|
|
// Character is not a capital or lowercase letter, return false
|
|
return false;
|
|
}
|
|
// All characters were uppercase or lowercase letters, return true
|
|
return true;
|
|
}
|
|
|
|
export function is_numeric(string: string): boolean {
|
|
for (let char_str of string) {
|
|
let char = char_str.charCodeAt(0);
|
|
if (char > 47 && char < 58) {
|
|
// Character is a number, continue
|
|
continue;
|
|
}
|
|
// Character is not number, return false
|
|
return false;
|
|
}
|
|
// All characters were numbers, return true
|
|
return true;
|
|
}
|
|
|
|
export async function save_file(path: string, content: string) {
|
|
let file_path = normalizePath(path);
|
|
let file = this.app.vault.getAbstractFileByPath(file_path)
|
|
if (file instanceof TFile) {
|
|
await this.app.vault.modify(file, content)
|
|
} else if (file === null) {
|
|
await this.app.vault.create(
|
|
file_path,
|
|
content,
|
|
)
|
|
}
|
|
}
|
|
|
|
export function translation_to_display_name(translation:Translation):string {
|
|
if (translation.language === "") {
|
|
return translation.display_name
|
|
}
|
|
return "{0} - {1} - {2}"
|
|
.format(translation.language, translation.abbreviated_name, translation.display_name)
|
|
;
|
|
}
|
|
|
|
export function cyrb128(str:string): number {
|
|
let h1 = 1779033703, h2 = 3144134277,
|
|
h3 = 1013904242, h4 = 2773480762;
|
|
for (let i = 0, k; i < str.length; i++) {
|
|
k = str.charCodeAt(i);
|
|
h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
|
|
h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
|
|
h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
|
|
h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
|
|
}
|
|
h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
|
|
h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
|
|
h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
|
|
h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
|
|
h1 ^= (h2 ^ h3 ^ h4), h2 ^= h1, h3 ^= h1, h4 ^= h1;
|
|
return h1 >>> 0
|
|
}
|
|
|
|
export function wait(seconds:number):Promise<void> {
|
|
return new Promise(function(resolve) {
|
|
setTimeout(resolve, seconds);
|
|
});
|
|
}
|
|
|
|
export default class MyBible extends Plugin {
|
|
bible_api: BibleAPI
|
|
settings: MyBibleSettings
|
|
progress_notice: Notice | null
|
|
legacyParser: legacy.VerseParser
|
|
depricationTimer: Promise<void>|undefined = undefined
|
|
|
|
static plugin:MyBible
|
|
|
|
async onload() {
|
|
MyBible.plugin = this
|
|
|
|
// @ts-ignore
|
|
globalThis["mb"] = mb
|
|
|
|
this.legacyParser = new legacy.VerseParser()
|
|
this.bible_api = new BollsLifeBibleAPI();
|
|
this.bible_api.plugin = this;
|
|
|
|
await this.loadSettings()
|
|
this.settings._plugin = this
|
|
|
|
this.settings.setLastOpenedVersion(
|
|
Version.fromString(this.manifest.version)
|
|
)
|
|
|
|
await this.settings.set_translation(this.settings.translation)
|
|
|
|
// This adds a simple command that can be triggered anywhere
|
|
this.addCommand({
|
|
id: 'create_bible_files',
|
|
name: 'Build Bible',
|
|
callback: async () => {
|
|
new BuilderModal(this.app, this).open()
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'quick_change_translation',
|
|
name: 'Change translation',
|
|
callback: async () => {
|
|
let modal = new QuickChangeTranslationeModal(this)
|
|
modal.translations = await this.bible_api.get_translations()
|
|
modal.onChose = async translation => {
|
|
this.settings.reading_translation = translation.abbreviated_name
|
|
await getPlugin().saveSettings()
|
|
}
|
|
modal.open()
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'clear_cache',
|
|
name: 'Clear local files',
|
|
callback: async () => {
|
|
new ClearLocalFilesModal(this.app, this).open();
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'download_bible',
|
|
name: 'Download translation',
|
|
callback: async () => {
|
|
let modal = new QuickChangeTranslationeModal(this)
|
|
modal.translations = await this.bible_api.get_translations()
|
|
modal.onChose = async translation => {
|
|
await this.bible_api.user_download_translation(
|
|
translation.abbreviated_name
|
|
)
|
|
}
|
|
modal.open()
|
|
|
|
}
|
|
});
|
|
|
|
this.registerMarkdownCodeBlockProcessor("verse", async (source, el, ctx) =>{
|
|
const DEPRICATE_VERSE_BLOCK = false
|
|
if (DEPRICATE_VERSE_BLOCK && this.depricationTimer === undefined) {
|
|
MarkdownRenderer.render(
|
|
this.app,
|
|
"> [!WARNING] `verse` codeblocks are depricated\n"
|
|
+ "> Rebuilding your Bible should resolve this issue. If you created this block yourself then use the `mybible` codeblock instead. For more information visit [the wiki]({0})."
|
|
.format("https://gslogimaker.github.io/my-bible-obsidian-plugin/documents/Code_Blocks.html#md:verse-1"),
|
|
el,
|
|
"",
|
|
this,
|
|
)
|
|
this.depricationTimer = (async () => {
|
|
await wait(3)
|
|
this.depricationTimer = undefined
|
|
return
|
|
})()
|
|
}
|
|
await this.legacyParser.parse(source, el)
|
|
});
|
|
|
|
this.registerMarkdownCodeBlockProcessor("mybible", async (source, el, ctx) => {
|
|
let code_context = {
|
|
file: this.app.vault.getAbstractFileByPath(ctx.sourcePath)
|
|
}
|
|
let parsed = parse_mybible(source)
|
|
if (parsed instanceof Error) {
|
|
MarkdownRenderer.render(
|
|
this.app,
|
|
"> [!ERROR] {0}\n> ```\n> {1}\n> ```".format(
|
|
parsed.name,
|
|
parsed.message
|
|
.replace(/\n/g, "\n> ")
|
|
.replace(/```/g, "")
|
|
),
|
|
el,
|
|
"",
|
|
this,
|
|
)
|
|
return
|
|
}
|
|
|
|
let text = ""
|
|
for (const X of parsed) {
|
|
if (X instanceof BBCodeTag) {
|
|
text += await X.toText(code_context)
|
|
} else {
|
|
text += X
|
|
}
|
|
}
|
|
MarkdownRenderer.render(this.app, text, el, "", this)
|
|
});
|
|
|
|
this.addSettingTab(new SettingsTab(this.app, this));
|
|
}
|
|
|
|
onunload() {
|
|
}
|
|
|
|
async build_bible() {
|
|
let bible_path = normalizePath(this.settings.bible_folder);
|
|
|
|
let bible_folder = this.app.vault.getAbstractFileByPath(bible_path);
|
|
if (bible_folder instanceof TFile) {
|
|
// Can't handle if bible path is a file. Abort
|
|
new ErrorModal(
|
|
this.app,
|
|
this,
|
|
"Failed to build bible",
|
|
"The bible folder defined in settings, \"{0}\", was expected to point to a folder, but it points to a file. Try changing the path to point to a folder, or change the file to a folder."
|
|
.format(this.settings.bible_folder),
|
|
).open();
|
|
return;
|
|
} else if (bible_folder === null) {
|
|
// Bible path doesn't exist. Create it
|
|
this.app.vault.adapter.mkdir(bible_path);
|
|
} else {
|
|
// Bible path is already a valid folder. No action needed
|
|
}
|
|
|
|
let folders_and_files = await this.app.vault.adapter.list(bible_path);
|
|
if (folders_and_files.files.length + folders_and_files.folders.length != 0) {
|
|
new ClearOldBibleFilesModal(this.app, this).open()
|
|
} else {
|
|
await this._build_bible(bible_path);
|
|
}
|
|
}
|
|
|
|
async _build_bible(bible_path: string) {
|
|
this.show_toast_progress(0, null)
|
|
try {
|
|
let translation = ""
|
|
if (this.settings.translation == SELECTED_TRANSLATION_OPTION_KEY) {
|
|
translation = this.settings.reading_translation
|
|
} else {
|
|
translation = this.settings.translation
|
|
}
|
|
|
|
// TODO: Build bibles according to translation in settings
|
|
this.settings._built_translation = translation;
|
|
await this.saveSettings();
|
|
|
|
let ctx = new BuildContext
|
|
ctx.plugin = this
|
|
ctx.translation = translation
|
|
ctx.set_books(await this.bible_api.get_books_data(ctx.translation))
|
|
ctx.verse_counts = await this.bible_api.get_verse_count(ctx.translation)
|
|
|
|
// Get translation texts
|
|
ctx.translation_texts = await this.bible_api
|
|
.get_translation(ctx.translation);
|
|
|
|
// Remove empty chapters from books (HACK: This should be done in a better place, but this is where all the needed information is)
|
|
if (ctx.translation_texts !== undefined) {
|
|
for (const BOOK_ID of Object.keys(ctx.books) as unknown as number[]) {
|
|
let book = ctx.books[BOOK_ID]
|
|
let to_remove = []
|
|
for (let i = 1; i != book.chapters.length+1; i++) {
|
|
let chapter_texts = ctx.translation_texts.books[book.id]
|
|
let verses = chapter_texts[i] || {}
|
|
if (Object.keys(verses).length === 0) {
|
|
to_remove.push(i)
|
|
}
|
|
}
|
|
for (const i of to_remove.reverse()) {
|
|
book.chapters.remove(i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Notify progress
|
|
let built_chapter_count = 0
|
|
let total_chapter_count = 0
|
|
for (const i in ctx.books) {
|
|
total_chapter_count += ctx.books[i].chapters.length
|
|
}
|
|
this.show_toast_progress(0, total_chapter_count)
|
|
|
|
// Index
|
|
if (this.settings.index_enabled) {
|
|
await save_file(
|
|
"{0}/{1}.md".format(bible_path, ctx.format_index_name()),
|
|
ctx.format_index(),
|
|
)
|
|
}
|
|
|
|
let file_promises: Array<Promise<any>> = [];
|
|
|
|
for (const BOOK_ID of Object.keys(ctx.books) as unknown as number[]) {
|
|
let book = ctx.books[BOOK_ID]
|
|
ctx.set_book_and_chapter(book, book.chapters[0])
|
|
let texts_of_book = ctx.translation_texts.books[book.id]
|
|
|
|
// Book path
|
|
let book_path = bible_path
|
|
if (this.settings.book_folders_enabled) {
|
|
book_path += "/" + ctx.format_book_name(ctx.book);
|
|
this.app.vault.adapter.mkdir(normalizePath(book_path));
|
|
}
|
|
|
|
// Chapter index
|
|
if (this.settings.chapter_index_enabled) {
|
|
file_promises.push(save_file(
|
|
"{0}/{1}.md".format(book_path, ctx.format_chapter_index_name(ctx.book)),
|
|
ctx.format_chapter_index(ctx.book),
|
|
))
|
|
}
|
|
|
|
for (const chapter of ctx.book.chapters) {
|
|
file_promises.push(new Promise(async () => {
|
|
ctx.set_chapter(chapter);
|
|
let texts_of_chapter = texts_of_book[chapter]
|
|
|
|
// Assemble verses
|
|
ctx.verses_text = ""
|
|
let added_verse_count = 0
|
|
for (const verse_key of Object.keys(texts_of_chapter)) {
|
|
const verse = Number(verse_key)
|
|
while (added_verse_count < verse) {
|
|
ctx.verse = added_verse_count + 1
|
|
let text = ctx.format_verse_body()
|
|
if (text.length === 0) {
|
|
added_verse_count += 1
|
|
continue
|
|
}
|
|
ctx.verses_text += text
|
|
if (
|
|
!(text.length === 0 && !this.settings.build_with_dynamic_verses)
|
|
&& verse_key !== Object.keys(texts_of_chapter).last())
|
|
{
|
|
ctx.verses_text += "\n";
|
|
}
|
|
added_verse_count += 1
|
|
}
|
|
}
|
|
|
|
// Chapter name
|
|
let chapter_note_name = ctx.format_chapter_name();
|
|
|
|
// Chapter body
|
|
let note_body = ctx.format_chapter_body();
|
|
|
|
// Save file
|
|
let file_path = book_path + "/" + chapter_note_name + ".md"
|
|
|
|
await save_file(file_path, note_body)
|
|
built_chapter_count += 1
|
|
this.show_toast_progress(
|
|
built_chapter_count,
|
|
total_chapter_count,
|
|
)
|
|
}))
|
|
}
|
|
}
|
|
|
|
await Promise.all(file_promises);
|
|
} catch (e) {
|
|
new ErrorModal(
|
|
this.app,
|
|
this,
|
|
"Failed to build bible",
|
|
String(e),
|
|
).open();
|
|
}
|
|
}
|
|
|
|
show_toast_error(error:string) {
|
|
if (this.progress_notice !== null) {
|
|
this.progress_notice?.hide()
|
|
this.progress_notice = null
|
|
}
|
|
new Notice("Error building bible: " + error, 0)
|
|
}
|
|
|
|
show_toast_progress(progress: number, finish: number|null) {
|
|
if (progress === finish && finish != null) {
|
|
this.progress_notice?.hide()
|
|
this.progress_notice = null
|
|
new Notice(BUILD_END_TOAST)
|
|
return
|
|
}
|
|
|
|
if (this.progress_notice == null) {
|
|
this.progress_notice = new Notice("", 0)
|
|
}
|
|
|
|
let msg = ""
|
|
if (finish == null) {
|
|
msg = "Building bible..."
|
|
} else {
|
|
msg = "Building bible... ({0}/{1})"
|
|
.format(String(progress), String(finish))
|
|
}
|
|
this.progress_notice.setMessage(msg)
|
|
}
|
|
|
|
async loadSettings() {
|
|
this.settings = Object.assign(
|
|
new MyBibleSettings,
|
|
DEFAULT_SETTINGS,
|
|
await this.loadData(),
|
|
)
|
|
}
|
|
|
|
async saveSettings() {
|
|
let saving:Record<string, any> = {}
|
|
Object.assign(saving, this.settings)
|
|
for (const key in saving) {
|
|
let value:any = (DEFAULT_SETTINGS as Record<string, any>)[key]
|
|
if (saving[key] === value) {
|
|
delete saving[key]
|
|
}
|
|
}
|
|
delete saving["_plugin"]
|
|
|
|
await this.saveData(saving)
|
|
}
|
|
|
|
async runJS(code:string, context?:any):Promise<any> {
|
|
if (!this.settings.enable_javascript_execution) {
|
|
throw new Error(
|
|
"Can't execute javascript because `Enable Javascript excecution` is not enabled. Enable in MyBible settings."
|
|
)
|
|
}
|
|
let call_result = await async function() {
|
|
return eval("(async () => { {0} })()".format(code))
|
|
}.call(context)
|
|
return call_result
|
|
}
|
|
}
|
|
|
|
class MBGeneralError extends Error {
|
|
toString():string {
|
|
let msg = "\n> [!ERROR] {0}\n".format(this.name)
|
|
if (this.message.length !== 0) {
|
|
msg += "> " + this.message + "\n"
|
|
}
|
|
return msg
|
|
}
|
|
}
|
|
|
|
class MBTagError extends MBGeneralError {}
|
|
|
|
class MBValueParseError extends MBTagError {
|
|
parsing_value:string
|
|
constructor(parsing_value:string) {
|
|
super("Parsing value: `{0}`".format(parsing_value))
|
|
parsing_value = parsing_value
|
|
this.name = "Failed to parse value"
|
|
}
|
|
}
|
|
|
|
class MBArgValueParseError extends MBTagError {
|
|
constructor(arg_name:string, err:MBValueParseError|undefined=undefined) {
|
|
let msg = ""
|
|
if (arg_name.length !== 0) {
|
|
msg += "Parsing argument: `{0}`".format(arg_name)
|
|
}
|
|
if (err !== undefined) {
|
|
msg += "\n> {0}".format(err.message)
|
|
}
|
|
super(msg)
|
|
this.name = "Failed to parse value for argument"
|
|
}
|
|
}
|
|
|
|
class FormatKeys {
|
|
static translation = /{translation}/g
|
|
static order = /{order}/g
|
|
static book = /{book}/g
|
|
static book_full = /{book_full}/g
|
|
static book_short = /{book_short}/g
|
|
static chapter = /{chapter}/g
|
|
static chapter_name = /{chapter_name}/g
|
|
static chapter_index = /{chapter_index}/g
|
|
static last_chapter = /{last_chapter}/g
|
|
static last_chapter_name = /{last_chapter_name}/g
|
|
static last_chapter_book = /{last_chapter_book}/g
|
|
static next_chapter = /{next_chapter}/g
|
|
static next_chapter_name = /{next_chapter_name}/g
|
|
static next_chapter_book = /{next_chapter_book}/g
|
|
static first_chapter = /{first_chapter}/g
|
|
static first_chapter_name = /{first_chapter_name}/g
|
|
static final_chapter = /{final_chapter}/g
|
|
static final_chapter_name = /{final_chapter_name}/g
|
|
static verses = /{verses}/g
|
|
static book_id = /{book_id}/g
|
|
static verse_text = /{verse_text}/g
|
|
static verse = /{verse}/g
|
|
static index = /{index}/g
|
|
static chapters = /{chapters}/g
|
|
static old_testament = /{old_testament}/g
|
|
static new_testament = /{new_testament}/g
|
|
static apocrypha = /{apocrypha}/g
|
|
}
|
|
|
|
class BuildContext {
|
|
translation: string = ""
|
|
translation_texts: TranslationData
|
|
books: Record<BookId, BookData> = {}
|
|
/// List of book IDs sorted by their ordering as specified in the build settings
|
|
sorted_book_ids: BookId[]
|
|
/// Book of the current chapter
|
|
book: BookData
|
|
/// Book of previous chapter
|
|
prev_book: BookData
|
|
/// Book of next chapter
|
|
next_book: BookData
|
|
chapter: number = 1
|
|
prev_chapter: number = 0
|
|
next_chapter: number = 0
|
|
chapters: ChapterData
|
|
last_chapters: ChapterData
|
|
next_chapters: ChapterData
|
|
chapter_verse_count: number = 0
|
|
verses_text: string = ""
|
|
verse: number = 0
|
|
verse_counts: VerseCounts
|
|
plugin: MyBible
|
|
|
|
/// Find index of book by ID
|
|
find_book(book_id:BookId):number {
|
|
return book_id
|
|
}
|
|
|
|
abbreviate_book_name(book:BookData, delimeter:string): string {
|
|
if (book.id == DEFAULT_NAME_MAP["Judges"]) {
|
|
return "Jdg"
|
|
}
|
|
if (book.id == DEFAULT_NAME_MAP["Philippians"]) {
|
|
return "Php"
|
|
}
|
|
if (book.id == DEFAULT_NAME_MAP["Philemon"]) {
|
|
return "Phm"
|
|
}
|
|
|
|
return book_id_to_name(book.id)
|
|
.replace(delimeter, "")
|
|
.slice(0,3);
|
|
}
|
|
|
|
format_book_name(
|
|
book:BookData|undefined=undefined,
|
|
style: BookNameStyle|undefined=BookNameStyle.Display,
|
|
): string {
|
|
if (book === undefined) {
|
|
book = this.book
|
|
}
|
|
let delim = this.plugin.settings.book_name_delimiter
|
|
let book_name = this.to_case(
|
|
book.name,
|
|
this.plugin.settings.book_name_capitalization,
|
|
delim
|
|
)
|
|
|
|
if (
|
|
(style === BookNameStyle.Abbreviated)
|
|
|| (style === BookNameStyle.Display && this.plugin.settings.book_name_abbreviated)
|
|
) {
|
|
book_name = this.abbreviate_book_name(book, delim)
|
|
}
|
|
|
|
return this.plugin.settings.book_name_format
|
|
.replace(FormatKeys.translation, String(this.translation))
|
|
.replace(FormatKeys.book, book_name)
|
|
.replace(
|
|
FormatKeys.order,
|
|
String(this.book_order(book))
|
|
.padStart(2 * Number(this.plugin.settings.padded_order), "0")
|
|
)
|
|
}
|
|
|
|
format_book_name_without_order(
|
|
book:BookData|undefined=undefined,
|
|
casing:string|undefined=undefined,
|
|
style: BookNameStyle|undefined=BookNameStyle.Display,
|
|
): string {
|
|
if (book === undefined) {
|
|
book = this.book
|
|
}
|
|
if (casing === undefined) {
|
|
casing = this.plugin.settings.book_name_capitalization
|
|
}
|
|
let delim = this.plugin.settings.book_name_delimiter
|
|
let book_name = this.to_case(
|
|
book.name,
|
|
casing,
|
|
delim
|
|
)
|
|
|
|
if ( (style === BookNameStyle.Abbreviated) ||
|
|
(style === BookNameStyle.Display && this.plugin.settings.book_name_abbreviated) ) {
|
|
book_name = this.abbreviate_book_name(book, delim)
|
|
}
|
|
|
|
return book_name
|
|
}
|
|
|
|
format_chapter_body(): string {
|
|
return this.plugin.settings.chapter_body_format
|
|
.replace(FormatKeys.translation, String(this.translation))
|
|
.replace(FormatKeys.book, this.format_book_name_without_order(this.book))
|
|
.replace(FormatKeys.book_full, this.format_book_name_without_order(this.book, undefined, BookNameStyle.Full))
|
|
.replace(FormatKeys.book_short, this.format_book_name_without_order(this.book, undefined, BookNameStyle.Abbreviated))
|
|
.replace(
|
|
FormatKeys.order,
|
|
String(this.book_order(this.book))
|
|
.padStart(2 * Number(this.plugin.settings.padded_order), "0")
|
|
)
|
|
.replace(FormatKeys.chapter, String(this.chapter))
|
|
.replace(FormatKeys.chapter_name, this.format_chapter_name())
|
|
.replace(FormatKeys.chapter_index, this.format_chapter_index_name(this.book))
|
|
.replace(FormatKeys.last_chapter, String(this.prev_chapter))
|
|
.replace(FormatKeys.last_chapter_name, this.format_chapter_name("last"))
|
|
.replace(FormatKeys.last_chapter_book, this.format_book_name_without_order(this.prev_book))
|
|
.replace(FormatKeys.next_chapter, String(this.next_chapter))
|
|
.replace(FormatKeys.next_chapter_name, this.format_chapter_name("next"))
|
|
.replace(FormatKeys.next_chapter_book, this.format_book_name_without_order(this.next_book))
|
|
.replace(FormatKeys.first_chapter, String(this.book.chapters.first()))
|
|
.replace(FormatKeys.first_chapter_name, this.format_chapter_name("first"))
|
|
.replace(FormatKeys.final_chapter, String(this.book.chapters.last()))
|
|
.replace(FormatKeys.final_chapter_name, this.format_chapter_name("final"))
|
|
.replace(FormatKeys.verses, this.verses_text)
|
|
}
|
|
|
|
format_chapter_name(tense:string="current", custom_chapter:number|null = null): string {
|
|
let format = this.plugin.settings.chapter_name_format;
|
|
if (format.length == 0) {
|
|
format = DEFAULT_SETTINGS.chapter_name_format;
|
|
}
|
|
|
|
let presentBook = this.book
|
|
let book_name = "";
|
|
let id = -1;
|
|
let chapter = custom_chapter
|
|
switch (tense) {
|
|
case "current": {
|
|
presentBook = this.book
|
|
book_name = this.format_book_name_without_order(this.book);
|
|
id = this.book.id
|
|
chapter = this.chapter
|
|
break;
|
|
}
|
|
case "last": {
|
|
presentBook = this.prev_book
|
|
book_name = this.format_book_name_without_order(this.prev_book);
|
|
id = this.prev_book.id
|
|
chapter = this.prev_chapter
|
|
break;
|
|
}
|
|
case "next": {
|
|
presentBook = this.next_book
|
|
book_name = this.format_book_name_without_order(this.next_book);
|
|
id = this.next_book.id
|
|
chapter = this.next_chapter
|
|
break;
|
|
}
|
|
case "first": {
|
|
presentBook = this.book
|
|
book_name = this.format_book_name_without_order(this.book);
|
|
id = this.book.id
|
|
chapter = this.book.chapters.first() || 1
|
|
break;
|
|
}
|
|
case "final": {
|
|
presentBook = this.book
|
|
book_name = this.format_book_name_without_order(this.book);
|
|
id = this.book.id
|
|
chapter = this.book.chapters.last() || 1
|
|
break;
|
|
}
|
|
case "custom": {
|
|
book_name = this.format_book_name_without_order(this.book);
|
|
id = this.book.id
|
|
chapter = custom_chapter;
|
|
break;
|
|
}
|
|
default: throw new Error("Unmatched switch case at tense '{0}'".format(tense));
|
|
}
|
|
|
|
if (chapter == null) {
|
|
throw new Error("Chapter is null");
|
|
}
|
|
|
|
let chapter_pad_by = 1
|
|
if (presentBook.chapters.length > 99) {
|
|
chapter_pad_by = 3
|
|
} else if (presentBook.chapters.length > 9) {
|
|
chapter_pad_by = 2
|
|
}
|
|
|
|
return format
|
|
.replace(FormatKeys.translation, String(this.translation))
|
|
.replace(FormatKeys.book, book_name)
|
|
.replace(
|
|
FormatKeys.order,
|
|
String(this.book_order(id))
|
|
.padStart(2 * Number(this.plugin.settings.padded_order), "0")
|
|
)
|
|
.replace(
|
|
FormatKeys.chapter,
|
|
String(chapter)
|
|
.padStart(chapter_pad_by * Number(this.plugin.settings.padded_chapter), "0"),
|
|
)
|
|
}
|
|
|
|
format_verse_body(
|
|
custom_text:string|undefined=undefined,
|
|
): string {
|
|
let book_name = this.format_book_name_without_order(
|
|
this.book,
|
|
"name_case",
|
|
);
|
|
|
|
let verse_text = ""
|
|
if (custom_text !== undefined) {
|
|
verse_text = custom_text
|
|
} else if (this.plugin.settings.build_with_dynamic_verses) {
|
|
verse_text = "``` verse\n"
|
|
+ "{0} {1}:{2}\n".format(
|
|
String(this.book.id),
|
|
String(this.chapter),
|
|
String(this.verse),
|
|
)
|
|
+ "```"
|
|
} else {
|
|
verse_text = this.plugin.bible_api.parse_html(
|
|
this.translation_texts.books[this.book.id][this.chapter][this.verse] ?? ""
|
|
)
|
|
if (verse_text === undefined) {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
if (verse_text === "") {
|
|
return ""
|
|
}
|
|
|
|
return this.plugin.settings.verse_body_format
|
|
.replace(FormatKeys.translation, String(this.translation))
|
|
.replace(FormatKeys.book, book_name)
|
|
.replace(FormatKeys.book_full, this.format_book_name_without_order(this.book, undefined, BookNameStyle.Full))
|
|
.replace(FormatKeys.book_short, this.format_book_name_without_order(this.book, undefined, BookNameStyle.Abbreviated))
|
|
.replace(FormatKeys.book_id, String(this.book.id))
|
|
.replace(
|
|
FormatKeys.order,
|
|
String(this.book_order(this.book))
|
|
.padStart(2 * Number(this.plugin.settings.padded_order), "0")
|
|
)
|
|
.replace(FormatKeys.chapter, String(this.chapter))
|
|
.replace(FormatKeys.chapter_name, this.format_chapter_name())
|
|
.replace(FormatKeys.verse_text, verse_text)
|
|
.replace(FormatKeys.verse, String(this.verse))
|
|
}
|
|
|
|
format_chapter_index(book: BookData): string {
|
|
let book_name = this.format_book_name_without_order(book)
|
|
|
|
let chapter_links = ""
|
|
|
|
// Format chapter links
|
|
for (let i = 0; i != book.chapters.length; i++) {
|
|
let chapter = book.get_chapter_number(i);
|
|
let link = this.format_chapter_index_element(book, book_name, chapter)
|
|
if (i != book.chapters.length-1) {
|
|
link += "\n"
|
|
}
|
|
chapter_links += link
|
|
}
|
|
|
|
return this.plugin.settings.chapter_index_format
|
|
.replace(FormatKeys.translation, String(this.translation))
|
|
.replace(FormatKeys.book, book_name)
|
|
.replace(FormatKeys.book_full, this.format_book_name_without_order(book, undefined, BookNameStyle.Full))
|
|
.replace(FormatKeys.book_short, this.format_book_name_without_order(book, undefined, BookNameStyle.Abbreviated))
|
|
.replace(FormatKeys.order, String(this.book_order(book)).padStart(2 * Number(this.plugin.settings.padded_order), "0"))
|
|
.replace(FormatKeys.index, this.format_index_name())
|
|
.replace(FormatKeys.chapters, chapter_links)
|
|
.replace(FormatKeys.chapter_index, this.format_chapter_index_name(book))
|
|
;
|
|
}
|
|
|
|
format_chapter_index_element(
|
|
book:BookData|undefined=undefined,
|
|
book_name: string|undefined=undefined,
|
|
chapter: number|undefined=undefined,
|
|
): string {
|
|
if (book === undefined) {
|
|
book = this.book
|
|
}
|
|
if (book_name === undefined) {
|
|
book_name = this.format_book_name_without_order(book)
|
|
}
|
|
if (chapter === undefined) {
|
|
chapter = this.chapter
|
|
}
|
|
return this.plugin.settings.chapter_index_link_format
|
|
.replace(FormatKeys.translation, String(this.translation))
|
|
.replace(FormatKeys.book, book_name)
|
|
.replace(FormatKeys.book_full, this.format_book_name_without_order(book, undefined, BookNameStyle.Full))
|
|
.replace(FormatKeys.book_short, this.format_book_name_without_order(book, undefined, BookNameStyle.Abbreviated))
|
|
.replace(FormatKeys.order, String(this.book_order(book)).padStart(2 * Number(this.plugin.settings.padded_order), "0"))
|
|
.replace(FormatKeys.chapter, String(chapter))
|
|
.replace(FormatKeys.chapter_name, this.format_chapter_name("custom", chapter))
|
|
}
|
|
|
|
format_chapter_index_name(book: BookData|undefined=undefined): string {
|
|
if (book === undefined) {
|
|
book = this.book
|
|
}
|
|
let book_name = this.format_book_name_without_order(book)
|
|
return this.plugin.settings.chapter_index_name_format
|
|
.replace(FormatKeys.order, String(this.book_order(book)).padStart(2 * Number(this.plugin.settings.padded_order), "0"))
|
|
.replace(FormatKeys.book, book_name)
|
|
.replace(FormatKeys.book_full, this.format_book_name_without_order(book, undefined, BookNameStyle.Full))
|
|
.replace(FormatKeys.book_short, this.format_book_name_without_order(book, undefined, BookNameStyle.Abbreviated))
|
|
.replace(FormatKeys.translation, String(this.translation))
|
|
}
|
|
|
|
format_index_element(book:BookData|BookId|undefined=undefined) {
|
|
let id = 0
|
|
if (book === undefined) {
|
|
id = this.book.id
|
|
} else if (book instanceof BookData) {
|
|
id = book.id
|
|
} else {
|
|
id = book
|
|
}
|
|
let book_name = this.format_book_name_without_order(this.books[id])
|
|
|
|
let link = this.plugin.settings.index_link_format
|
|
.replace(FormatKeys.translation, String(this.translation))
|
|
.replace(FormatKeys.book, book_name)
|
|
.replace(FormatKeys.order, String(this.book_order(Number(id))).padStart(2 * Number(this.plugin.settings.padded_order), "0"))
|
|
.replace(FormatKeys.chapter_index, this.format_chapter_index_name(this.books[id]))
|
|
+ '\n'
|
|
return link
|
|
}
|
|
|
|
format_index_name(): string {
|
|
return this.plugin.settings.index_name_format
|
|
.replace(FormatKeys.translation, this.translation)
|
|
}
|
|
|
|
format_index(): string {
|
|
let old_t_links = ""
|
|
let new_t_links = ""
|
|
let apocr_links = ""
|
|
|
|
// Format all book links
|
|
for (const ID_ in this.books) {
|
|
const ID = Number(ID_)
|
|
let link = this.format_index_element(ID)
|
|
|
|
if (this.books[ID].id < 40) {
|
|
old_t_links += link
|
|
} else if (this.books[ID].id < 67) {
|
|
new_t_links += link
|
|
} else {
|
|
apocr_links += link
|
|
}
|
|
}
|
|
|
|
old_t_links = old_t_links.slice(0, old_t_links.length-1)
|
|
new_t_links = new_t_links.slice(0, new_t_links.length-1)
|
|
apocr_links = apocr_links.slice(0, apocr_links.length-1)
|
|
|
|
return this.plugin.settings.index_format
|
|
.replace(FormatKeys.translation, this.translation)
|
|
.replace(FormatKeys.old_testament, old_t_links)
|
|
.replace(FormatKeys.new_testament, new_t_links)
|
|
.replace(FormatKeys.apocrypha, apocr_links)
|
|
}
|
|
|
|
set_book(book:BookData) {
|
|
this.book = book;
|
|
|
|
let [next_book, next_chapter] = this.next_chapter_of(this.book, this.chapter)
|
|
let [prev_book, prev_chapter] = this.prev_chapter_of(this.book, this.chapter)
|
|
this.next_book = next_book
|
|
this.next_chapter = next_chapter
|
|
this.prev_book = prev_book
|
|
this.prev_chapter = prev_chapter
|
|
}
|
|
|
|
set_book_and_chapter(book:BookData, chapter:number) {
|
|
this.chapter = chapter
|
|
this.set_book(book)
|
|
}
|
|
|
|
set_books(books:Record<BookId, BookData>) {
|
|
this.books = books
|
|
this.sorted_book_ids = (
|
|
Object.keys(this.books)
|
|
).map((x) => Number(x)).sort((a, b) => this.book_order(a)-this.book_order(b))
|
|
}
|
|
|
|
next_chapter_of(
|
|
book: BookData,
|
|
chapter:number,
|
|
): [next_book:BookData, next_chapter:number] {
|
|
let next_book:BookData = book
|
|
if (chapter === book.chapters.last()) {
|
|
// This is the final chapter. Next book is not the same
|
|
// as the current book
|
|
let current_book_index = 0
|
|
for (const i of this.sorted_book_ids.keys()) {
|
|
if (this.sorted_book_ids[i] === book.id) {
|
|
current_book_index = i
|
|
break;
|
|
}
|
|
}
|
|
if (current_book_index == this.sorted_book_ids.length-1) {
|
|
// `book` is last book. Next book is first book
|
|
next_book = this.books[this.sorted_book_ids[0]]
|
|
} else {
|
|
// Set next_book to book after current
|
|
next_book = this.books[this.sorted_book_ids[current_book_index+1]]
|
|
}
|
|
}
|
|
|
|
let next_chapter = chapter+1
|
|
if (next_book.id !== book.id) {
|
|
// Get first chapter of next book
|
|
next_chapter = next_book.chapters[0]
|
|
}
|
|
|
|
return [next_book, next_chapter]
|
|
}
|
|
|
|
prev_chapter_of(
|
|
book: BookData,
|
|
chapter:number,
|
|
): [prev_book:BookData, prev_chapter:number] {
|
|
let prev_book:BookData = book
|
|
if (chapter === book.chapters.first()) {
|
|
// This is the first chapter. Previous book is not the same
|
|
// as the current book
|
|
let current_book_index = 0
|
|
for (const i of this.sorted_book_ids.keys()) {
|
|
if (this.sorted_book_ids[i] === book.id) {
|
|
current_book_index = i
|
|
break;
|
|
}
|
|
}
|
|
if (current_book_index == 0) {
|
|
// `book` is first book. Previous book is last book
|
|
prev_book = this.books[
|
|
this.sorted_book_ids[this.sorted_book_ids.length-1]
|
|
]
|
|
} else {
|
|
// Set prev_book to book before current
|
|
prev_book = this.books[this.sorted_book_ids[current_book_index-1]]
|
|
}
|
|
}
|
|
|
|
let prev_chapter = chapter-1
|
|
if (prev_book.id !== book.id) {
|
|
// Get last chapter of previous book
|
|
prev_chapter = prev_book.chapters[prev_book.chapters.length-1]
|
|
}
|
|
|
|
return [prev_book, prev_chapter]
|
|
}
|
|
|
|
book_order(
|
|
book: BookData|BookId|null = null,
|
|
) {
|
|
let book_obj:BookData
|
|
if (book == null) {
|
|
book_obj = this.book
|
|
} else if (book instanceof BookData) {
|
|
book_obj = book
|
|
} else {
|
|
book_obj = this.books[book]
|
|
}
|
|
|
|
if (this.plugin.settings.book_ordering === "hebraic") {
|
|
return to_hebraic_order(book_obj.id)
|
|
}
|
|
return book_obj.id
|
|
}
|
|
|
|
set_chapter(chapter:number) {
|
|
this.chapter = chapter
|
|
this.set_book(this.book)
|
|
}
|
|
|
|
// Sets the capitalization of the given name depending on *name_case*.
|
|
to_case(name:string, name_case:string, delimeter:string): string {
|
|
name = name.replace(/ /g, delimeter)
|
|
if (name_case == "lower_case") {
|
|
name = name.toLowerCase()
|
|
} else if (name_case == "upper_case") {
|
|
name = name.toUpperCase()
|
|
}
|
|
return name;
|
|
}
|
|
|
|
get_verse_count():number {
|
|
return this.verse_counts.get_count_for(this.book.id, this.chapter);
|
|
}
|
|
}
|
|
|
|
class VerseCounts {
|
|
books: Record<BookId, Record<number, number>>
|
|
|
|
get_count_for(book:BookId, chapter:number): number {
|
|
return this.books[book][chapter] || 0
|
|
}
|
|
}
|
|
|
|
interface TranslationData {
|
|
translation: string;
|
|
books: Record<BookId, Record<number, ChapterData>>;
|
|
}
|
|
class BookData {
|
|
/// The Book's numeric ID
|
|
id: number;
|
|
/// The chapters included in this book by their numeric order
|
|
chapters: number[];
|
|
/// The display name for the book
|
|
name: string;
|
|
|
|
constructor(id:number, name:string, chapters:number[]) {
|
|
this.id = id
|
|
this.name = name
|
|
this.chapters = chapters
|
|
}
|
|
|
|
/// Returns the number of the chapter at index
|
|
get_chapter_number(index:number): number {
|
|
if (index >= this.chapters.length) {
|
|
throw new Error("")
|
|
}
|
|
return this.chapters[index]
|
|
}
|
|
|
|
has_chapter(chapter:number): boolean {
|
|
return this.chapters.find((value, _, __) => value === chapter) !== undefined
|
|
}
|
|
}
|
|
|
|
type BookId = number;
|
|
|
|
/// A record of verses to texts
|
|
type ChapterData = Record<number, string>;
|
|
|
|
/// Ensures all coroutines wait for the same piece of code to finish
|
|
class SyncCache {
|
|
syncs:Record<string, SyncCacheItem> = {}
|
|
|
|
async sync(key:string, body:() => any): Promise<any> {
|
|
if (!(key in this.syncs)) {
|
|
this.syncs[key] = new SyncCacheItem()
|
|
}
|
|
let sync_item = this.syncs[key]
|
|
|
|
sync_item.ref_count += 1
|
|
|
|
|
|
try {
|
|
await sync_item.mutex
|
|
.acquire()
|
|
.then(async () => {
|
|
sync_item.data = await body()
|
|
|
|
sync_item.mutex.cancel()
|
|
sync_item.mutex.release()
|
|
})
|
|
.catch(e => {
|
|
if (e === E_CANCELED) {
|
|
/*pass*/
|
|
} else {
|
|
throw e
|
|
}
|
|
})
|
|
;
|
|
|
|
} finally {
|
|
sync_item.ref_count -= 1
|
|
if (sync_item.ref_count === 0) {
|
|
delete this.syncs[key]
|
|
}
|
|
}
|
|
|
|
|
|
return sync_item.data
|
|
}
|
|
}
|
|
class SyncCacheItem {
|
|
mutex: Mutex = new Mutex()
|
|
data: any = null
|
|
ref_count: number = 0
|
|
}
|
|
|
|
class BibleAPI {
|
|
cache_clear_timer: Promise<null> | null = null
|
|
cache_clear_timer_promise_err: (reason?: any) => void
|
|
sync_cache: SyncCache = new SyncCache()
|
|
plugin: MyBible
|
|
|
|
async _get_book_data(translation: string, book_id:BookId): Promise<BookData> {
|
|
throw new Error("unimplemented")
|
|
}
|
|
|
|
async _get_books_data(translation: string): Promise<Record<BookId, BookData>> {
|
|
throw new Error("unimplemented")
|
|
}
|
|
|
|
async _get_chapter(
|
|
translation: string,
|
|
book_id: number,
|
|
chapter: number,
|
|
): Promise<ChapterData> {
|
|
throw new Error("unimplemented")
|
|
}
|
|
|
|
async _get_translation(translation: string): Promise<TranslationData> {
|
|
throw new Error("unimplemented")
|
|
}
|
|
|
|
async _get_translations(): Promise<Translations> {
|
|
throw new Error("unimplemented")
|
|
}
|
|
|
|
async _get_default_translation(): Promise<string> {
|
|
throw new Error("unimplemented")
|
|
}
|
|
|
|
async _get_verse(
|
|
translation: string,
|
|
book_id: number,
|
|
chapter: number,
|
|
verse: number,
|
|
): Promise<string> {
|
|
let chapter_data = await this.get_chapter(
|
|
translation,
|
|
book_id,
|
|
chapter,
|
|
);
|
|
return chapter_data[verse] || "";
|
|
}
|
|
|
|
async _get_verse_count(translation:string): Promise<VerseCounts> {
|
|
throw new Error("unimplemented")
|
|
}
|
|
|
|
/// Downloads a translation and saves it locally.
|
|
async user_download_translation(translation:string) {
|
|
let notify = new Notice(
|
|
"Downloading {0} translation".format(translation),
|
|
0,
|
|
)
|
|
|
|
let promises = []
|
|
let translation_data = await this.get_translation(translation)
|
|
for (const book_id_ in translation_data.books) {
|
|
const book_id = Number(book_id_)
|
|
const chapters = translation_data.books[book_id]
|
|
for (const chapter_key of Object.keys(chapters)) {
|
|
const chapter = Number(chapter_key)
|
|
const chapter_data = chapters[chapter]
|
|
promises.push(this.save_chapter_data(
|
|
chapter_data,
|
|
translation,
|
|
book_id,
|
|
chapter,
|
|
))
|
|
}
|
|
}
|
|
|
|
notify.hide()
|
|
notify = new Notice("Saving {0} translation files".format(translation), 0)
|
|
await Promise.all(promises)
|
|
|
|
notify.hide()
|
|
new Notice("Download complete")
|
|
}
|
|
|
|
// Get book ID from translation and book name
|
|
async book_id(translation:string, book_name:string): Promise<number> {
|
|
let books = await this.get_books_data(translation);
|
|
for (const i of Object.keys(books) as unknown as BookId[]) {
|
|
if (books[i].name === book_name) {
|
|
return books[i].id;
|
|
}
|
|
}
|
|
if (book_name in DEFAULT_NAME_MAP) {
|
|
return DEFAULT_NAME_MAP[book_name];
|
|
}
|
|
|
|
throw new Error(
|
|
"Translation '{0}' has no book named '{1}'"
|
|
.format(translation, book_name)
|
|
);
|
|
}
|
|
|
|
async get_book_data(translation: string, book_id:BookId): Promise<BookData> {
|
|
let book_key = "{0}_{1}".format(translation, String(book_id));
|
|
|
|
let data = await this.sync_cache.sync(
|
|
"get_book_data_" + book_key,
|
|
() => this._get_book_data(translation, book_id),
|
|
)
|
|
|
|
if (data == null) {
|
|
throw new Error();
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
async get_books_data(translation: string): Promise<Record<BookId, BookData>> {
|
|
return await this.sync_cache.sync(
|
|
"get_books_data_" + translation,
|
|
() => this._get_books_data(translation),
|
|
)
|
|
}
|
|
|
|
async cache_chapter(
|
|
translation: string,
|
|
book_id: number,
|
|
chapter: number,
|
|
chapter_data: ChapterData | null,
|
|
save_locally: boolean,
|
|
): Promise<void> {
|
|
let key = this.make_chapter_key(translation, book_id, chapter);
|
|
|
|
// Save chapter to local file sytem
|
|
let cache_path = this.get_cache_path();
|
|
this.plugin.app.vault.adapter.mkdir(cache_path);
|
|
let cached_file_name = "{0} {1} {2}.txt"
|
|
.format(translation, String(book_id), String(chapter));
|
|
let cached_file_path = normalizePath(
|
|
cache_path + "/" + cached_file_name,
|
|
);
|
|
if (
|
|
chapter_data != null
|
|
&& !await this.plugin.app.vault.adapter.exists(cached_file_path)
|
|
) {
|
|
await this.plugin.app.vault.adapter.write(
|
|
cached_file_path,
|
|
this.make_chapter_file(chapter_data),
|
|
);
|
|
}
|
|
|
|
// Start/refresh cache clear timer
|
|
if (this.cache_clear_timer !== null && this.cache_clear_timer_promise_err !== null) {
|
|
// Clear out old promise
|
|
this.cache_clear_timer_promise_err(null);
|
|
}
|
|
this.cache_clear_timer = new Promise((ok, err) => {
|
|
this.cache_clear_timer_promise_err = err;
|
|
setTimeout(ok, 60000 * 60) // Timeout after 1 hour
|
|
});
|
|
this.cache_clear_timer
|
|
.then(() => this.clear_local_files())
|
|
.catch(err => { });
|
|
}
|
|
|
|
async get_chapter(
|
|
translation: string,
|
|
book_id: number,
|
|
chapter: number,
|
|
): Promise<ChapterData> {
|
|
let chapter_key = this.make_chapter_key(translation, book_id, chapter);
|
|
|
|
let file_name = this.make_chapter_file_name(translation, book_id, chapter)
|
|
|
|
let download_path = this.get_download_path()
|
|
this.plugin.app.vault.adapter.mkdir(download_path)
|
|
let download_file_path = normalizePath(
|
|
download_path + "/" + file_name
|
|
)
|
|
|
|
let cache_path = this.get_cache_path()
|
|
this.plugin.app.vault.adapter.mkdir(cache_path)
|
|
let cached_file_path = normalizePath(
|
|
cache_path + "/" + file_name
|
|
)
|
|
|
|
let data = await this.sync_cache.sync(
|
|
"get_chapter_"+chapter_key,
|
|
async () => {
|
|
// Attempt to load chapter locally
|
|
if ( await this.plugin.app.vault.adapter
|
|
.exists(download_file_path)
|
|
) {
|
|
// Load from download
|
|
return await this.load_chapter_file(download_path, file_name)
|
|
} else if ( await this.plugin.app.vault.adapter
|
|
.exists(cached_file_path)
|
|
) {
|
|
// Load from cache
|
|
return await this.load_chapter_file(cache_path, file_name)
|
|
}
|
|
|
|
// Fetch chapter from the web
|
|
let chapterData = await this._get_chapter(
|
|
translation,
|
|
book_id,
|
|
chapter,
|
|
)
|
|
for (const KEY_ of Object.keys(chapterData)) {
|
|
const KEY = Number(KEY_)
|
|
chapterData[KEY] = chapterData[KEY]
|
|
}
|
|
return chapterData
|
|
},
|
|
)
|
|
|
|
if (
|
|
data === null
|
|
|| Object.keys(data).length == 0
|
|
) {
|
|
// Failed to load or download chapter
|
|
return [];
|
|
}
|
|
|
|
new Promise(async (ok, err) => {
|
|
// Cache chapter if not downloaded
|
|
try {
|
|
if (
|
|
!await this.plugin.app.vault.adapter
|
|
.exists(download_file_path)
|
|
) {
|
|
this.cache_chapter(
|
|
translation,
|
|
book_id,
|
|
chapter,
|
|
data,
|
|
this.plugin.settings.store_locally,
|
|
);
|
|
}
|
|
ok(null)
|
|
} catch (e) {
|
|
err(e)
|
|
}
|
|
})
|
|
|
|
return data;
|
|
}
|
|
|
|
get_cache_path(): string {
|
|
return normalizePath(this.plugin.manifest.dir + "/.mybiblecache");
|
|
}
|
|
|
|
get_download_path(): string {
|
|
return normalizePath(this.plugin.manifest.dir + "/.chapters");
|
|
}
|
|
|
|
async get_translation(translation: string): Promise<TranslationData> {
|
|
return await this.sync_cache.sync(
|
|
"get_translation_" + translation,
|
|
() => this._get_translation(translation),
|
|
)
|
|
}
|
|
|
|
async get_default_translation(): Promise<string> {
|
|
return await this.sync_cache.sync(
|
|
"get_default_translation",
|
|
() => this._get_default_translation(),
|
|
)
|
|
}
|
|
|
|
async get_translations(): Promise<Translations> {
|
|
return await this.sync_cache.sync(
|
|
"get_translations",
|
|
() => this._get_translations(),
|
|
)
|
|
}
|
|
|
|
async get_verse(
|
|
translation: string,
|
|
book_id: number,
|
|
chapter: number,
|
|
verse: number,
|
|
): Promise<string> {
|
|
let key = "get_verse_{0}_{1}_{2}_{3}".format(translation, String(book_id), String(chapter), String(verse))
|
|
return await this.sync_cache.sync(
|
|
key,
|
|
() => this._get_verse(
|
|
translation,
|
|
book_id,
|
|
chapter,
|
|
verse,
|
|
),
|
|
)
|
|
}
|
|
|
|
async get_verse_count(translation:string): Promise<VerseCounts> {
|
|
return await this.sync_cache.sync(
|
|
"get_verse_count_"+translation,
|
|
() => this._get_verse_count(translation),
|
|
)
|
|
}
|
|
|
|
async clear_local_files() {
|
|
this.cache_clear_timer = null;
|
|
|
|
let cache_path = this.get_cache_path();
|
|
if (await this.plugin.app.vault.adapter.exists(cache_path)) {
|
|
if (!await this.plugin.app.vault.adapter.trashSystem(cache_path)) {
|
|
await this.plugin.app.vault.adapter.trashLocal(cache_path);
|
|
}
|
|
}
|
|
|
|
let download_path = this.get_download_path();
|
|
if (await this.plugin.app.vault.adapter.exists(download_path)) {
|
|
if (!await this.plugin.app.vault.adapter.trashSystem(download_path)) {
|
|
await this.plugin.app.vault.adapter.trashLocal(download_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
async load_chapter_file(folder:string, path: string): Promise<ChapterData> {
|
|
this.plugin.app.vault.adapter.mkdir(folder)
|
|
let raw = await this.plugin.app.vault.adapter
|
|
.read(folder + "/" + path)
|
|
;
|
|
|
|
let verse_list = []
|
|
if (raw.startsWith("[")) {
|
|
verse_list = JSON.parse(raw)
|
|
} else {
|
|
verse_list = raw.split("\n")
|
|
}
|
|
|
|
let verses:ChapterData = {}
|
|
for (let i = 0; i != verse_list.length; i++) {
|
|
if (verse_list[i].length == 0) {
|
|
continue
|
|
}
|
|
verses[i+1] = verse_list[i]
|
|
}
|
|
return verses
|
|
}
|
|
|
|
make_chapter_file(chapter_data:ChapterData): string {
|
|
let added_verse_count = 0
|
|
let body = "";
|
|
for (let i = 1; added_verse_count !== Object.keys(chapter_data).length; i++) {
|
|
if (i in chapter_data) {
|
|
body += chapter_data[i]
|
|
added_verse_count += 1
|
|
}
|
|
if (added_verse_count !== Object.keys(chapter_data).length) {
|
|
body += "\n"
|
|
}
|
|
}
|
|
|
|
return body
|
|
}
|
|
|
|
/// Makes the file name for downloaded chapters
|
|
make_chapter_file_name(translation: string, book_id: number, chapter: Number): string {
|
|
return "{0} {1} {2}.txt".format(translation, String(book_id), String(chapter));
|
|
|
|
}
|
|
|
|
make_chapter_key(translation: string, book_id: number, chapter: Number) {
|
|
return "{0}_{1}_{2}".format(translation, String(book_id), String(chapter));
|
|
}
|
|
|
|
parse_html(html:string):string {
|
|
html = html
|
|
// Replace <i>x with *x
|
|
.replace(/<\s*i\s*>(\s*)/g, (_, ws) => { return ws + "*" })
|
|
// Replace x</i> with x*
|
|
.replace(/(\s*)<\s*\/\s*i\s*>/g, (_, ws) => { return "*" + ws })
|
|
// Replace <b>x with **x
|
|
.replace(/<\s*b\s*>(\s*)/g, (_, ws) => { return ws + "**" })
|
|
// Replace x</b> with x**
|
|
.replace(/(\s*)<\s*\/\s*b\s*>/g, (_, ws) => { return "**" + ws })
|
|
// Replace <S>x with <sup>*x
|
|
.replace(/<\s*[S]\s*>(\s*)/g, (_, ws) => { return ws + "<sup>" })
|
|
// Replace x</S> with x*</sup>
|
|
.replace(/(\s*)<\s*\/\s*[S]\s*>/g, (_, ws) => { return "</sup>" + ws })
|
|
// Breakline tag
|
|
.replace(/<\s*br\s*\/?>/g, "\n")
|
|
;
|
|
return html
|
|
}
|
|
|
|
async pick_random_verse(seed:string|undefined=undefined): Promise<string> {
|
|
return mb.randRef(seed).toString()
|
|
}
|
|
|
|
/// Saves chapter data to local disk
|
|
async save_chapter_data(
|
|
chapter_data:ChapterData,
|
|
translation:string,
|
|
book_id:number,
|
|
chapter:number,
|
|
) {
|
|
// Save chapter to local file sytem
|
|
let path = this.get_download_path();
|
|
this.plugin.app.vault.adapter.mkdir(path);
|
|
let file_name = this
|
|
.make_chapter_file_name(translation, book_id, chapter);
|
|
let file_path = normalizePath(
|
|
path + "/" + file_name,
|
|
);
|
|
if (
|
|
!await this.plugin.app.vault.adapter.exists(file_path)
|
|
) {
|
|
let body = "";
|
|
for (const i_ in chapter_data) {
|
|
const i = Number(i_)
|
|
body += chapter_data[i];
|
|
if (i !== Object.keys(chapter_data).length) {
|
|
body += "\n"
|
|
}
|
|
}
|
|
await this.plugin.app.vault.adapter.write(
|
|
file_path,
|
|
body,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
type Translations = Record<string, Translation>;
|
|
interface Translation {
|
|
display_name: string,
|
|
abbreviated_name: string,
|
|
language: string,
|
|
}
|
|
|
|
class BollsLifeBibleAPI extends BibleAPI {
|
|
plugin: MyBible
|
|
translations: Translations = {}
|
|
translation_maps: Record<string, Record<BookId, BookData>> = {}
|
|
cache_clear_timer: Promise<null> | null = null
|
|
sync_cache: SyncCache = new SyncCache()
|
|
|
|
async _get_chapter(
|
|
translation: string,
|
|
book_id: number,
|
|
chapter: number,
|
|
): Promise<ChapterData> {
|
|
// Fetch chapter from the web
|
|
try {
|
|
let verse_data_list = JSON.parse(await httpGet(
|
|
"https://bolls.life/get-chapter/{0}/{1}/{2}/"
|
|
.format(translation, String(book_id), String(chapter))
|
|
));
|
|
|
|
let texts:ChapterData = {}
|
|
for (const data of verse_data_list) {
|
|
let verse = data["verse"]
|
|
texts[verse] = String(data["text"])
|
|
}
|
|
|
|
return texts
|
|
} catch (e) {
|
|
if (e instanceof Error && e.message.startsWith("No book exists by name")) {
|
|
return [];
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async _get_translation(translation: string): Promise<TranslationData> {
|
|
let verses = JSON.parse(
|
|
await httpGet(
|
|
"https://bolls.life/static/translations/{0}.json".format(translation)
|
|
)
|
|
);
|
|
|
|
let bible: TranslationData = {
|
|
translation: translation,
|
|
books: {},
|
|
}
|
|
|
|
await new Promise(async (ok, err) => {
|
|
try {
|
|
let i = 0;
|
|
let curr_book_id = -1;
|
|
let curr_chapter = -1;
|
|
for (const data of verses) {
|
|
let verse_text = String(data["text"])
|
|
|
|
let verse = data["verse"]
|
|
|
|
curr_book_id = data["book"]
|
|
curr_chapter = data["chapter"]
|
|
|
|
if (!(curr_book_id in bible.books)) {
|
|
bible.books[curr_book_id] = []
|
|
}
|
|
if (!(curr_chapter in bible.books[curr_book_id])) {
|
|
bible.books[curr_book_id][curr_chapter] = {}
|
|
}
|
|
|
|
bible.books[curr_book_id][curr_chapter][verse] = verse_text
|
|
i += 1;
|
|
}
|
|
ok(null);
|
|
} catch (e) {
|
|
err(e)
|
|
}
|
|
}).catch(e => {
|
|
throw e
|
|
})
|
|
|
|
return bible;
|
|
}
|
|
|
|
async _get_book_data(translation: string, book_id:BookId): Promise<BookData> {
|
|
let books = await this.get_books_data(translation);
|
|
for (const BOOK_ID of Object.keys(books) as unknown as BookId[]) {
|
|
let book = books[BOOK_ID]
|
|
if (book.id == book_id) {
|
|
return book;
|
|
}
|
|
}
|
|
throw new Error();
|
|
}
|
|
|
|
async _get_books_data(translation: string): Promise<Record<BookId, BookData>> {
|
|
let books = await this.get_translation_map(translation);
|
|
return books;
|
|
}
|
|
|
|
async _get_default_translation(): Promise<string> {
|
|
return "YLT";
|
|
}
|
|
|
|
async _get_translations(): Promise<Translations> {
|
|
if (Object.keys(this.translations).length === 0) {
|
|
let list = JSON.parse(await httpGet(
|
|
"https://bolls.life/static/bolls/app/views/languages.json"
|
|
));
|
|
for (let language of list) {
|
|
for (let item of language["translations"]) {
|
|
if (item["short_name"] in this.translations) {
|
|
throw new Error("Translation already added to map");
|
|
}
|
|
this.translations[item["short_name"]] = {
|
|
abbreviated_name: item["short_name"],
|
|
display_name: item["full_name"],
|
|
language: language["language"],
|
|
};
|
|
}
|
|
}
|
|
}
|
|
return this.translations;
|
|
}
|
|
|
|
async _get_verse_count(translation:string): Promise<VerseCounts> {
|
|
let json = JSON.parse(await httpGet(
|
|
"https://bolls.life/get-verse-counts/{0}/".format(translation)
|
|
));
|
|
let counts = new VerseCounts();
|
|
counts.books = json;
|
|
return counts
|
|
}
|
|
|
|
async book_to_id(translation: string, book: string): Promise<number> {
|
|
let book_ = book.toLowerCase();
|
|
let map = await this.get_translation_map(translation);
|
|
for (let i in map) {
|
|
let book_data = map[i];
|
|
if (book_ == book_data["name"].toLowerCase()) {
|
|
return Number(i) + 1;
|
|
}
|
|
}
|
|
throw new Error('No book exists by name {0}.'.format(book));
|
|
}
|
|
|
|
chapter_key(translation: string, book_id: number, chapter: Number) {
|
|
return "{0}.{1}.{2}".format(translation, String(book_id), String(chapter));
|
|
}
|
|
|
|
async generate_translation_map(translation: string) {
|
|
let map: Array<Record<string, any>> = JSON.parse(await httpGet(
|
|
"https://bolls.life/get-books/{0}/".format(translation)
|
|
));
|
|
let book_data: Record<BookId, BookData> = [];
|
|
for (let item of map) {
|
|
let chapter_list = [...Array(item["chapters"]).keys()].map(x => x+1)
|
|
book_data[item["bookid"]] = new BookData(
|
|
item["bookid"],
|
|
item["name"],
|
|
chapter_list,
|
|
);
|
|
}
|
|
this.translation_maps[translation] = book_data;
|
|
}
|
|
|
|
async id_to_book(translation: string, book_id: number): Promise<string> {
|
|
let map = await this.get_translation_map(translation);
|
|
return map[book_id - 1]["name"];
|
|
}
|
|
|
|
async get_translation_map(translation: string): Promise<Record<BookId, BookData>> {
|
|
if (!(translation in this.translation_maps)) {
|
|
await this.generate_translation_map(translation);
|
|
}
|
|
return this.translation_maps[translation];
|
|
}
|
|
}
|
|
|
|
const BOOK_ORDERING_LINK = "https://gslogimaker.github.io/my-bible-obsidian-plugin/documents/Book_Ordering"
|
|
const CHAPTER_NAME_FORMAT_LINK = "https://gslogimaker.github.io/my-bible-obsidian-plugin/documents/Format_Settings.html#md:chapters--name-format"
|
|
const CHAPTER_BODY_FORMAT_LINK = "https://gslogimaker.github.io/my-bible-obsidian-plugin/documents/Format_Settings.html#md:chapters--body-format"
|
|
const VERSES_FORMAT_LINK = "https://gslogimaker.github.io/my-bible-obsidian-plugin/documents/Format_Settings.html#md:verses--format"
|
|
const BOOK_NAME_FORMAT_LINK = "https://gslogimaker.github.io/my-bible-obsidian-plugin/documents/Format_Settings.html#md:books--name-format"
|
|
const BOOK_INDEX_NAME_FORMAT_LINK = "https://gslogimaker.github.io/my-bible-obsidian-plugin/documents/Format_Settings.html#md:book-index--name-format"
|
|
const BOOK_INDEX_ELEMENT_FORMAT_LINK = "https://gslogimaker.github.io/my-bible-obsidian-plugin/documents/Format_Settings.html#md:book-index--book-element-format"
|
|
const BOOK_INDEX_BODY_FORMAT_LINK = "https://gslogimaker.github.io/my-bible-obsidian-plugin/documents/Format_Settings.html#md:book-index--body-format"
|
|
const CHAPTER_INDEXS_NAME_FORMAT_LINK = "https://gslogimaker.github.io/my-bible-obsidian-plugin/documents/Format_Settings.html#md:chapter-indexes--name-format"
|
|
const CHAPTER_INDEXS_ELEMENT_FORMAT_LINK = "https://gslogimaker.github.io/my-bible-obsidian-plugin/documents/Format_Settings.html#md:chapter-indexes--book-element-format"
|
|
const CHAPTER_INDEXS_BODY_FORMAT_LINK = "https://gslogimaker.github.io/my-bible-obsidian-plugin/documents/Format_Settings.html#md:chapter-indexes--body-format"
|
|
|
|
class BuilderModal extends Modal {
|
|
plugin: MyBible
|
|
builder: BuildContext
|
|
|
|
description_updators: Record<string, ()=>void>
|
|
|
|
constructor(app: App, plugin: MyBible) {
|
|
super(app);
|
|
this.plugin = plugin;
|
|
this.description_updators = {}
|
|
|
|
this.builder = new BuildContext
|
|
this.builder.plugin = plugin
|
|
this.builder.translation = this.plugin.settings.translation
|
|
this.builder.verse = 35
|
|
this.builder.translation_texts = {
|
|
"translation": this.builder.translation,
|
|
"books": {
|
|
1: {1: ["UNREACHABLE"]},
|
|
22: {5: ["UNREACHABLE"]},
|
|
43: {11: [
|
|
"", "", "", "", "", "", "", "", "", "",
|
|
"", "", "", "", "", "", "", "", "", "",
|
|
"", "", "", "", "", "", "", "", "", "",
|
|
"Jesus wept.",
|
|
]}
|
|
}
|
|
}
|
|
this.builder.chapter = 11
|
|
this.builder.set_books({
|
|
1: new BookData(1, "Genesis", [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
|
|
22: new BookData(22, "Song of Solomon", [0, 0, 0, 0, 1]),
|
|
43: new BookData(43, "John", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35]),
|
|
})
|
|
this.builder.set_book(this.builder.books[43])
|
|
|
|
this.render();
|
|
}
|
|
|
|
render() {
|
|
let containerEl = this.contentEl;
|
|
|
|
new Setting(containerEl).nameEl.createEl("h1", { text: "Book builder" })
|
|
|
|
// Top settings
|
|
|
|
this.renderBibleFolder(new Setting(containerEl));
|
|
|
|
new Setting(containerEl)
|
|
.setName('Translation')
|
|
.setDesc('Builds your Bible according to the layout of this tranlsation. If "Build with dynamic verses" is active, then the text from this version will be built into your Bible.')
|
|
.addDropdown(async drop => {
|
|
drop.addOption(
|
|
SELECTED_TRANSLATION_OPTION_KEY,
|
|
SELECTED_TRANSLATION_OPTION
|
|
.format(this.plugin.settings.reading_translation)
|
|
)
|
|
|
|
let translations = await this.plugin.bible_api.get_translations()
|
|
|
|
let translations_list = []
|
|
for (const key in translations) {
|
|
translations_list.push(key)
|
|
}
|
|
translations_list = translations_list.sort((a, b) => {
|
|
if (translations[a].language < translations[b].language) {
|
|
return -1
|
|
}
|
|
if (translations[a].language > translations[b].language) {
|
|
return 1
|
|
}
|
|
|
|
if (a < b) {
|
|
return -1
|
|
}
|
|
if (a > b) {
|
|
return 1
|
|
}
|
|
|
|
return 0
|
|
})
|
|
|
|
for (const i in translations_list) {
|
|
let key = translations_list[i];
|
|
drop.addOption(
|
|
key,
|
|
translation_to_display_name(translations[key]),
|
|
);
|
|
}
|
|
drop.onChange(async value => {
|
|
await this.plugin.settings.set_translation(value)
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings()
|
|
})
|
|
drop.setValue(this.plugin.settings.translation)
|
|
})
|
|
;
|
|
|
|
this.renderBuildButton(new Setting(containerEl));
|
|
|
|
new Setting(containerEl).nameEl.createEl("h3", { text: "Advanced" })
|
|
|
|
|
|
// Chapters
|
|
|
|
new Setting(containerEl).nameEl.createEl("h4", { text: "Chapters" })
|
|
|
|
this.renderChapterName(new Setting(containerEl));
|
|
|
|
this.renderChapterFormat(new Setting(containerEl));
|
|
|
|
let chapter_padding_setting = new Setting(containerEl)
|
|
.setName('Pad numbers')
|
|
.addToggle(toggle => toggle
|
|
.setTooltip("Toggle alignment padding of chapter numbers")
|
|
.setValue(this.plugin.settings.padded_chapter)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.padded_chapter = value
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings()
|
|
}))
|
|
this.description_updators["chapter_padding"] = () => {
|
|
let desc = chapter_padding_setting.descEl
|
|
this.builder.set_book_and_chapter(this.builder.books[1], 1)
|
|
desc.empty()
|
|
desc.appendText('When active, pads chapter numbers with extra zeros.')
|
|
desc.appendText('For example, "Psalms 5" would become "Psalms 005".')
|
|
desc.createEl("br")
|
|
desc.appendText('Current syntax: ')
|
|
desc.createEl("br")
|
|
desc.createEl("b", {
|
|
"text": this.builder.format_chapter_name(),
|
|
"cls": "u-pop",
|
|
})
|
|
}
|
|
|
|
// Verses
|
|
|
|
new Setting(containerEl).nameEl.createEl("h4", { text: "Verses" })
|
|
|
|
this.renderVerseFormat(new Setting(containerEl))
|
|
|
|
new Setting(containerEl)
|
|
.setName('Build with dynamic verses')
|
|
.setDesc('When active, your Bible will be built with verse references that can be quickly changed when you switch translations. When inactive, the text of the translation is built directly into your Bible.')
|
|
.addToggle(toggle => toggle
|
|
.setTooltip("Toggle generating with dynamic verses")
|
|
.setValue(this.plugin.settings.build_with_dynamic_verses)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.build_with_dynamic_verses = value;
|
|
await this.plugin.saveSettings();
|
|
}))
|
|
|
|
// Books
|
|
let books_header = new Setting(containerEl)
|
|
books_header.nameEl.createEl("h4", { text: "Books" })
|
|
books_header.addToggle(toggle => {toggle
|
|
.setTooltip("Toggle if books should generate folders")
|
|
.setValue(this.plugin.settings.book_folders_enabled)
|
|
.onChange(async value => {
|
|
this.plugin.settings.book_folders_enabled = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
})
|
|
this.renderBookFormat(new Setting(containerEl));
|
|
this.renderNameDelimeter(new Setting(containerEl));
|
|
this.renderBookCapitalization(new Setting(containerEl));
|
|
this.renderBookAbbreviation(new Setting(containerEl));
|
|
this.renderPaddedOrderNums(new Setting(containerEl));
|
|
this.renderBookOrdering(new Setting(containerEl));
|
|
|
|
// Book index
|
|
let index_header = new Setting(containerEl)
|
|
let index_name = new Setting(containerEl)
|
|
let index_links = new Setting(containerEl)
|
|
let index_body = new Setting(containerEl)
|
|
index_header
|
|
.addToggle(toggle => {toggle
|
|
.setTooltip("Toggle book index generation")
|
|
.setValue(this.plugin.settings.index_enabled)
|
|
.onChange(async value => {
|
|
this.plugin.settings.index_enabled = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
})
|
|
.nameEl.createEl("h4", { text: "Book index" })
|
|
;
|
|
|
|
this.renderIndexName(index_name)
|
|
this.renderIndexBookLink(index_links)
|
|
this.renderIndexBody(index_body)
|
|
|
|
// Chapter index
|
|
let chapter_index_header = new Setting(containerEl)
|
|
let chapter_index_name = new Setting(containerEl)
|
|
let chapter_index_links = new Setting(containerEl)
|
|
let chapter_index_body = new Setting(containerEl)
|
|
chapter_index_header
|
|
.addToggle(toggle => {toggle
|
|
.setTooltip("Toggle chapter index generation")
|
|
.setValue(this.plugin.settings.chapter_index_enabled)
|
|
.onChange(async value => {
|
|
this.plugin.settings.chapter_index_enabled = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
})
|
|
.nameEl.createEl("h4", { text: "Chapter indexes" })
|
|
;
|
|
this.renderChapterIndexName(chapter_index_name)
|
|
this.renderChapterIndexLink(chapter_index_links)
|
|
this.renderChapterIndexBody(chapter_index_body)
|
|
|
|
// After
|
|
|
|
this.renderBuildButton(new Setting(containerEl));
|
|
|
|
this.update_descriptions()
|
|
}
|
|
|
|
// Top renderers
|
|
|
|
renderBibleFolder(setting: Setting) {
|
|
setting.clear()
|
|
setting
|
|
.setName('Folder')
|
|
.setDesc('The path to the folder where your Bible will be placed. The folder should be empty for your Bible. If the path does not exist it will be created.')
|
|
.addExtraButton(btn => btn
|
|
.setIcon("rotate-ccw")
|
|
.setTooltip("Reset value")
|
|
.onClick(async () => {
|
|
this.plugin.settings.bible_folder
|
|
= DEFAULT_SETTINGS.bible_folder;
|
|
this.renderBibleFolder(setting);
|
|
await this.plugin.saveSettings();
|
|
})
|
|
)
|
|
.addText(text => text
|
|
.setPlaceholder("Example: " + DEFAULT_SETTINGS.bible_folder)
|
|
.setValue(this.plugin.settings.bible_folder)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.bible_folder = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
)
|
|
;
|
|
}
|
|
|
|
renderBuildButton(setting: Setting) {
|
|
setting
|
|
.addButton(btn => btn
|
|
.setButtonText("Build")
|
|
.setTooltip("Build your bible")
|
|
.setCta()
|
|
.onClick(async _ => {
|
|
this.close()
|
|
await this.plugin.build_bible()
|
|
})
|
|
)
|
|
;
|
|
}
|
|
|
|
// Book renderers
|
|
|
|
renderBookFormat(setting: Setting) {
|
|
setting.clear()
|
|
setting
|
|
.setName('Name format')
|
|
.setDesc('The format for the names of the folders of each book of the bible. For example, "{order} {name}" would become "2 Exodus". Leave blank to not have folders for each book.')
|
|
.addExtraButton(btn => btn
|
|
.setIcon("rotate-ccw")
|
|
.setTooltip("Reset value")
|
|
.onClick(async () => {
|
|
this.plugin.settings.book_name_format
|
|
= DEFAULT_SETTINGS.book_name_format
|
|
this.renderBookFormat(setting)
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings()
|
|
})
|
|
)
|
|
.addText(text => text
|
|
.setPlaceholder("Example: " + DEFAULT_SETTINGS.book_name_format)
|
|
.setValue(this.plugin.settings.book_name_format)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.book_name_format = value
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings()
|
|
})
|
|
)
|
|
;
|
|
this.description_updators["book_name"] = () => {
|
|
let desc = setting.descEl
|
|
this.builder.set_book_and_chapter(this.builder.books[43], 11)
|
|
desc.empty()
|
|
desc.appendText('The format for the names of the folders of each book of the bible. For example, "{order} {name}" would become "2 Exodus". Leave blank to not have folders for each book. ')
|
|
desc.createEl("a", {
|
|
"text":"More about formatting",
|
|
"href":BOOK_NAME_FORMAT_LINK,
|
|
})
|
|
desc.appendText(". ")
|
|
desc.createEl("br", "")
|
|
desc.appendText("Current syntax: ")
|
|
desc.createEl("br")
|
|
desc.createEl("b", {
|
|
"text": "{0}".format(this.builder.format_book_name()),
|
|
"cls": "u-pop",
|
|
})
|
|
}
|
|
}
|
|
|
|
renderBookAbbreviation(setting: Setting) {
|
|
setting.clear()
|
|
setting
|
|
.setName('Abbreviate names')
|
|
.setDesc('When active, The names of books will be abbreviated to three letters. For example, "Genesis" becomes "Gen" and "1 Kings" becomes "1Ki". (May cause issues in some translations.)')
|
|
.addToggle(toggle => toggle
|
|
.setTooltip("Toggle book name abbreviation")
|
|
.setValue(this.plugin.settings.book_name_abbreviated)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.book_name_abbreviated = value;
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings();
|
|
})
|
|
)
|
|
;
|
|
}
|
|
|
|
renderBookCapitalization(setting: Setting) {
|
|
setting.clear()
|
|
setting
|
|
.setName('Name capitalization')
|
|
.setDesc('Dictates the capitalization of book names.')
|
|
.addDropdown(drop => drop
|
|
.addOption("lower_case", "Lower case")
|
|
.addOption("name_case", "Name case")
|
|
.addOption("upper_case", "Upper case")
|
|
.setValue(this.plugin.settings.book_name_capitalization)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.book_name_capitalization = value
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings()
|
|
})
|
|
)
|
|
;
|
|
}
|
|
|
|
renderNameDelimeter(setting: Setting) {
|
|
setting.clear()
|
|
setting
|
|
.setName('Name delimiter')
|
|
.addExtraButton(btn => btn
|
|
.setIcon("rotate-ccw")
|
|
.setTooltip("Reset value")
|
|
.onClick(async () => {
|
|
this.plugin.settings.book_name_delimiter
|
|
= DEFAULT_SETTINGS.book_name_delimiter;
|
|
this.renderNameDelimeter(setting);
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings();
|
|
})
|
|
)
|
|
.addText(text => text
|
|
.setPlaceholder('Example: "' + DEFAULT_SETTINGS.book_name_delimiter + '"')
|
|
.setValue(this.plugin.settings.book_name_delimiter)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.book_name_delimiter = value;
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings();
|
|
})
|
|
)
|
|
;
|
|
this.description_updators["name_delimeter"] = () => {
|
|
let desc = setting.descEl
|
|
this.builder.set_book_and_chapter(this.builder.books[22], 5)
|
|
desc.empty()
|
|
desc.appendText('The characters separating words in book names, such as the spaces in "1 John" or "Song of Solomon".');
|
|
desc.createEl("br")
|
|
|
|
desc.appendText("Current syntax: ")
|
|
desc.createEl("br")
|
|
desc.createEl("b", {
|
|
"text": "{0}".format(this.builder.format_book_name(
|
|
new BookData(
|
|
DEFAULT_NAME_MAP["Song of Solomon"],
|
|
"Song of Solomon",
|
|
[],
|
|
),
|
|
BookNameStyle.Full,
|
|
))
|
|
.slice(3),
|
|
"cls": "u-pop",
|
|
})
|
|
}
|
|
}
|
|
|
|
renderPaddedOrderNums(setting: Setting) {
|
|
setting.clear()
|
|
setting
|
|
.setName('Pad order numbers')
|
|
.addToggle(toggle => toggle
|
|
.setTooltip("Toggle alignment padding of book order numbers")
|
|
.setValue(this.plugin.settings.padded_order)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.padded_order = value
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings()
|
|
})
|
|
)
|
|
;
|
|
this.description_updators["padded_order"] = () => {
|
|
let desc = setting.descEl
|
|
this.builder.set_book_and_chapter(this.builder.books[1], 1)
|
|
desc.empty()
|
|
desc.appendText('When Active, pads order numbers with extra zeros. ');
|
|
desc.createEl("br")
|
|
desc.appendText("Current syntax: ")
|
|
desc.createEl("br")
|
|
desc.createEl("b", {
|
|
"text": "{0}".format(this.builder.format_book_name()),
|
|
"cls": "u-pop",
|
|
})
|
|
}
|
|
}
|
|
|
|
renderBookOrdering(setting: Setting) {
|
|
setting.clear()
|
|
setting
|
|
.setName('Book ordering')
|
|
.addDropdown((x) => x
|
|
.addOption("christian", "Christian")
|
|
.addOption("hebraic", "Hebraic")
|
|
.setValue(this.plugin.settings.book_ordering)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.book_ordering = value
|
|
await this.plugin.saveSettings()
|
|
})
|
|
)
|
|
;
|
|
|
|
this.description_updators["book_ordering"] = () => {
|
|
let desc = setting.descEl
|
|
this.builder.set_book_and_chapter(this.builder.books[43], 11)
|
|
desc.empty()
|
|
desc.appendText("Choose how you want the books ordered in your Bible. ");
|
|
desc.createEl("a", {
|
|
"text": "More about book ordering",
|
|
"href": BOOK_ORDERING_LINK,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Chapter renderers
|
|
|
|
renderChapterFormat(setting: Setting) {
|
|
setting.clear()
|
|
setting
|
|
.setName('Body format')
|
|
.addExtraButton(btn => btn
|
|
.setIcon("rotate-ccw")
|
|
.setTooltip("Reset value")
|
|
.onClick(async () => {
|
|
this.plugin.settings.chapter_body_format
|
|
= DEFAULT_SETTINGS.chapter_body_format;
|
|
this.renderChapterFormat(setting);
|
|
await this.plugin.saveSettings();
|
|
})
|
|
)
|
|
.addTextArea(text => text
|
|
.setPlaceholder("Example: " + DEFAULT_SETTINGS.chapter_body_format)
|
|
.setValue(this.plugin.settings.chapter_body_format)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.chapter_body_format = value;
|
|
await this.plugin.saveSettings();
|
|
}
|
|
)
|
|
)
|
|
;
|
|
this.description_updators["chapter_body"] = () => {
|
|
let desc = setting.descEl
|
|
desc.empty()
|
|
desc.appendText("Formats the contents of chapters. ")
|
|
desc.createEl("a", {
|
|
"text": "More on formatting",
|
|
"href": CHAPTER_BODY_FORMAT_LINK,
|
|
})
|
|
desc.appendText(".")
|
|
}
|
|
this.description_updators["chapter_body"]()
|
|
}
|
|
|
|
renderChapterName(setting: Setting) {
|
|
setting.clear()
|
|
setting
|
|
.setName('Name format')
|
|
.addExtraButton(btn => btn
|
|
.setIcon("rotate-ccw")
|
|
.setTooltip("Reset value")
|
|
.onClick(async () => {
|
|
this.plugin.settings.chapter_name_format
|
|
= DEFAULT_SETTINGS.chapter_name_format;
|
|
this.renderChapterName(setting);
|
|
await this.plugin.saveSettings();
|
|
})
|
|
)
|
|
.addText(text => text
|
|
.setPlaceholder(this.plugin.settings.chapter_name_format)
|
|
.setValue(this.plugin.settings.chapter_name_format)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.chapter_name_format = value
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings()
|
|
})
|
|
)
|
|
;
|
|
|
|
this.description_updators["chapter_name"] = () => {
|
|
let desc = setting.descEl
|
|
this.builder.set_book_and_chapter(this.builder.books[43], 11)
|
|
desc.empty()
|
|
desc.appendText("The format for the names of chapters. ")
|
|
desc.createEl("a", {
|
|
"text": "More about formatting",
|
|
"href": CHAPTER_NAME_FORMAT_LINK,
|
|
})
|
|
desc.appendText(". ")
|
|
desc.createEl("br")
|
|
desc.appendText("Current syntax: ")
|
|
desc.createEl("br")
|
|
desc.createEl("b", {
|
|
"text": "{0}".format(this.builder.format_chapter_name()),
|
|
"cls": "u-pop",
|
|
})
|
|
}
|
|
this.description_updators["chapter_name"]()
|
|
}
|
|
|
|
// Verse renders
|
|
|
|
renderVerseFormat(setting: Setting) {
|
|
setting.clear()
|
|
setting
|
|
.setName('Format')
|
|
.setDesc('Formatting for individual verses.')
|
|
.addExtraButton(btn => btn
|
|
.setIcon("rotate-ccw")
|
|
.setTooltip("Reset value")
|
|
.onClick(async () => {
|
|
this.plugin.settings.verse_body_format
|
|
= DEFAULT_SETTINGS.verse_body_format;
|
|
this.renderVerseFormat(setting);
|
|
await this.plugin.saveSettings();
|
|
})
|
|
)
|
|
.addTextArea(text => text
|
|
.setPlaceholder("Example: " + DEFAULT_SETTINGS.verse_body_format)
|
|
.setValue(this.plugin.settings.verse_body_format)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.verse_body_format = value;
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings();
|
|
})
|
|
)
|
|
;
|
|
this.description_updators["verse_body"] = () => {
|
|
let desc = setting.descEl
|
|
this.builder.set_book_and_chapter(this.builder.books[43], 11)
|
|
desc.empty()
|
|
desc.appendText("Formats individual verses. ")
|
|
desc.createEl("a", {
|
|
"text": "More about formatting",
|
|
"href": VERSES_FORMAT_LINK,
|
|
})
|
|
desc.appendText(". ")
|
|
desc.createEl("br")
|
|
desc.appendText("Current syntax: ")
|
|
for (const line of this.builder.format_verse_body("Jesus wept.").split("\n")) {
|
|
desc.createEl("br")
|
|
desc.createEl("b", {
|
|
"text": line,
|
|
"cls": "u-pop",
|
|
})
|
|
}
|
|
}
|
|
this.description_updators["verse_body"]()
|
|
}
|
|
|
|
// Book index
|
|
|
|
renderIndexName(setting: Setting) {
|
|
setting.clear()
|
|
setting
|
|
.setName('Name format')
|
|
.setDesc('The format for the name of the book index.')
|
|
.addExtraButton(btn => btn
|
|
.setIcon("rotate-ccw")
|
|
.setTooltip("Reset value")
|
|
.onClick(async () => {
|
|
this.plugin.settings.index_name_format
|
|
= DEFAULT_SETTINGS.index_name_format;
|
|
this.renderIndexName(setting)
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings();
|
|
})
|
|
)
|
|
.addText(text => text
|
|
.setPlaceholder(DEFAULT_SETTINGS.index_name_format)
|
|
.setValue(this.plugin.settings.index_name_format)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.index_name_format = value
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings()
|
|
})
|
|
)
|
|
;
|
|
this.description_updators["index_name"] = () => {
|
|
let desc = setting.descEl
|
|
this.builder.set_book_and_chapter(this.builder.books[22], 5)
|
|
desc.empty()
|
|
desc.appendText('The format for the name of the book index. ')
|
|
desc.createEl("a", {
|
|
"text": "More about formatting",
|
|
"href": BOOK_INDEX_NAME_FORMAT_LINK,
|
|
})
|
|
desc.appendText(". ")
|
|
desc.createEl("br")
|
|
|
|
desc.appendText("Current syntax: ")
|
|
desc.createEl("br")
|
|
desc.createEl("b", {
|
|
"text": this.builder.format_index_name(),
|
|
"cls": "u-pop",
|
|
})
|
|
}
|
|
this.description_updators["index_name"]()
|
|
}
|
|
|
|
renderIndexBookLink(setting: Setting) {
|
|
setting.clear()
|
|
setting
|
|
.setName('Book element format')
|
|
.addExtraButton(btn => btn
|
|
.setIcon("rotate-ccw")
|
|
.setTooltip("Reset value")
|
|
.onClick(async () => {
|
|
this.plugin.settings.index_link_format
|
|
= DEFAULT_SETTINGS.index_link_format
|
|
this.renderIndexBookLink(setting)
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings()
|
|
})
|
|
)
|
|
.addText(text => text
|
|
.setPlaceholder(DEFAULT_SETTINGS.index_link_format)
|
|
.setValue(this.plugin.settings.index_link_format)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.index_link_format = value;
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings();
|
|
})
|
|
)
|
|
;
|
|
setting.setDisabled(!this.plugin.settings.index_enabled)
|
|
|
|
this.description_updators["index_element"] = () => {
|
|
let desc = setting.descEl
|
|
this.builder.set_book_and_chapter(this.builder.books[22], 5)
|
|
desc.empty()
|
|
desc.appendText('The format for each book element in this index list. ')
|
|
desc.createEl("a", {
|
|
"text": "More about formatting",
|
|
"href": BOOK_INDEX_ELEMENT_FORMAT_LINK,
|
|
})
|
|
desc.appendText(". ")
|
|
|
|
desc.createEl("br")
|
|
desc.appendText("Current syntax: ")
|
|
desc.createEl("br")
|
|
desc.createEl("b", {
|
|
"text": "{0}".format(this.builder.format_index_element()),
|
|
"cls": "u-pop",
|
|
})
|
|
}
|
|
this.description_updators["index_element"]()
|
|
}
|
|
|
|
renderIndexBody(setting: Setting) {
|
|
setting.clear()
|
|
setting
|
|
.setName('Body format')
|
|
.setDesc('The format for the content of the book index.')
|
|
.addExtraButton(btn => btn
|
|
.setIcon("rotate-ccw")
|
|
.setTooltip("Reset value")
|
|
.onClick(async () => {
|
|
this.plugin.settings.index_format
|
|
= DEFAULT_SETTINGS.index_format
|
|
this.renderIndexBody(setting)
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings()
|
|
})
|
|
)
|
|
.addTextArea(text => text
|
|
.setPlaceholder(DEFAULT_SETTINGS.index_format)
|
|
.setValue(this.plugin.settings.index_format)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.index_format = value
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings()
|
|
})
|
|
)
|
|
;
|
|
setting.setDisabled(!this.plugin.settings.index_enabled)
|
|
|
|
this.description_updators["index_body"] = () => {
|
|
let desc = setting.descEl
|
|
this.builder.set_book_and_chapter(this.builder.books[22], 5)
|
|
desc.empty()
|
|
desc.appendText('The format for each book element in this index list. ')
|
|
desc.createEl("a", {
|
|
"text": "More about formatting",
|
|
"href": BOOK_INDEX_BODY_FORMAT_LINK,
|
|
})
|
|
desc.appendText(". ")
|
|
}
|
|
this.description_updators["index_body"]()
|
|
}
|
|
|
|
// Chapter index
|
|
|
|
renderChapterIndexName(setting: Setting) {
|
|
setting.clear()
|
|
setting
|
|
.setName('Name format')
|
|
.addExtraButton(btn => btn
|
|
.setIcon("rotate-ccw")
|
|
.setTooltip("Reset value")
|
|
.onClick(async () => {
|
|
this.plugin.settings.chapter_index_name_format
|
|
= DEFAULT_SETTINGS.chapter_index_name_format
|
|
this.renderChapterIndexName(setting)
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings()
|
|
})
|
|
)
|
|
.addText(text => text
|
|
.setPlaceholder(DEFAULT_SETTINGS.chapter_index_name_format)
|
|
.setValue(this.plugin.settings.chapter_index_name_format)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.chapter_index_name_format = value
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings()
|
|
})
|
|
)
|
|
;
|
|
setting.setDisabled(!this.plugin.settings.index_enabled)
|
|
|
|
this.description_updators["chapter_index_name"] = () => {
|
|
let desc = setting.descEl
|
|
this.builder.set_book_and_chapter(this.builder.books[43], 11)
|
|
desc.empty()
|
|
desc.appendText('The format for the name of the chapter index. ')
|
|
desc.createEl("a", {
|
|
"text": "More on formatting",
|
|
"href": CHAPTER_INDEXS_NAME_FORMAT_LINK
|
|
})
|
|
desc.appendText(".")
|
|
|
|
desc.createEl("br")
|
|
desc.appendText("Current syntax: ")
|
|
desc.createEl("br")
|
|
desc.createEl("b", {
|
|
"text": this.builder.format_chapter_index_name(),
|
|
"cls": "u-pop",
|
|
})
|
|
}
|
|
this.description_updators["chapter_index_name"]()
|
|
}
|
|
|
|
renderChapterIndexLink(setting: Setting) {
|
|
setting.clear()
|
|
setting
|
|
.setName('Chapter element format')
|
|
.addExtraButton(btn => btn
|
|
.setIcon("rotate-ccw")
|
|
.setTooltip("Reset value")
|
|
.onClick(async () => {
|
|
this.plugin.settings.chapter_index_link_format
|
|
= DEFAULT_SETTINGS.chapter_index_link_format
|
|
this.renderChapterIndexLink(setting)
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings()
|
|
})
|
|
)
|
|
.addText(text => text
|
|
.setPlaceholder(DEFAULT_SETTINGS.chapter_index_link_format)
|
|
.setValue(this.plugin.settings.chapter_index_link_format)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.chapter_index_link_format = value
|
|
this.update_descriptions()
|
|
await this.plugin.saveSettings()
|
|
})
|
|
)
|
|
;
|
|
setting.setDisabled(!this.plugin.settings.index_enabled)
|
|
|
|
this.description_updators["chapter_index_element"] = () => {
|
|
let desc = setting.descEl
|
|
this.builder.set_book_and_chapter(this.builder.books[43], 11)
|
|
desc.empty()
|
|
desc.appendText('The format for each chapter element in a list. ')
|
|
desc.createEl("a", {
|
|
"text": "More on formatting",
|
|
"href": CHAPTER_INDEXS_ELEMENT_FORMAT_LINK,
|
|
})
|
|
desc.appendText(".")
|
|
|
|
desc.createEl("br")
|
|
desc.appendText("Current syntax: ")
|
|
desc.createEl("br")
|
|
desc.createEl("b", {
|
|
"text": this.builder.format_chapter_index_element(),
|
|
"cls": "u-pop",
|
|
})
|
|
}
|
|
this.description_updators["chapter_index_element"]()
|
|
}
|
|
|
|
renderChapterIndexBody(setting: Setting) {
|
|
setting.clear()
|
|
setting
|
|
.setName('Body format')
|
|
.setDesc('The format for the content of the chapter index.')
|
|
.addExtraButton(btn => btn
|
|
.setIcon("rotate-ccw")
|
|
.setTooltip("Reset value")
|
|
.onClick(async () => {
|
|
this.plugin.settings.chapter_index_format
|
|
= DEFAULT_SETTINGS.chapter_index_format;
|
|
this.renderChapterIndexBody(setting);
|
|
await this.plugin.saveSettings();
|
|
})
|
|
)
|
|
.addTextArea(text => text
|
|
.setPlaceholder(DEFAULT_SETTINGS.chapter_index_format)
|
|
.setValue(this.plugin.settings.chapter_index_format)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.chapter_index_format = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
)
|
|
;
|
|
setting.setDisabled(!this.plugin.settings.index_enabled)
|
|
|
|
this.description_updators["chapter_index_body"] = () => {
|
|
let desc = setting.descEl
|
|
this.builder.set_book_and_chapter(this.builder.books[43], 11)
|
|
desc.empty()
|
|
desc.appendText('The format for the content of the chapter index. ')
|
|
desc.createEl("a", {
|
|
"text": "More on formatting",
|
|
"href": CHAPTER_INDEXS_BODY_FORMAT_LINK,
|
|
})
|
|
desc.appendText(".")
|
|
}
|
|
this.description_updators["chapter_index_body"]()
|
|
}
|
|
|
|
|
|
refresh() {
|
|
this.contentEl.empty()
|
|
this.render()
|
|
}
|
|
|
|
update_descriptions() {
|
|
for (const key of Object.keys(this.description_updators)) {
|
|
this.description_updators[key]()
|
|
}
|
|
}
|
|
}
|
|
|
|
class ErrorModal extends Modal {
|
|
plugin: MyBible
|
|
title: string
|
|
body: string
|
|
|
|
constructor(app: App, plugin: MyBible, title:string, body:string) {
|
|
super(app);
|
|
this.plugin = plugin;
|
|
this.title = title;
|
|
this.body = body;
|
|
}
|
|
|
|
onOpen() {
|
|
const { contentEl } = this;
|
|
|
|
contentEl.createEl("h1", { text: this.title });
|
|
contentEl.createEl("span", { text: this.body });
|
|
|
|
contentEl.createEl("p", {});
|
|
|
|
new Setting(contentEl)
|
|
.addButton((btn) =>
|
|
btn
|
|
.setButtonText("Report an issue")
|
|
.setCta()
|
|
.onClick(() => {
|
|
window.open(
|
|
REPORT_ISSUE_URL,
|
|
'_blank'
|
|
);
|
|
})
|
|
)
|
|
.addButton((btn) =>
|
|
btn
|
|
.setButtonText("Dismiss")
|
|
.onClick(() => {
|
|
this.close();
|
|
})
|
|
)
|
|
}
|
|
|
|
onClose() {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
}
|
|
}
|
|
|
|
class ClearLocalFilesModal extends Modal {
|
|
plugin: MyBible;
|
|
|
|
constructor(app: App, plugin: MyBible) {
|
|
super(app);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
onOpen() {
|
|
const { contentEl } = this;
|
|
|
|
let bible_path = this.plugin.settings.bible_folder;
|
|
|
|
contentEl.createEl("h1", { text: "Clear local files?" });
|
|
contentEl.createEl("span", {
|
|
text: "You are about to clear out all files added by My Bible from your file system. This includes temporay cached chapters and translations you manually downloaded."
|
|
.format(bible_path)
|
|
});
|
|
contentEl.createEl("p", {
|
|
text: "Do you want to clear these files?"
|
|
});
|
|
contentEl.createEl("p", {});
|
|
|
|
new Setting(contentEl)
|
|
.addButton((btn) =>
|
|
btn
|
|
.setButtonText("Cancel")
|
|
.onClick(() => {
|
|
this.close();
|
|
})
|
|
)
|
|
.addButton((btn) =>
|
|
btn
|
|
.setButtonText("Clear files")
|
|
.setCta()
|
|
.onClick(async () => {
|
|
this.close();
|
|
await this.plugin.bible_api.clear_local_files();
|
|
new Notice("Files cleared!");
|
|
})
|
|
);
|
|
}
|
|
|
|
onClose() {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
}
|
|
}
|
|
|
|
class ClearOldBibleFilesModal extends Modal {
|
|
plugin: MyBible
|
|
|
|
constructor(app: App, plugin: MyBible) {
|
|
super(app);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
onOpen() {
|
|
const { contentEl } = this;
|
|
|
|
let bible_path = this.plugin.settings.bible_folder;
|
|
|
|
contentEl.createEl("h1", { text: "Bible folder is not empty" });
|
|
contentEl.createEl("span", {
|
|
text: "The Bible folder in your settings is not empty. If you built a Bible in this folder before the new bible may be interlaced with the old one."
|
|
.format(bible_path)
|
|
});
|
|
contentEl.createEl("p", {
|
|
text: "Do you want to clear the folder before building your Bible?"
|
|
});
|
|
contentEl.createEl("p", {});
|
|
|
|
new Setting(contentEl)
|
|
.addButton((btn) =>
|
|
btn
|
|
.setButtonText("Cancel")
|
|
.onClick(() => {
|
|
this.close();
|
|
|
|
})
|
|
)
|
|
.addButton((btn) =>
|
|
btn
|
|
.setButtonText("Build without clearing")
|
|
.setCta()
|
|
.onClick(async () => {
|
|
this.close();
|
|
await this.plugin._build_bible(bible_path);
|
|
})
|
|
)
|
|
.addButton((btn) =>
|
|
btn
|
|
.setButtonText("Clear and build")
|
|
.setCta()
|
|
.onClick(async () => {
|
|
this.close();
|
|
let notice = new Notice("Clearing bible folder...", 0)
|
|
|
|
let abstract = this.app.vault.getAbstractFileByPath(
|
|
normalizePath(bible_path,)
|
|
);
|
|
if (abstract != null) {
|
|
await this.app.vault.delete(abstract, true);
|
|
}
|
|
this.app.vault.adapter.mkdir(normalizePath(bible_path));
|
|
|
|
notice.hide()
|
|
await this.plugin._build_bible(bible_path);
|
|
})
|
|
);
|
|
}
|
|
|
|
onClose() {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
}
|
|
}
|
|
|
|
class DownloadBibleModal extends Modal {
|
|
plugin: MyBible;
|
|
|
|
constructor(app: App, plugin: MyBible) {
|
|
super(app);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
onOpen() {
|
|
const { contentEl } = this;
|
|
|
|
let translation = this.plugin.settings.translation;
|
|
|
|
contentEl.createEl("h1", { text: "Download bible?" });
|
|
contentEl.createEl("span", {
|
|
text: "You are about to download the entire {0} translation of the Bible, according to your settings."
|
|
.format(translation)
|
|
});
|
|
contentEl.createEl("p", {
|
|
text: "Do you want to continue?"
|
|
});
|
|
contentEl.createEl("p", {});
|
|
|
|
new Setting(contentEl)
|
|
.addButton((btn) =>
|
|
btn
|
|
.setButtonText("Cancel")
|
|
.onClick(() => {
|
|
this.close();
|
|
})
|
|
)
|
|
.addButton((btn) =>
|
|
btn
|
|
.setButtonText("Download")
|
|
.setCta()
|
|
.onClick(() => {
|
|
this.close();
|
|
this.downloadBible(translation);
|
|
})
|
|
);
|
|
}
|
|
|
|
onClose() {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
}
|
|
|
|
async downloadBible(translation: string) {
|
|
new Notice(
|
|
'Started download of the {0} translation!'
|
|
.format(translation)
|
|
);
|
|
|
|
let bible = await this.plugin.bible_api.get_translation(translation);
|
|
for (const book_id in bible.books) {
|
|
for (const chapter_key of Object.keys(bible.books[book_id])) {
|
|
const chapter = Number(chapter_key)
|
|
await this.plugin.bible_api.cache_chapter(
|
|
translation,
|
|
Number(book_id),
|
|
chapter + 1,
|
|
bible.books[book_id][chapter],
|
|
true,
|
|
);
|
|
}
|
|
}
|
|
|
|
new Notice(
|
|
'Completed download of the {0} translation!'
|
|
.format(translation)
|
|
);
|
|
}
|
|
}
|
|
|
|
export class QuickChangeTranslationeModal extends FuzzySuggestModal<Translation> {
|
|
plugin: MyBible
|
|
translations: Translations
|
|
onChose: (chosen:Translation) => (void|Promise<void>) | null
|
|
|
|
constructor(plugin: MyBible) {
|
|
super(plugin.app);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
// Returns all available suggestions.
|
|
getItems(): Translation[] {
|
|
let translations_list = Object.keys(this.translations).map(k => this.translations[k])
|
|
return translations_list
|
|
}
|
|
|
|
getItemText(item:Translation):string {
|
|
return translation_to_display_name(item)
|
|
}
|
|
|
|
// Perform action on the selected suggestion.
|
|
onChooseItem(item: Translation, evt: MouseEvent | KeyboardEvent) {
|
|
if (this.onChose !== null) {
|
|
this.onChose(item)
|
|
}
|
|
}
|
|
}
|
|
|
|
class SettingsTab extends PluginSettingTab {
|
|
plugin: MyBible;
|
|
|
|
constructor(app: App, plugin: MyBible) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
display(): void {
|
|
const { containerEl } = this;
|
|
|
|
containerEl.empty();
|
|
|
|
new Setting(containerEl)
|
|
.setName('Translation')
|
|
.setDesc('The version of the Bible that will be displayed in dynamic verses.')
|
|
.addDropdown(async drop => {
|
|
let translations = await this.plugin.bible_api.get_translations();
|
|
|
|
let translations_list = [];
|
|
for (const key in translations) {
|
|
translations_list.push(key);
|
|
}
|
|
translations_list = translations_list.sort((a, b) => {
|
|
if (translations[a].language < translations[b].language) {
|
|
return -1;
|
|
}
|
|
if (translations[a].language > translations[b].language) {
|
|
return 1;
|
|
}
|
|
|
|
if (a < b) {
|
|
return -1;
|
|
}
|
|
if (a > b) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
})
|
|
|
|
for (const i in translations_list) {
|
|
let key = translations_list[i]
|
|
drop.addOption(
|
|
key,
|
|
"{0} - {1} - {2}".format(
|
|
translations[key].language,
|
|
key,
|
|
translations[key].display_name,
|
|
)
|
|
);
|
|
}
|
|
drop.onChange(async value => {
|
|
this.plugin.settings.reading_translation = value;
|
|
await this.plugin.saveSettings()
|
|
})
|
|
drop.setValue(this.plugin.settings.reading_translation)
|
|
})
|
|
;
|
|
|
|
let e_js_e = new Setting(containerEl)
|
|
.setName('Enable Javascript execution')
|
|
.setDesc('Enables the execution of Javascript code in `mybible` codeblocks.')
|
|
.addToggle(x => x
|
|
.setValue(this.plugin.settings.enable_javascript_execution)
|
|
.setTooltip("Enable or disable Javascript execution")
|
|
.onChange(async v => {
|
|
this.plugin.settings.enable_javascript_execution = v
|
|
await this.plugin.saveSettings()
|
|
})
|
|
)
|
|
;
|
|
e_js_e.descEl.appendText(" For more about using Javascript in MyBible see the ")
|
|
e_js_e.descEl.createEl("a", {
|
|
text: "Javascript API",
|
|
href: "https://gslogimaker.github.io/my-bible-obsidian-plugin/modules/api.mb",
|
|
})
|
|
e_js_e.descEl.appendText(".")
|
|
|
|
new Setting(containerEl)
|
|
.setDesc('Looking for build settings? They have been moved to the `Book builder` menu. You can access the menu via the `My Bible: Build Bible` command.')
|
|
;
|
|
}
|
|
}
|
|
|
|
function book_id_to_name(id: BookId):string {
|
|
for (const x of Object.keys(DEFAULT_NAME_MAP)) {
|
|
if (DEFAULT_NAME_MAP[x] === id) {
|
|
return x
|
|
}
|
|
}
|
|
throw new Error("No book by ID {0} exists".format(String(id)))
|
|
}
|
|
|
|
function to_hebraic_order(id: BookId) {
|
|
const BOOK_NAME = book_id_to_name(id)
|
|
if (!(BOOK_NAME in HEBRAIC_ORDER)) {
|
|
return id
|
|
}
|
|
return HEBRAIC_ORDER[BOOK_NAME]
|
|
}
|
|
|
|
export const DEFAULT_NAME_MAP: Record<string, BookId> = {
|
|
"Genesis": 1,
|
|
"Exodus": 2,
|
|
"Leviticus": 3,
|
|
"Numbers": 4,
|
|
"Deuteronomy": 5,
|
|
"Joshua": 6,
|
|
"Judges": 7,
|
|
"Ruth": 8,
|
|
"1 Samuel": 9,
|
|
"2 Samuel": 10,
|
|
"1 Kings": 11,
|
|
"2 Kings": 12,
|
|
"1 Chronicles": 13,
|
|
"2 Chronicles": 14,
|
|
"Ezra": 15,
|
|
"Nehemiah": 16,
|
|
"Esther": 17,
|
|
"Job": 18,
|
|
"Psalms": 19,
|
|
"Psalm": 19, // Alternate spelling
|
|
"Proverbs": 20,
|
|
"Ecclesiastes": 21,
|
|
"Song of Solomon": 22,
|
|
"Isaiah": 23,
|
|
"Jeremiah": 24,
|
|
"Lamentations": 25,
|
|
"Ezekiel": 26,
|
|
"Daniel": 27,
|
|
"Hosea": 28,
|
|
"Joel": 29,
|
|
"Amos": 30,
|
|
"Obadiah": 31,
|
|
"Jonah": 32,
|
|
"Micah": 33,
|
|
"Nahum": 34,
|
|
"Habakkuk": 35,
|
|
"Zephaniah": 36,
|
|
"Haggai": 37,
|
|
"Zechariah": 38,
|
|
"Malachi": 39,
|
|
"Matthew": 40,
|
|
"Mark": 41,
|
|
"Luke": 42,
|
|
"John": 43,
|
|
"Acts": 44,
|
|
"Romans": 45,
|
|
"1 Corinthians": 46,
|
|
"2 Corinthians": 47,
|
|
"Galatians": 48,
|
|
"Ephesians": 49,
|
|
"Philippians": 50,
|
|
"Colossians": 51,
|
|
"1 Thessalonians": 52,
|
|
"2 Thessalonians": 53,
|
|
"1 Timothy": 54,
|
|
"2 Timothy": 55,
|
|
"Titus": 56,
|
|
"Philemon": 57,
|
|
"Hebrews": 58,
|
|
"James": 59,
|
|
"1 Peter": 60,
|
|
"2 Peter": 61,
|
|
"1 John": 62,
|
|
"2 John": 63,
|
|
"3 John": 64,
|
|
"Jude": 65,
|
|
"Revelation": 66,
|
|
"1 Esdras": 67,
|
|
"Tobit": 68,
|
|
"Judith": 69,
|
|
"Wisdom": 70,
|
|
"Sirach": 71,
|
|
"Epistle of Jeremiah": 72,
|
|
"Baruch": 73,
|
|
"1 Maccabees": 74,
|
|
"2 Maccabees": 75,
|
|
"3 Maccabees": 76,
|
|
"2 Esdras": 77,
|
|
"Susanna": 78,
|
|
"Bel and Dragon": 79,
|
|
"4 Maccabees": 80,
|
|
"Greek Additions to Esther": 81,
|
|
"3 Holy Children's Song": 82,
|
|
"Prayer of Manasseh": 83,
|
|
"Azariah": 88, // TODO: Fill in this jump in number
|
|
}
|
|
|
|
export const BOOK_ID_TO_NAME: Record<BookId, string> = {
|
|
1: "Genesis",
|
|
2: "Exodus",
|
|
3: "Leviticus",
|
|
4: "Numbers",
|
|
5: "Deuteronomy",
|
|
6: "Joshua",
|
|
7: "Judges",
|
|
8: "Ruth",
|
|
9: "1 Samuel",
|
|
10: "2 Samuel",
|
|
11: "1 Kings",
|
|
12: "2 Kings",
|
|
13: "1 Chronicles",
|
|
14: "2 Chronicles",
|
|
15: "Ezra",
|
|
16: "Nehemiah",
|
|
17: "Esther",
|
|
18: "Job",
|
|
19: "Psalms",
|
|
20: "Proverbs",
|
|
21: "Ecclesiastes",
|
|
22: "Song of Solomon",
|
|
23: "Isaiah",
|
|
24: "Jeremiah",
|
|
25: "Lamentations",
|
|
26: "Ezekiel",
|
|
27: "Daniel",
|
|
28: "Hosea",
|
|
29: "Joel",
|
|
30: "Amos",
|
|
31: "Obadiah",
|
|
32: "Jonah",
|
|
33: "Micah",
|
|
34: "Nahum",
|
|
35: "Habakkuk",
|
|
36: "Zephaniah",
|
|
37: "Haggai",
|
|
38: "Zechariah",
|
|
39: "Malachi",
|
|
40: "Matthew",
|
|
41: "Mark",
|
|
42: "Luke",
|
|
43: "John",
|
|
44: "Acts",
|
|
45: "Romans",
|
|
46: "1 Corinthians",
|
|
47: "2 Corinthians",
|
|
48: "Galatians",
|
|
49: "Ephesians",
|
|
50: "Philippians",
|
|
51: "Colossians",
|
|
52: "1 Thessalonians",
|
|
53: "2 Thessalonians",
|
|
54: "1 Timothy",
|
|
55: "2 Timothy",
|
|
56: "Titus",
|
|
57: "Philemon",
|
|
58: "Hebrews",
|
|
59: "James",
|
|
60: "1 Peter",
|
|
61: "2 Peter",
|
|
62: "1 John",
|
|
63: "2 John",
|
|
64: "3 John",
|
|
65: "Jude",
|
|
66: "Revelation",
|
|
67: "1 Esdras",
|
|
68: "Tobit",
|
|
69: "Judith",
|
|
70: "Wisdom",
|
|
71: "Sirach",
|
|
72: "Epistle of Jeremiah",
|
|
73: "Baruch",
|
|
74: "1 Maccabees",
|
|
75: "2 Maccabees",
|
|
76: "3 Maccabees",
|
|
77: "2 Esdras",
|
|
78: "Susanna",
|
|
79: "Bel and Dragon",
|
|
80: "4 Maccabees",
|
|
81: "Greek Additions to Esther",
|
|
82: "3 Holy Children's Song",
|
|
83: "Prayer of Manasseh",
|
|
88: "Azariah", // TODO: Fill in this jump in number
|
|
}
|
|
|
|
/// Maps book IDs to their hebraic ordering.
|
|
/// Absent books retain the order according to their IDs.
|
|
const HEBRAIC_ORDER: Record<string, BookId> = {
|
|
"Genesis": 1, // Torah
|
|
"Exodus": 2,
|
|
"Leviticus": 3,
|
|
"Numbers": 4,
|
|
"Deuteronomy": 5,
|
|
"Joshua": 6, // Nevi'im
|
|
"Judges": 7,
|
|
"1 Samuel": 8,
|
|
"2 Samuel": 9,
|
|
"1 Kings": 10,
|
|
"2 Kings": 11,
|
|
"Isaiah": 12, // Ketuvim
|
|
"Jeremiah": 13,
|
|
"Ezekiel": 14,
|
|
"Hosea": 15,
|
|
"Joel": 16,
|
|
"Amos": 17,
|
|
"Obadiah": 18,
|
|
"Jonah": 19,
|
|
"Micah": 20,
|
|
"Nahum": 21,
|
|
"Habakkuk": 22,
|
|
"Zephaniah": 23,
|
|
"Haggai": 24,
|
|
"Zechariah": 25,
|
|
"Malachi": 26,
|
|
"Psalms": 27,
|
|
"Proverbs": 28,
|
|
"Job": 29,
|
|
"Song of Solomon": 30,
|
|
"Ruth": 31,
|
|
"Lamentations": 32,
|
|
"Ecclesiastes": 33,
|
|
"Esther": 34,
|
|
"Daniel": 35,
|
|
"Ezra": 36,
|
|
"Nehemiah": 37,
|
|
"1 Chronicles": 38,
|
|
"2 Chronicles": 39,
|
|
}
|