Add myBibleAPI for use in myBible codeblocks

This commit is contained in:
GsLogimaker 2024-11-13 14:23:57 -06:00
parent 3c237fef55
commit 95d2ddd863
4 changed files with 546 additions and 839 deletions

252
api.ts Normal file
View file

@ -0,0 +1,252 @@
import MyBible, { DEFAULT_NAME_MAP, getPlugin, is_alpha, is_alphanumeric, is_numeric } from "main";
export class PluginAccess {
static plugin:MyBible
}
export module myBibleAPI {
export module types {
export type BookID = number
export type ChapterID = number
export type VerseID = number
export type TranslationID = string
class ReferenceError extends Error {
constructor(message?:string) {
super(message)
this.name = "Scripture Reference Error"
}
}
}
/// A reference to a scripture verse or verses
export class Reference {
book: types.BookID
chapter: types.ChapterID
verseStart?: types.VerseID
verseEnd?: types.VerseID
translation?: types.TranslationID
constructor(text:string) {
const ref = text.trim().replace(/[:-]/g, " ").split(" ")
let book:string|undefined = undefined
let book_id = undefined
let chapter = undefined
let verse = undefined
let verse_end = undefined
let maybe_translation:string|undefined = undefined
let i = 0
while (i != ref.length) {
// Compose book name
if (i == 0) {
// Always add first item, no matter what it is
book = (book || "") + ref[i];
} else {
// Only add items that are words, and not numbers
if (!is_alpha(ref[i])) {
break;
}
book += " " + ref[i]
}
i += 1;
}
if (is_numeric(book || "")) {
// Compose book_id
book_id = Number(book)
}
if (i != ref.length && is_numeric(ref[i])) {
// Compose chapter
chapter = Number(ref[i]);
i += 1;
} else {
chapter = 0
}
if (i != ref.length && is_numeric(ref[i])) {
// Compose verse
verse = Number(ref[i]);
i += 1;
}
if (i != ref.length && is_numeric(ref[i])) {
// Compose range
verse_end = Number(ref[i]);
i += 1;
}
if (i != ref.length && is_alphanumeric(ref[i])) {
// Compose translation
maybe_translation = ref[i];
i += 1;
}
if (book_id === undefined && book !== undefined) {
book_id = DEFAULT_NAME_MAP[book]
?? getPlugin()
.bible_api
.book_id(book, maybe_translation ?? "YLT")
}
verse_end = verse_end ?? verse
// Errors
if ((verse_end ?? 0) < (verse ?? 0)) {
throw new ReferenceError(
"End of verse range must be after beginning"
)
} else if (book_id === undefined) {
throw new ReferenceError(
"Invalid book name `{0}`".format(book??"")
)
}
this.book = book_id
this.chapter = chapter
this.verseStart = verse
this.verseEnd = verse_end
this.translation = maybe_translation
}
get_translation():string {
return this.translation ?? getPlugin().settings.translation
}
async body(
withVerseNumbers?:boolean,
separator?:string,
):Promise<string> {
return await scripture(
this,
withVerseNumbers,
separator,
)
}
async toString():Promise<string> {
let bookData = await getPlugin()
.bible_api
.get_book_data(this.get_translation(), this.book)
let text = bookData.name + " " + this.chapter
if (this.verseStart !== undefined) {
text += ":" + String(this.verseStart)
}
if (this.verseEnd !== undefined && this.verseEnd != this.verseStart) {
text += "-" + String(this.verseEnd)
}
if (this.translation !== undefined) {
text += " " + String(this.translation)
}
return text
}
}
export async function randRef(seed?:string):Promise<Reference> {
let verse = await getPlugin()
.bible_api
.pick_random_verse(seed)
return new Reference(verse)
}
export function ref(reference:string):Reference {
return new Reference(reference)
}
/// Returns the text of a bible verse
export async function scripture(
ref:Reference|string,
withVerseNumbers?:boolean,
separator?:string,
):Promise<string> {
withVerseNumbers = withVerseNumbers ?? false
separator = separator ?? " "
if (!(ref instanceof Reference)) {
ref = new Reference(ref)
}
let version = ref.translation ?? getPlugin().settings.translation
let translation = ref.translation
?? getPlugin().settings.reading_translation
let bookName = (
await getPlugin().bible_api.get_book_data(translation, ref.book)
).name
let text = "";
if (bookName.length === 0) {
text = "\n> [!ERROR] Book and chapter must be provided\n"
} else if (ref.verseStart === undefined || ref.verseStart <= 0) {
// Whole chapter
let verses = await getPlugin().bible_api.get_chapter(
translation,
ref.book,
ref.chapter,
);
for (const verse_i_ of Object.keys(verses)) {
const verse_i = Number(verse_i_)
let verse = verses[verse_i];
text += "<sup>" + (verse_i) + "</sup> " + verse;
if (verse_i != Object.keys(verses).length+1) {
text += " ";
// text += "<br>";
}
}
if (text.length === 0) {
text = "\n> [!WARNING] No text found for {0} in translation {2}\n"
.format(String(ref), translation)
}
} else if (ref.verseEnd === undefined || ref.verseEnd < ref.verseStart) {
// Single verse
text = await getPlugin().bible_api.get_verse(
translation,
ref.book,
ref.chapter,
ref.verseStart,
)
if (text.length === 0) {
text = "\n> [!WARNING] No text found for {1} {0}:{2} in translation {3}\n"
.format(
String(ref.chapter),
bookName,
String(ref.verseStart),
translation,
)
}
} else {
// Verse range
let verses = await getPlugin().bible_api.get_chapter(
translation,
ref.book,
ref.chapter,
);
let j = ref.verseStart;
while (j < ref.verseEnd + 1 && j < Object.keys(verses).length) {
if (withVerseNumbers) {
text += "<sup>" + j + "</sup> " + verses[j];
if (j != ref.verseEnd) {
text += separator
}
} else {
text += verses[j] + separator
}
j += 1;
}
if (text.length === 0) {
text = "\n> [!WARNING] No text found for {1} {0}:{2}-{3} in translation {4}\n"
.format(
String(ref.chapter),
bookName,
String(ref.verseStart),
String(ref.verseEnd),
translation,
)
}
}
return text
}
}

859
main.ts
View file

@ -12,11 +12,13 @@ import {
requestUrl,
MarkdownPostProcessorContext,
MarkdownRenderer,
} from 'obsidian';
} from 'obsidian'
import { E_CANCELED, Mutex } from 'async-mutex';
import { match } from 'assert';
import { randomBytes, randomInt } from 'crypto';
import { E_CANCELED, Mutex } from 'async-mutex'
import { match } from 'assert'
import { randomBytes, randomInt } from 'crypto'
import { BBCodeTag, legacy, parse_mybible } from 'mybible_parser'
import { myBibleAPI, PluginAccess } from 'api'
const BUILD_END_TOAST = "Bible build finished!";
const SELECTED_TRANSLATION_OPTION = "<Selected reading translation, {0}>"
@ -132,7 +134,11 @@ const DEFAULT_SETTINGS: MyBibleSettings = {
}
}
function httpGet(theUrl: string): Promise<string> {
export function getPlugin():MyBible {
return MyBible.plugin
}
export function httpGet(theUrl: string): Promise<string> {
try {
return new Promise(async (ok, err) => {
ok(await requestUrl(theUrl).text);
@ -145,7 +151,7 @@ function httpGet(theUrl: string): Promise<string> {
}
}
function is_alpha(string: string): boolean {
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)) {
@ -159,7 +165,7 @@ function is_alpha(string: string): boolean {
return true;
}
function is_alphanumeric(string: string): boolean {
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)) {
@ -173,7 +179,7 @@ function is_alphanumeric(string: string): boolean {
return true;
}
function is_numeric(string: string): boolean {
export function is_numeric(string: string): boolean {
for (let char_str of string) {
let char = char_str.charCodeAt(0);
if (char > 47 && char < 58) {
@ -187,8 +193,7 @@ function is_numeric(string: string): boolean {
return true;
}
async function save_file(path: string, content: string) {
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) {
@ -201,13 +206,13 @@ async function save_file(path: string, content: string) {
}
}
function translation_to_display_name(translation:Translation):string {
export function translation_to_display_name(translation:Translation):string {
return "{0} - {1} - {2}"
.format(translation.language, translation.abbreviated_name, translation.display_name)
;
}
function cyrb128(str:string): number {
export function cyrb128(str:string): number {
let h1 = 1779033703, h2 = 3144134277,
h3 = 1013904242, h4 = 2773480762;
for (let i = 0, k; i < str.length; i++) {
@ -225,23 +230,21 @@ function cyrb128(str:string): number {
return h1>>>0
}
export default class MyBible extends Plugin {
bible_api: BibleAPI
settings: MyBibleSettings
progress_notice: Notice | null
parser: MBInterpreter
legacyParser: legacy.VerseParser
static plugin:MyBible
async onload() {
this.parser = new MBInterpreter(this)
this.parser.add_command("random")
.arg("seed", "string", "")
.callback(async (args, el, mb) => {
let verse = await this.bible_api.pick_random_verse(args["seed"])
el.createEl("h3", {"text": verse})
await mb.parser.render_verse(verse, el, false)
})
;
MyBible.plugin = this
// @ts-ignore
globalThis["mb"] = myBibleAPI
this.legacyParser = new legacy.VerseParser()
await this.loadSettings();
@ -297,13 +300,12 @@ export default class MyBible extends Plugin {
});
this.registerMarkdownCodeBlockProcessor("verse", async (source, el, ctx) =>
await this.parser.parse(source, el)
await this.legacyParser.parse(source, el)
);
this.registerMarkdownCodeBlockProcessor("mybible", async (source, el, ctx) => {
let code_context = {}
let parsed = parse_mybible(source)
console.log(parsed)
if (parsed instanceof Error) {
MarkdownRenderer.render(
this.app,
@ -323,13 +325,12 @@ export default class MyBible extends Plugin {
let text = ""
for (const X of parsed) {
if (X instanceof BBCodeTag) {
text += await X.toText(this.parser)
text += await X.toText(code_context)
} else {
text += X
}
}
MarkdownRenderer.render(this.app, text, el, "", this)
// let display = await this.parser.process_bbcode_tag(source, code_context)
});
this.addSettingTab(new SettingsTab(this.app, this));
@ -544,767 +545,11 @@ export default class MyBible extends Plugin {
await this.saveData(saving)
}
}
interface MBParseResult {
tag: string
args: string
content: string
remainder: string
range_start: number
range_end: number
}
export class MBInterpreter {
plugin: MyBible
commands: MBCommand[]
context: any
constructor(plugin:MyBible) {
this.plugin = plugin
this.commands = []
this.context = {}
}
run_js(code:string, context:any) {
let call_result:any = ""
let run_code = new Function(
"__code__",
"return eval(__code__)"
)
try {
call_result = run_code.call(context, code)
if (String(call_result) == "[object Object]") {
call_result = JSON.stringify(call_result)
}
} catch(e) {
call_result = "\n> [!ERROR] " + String(e) + "\n"
}
}
parse_bbcode_tag(code:string): MBParseResult|undefined {
// tag, args, content, remainder
let unproccessed_code = code
let opened = []
while (true) {
let offset = code.length - unproccessed_code.length
let match = unproccessed_code.match(/(?:\\)?(\[\s*([^=\s\]\[]*)\s*(=?[^\]]*)\])([\s\S]*)/)
if (match === null) {
break
}
if (match.input === undefined) {
throw ""
}
let full_match_txt = match[0]
let tag = match[1]
let tag_name = match[2]
.toLocaleLowerCase()
.replace(/[_\-]/g, "")
let args = match[3]
let remaining = match[4]
if (tag_name === undefined) {
throw new Error("Expected tag to have a name")
}
if (tag_name.startsWith("/")) {
// Closing tag
tag_name = tag_name.slice(1)
let latest_open_i = opened.findLastIndex((x) =>
x[2].toLocaleLowerCase().replace(/[_\-]/g, "") === tag_name
)
if (latest_open_i === -1) {
// TODO: Handle closing tags with no opening
throw new Error("Encountered closing tag without opening tag")
}
if (opened.length === 1) {
let open_match = opened[0]
let openTag = open_match[1]
let open_args = open_match[3]
let range_start = open_match.index ?? 0
let range_end = offset + (match.index ?? 0) + tag.length
let content_start = range_start + openTag.length
let content_end = range_end - tag.length
return {
tag: tag_name,
args: open_args,
content: code.slice(content_start, content_end),
remainder: remaining,
range_start: range_start,
range_end: range_end,
}
}
opened.splice(latest_open_i, 1)
} else {
// Opening tag
opened.push(match)
}
if (remaining === undefined) {
return
}
unproccessed_code = remaining ?? unproccessed_code
}
if (opened.length === 0) {
return
}
let open_match = opened[0]
let openTag = open_match[1]
let openTag_name = open_match[2]
.toLocaleLowerCase()
.replace(/[_\-]/g, "")
let open_args = open_match[3]
let range_start = open_match.index ?? 0
let range_end = range_start + open_match[0].length
let content_start = range_start + openTag.length
let content_end = range_end
return {
tag: openTag_name,
args: open_args,
content: code.slice(content_start, content_end),
remainder: "",
range_start: range_start,
range_end: range_end,
}
}
async process_bbcode_tag(
code:string,
context:any,
): Promise<string|Error|undefined> {
try {
let result = this.parse_bbcode_tag(code)
if (result === undefined) {
// No tags found, return as is
return code
}
let pre_tag = code.slice(0, result.range_start)
let post_tag = code.slice(result.range_end)
if (result.tag === "verse") {
let args = result.args
if (result.args.startsWith("=")) {
args = result.args.slice(1)
}
return pre_tag
+ await this.process_verse_reference(args)
+ await this.process_bbcode_tag(result.content + post_tag, context)
} else if (result.tag === "randomverse") {
let args = this.parse_bbcode_args(result.args) ?? {}
if (args instanceof Error) {
return String(args)
}
let random_verse = await this.plugin.bible_api
.pick_random_verse(args["seed"])
let with_verse_numbers = args["withnumbers"] ?? false
let separator = (args["separator"] ?? " ")
.replace(/\\n/, "\n")
let translation = args["translation"]
return pre_tag
+ await this.process_verse_reference(
random_verse,
with_verse_numbers,
separator,
translation,
) + await this.process_bbcode_tag(
result.content + post_tag,
context,
)
} else if (result.tag === "js") {
let call_result:any = ""
let run_code = new Function(
"__code__",
"return eval(__code__)"
)
try {
call_result = run_code.call(context, result?.content)
if (String(call_result) == "[object Object]") {
call_result = JSON.stringify(call_result)
}
} catch(e) {
call_result = "\n> [!ERROR] " + String(e) + "\n"
}
return pre_tag
+ String(call_result)
+ await this.process_bbcode_tag(post_tag, context)
}
} catch (e) {
if (!(e instanceof MBGeneralError)) {
let ge = new MBGeneralError(e.message)
ge.name = e.name
ge.stack = e.stack
return ge
}
return e
}
return code
}
parse_bbcode_args(
source:string,
): Record<string, any>|MBArgValueParseError|undefined {
source = source.trim()
let parsed:any = {}
if (source.startsWith("=")) {
source = source.slice(1)
let value_result = this.parse_value(source)
if (value_result instanceof MBValueParseError) {
return new MBArgValueParseError("", value_result)
}
parsed[""] = value_result[0]
source = value_result[1]
}
let arg_match:RegExpMatchArray | null = null
while ((arg_match = source.match(/([\w\-]*)\s*=([\s\S]*)/)) !== null) {
let arg_name = (arg_match[1] ?? "")
.toLocaleLowerCase()
.replace(/[_\-]/g, "")
let remainder = arg_match[2] ?? ""
let value_result = this.parse_value(remainder)
if (value_result instanceof MBValueParseError) {
return new MBArgValueParseError(arg_name, value_result)
}
parsed[arg_name] = value_result[0]
remainder = value_result[1]
source = remainder
}
return parsed
}
parse_value(source:string):MBValueParseError|any[] {
let trimmed_source = source.trim()
let match:RegExpMatchArray|null = null
if (
(match = trimmed_source.match(/""|".*?[^\\]"|''|'.*?[^\\]'/))
!== null
&& match.index === 0
) {
// Match string
let remaining = match.input?.slice(match[0].length)
return [match[0].slice(1, -1), remaining]
} else if (
(match = trimmed_source.match(/true|false/))
!== null
&& match.index === 0
) {
// Match boolean
let remaining = match.input?.slice(match[0].length)
return [Boolean(match[0][0] === "t"), remaining]
} else if (
(match = trimmed_source.match(/([\-\+]*)?([\d_]+)?(?:(\.)([\d_]*))?/))
!== null
&& match.index === 0
&& match[0].length !== 0
) {
// Match number
let remaining = match.input?.slice(match[0].length)
return [Number(match[0].replace("_", "")), remaining]
}
return new MBValueParseError(source)
}
add_command(
keyword:string,
): MBCommand {
let cmd = new MBCommand
this.commands.push(cmd)
cmd.keyword = keyword
return cmd
}
parse(source:string, el:HTMLElement) {
for (const LINE of source.split("\n")) {
this.render_line(LINE, el)
}
}
parse_cmd_args(
cmd:MBCommand,
args:string,
): Record<string, any> | MBParseError {
let result:Record<string, any> = {}
let remaining_args = args
let args_offset = Number(args.match(/\s*/)?.[0].length)
for (let i=0; args.slice(args_offset).length !== 0; i++) {
let potential_offset = 0
// Match arg name
let arg_key = ""
let arg_name_match = args.slice(args_offset).match(/(\w*)\s*=/)
if (arg_name_match !== null) {
arg_key = String(arg_name_match?.[1])
potential_offset += arg_name_match?.[0].length
potential_offset += Number(args.slice(args_offset).match(/\s*/)?.[0].length)
} else {
arg_key = cmd.arguments[i].key
}
let has_key = false
for (const CMD of cmd.arguments) {
if (arg_key === CMD.key) {
has_key = true
break
}
}
if (!has_key) {
return new MBParseError(
"No argument by name \"{0}\"".format(arg_key),
args,
args_offset,
)
}
// Match string
let str_match = args.slice(args_offset)
.match(/""|".*?[^\\]"|''|'.*?[^\\]'/)
if (str_match !== null) {
result[arg_key] = str_match[0].slice(1, -1)
potential_offset += str_match[0].length
}
if (Object.keys(result).length !== i+1) {
return new MBParseError(
"Failed to parse argument",
args,
args_offset,
)
}
if (Object.keys(result).length > cmd.arguments.length) {
return new MBParseError(
"Unexpected argument",
args,
args_offset,
)
}
if (typeof(result[arg_key]) !== cmd.arguments[i].type) {
return new MBParseError(
"Invalid argument",
args,
args_offset,
)
}
if (result[arg_key] === undefined) {
return new MBParseError(
"Absent required argument",
args,
args_offset,
)
}
args_offset += potential_offset
args_offset += Number(args.slice(args_offset).match(/\s*/)?.[0].length)
}
return result
}
async render_mb_code(source:string, el:HTMLElement) {
let promises = []
for (const LINE of source.split("\n")) {
promises.push(this.render_line(LINE, el.createDiv()))
}
await Promise.all(promises)
}
async render_line(line:string, el: HTMLElement) {
let cmd_txt = line
let args_txt = ""
if (line.contains(" ")) {
cmd_txt = line.slice(0, line.indexOf(" "))
args_txt = line.slice(line.indexOf(" "))
}
let cmd = cmd_txt.toLowerCase().replace(/[_-]/g, "")
if (cmd.startsWith("/")) {
cmd = cmd.slice(1)
for (const CMD of this.commands) {
if (cmd === CMD.keyword) {
let args = this.parse_cmd_args(CMD, args_txt)
if (args instanceof MBParseError) {
args.render("/"+cmd, el)
return
}
await CMD.call(args, el, this.plugin)
return
}
}
new MBParseError(
"No command exists with this name".format(cmd),
line,
0
).render(undefined, el)
} else {
await this.render_verse(line, el)
}
}
/// Processes a verse reference to verse text
async process_verse_reference(
raw_ref:string,
with_verse_numbers:boolean=true,
separator:string=" ",
version:string|undefined = undefined,
): Promise<string> {
const ref = raw_ref.trim().replace(/[:-]/g, " ").split(" ");
let book:string|null = null;
let book_id = -1;
let chapter = -1;
let verse = -1;
let verse_end = -1;
let maybe_translation:string|undefined = undefined;
let i = 0;
while (i != ref.length) {
// Compose book name
if (i == 0) {
// Always add first item, no matter what it is
book = (book || "") + ref[i];
} else {
// Only add items that are words, and not numbers
if (!is_alpha(ref[i])) {
break;
}
book += " " + ref[i]
}
i += 1;
}
if (is_numeric(book || "")) {
// Compose book_id
book_id = Number(book);
book = null;
} else {
book_id = 0
}
if (i != ref.length && is_numeric(ref[i])) {
// Compose chapter
chapter = Number(ref[i]);
i += 1;
} else {
chapter = 0
}
if (i != ref.length && is_numeric(ref[i])) {
// Compose verse
verse = Number(ref[i]);
i += 1;
}
if (i != ref.length && is_numeric(ref[i])) {
// Compose range
verse_end = Number(ref[i]);
i += 1;
}
if (i != ref.length && is_alphanumeric(ref[i])) {
// Compose translation
maybe_translation = ref[i];
i += 1;
}
let translation = version
?? maybe_translation
?? this.plugin.settings.reading_translation;
if (book !== null) {
book_id = await this.plugin.bible_api
.book_id(this.plugin.settings._built_translation||translation, book);
}
if (book_id === 0) {
return "\n> [!ERROR] Book and chapter must be provided\n"
}
if (chapter === 0) {
return "\n> [!ERROR] Chapter must be provided\n"
}
book = (await this.plugin.bible_api.get_book_data(translation, book_id)).name;
let text = "";
if (book.length === 0) {
text = "\n> [!ERROR] Book and chapter must be provided\n"
} else if (chapter === -1) {
text = "\n> [!ERROR] Chapter must be provided\n"
} else if (verse === -1) {
// Whole chapter
let verses = await this.plugin.bible_api.get_chapter(
translation,
book_id,
chapter,
);
for (const verse_i_ of Object.keys(verses)) {
const verse_i = Number(verse_i_)
let verse = verses[verse_i];
text += "<sup>" + (verse_i) + "</sup> " + verse;
if (verse_i != Object.keys(verses).length+1) {
text += " ";
// text += "<br>";
}
}
if (text.length === 0) {
text = "\n> [!WARNING] No text found for {1} {0} in translation {2}\n"
.format(String(chapter), book, translation)
}
} else if (verse_end < verse) {
// Single verse
text = await this.plugin.bible_api.get_verse(
translation,
book_id,
chapter,
verse,
)
if (text.length === 0) {
text = "\n> [!WARNING] No text found for {1} {0}:{2} in translation {3}\n"
.format(String(chapter), book, String(verse), translation)
}
} else {
// Verse range
let verses = await this.plugin.bible_api.get_chapter(
translation,
book_id,
chapter,
);
let j = verse;
while (j < verse_end + 1 && j < Object.keys(verses).length) {
if (with_verse_numbers) {
text += "<sup>" + j + "</sup> " + verses[j];
if (j != verse_end) {
text += separator
}
} else {
text += verses[j] + separator
}
j += 1;
}
if (text.length === 0) {
text = "\n> [!WARNING] No text found for {1} {0}:{2}-{3} in translation {4}\n"
.format(String(chapter), book, String(verse), String(verse_end), translation)
}
}
return text
}
async render_verse(line:string, el:HTMLElement, with_verse_numbers:boolean=true) {
let div = el.createDiv()
const ref = line.trim().replace(/[:-]/g, " ").split(" ");
let book:string|null = null;
let book_id = -1;
let chapter = -1;
let verse = -1;
let verse_end = -1;
let maybe_translation: string | null = null;
let i = 0;
while (i != ref.length) {
// Compose book name
if (i == 0) {
// Always add first item, no matter what it is
book = (book || "") + ref[i];
} else {
// Only add items that are words, and not numbers
if (!is_alpha(ref[i])) {
break;
}
book += " " + ref[i]
}
i += 1;
}
if (is_numeric(book || "")) {
// Compose book_id
book_id = Number(book);
book = null;
}
if (i != ref.length && is_numeric(ref[i])) {
// Compose chapter
chapter = Number(ref[i]);
i += 1;
}
if (i != ref.length && is_numeric(ref[i])) {
// Compose verse
verse = Number(ref[i]);
i += 1;
}
if (i != ref.length && is_numeric(ref[i])) {
// Compose range
verse_end = Number(ref[i]);
i += 1;
}
if (i != ref.length && is_alphanumeric(ref[i])) {
// Compose translation
maybe_translation = ref[i];
i += 1;
}
let translation = maybe_translation || this.plugin.settings.reading_translation;
if (book !== null) {
book_id = await this.plugin.bible_api
.book_id(this.plugin.settings._built_translation||translation, book);
}
book = (await this.plugin.bible_api.get_book_data(translation, book_id)).name;
let text = "";
if (book.length === 0) {
text = "[Book and chapter must be provided]";
} else if (chapter === -1) {
text = "[Chapter must be provided]";
} else if (verse === -1) {
// Whole chapter
let verses = await this.plugin.bible_api.get_chapter(
translation,
book_id,
chapter,
);
for (const verse_i_ of Object.keys(verses)) {
const verse_i = Number(verse_i_)
let verse = verses[verse_i];
text += "<sup>" + (verse_i) + "</sup> " + verse;
if (verse_i != Object.keys(verses).length+1) {
text += " ";
// text += "<br>";
}
}
if (text.length === 0) {
text = "<No text found for {1} {0} in translation {2}>"
.format(String(chapter), book, translation)
}
} else if (verse_end < verse) {
// Single verse
text = await this.plugin.bible_api.get_verse(
translation,
book_id,
chapter,
verse,
)
if (text.length === 0) {
text = "<No text found for {1} {0}:{2} in translation {3}>"
.format(String(chapter), book, String(verse), translation)
}
} else {
// Verse range
let verses = await this.plugin.bible_api.get_chapter(
translation,
book_id,
chapter,
);
let j = verse;
while (j < verse_end + 1 && j < Object.keys(verses).length) {
if (with_verse_numbers) {
text += "<sup>" + j + "</sup> " + verses[j];
if (j != verse_end) {
text += "<br>";
}
} else {
text += verses[j] + "<br>"
}
j += 1;
}
if (text.length === 0) {
text = "<No text found for {1} {0}:{2}-{3} in translation {4}>"
.format(String(chapter), book, String(verse), String(verse_end), translation)
}
}
let span = div.createSpan({
text: "",
});
let tags = text.matchAll(
/(?:<\s*([\w]*)\s*>([\s\S]*?)<\s*\/\1\s*>)|<\s*(br|\/br|br\/)\s*>|([\s\S]+?(?:(?=<\s*[/\\\w]*\s*>)|$))/g
);
for (let match of tags) {
let tag_type = match[1];
let tag_text = match[2];
let lone_tag_type = match[3];
let normal_text = match[4];
if (normal_text !== undefined) {
span.createSpan({
text: normal_text,
});
} else if (lone_tag_type === "br") {
span.createEl(lone_tag_type);
} else if (lone_tag_type === "br/" || lone_tag_type === "/br") {
span.createEl("br");
span.createSpan({
text: "\n",
});
} else if (lone_tag_type === "/J") {
/* Do nothing */
} else if (tag_type === "sup") {
span.createEl(tag_type, { text: tag_text });
} else if (tag_type === "sub") {
span.createEl(tag_type, { text: tag_text });
} else if (tag_type === "S") {
span.createEl(
"sub",
{ text: tag_text, attr: { style: "opacity: 0.5" } },
);
} else if (tag_type === "i") {
span.createEl(tag_type, { text: tag_text });
} else if (tag_type === "b") {
span.createEl(tag_type, { text: tag_text });
} else if (tag_type === "e") {
/// A quote from from elsewhere in the bible.
span.createEl("i").createEl("q", { text: tag_text });
} else {
span.createSpan(
{ text: "<{0}>{1}</{0}>".format(tag_type, tag_text) },
);
}
}
}
}
class MBCommand {
keyword: string = ""
arguments: MBArg[] = []
call: (args:Record<string, any>, el:HTMLElement, plugin: MyBible) => void
callback(
c: (args:Record<string, any>, el:HTMLElement, plugin: MyBible) => void
) {
this.call = c
return this
}
arg(key:string, type:string, default_value:any=undefined) {
let arg = new MBArg(key, type, default_value)
this.arguments.push(arg)
return this
}
opitonal_args(args: MBArg[]) {
this.arguments.concat(args)
return this
}
}
class MBArg {
key: string = ""
type: string = ""
default_value: any = undefined
constructor(key:string, type:string, default_value:any=undefined) {
this.key = key
this.type = type
this.default_value = default_value
async runJS(code:string, context?:any):Promise<any> {
let call_result = await async function() {
return eval("(async () => { {0} })()".format(code))
}.call(context)
return call_result
}
}
@ -1318,42 +563,6 @@ class MBGeneralError extends Error {
}
}
class MBParseError extends Error {
name: string = ""
code: string = ""
stack?: string;
offset: number = 0
toString():string {
return this.name
}
constructor(name:string, code:string="", offset:number=0) {
super(name)
this.name = name
this.code = code
this.offset = offset
}
render(code_prefix:string="", el:HTMLElement) {
let err_block = el.createDiv()
err_block.setAttr("class", "callout")
err_block.setAttr("data-callout", "error")
let line_1 = code_prefix + this.code
let line_2 = "_".repeat(code_prefix.length+this.offset) + "^".repeat(this.code.length-this.offset)
err_block.appendText(this.name)
let block = err_block.createEl("div", {
"cls": "HyperMD-codeblock HyperMD-codeblock-bg cm-line",
})
block.setAttr("data-callout", "error")
block.appendText(line_1 + "\n")
block.createEl("br")
block.appendText(line_2)
return el
}
}
class MBTagError extends MBGeneralError {}
class MBValueParseError extends MBTagError {
@ -3755,7 +2964,7 @@ function to_hebraic_order(id: BookId) {
return HEBRAIC_ORDER[BOOK_NAME]
}
const DEFAULT_NAME_MAP: Record<string, BookId> = {
export const DEFAULT_NAME_MAP: Record<string, BookId> = {
"Genesis": 1,
"Exodus": 2,
"Leviticus": 3,

View file

@ -1,4 +1,5 @@
import { MBInterpreter } from "main"
import { myBibleAPI } from "api"
import MyBible, { getPlugin, is_alpha, is_alphanumeric, is_numeric } from "main"
const ohm = require('ohm-js')
@ -97,7 +98,6 @@ const myBibleSemmantics = myBibleGrammar.createSemantics().addOperation('eval',
// HACK: Using flat() to remove recursive arrays from _content
open["content"] = _content.eval().flat()
}
console.log(open)
return open
},
unclosedTag(_open:any, _content:any) {
@ -149,39 +149,53 @@ export class BBCodeTag {
this.args = args ?? {}
}
async toText(parser:MBInterpreter):Promise<string> {
async toText(context:any):Promise<string> {
let text = ""
if (this.name === "verse") {
let verse_text = await parser
.process_verse_reference(this.args[""] ?? "1 1 1")
text += verse_text + await this.contentString(parser)
let verse_text = await myBibleAPI.scripture(
this.args[""] ?? "1 1 1"
)
text += verse_text + await this.contentString(context)
} else if (this.name === "js") {
parser.run_js(await this.contentString(parser), {})
try {
let result = await getPlugin()
.runJS(await this.contentString(context), context)
result = result ?? ""
text += await result.toString()
} catch(e) {
text += "\n> [!ERROR] JS {0}\n{1}\n"
.format(e.name, e.message)
}
} else if (this.name === "randomverse") {
let seed = String(this.args["seed"])
let verse_numbers = this.args["versenumbers"] ?? false
let separator = this.args["separator"] ?? " "
let version = this.args["version"]
let verse = await parser.plugin.bible_api.pick_random_verse(seed)
let verse_text = await parser.process_verse_reference(
let verse = await getPlugin().bible_api.pick_random_verse(seed)
let verse_text = await myBibleAPI.scripture(
verse ?? "1 1 1",
verse_numbers,
separator,
version,
)
text += verse_text + await this.contentString(parser)
text += verse_text + await this.contentString(context)
} else {
text += "> [!ERROR] Tag error\n> Tag `{0}` is not a valid tag\n{1}"
.format(this.name, await this.contentString(parser))
.format(this.name, await this.contentString(context))
}
return text
}
async contentString(parser:MBInterpreter):Promise<string> {
async contentString(context:any):Promise<string> {
let text = ""
for (const X of this.content) {
if (X instanceof BBCodeTag) {
text += await X.toText(parser)
text += await X.toText(context)
} else {
text += X
}
@ -189,3 +203,235 @@ export class BBCodeTag {
return text
}
}
export module legacy {
class MBCommand {
keyword: string = ""
arguments: MBArg[] = []
call: (args:Record<string, any>, el:HTMLElement, plugin: MyBible) => void
callback(
c: (args:Record<string, any>, el:HTMLElement, plugin: MyBible) => void
) {
this.call = c
return this
}
arg(key:string, type:string, default_value:any=undefined) {
let arg = new MBArg(key, type, default_value)
this.arguments.push(arg)
return this
}
opitonal_args(args: MBArg[]) {
this.arguments.concat(args)
return this
}
}
class MBArg {
key: string = ""
type: string = ""
default_value: any = undefined
constructor(key:string, type:string, default_value:any=undefined) {
this.key = key
this.type = type
this.default_value = default_value
}
}
export class VerseParser {
commands: MBCommand[]
context: any
constructor() {
this.commands = []
this.context = {}
}
parse(source:string, el:HTMLElement) {
for (const LINE of source.split("\n")) {
this.render_verse(LINE, el)
}
}
async render_verse(
line:string,
el:HTMLElement,
with_verse_numbers:boolean=true,
) {
let div = el.createDiv()
const ref = line.trim().replace(/[:-]/g, " ").split(" ");
let book:string|null = null;
let book_id = -1;
let chapter = -1;
let verse = -1;
let verse_end = -1;
let maybe_translation: string | null = null;
let i = 0;
while (i != ref.length) {
// Compose book name
if (i == 0) {
// Always add first item, no matter what it is
book = (book || "") + ref[i];
} else {
// Only add items that are words, and not numbers
if (!is_alpha(ref[i])) {
break;
}
book += " " + ref[i]
}
i += 1;
}
if (is_numeric(book || "")) {
// Compose book_id
book_id = Number(book);
book = null;
}
if (i != ref.length && is_numeric(ref[i])) {
// Compose chapter
chapter = Number(ref[i]);
i += 1;
}
if (i != ref.length && is_numeric(ref[i])) {
// Compose verse
verse = Number(ref[i]);
i += 1;
}
if (i != ref.length && is_numeric(ref[i])) {
// Compose range
verse_end = Number(ref[i]);
i += 1;
}
if (i != ref.length && is_alphanumeric(ref[i])) {
// Compose translation
maybe_translation = ref[i];
i += 1;
}
let translation = maybe_translation || getPlugin().settings.reading_translation;
if (book !== null) {
book_id = await getPlugin().bible_api
.book_id(getPlugin().settings._built_translation||translation, book);
}
book = (await getPlugin().bible_api.get_book_data(translation, book_id)).name;
let text = "";
if (book.length === 0) {
text = "[Book and chapter must be provided]";
} else if (chapter === -1) {
text = "[Chapter must be provided]";
} else if (verse === -1) {
// Whole chapter
let verses = await getPlugin().bible_api.get_chapter(
translation,
book_id,
chapter,
);
for (const verse_i_ of Object.keys(verses)) {
const verse_i = Number(verse_i_)
let verse = verses[verse_i];
text += "<sup>" + (verse_i) + "</sup> " + verse;
if (verse_i != Object.keys(verses).length+1) {
text += " ";
// text += "<br>";
}
}
if (text.length === 0) {
text = "<No text found for {1} {0} in translation {2}>"
.format(String(chapter), book, translation)
}
} else if (verse_end < verse) {
// Single verse
text = await getPlugin().bible_api.get_verse(
translation,
book_id,
chapter,
verse,
)
if (text.length === 0) {
text = "<No text found for {1} {0}:{2} in translation {3}>"
.format(String(chapter), book, String(verse), translation)
}
} else {
// Verse range
let verses = await getPlugin().bible_api.get_chapter(
translation,
book_id,
chapter,
);
let j = verse;
while (j < verse_end + 1 && j < Object.keys(verses).length) {
if (with_verse_numbers) {
text += "<sup>" + j + "</sup> " + verses[j];
if (j != verse_end) {
text += "<br>";
}
} else {
text += verses[j] + "<br>"
}
j += 1;
}
if (text.length === 0) {
text = "<No text found for {1} {0}:{2}-{3} in translation {4}>"
.format(String(chapter), book, String(verse), String(verse_end), translation)
}
}
let span = div.createSpan({
text: "",
});
let tags = text.matchAll(
/(?:<\s*([\w]*)\s*>([\s\S]*?)<\s*\/\1\s*>)|<\s*(br|\/br|br\/)\s*>|([\s\S]+?(?:(?=<\s*[/\\\w]*\s*>)|$))/g
);
for (let match of tags) {
let tag_type = match[1];
let tag_text = match[2];
let lone_tag_type = match[3];
let normal_text = match[4];
if (normal_text !== undefined) {
span.createSpan({
text: normal_text,
});
} else if (lone_tag_type === "br") {
span.createEl(lone_tag_type);
} else if (lone_tag_type === "br/" || lone_tag_type === "/br") {
span.createEl("br");
span.createSpan({
text: "\n",
});
} else if (lone_tag_type === "/J") {
/* Do nothing */
} else if (tag_type === "sup") {
span.createEl(tag_type, { text: tag_text });
} else if (tag_type === "sub") {
span.createEl(tag_type, { text: tag_text });
} else if (tag_type === "S") {
span.createEl(
"sub",
{ text: tag_text, attr: { style: "opacity: 0.5" } },
);
} else if (tag_type === "i") {
span.createEl(tag_type, { text: tag_text });
} else if (tag_type === "b") {
span.createEl(tag_type, { text: tag_text });
} else if (tag_type === "e") {
/// A quote from from elsewhere in the bible.
span.createEl("i").createEl("q", { text: tag_text });
} else {
span.createSpan(
{ text: "<{0}>{1}</{0}>".format(tag_type, tag_text) },
);
}
}
}
}
}

File diff suppressed because one or more lines are too long