Plugin built.

This commit is contained in:
Caleb Campbell
2024-06-28 11:41:14 +10:00
parent e60294b950
commit 0ffb5f83a1
9 changed files with 1167 additions and 137 deletions

View File

@@ -0,0 +1,53 @@
import LocalBibleRefPlugin from "main";
import { App, Notice, PluginSettingTab, Setting } from "obsidian";
export default class LocalBibleRefSettingTab extends PluginSettingTab {
plugin: LocalBibleRefPlugin;
constructor(app: App, plugin: LocalBibleRefPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
this.containerEl.createEl('h1', { text: 'Local Bible Ref' });
let biblesPathTimeout: NodeJS.Timeout;
new Setting(containerEl)
.setName("Bibles Path")
.setDesc("The path to the folder containing your bibles.")
.addText(text => text
.setPlaceholder("e.g. Data/Bibles")
.onChange(async (value) => {
this.plugin.settings.biblesPath = value;
await this.plugin.saveSettings();
clearTimeout(biblesPathTimeout);
biblesPathTimeout = setTimeout(async () => {
const exists = await this.app.vault.adapter.exists(value);
if (!exists) new Notice(`Bibles folder doesn't exist at path: ${value}.`);
}, 500);
}));
let defaultVersionTimeout: NodeJS.Timeout;
new Setting(containerEl)
.setName("Default Version Shorthand")
.setDesc("The version to use by default - shorthand.")
.addText(text => text
.setPlaceholder("e.g. ESV")
.onChange(async (value) => {
this.plugin.settings.defaultVersionShorthand = value;
await this.plugin.saveSettings();
clearTimeout(defaultVersionTimeout);
defaultVersionTimeout = setTimeout(async () => {
const path = `${this.plugin.settings.biblesPath}/${value}`;
const exists = await this.app.vault.adapter.exists(path);
if (!exists) new Notice(`Version folder doesn't exist at path: ${path}.`);
}, 500);
}));
}
}

356
src/PassageSuggester.ts Normal file
View File

@@ -0,0 +1,356 @@
import {
App,
Editor,
EditorPosition,
EditorSuggest,
EditorSuggestContext,
Notice,
TFile
} from "obsidian";
import { BOOKS, Book } from "./data/books";
import { LocalBibleRefSettings } from "./config/settings";
interface ChapterReference {
startChapter: number;
startVerse: number;
endChapter: number;
endVerse: number;
}
interface PassageReference extends ChapterReference {
book: Book;
}
interface PassageSuggestion {
passageRef: string;
suggestText: string;
fullText: string;
}
export class PassageSuggester extends EditorSuggest<PassageSuggestion> {
private settings: LocalBibleRefSettings;
private passageRegex: RegExp;
constructor(app: App, settings: LocalBibleRefSettings) {
super(app);
this.settings = settings;
// builds the book matching regex
let regexString = BOOKS.reduce((string, book) => {
return `${string}${book.name}|${book.aliases.join("|")}|`;
}, "^\\-\\- ?(");
regexString = regexString.slice(0, -1);
regexString += ") ?(\\d{1,3}(?::\\d{1,3})?(?: ?\\- ?\\d{1,3}(?::\\d{1,3})?)?)$";
this.passageRegex = new RegExp(regexString, "i");
}
onTrigger(cursor: EditorPosition, editor: Editor, _: TFile | null) {
// min ref length is 6 ("--gen1")
if (cursor.ch < 6) return null;
// line must start with "--"
const line = editor.getLine(cursor.line);
if (!line.startsWith("--")) return null;
// must be a passage ref
const isPassage = this.passageRegex.test(line);
if (!isPassage) return null;
// trigger info
return { end: cursor, query: line, start: { ch: 0, line: cursor.line } };
}
async getSuggestions(context: EditorSuggestContext): Promise<PassageSuggestion[]> {
const passageRef = this.parsePassageRef(context.query);
if (!passageRef) return [];
// grab all chapters in the range
let chapterTexts = await this.getChapterTexts(passageRef);
console.log(passageRef);
console.log(BOOKS);
if (!chapterTexts) return [];
// split first chapter by start verse
const chapterFromVerse = this.getChapterFromStartVerse(chapterTexts[0], passageRef);
if (!chapterFromVerse) return [];
chapterTexts[0] = chapterFromVerse;
// split last chapter by end verse
const lastIndex = chapterTexts.length - 1;
const chapterToVerse = this.getChapterToEndVerse(chapterTexts[lastIndex], passageRef);
if (!chapterToVerse) return [];
chapterTexts[lastIndex] = chapterToVerse;
// clean up chapter texts
chapterTexts = chapterTexts.map((text, i) => {
text = this.removeHeadings(text);
text = this.removeFootnoteRefs(text);
text = this.removeEOF(text);
const chapter = passageRef.startChapter + i;
if (text.startsWith("> ")) {
const quoteMd = text.match(/^(?:> )+/)![0];
return text.replace(quoteMd, `${quoteMd}**${chapter}.** `);
}
if (text.startsWith("- ")) {
const listMd = text.match(/^(?:- )+/)![0];
return text.replace(listMd, `${listMd}**${chapter}.** `);
}
return text = `**${chapter}.** ${text}`;
});
// suggest
return [{
passageRef: this.encodePassageRef(passageRef),
suggestText: this.generateSuggestText(chapterTexts[0]),
fullText: chapterTexts.join("\n\n")
}];
}
renderSuggestion(item: PassageSuggestion, el: HTMLElement): void {
el.innerText = item.suggestText;
}
selectSuggestion(item: PassageSuggestion, _: MouseEvent | KeyboardEvent): void {
if (!this.context) return;
let text = item.fullText.replace(/\n/gm, "\n> ");
text = `> [!bible]+ ${item.passageRef}\n> ${text}`;
this.context.editor.replaceRange(text, this.context.start, this.context.end);
}
/**
* Parses a passage reference from the given text.
*
* @param text the text to parse.
* @returns a parsed PassageReference or null.
*/
private parsePassageRef(text: string): PassageReference | null {
const passageRefMatch = text.match(this.passageRegex);
if (!passageRefMatch) return null;
let chapterRef = this.parseMultiChapterRef(passageRefMatch[2]);
if (!chapterRef) chapterRef = this.parseMultiPartChapterRef(passageRefMatch[2]);
if (!chapterRef) chapterRef = this.parsePartChapterRef(passageRefMatch[2]);
if (!chapterRef) return null;
const book = this.getBook(passageRefMatch[1]);
if (!book) return null;
return { book, ...chapterRef };
}
/**
* Parses a multi-chapter reference from the given text.
* Reference is in the format: `startChapter[ - endChapter]`.
*
* @param text the text to parse.
* @returns a parsed ChapterReference or null.
*/
private parseMultiChapterRef(text: string): ChapterReference | null {
const regex = /^(\d{1,3})(?: ?- ?(\d{1,3}))?$/i;
const match = text.match(regex);
if (!match) return null;
const startChapter = parseInt(match[1]);
return {
startChapter,
startVerse: 1,
endChapter: match[2] ? parseInt(match[2]) : startChapter,
endVerse: -1,
};
}
/**
* Parses a multi-(part)-chapter reference from the given text.
* Reference is in the format: `startChapter:startVerse - endChapter:endVerse`.
*
* @param text the text to parse.
* @returns a parsed ChapterReference or null.
*/
private parseMultiPartChapterRef(text: string): ChapterReference | null {
const regex = /^(\d{1,3}):(\d{1,3}) ?- ?(\d{1,3}):(\d{1,3})$/i;
const match = text.match(regex);
if (!match) return null;
return {
startChapter: parseInt(match[1]),
startVerse: parseInt(match[2]),
endChapter: parseInt(match[3]),
endVerse: parseInt(match[4]),
};
}
/**
* Parses a part-chapter reference from the given text.
* Reference is in the format: `startChapter:startVerse[-endVerse]`.
*
* @param text the text to parse.
* @returns a parsed ChapterReference or null.
*/
private parsePartChapterRef(text: string): ChapterReference | null {
const regex = /^(\d{1,3}):(\d{1,3})(?: ?- ?(\d{1,3}))?$/i;
const match = text.match(regex);
if (!match) return null;
const startChapter = parseInt(match[1]);
const startVerse = parseInt(match[2]);
return {
startChapter,
startVerse,
endChapter: startChapter,
endVerse: match[3] ? parseInt(match[3]) : startVerse,
};
}
/**
* Retrieves a book based on it's alias.
* @param alias the alias of the book.
* @returns the book object if found, otherwise undefined.
*/
private getBook(alias: string): Book | undefined {
alias = alias.toLowerCase();
return BOOKS.find((book) => {
if (book.name.toLowerCase() === alias) return book;
if (book.aliases.includes(alias)) return book;
})
}
/**
* Retrieves the texts of the chapters within the specified passage reference.
*
* @param ref the passage reference.
* @returns a promise - an array of chapter texts or null.
*/
private async getChapterTexts(ref: PassageReference): Promise<string[] | null> {
let basePath = "";
for (let alias of [ref.book.name, ...ref.book.aliases]) {
console.log(alias);
basePath = [
this.settings.biblesPath,
this.settings.defaultVersionShorthand,
alias
].join("/");
console.log(basePath);
if (await this.app.vault.adapter.exists(basePath)) {
ref.book.aliases.push(ref.book.name);
ref.book.aliases.remove(alias);
ref.book.name = alias;
console.log(ref);
break;
}
}
const chapterTexts: string[] = [];
for (let ch = ref.startChapter; ch <= ref.endChapter; ch++) {
const path = basePath + `/${ref.book.name} ${ch}.md`;
const file = this.app.vault.getFileByPath(path);
if (!file) {
new Notice(`Could not find chapter at: ${path}`);
return null;
}
chapterTexts.push(await this.app.vault.cachedRead(file));
}
return chapterTexts;
}
/**
* Retrieves the text in the chapter from the start verse to the end.
*
* @param text the text to cut.
* @param ref the passage reference.
* @returns the text from the start verse or null.
*/
private getChapterFromStartVerse(text: string, ref: PassageReference): string | null {
const regex = new RegExp(`(?:> |- )*<sup>${ref.startVerse}</sup>`);
const match = text.match(regex);
if (!match) {
const verse = ref.startVerse;
const chapter = ref.startChapter;
const book = ref.book.name;
new Notice(`Could not find verse ${verse} in ${book} ${chapter}.`);
return null;
}
const verseLabel = match[0];
const parts = text.split(regex);
return verseLabel + parts[1];
};
/**
* Retrieves the text in the chapter from the start to the end verse.
*
* @param text the text to cut.
* @param ref the passage reference.
* @returns the text up to the end verse or null.
*/
private getChapterToEndVerse(text: string, ref: PassageReference): string | null {
if (ref.endVerse === -1) return text;
const regex = new RegExp(`(?:^(?:> |- )*)?<sup>${ref.endVerse + 1}</sup>`, "m");
return text.split(regex, 1)[0].trim();
};
/** Removes headings from the given text. */
private removeHeadings(text: string): string {
return text.replace(/^#.*[\n\r\f]*/gm, "");
}
/** Removes footnote refs from the given text. */
private removeFootnoteRefs(text: string): string {
return text.replace(/ \[\^\d{1,4}\]/g, "");
}
/** Removes the end-of-file content from the given text. */
private removeEOF(text: string): string {
// split at chapter divider
let split = text.split(/^\-\-\-$/m);
// split at footnotes
split = split[0].split(/^\[\^\d+\]:/m, 1);
return split[0].trim();
}
/** Generates suggestion text. */
private generateSuggestText(text: string): string {
text = text.split(/<\/sup>/, 2)[1];
text = text.replace(/(?:<sup>\d+<\/sup>|> |- )/g, "");
text = text.replace(/<span style="font-variant: small-caps;">Lord<\/span>/g, "Lord");
text = text.replace(/\n/g, " ");
text = text.replace(/ /g, " ");
return text.slice(0, 45) + "...";
}
/**
* Encodes a PassageReference into a string representation.
*
* @param ref the PassageReference to encode.
* @returns the encoded string representation.
*/
private encodePassageRef(ref: PassageReference): string {
// whole chapter/s ref
if (ref.startVerse === 1 && ref.endVerse === -1) {
if (ref.startChapter === ref.endChapter) {
return `${ref.book.name} ${ref.startChapter}`;
}
return `${ref.book.name} ${ref.startChapter}-${ref.endChapter}`;
}
// part chapter ref
if (ref.startChapter === ref.endChapter) {
if (ref.startVerse === ref.endVerse) {
return `${ref.book.name} ${ref.startChapter}:${ref.startVerse}`;
}
return `${ref.book.name} ${ref.startChapter}:${ref.startVerse}-${ref.endVerse}`;
}
// part chapters ref
const a = `${ref.startChapter}:${ref.startVerse}`;
const b = `${ref.endChapter}:${ref.endVerse}`;
return `${ref.book.name} ${a}-${b}`;
}
}

4
src/config/settings.ts Normal file
View File

@@ -0,0 +1,4 @@
export interface LocalBibleRefSettings {
biblesPath: string;
defaultVersionShorthand: string;
}

73
src/data/books.ts Normal file
View File

@@ -0,0 +1,73 @@
export interface Book {
name: string;
aliases: string[];
}
export const BOOKS: Book[] = [
{ name: "Genesis", aliases: ["gen"] },
{ name: "Exodus", aliases: ["exo"] },
{ name: "Leviticus", aliases: ["lev"] },
{ name: "Numbers", aliases: ["num"] },
{ name: "Deuteronomy", aliases: ["deu"] },
{ name: "Joshua", aliases: ["jos"] },
{ name: "Judges", aliases: ["jdg"] },
{ name: "Ruth", aliases: ["rut"] },
{ name: "1 Samuel", aliases: ["1sa"] },
{ name: "2 Samuel", aliases: ["2sa"] },
{ name: "1 Kings", aliases: ["1ki"] },
{ name: "2 Kings", aliases: ["2ki"] },
{ name: "1 Chronicles", aliases: ["1ch"] },
{ name: "2 Chronicles", aliases: ["2ch"] },
{ name: "Ezra", aliases: ["ezr"] },
{ name: "Nehemiah", aliases: ["neh"] },
{ name: "Esther", aliases: ["est"] },
{ name: "Job", aliases: ["job"] },
{ name: "Psalms", aliases: ["psa"] },
{ name: "Proverbs", aliases: ["pro"] },
{ name: "Ecclesiastes", aliases: ["ecc"] },
{ name: "Song of Songs", aliases: ["sng", "Song of Solomon"] },
{ name: "Isaiah", aliases: ["isa"] },
{ name: "Jeremiah", aliases: ["jer"] },
{ name: "Lamentations", aliases: ["lam"] },
{ name: "Ezekiel", aliases: ["ezk"] },
{ name: "Daniel", aliases: ["dan"] },
{ name: "Hosea", aliases: ["hos"] },
{ name: "Joel", aliases: ["jol"] },
{ name: "Amos", aliases: ["amo"] },
{ name: "Obadiah", aliases: ["oba"] },
{ name: "Jonah", aliases: ["jon"] },
{ name: "Micah", aliases: ["mic"] },
{ name: "Nahum", aliases: ["nam"] },
{ name: "Habakkuk", aliases: ["hab"] },
{ name: "Zephaniah", aliases: ["zep"] },
{ name: "Haggai", aliases: ["hag"] },
{ name: "Zechariah", aliases: ["zec"] },
{ name: "Malachi", aliases: ["mal"] },
{ name: "Matthew", aliases: ["mat"] },
{ name: "Mark", aliases: ["mrk"] },
{ name: "Luke", aliases: ["luk"] },
{ name: "John", aliases: ["jhn"] },
{ name: "Acts", aliases: ["act"] },
{ name: "Romans", aliases: ["rom"] },
{ name: "1 Corinthians", aliases: ["1co"] },
{ name: "2 Corinthians", aliases: ["2co"] },
{ name: "Galatians", aliases: ["gal"] },
{ name: "Ephesians", aliases: ["eph"] },
{ name: "Philippians", aliases: ["php"] },
{ name: "Colossians", aliases: ["col"] },
{ name: "1 Thessalonians", aliases: ["1th"] },
{ name: "2 Thessalonians", aliases: ["2th"] },
{ name: "1 Timothy", aliases: ["1ti"] },
{ name: "2 Timothy", aliases: ["2ti"] },
{ name: "Titus", aliases: ["tit"] },
{ name: "Philemon", aliases: ["phm"] },
{ name: "Hebrews", aliases: ["heb"] },
{ name: "James", aliases: ["jas"] },
{ name: "1 Peter", aliases: ["1pe"] },
{ name: "2 Peter", aliases: ["2pe"] },
{ name: "1 John", aliases: ["1jn"] },
{ name: "2 John", aliases: ["2jn"] },
{ name: "3 John", aliases: ["3jn"] },
{ name: "Jude", aliases: ["jud"] },
{ name: "Revelation", aliases: ["rev"] },
];