Cleaned up and added options to passage ref query.
This commit is contained in:
14
main.ts
14
main.ts
@@ -1,7 +1,8 @@
|
||||
import { Plugin } from 'obsidian';
|
||||
import LocalBibleRefSettingTab from 'src/LocalBibleRefSettingTab';
|
||||
import { PassageSuggester } from 'src/PassageSuggester';
|
||||
import { LocalBibleRefSettings } from 'src/config/settings';
|
||||
import LocalBibleRefSettingTab from 'src/local-bible-ref-setting-tab';
|
||||
import { PassageFormat } from 'src/passage-reference';
|
||||
import { PassageSuggest } from 'src/passage-suggest';
|
||||
import { LocalBibleRefSettings } from 'src/settings';
|
||||
|
||||
export default class LocalBibleRefPlugin extends Plugin {
|
||||
settings: LocalBibleRefSettings;
|
||||
@@ -9,13 +10,18 @@ export default class LocalBibleRefPlugin extends Plugin {
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
this.addSettingTab(new LocalBibleRefSettingTab(this.app, this));
|
||||
this.registerEditorSuggest(new PassageSuggester(this.app, this.settings));
|
||||
this.registerEditorSuggest(new PassageSuggest(this.app, this.settings));
|
||||
}
|
||||
|
||||
onunload() {}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = await this.loadData();
|
||||
this.settings ??= {
|
||||
biblesPath: '',
|
||||
defaultVersionShorthand: '',
|
||||
defaultPassageFormat: PassageFormat.Callout
|
||||
};
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"name": "Local Bible Ref",
|
||||
"version": "1.0.1",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Quickly and easily reference Bible verses stored locally in your Obsidian vault.",
|
||||
"author": "calebcampbell",
|
||||
"description": "Quickly and easily reference Bible passages stored locally in your Obsidian vault.",
|
||||
"author": "Caleb Campbell",
|
||||
"authorUrl": "https://mastodon.social/@calebcampbell",
|
||||
"fundingUrl": "",
|
||||
"isDesktopOnly": false
|
||||
|
||||
1985
package-lock.json
generated
1985
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "local-bible-ref",
|
||||
"version": "1.0.1",
|
||||
"description": "Quickly and easily reference Bible verses stored locally in your Obsidian vault.",
|
||||
"description": "Quickly and easily reference Bible passages stored locally in your Obsidian vault.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
@@ -9,7 +9,7 @@
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"author": "Caleb Campbell",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,12 @@ export const BOOKS: Book[] = [
|
||||
{ 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: "1 Samuel", aliases: ["1sa", "1samuel"] },
|
||||
{ name: "2 Samuel", aliases: ["2sa", "2samuel"] },
|
||||
{ name: "1 Kings", aliases: ["1ki", "1kings"] },
|
||||
{ name: "2 Kings", aliases: ["2ki", "2kings"] },
|
||||
{ name: "1 Chronicles", aliases: ["1ch", "1chronicles"] },
|
||||
{ name: "2 Chronicles", aliases: ["2ch", "2chronicles"] },
|
||||
{ name: "Ezra", aliases: ["ezr"] },
|
||||
{ name: "Nehemiah", aliases: ["neh"] },
|
||||
{ name: "Esther", aliases: ["est"] },
|
||||
@@ -49,25 +49,25 @@ export const BOOKS: Book[] = [
|
||||
{ name: "John", aliases: ["jhn"] },
|
||||
{ name: "Acts", aliases: ["act"] },
|
||||
{ name: "Romans", aliases: ["rom"] },
|
||||
{ name: "1 Corinthians", aliases: ["1co"] },
|
||||
{ name: "2 Corinthians", aliases: ["2co"] },
|
||||
{ name: "1 Corinthians", aliases: ["1co", "1corinthians"] },
|
||||
{ name: "2 Corinthians", aliases: ["2co", "2corinthians"] },
|
||||
{ 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: "1 Thessalonians", aliases: ["1th", "1thessalonians"] },
|
||||
{ name: "2 Thessalonians", aliases: ["2th", "2thessalonians"] },
|
||||
{ name: "1 Timothy", aliases: ["1ti", "1timothy"] },
|
||||
{ name: "2 Timothy", aliases: ["2ti", "2timothy"] },
|
||||
{ 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: "1 Peter", aliases: ["1pe", "1peter"] },
|
||||
{ name: "2 Peter", aliases: ["2pe", "2peter"] },
|
||||
{ name: "1 John", aliases: ["1jn", "1john"] },
|
||||
{ name: "2 John", aliases: ["2jn", "2john"] },
|
||||
{ name: "3 John", aliases: ["3jn", "3john"] },
|
||||
{ name: "Jude", aliases: ["jud"] },
|
||||
{ name: "Revelation", aliases: ["rev"] },
|
||||
];
|
||||
@@ -1,5 +1,6 @@
|
||||
import LocalBibleRefPlugin from "main";
|
||||
import { App, Notice, PluginSettingTab, Setting } from "obsidian";
|
||||
import LocalBibleRefPlugin from 'main';
|
||||
import { App, Notice, PluginSettingTab, Setting } from 'obsidian';
|
||||
import { PassageFormat } from './passage-reference';
|
||||
|
||||
export default class LocalBibleRefSettingTab extends PluginSettingTab {
|
||||
plugin: LocalBibleRefPlugin;
|
||||
@@ -13,14 +14,13 @@ export default class LocalBibleRefSettingTab extends PluginSettingTab {
|
||||
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.")
|
||||
.setName('Bibles Path')
|
||||
.setDesc('The path to the folder containing your bibles.')
|
||||
.addText(text => text
|
||||
.setPlaceholder("e.g. Data/Bibles")
|
||||
.setPlaceholder('e.g. Data/Bibles')
|
||||
.setValue(this.plugin.settings.biblesPath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.biblesPath = value;
|
||||
await this.plugin.saveSettings();
|
||||
@@ -29,15 +29,16 @@ export default class LocalBibleRefSettingTab extends PluginSettingTab {
|
||||
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);
|
||||
}, 1000);
|
||||
}));
|
||||
|
||||
let defaultVersionTimeout: NodeJS.Timeout;
|
||||
new Setting(containerEl)
|
||||
.setName("Default Version Shorthand")
|
||||
.setDesc("The version to use by default - shorthand.")
|
||||
.setName('Default Version Shorthand')
|
||||
.setDesc('The version to use by default - shorthand.')
|
||||
.addText(text => text
|
||||
.setPlaceholder("e.g. ESV")
|
||||
.setPlaceholder('e.g. NIV')
|
||||
.setValue(this.plugin.settings.defaultVersionShorthand)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.defaultVersionShorthand = value;
|
||||
await this.plugin.saveSettings();
|
||||
@@ -47,7 +48,22 @@ export default class LocalBibleRefSettingTab extends PluginSettingTab {
|
||||
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);
|
||||
}, 1000);
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Default Passage Format')
|
||||
.setDesc('The markdown format to use for passages by default.')
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOptions({
|
||||
paragraph: 'Paragraph',
|
||||
quote: 'Quote',
|
||||
callout: 'Callout',
|
||||
})
|
||||
.setValue(this.plugin.settings.defaultPassageFormat)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.defaultPassageFormat = value as PassageFormat;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
}
|
||||
223
src/passage-reference.ts
Normal file
223
src/passage-reference.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { Book, BOOKS } from "./books";
|
||||
|
||||
export class PassageReference implements ChapterReference, PassageOptions {
|
||||
startChapter: number;
|
||||
startVerse: number;
|
||||
endChapter: number;
|
||||
endVerse: number;
|
||||
book: Book;
|
||||
version: string;
|
||||
format: PassageFormat;
|
||||
|
||||
constructor(
|
||||
chapterRef: ChapterReference,
|
||||
book: Book,
|
||||
passageOptions: PassageOptions
|
||||
) {
|
||||
this.startChapter = chapterRef.startChapter;
|
||||
this.startVerse = chapterRef.startVerse;
|
||||
this.endChapter = chapterRef.endChapter;
|
||||
this.endVerse = chapterRef.endVerse;
|
||||
this.book = book;
|
||||
this.version = passageOptions.version;
|
||||
this.format = passageOptions.format;
|
||||
}
|
||||
|
||||
/** Parses a passage reference from the given text. */
|
||||
static parse(
|
||||
text: string,
|
||||
defaultVersionShorthand: string,
|
||||
defaultPassageFormat: PassageFormat
|
||||
): PassageReference | null {
|
||||
const match = text.match(this.regExp);
|
||||
if (!match) return null;
|
||||
|
||||
let chapterRef = this.parseMultiChapterRef(match[2]);
|
||||
if (!chapterRef) chapterRef = this.parseMultiChapterVerseRef(match[2]);
|
||||
if (!chapterRef) chapterRef = this.parseMultiVerseRef(match[2]);
|
||||
if (!chapterRef) return null;
|
||||
|
||||
const book = this.getBook(match[1]);
|
||||
if (!book) return null;
|
||||
|
||||
const options = this.parseOptions(
|
||||
match[3],
|
||||
defaultVersionShorthand,
|
||||
defaultPassageFormat
|
||||
);
|
||||
|
||||
return new PassageReference(chapterRef, book, options);
|
||||
}
|
||||
|
||||
/** Builds the passage matching regular expression. */
|
||||
static get regExp(): RegExp {
|
||||
let regExpString = "^\\-\\- ?(";
|
||||
regExpString += BOOKS.map(
|
||||
(b) => `${b.name}|${b.aliases.join("|")}`
|
||||
).join("|");
|
||||
regExpString += ") ?(\\d{1,3}(?::\\d{1,3})?" +
|
||||
"(?: ?\\- ?\\d{1,3}(?::\\d{1,3})?)?)" +
|
||||
"((?: ?\\+[a-z]+(?::[a-z]+)?){0,2})$";
|
||||
|
||||
return new RegExp(regExpString, "i");
|
||||
}
|
||||
|
||||
/** Stringifies the passage reference back into text. */
|
||||
stringify(): string {
|
||||
// multi-chapter ref
|
||||
if (this.startVerse === 1 && this.endVerse === -1) {
|
||||
if (this.startChapter === this.endChapter)
|
||||
return (
|
||||
this.book.name + `${this.startChapter} - ${this.version}`
|
||||
);
|
||||
return (
|
||||
`${this.book.name} ${this.startChapter}-` +
|
||||
`${this.endChapter} - ${this.version}`
|
||||
);
|
||||
}
|
||||
|
||||
// multi-verse ref
|
||||
if (this.startChapter === this.endChapter) {
|
||||
if (this.startVerse === this.endVerse)
|
||||
return (
|
||||
this.book.name +
|
||||
`${this.startChapter}:${this.startVerse} - ${this.version}`
|
||||
);
|
||||
return (
|
||||
`${this.book.name} ${this.startChapter}:` +
|
||||
`${this.startVerse}-${this.endVerse} - ${this.version}`
|
||||
);
|
||||
}
|
||||
|
||||
// multi-chapter-and-verse ref
|
||||
const a = `${this.startChapter}:${this.startVerse}`;
|
||||
const b = `${this.endChapter}:${this.endVerse}`;
|
||||
return `${this.book.name} ${a}-${b} - ${this.version}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a multi-chapter reference from the given text.
|
||||
* Reference format: `startChapter[[ ]-[ ]endChapter]`.
|
||||
*/
|
||||
private static parseMultiChapterRef(text: string): ChapterReference | null {
|
||||
const regExp = /^(\d{1,3})(?: ?- ?(\d{1,3}))?$/i;
|
||||
const match = text.match(regExp);
|
||||
if (!match) return null;
|
||||
|
||||
return {
|
||||
startChapter: +match[1],
|
||||
startVerse: 1,
|
||||
endChapter: match[2] ? +match[2] : +match[1],
|
||||
endVerse: -1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a multi-chapter-and-verse reference from the given text.
|
||||
* Reference format: `startChapter:startVerse[ ]-[ ]endChapter:endVerse`.
|
||||
*/
|
||||
private static parseMultiChapterVerseRef(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: +match[1],
|
||||
startVerse: +match[2],
|
||||
endChapter: +match[3],
|
||||
endVerse: +match[4],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a multi-verse reference from the given text.
|
||||
* Reference format: `startChapter:startVerse[-endVerse]`.
|
||||
*/
|
||||
private static parseMultiVerseRef(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;
|
||||
|
||||
return {
|
||||
startChapter: +match[1],
|
||||
startVerse: +match[2],
|
||||
endChapter: +match[1],
|
||||
endVerse: match[3] ? +match[3] : +match[2],
|
||||
};
|
||||
}
|
||||
|
||||
/** Retrieves a book based on its alias. */
|
||||
private static getBook(alias: string): Book | undefined {
|
||||
alias = alias.toLowerCase();
|
||||
return BOOKS.find((book) => {
|
||||
const aliases = book.aliases.map((a) => a.toLowerCase());
|
||||
if (book.name.toLowerCase() === alias) return book;
|
||||
if (aliases.includes(alias)) return book;
|
||||
});
|
||||
}
|
||||
|
||||
/** Parses passage options from the given text. */
|
||||
private static parseOptions(
|
||||
text: string,
|
||||
defaultVersionShorthand: string,
|
||||
defaultPassageFormat: PassageFormat
|
||||
): PassageOptions {
|
||||
const optionArgs = text
|
||||
.toLowerCase()
|
||||
.split("+")
|
||||
.filter(Boolean)
|
||||
.map((x) => x.trim());
|
||||
|
||||
const options: PassageOptions = {
|
||||
version: defaultVersionShorthand,
|
||||
format: defaultPassageFormat,
|
||||
};
|
||||
|
||||
// there are special keywords for formatting (m or manuscript, for
|
||||
// example) - anything else is treated as a Bible version code
|
||||
for (let option of optionArgs) {
|
||||
switch (option) {
|
||||
case "m":
|
||||
case "manuscript":
|
||||
options.format = PassageFormat.Manuscript;
|
||||
break;
|
||||
case "p":
|
||||
case "paragraph":
|
||||
options.format = PassageFormat.Paragraph;
|
||||
break;
|
||||
case "q":
|
||||
case "quote":
|
||||
options.format = PassageFormat.Quote;
|
||||
break;
|
||||
case "c":
|
||||
case "callout":
|
||||
options.format = PassageFormat.Callout;
|
||||
break;
|
||||
default:
|
||||
options.version = option.toUpperCase();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
export enum PassageFormat {
|
||||
Manuscript = "manuscript",
|
||||
Paragraph = "paragraph",
|
||||
Quote = "quote",
|
||||
Callout = "callout",
|
||||
}
|
||||
|
||||
interface ChapterReference {
|
||||
startChapter: number;
|
||||
startVerse: number;
|
||||
endChapter: number;
|
||||
endVerse: number;
|
||||
}
|
||||
|
||||
interface PassageOptions {
|
||||
version: string;
|
||||
format: PassageFormat;
|
||||
}
|
||||
300
src/passage-suggest.ts
Normal file
300
src/passage-suggest.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
import {
|
||||
App,
|
||||
Editor,
|
||||
EditorPosition,
|
||||
EditorSuggest,
|
||||
EditorSuggestContext,
|
||||
EditorSuggestTriggerInfo,
|
||||
TFolder,
|
||||
} from "obsidian";
|
||||
import { PassageFormat, PassageReference } from "./passage-reference";
|
||||
import { LocalBibleRefSettings } from "./settings";
|
||||
|
||||
export class PassageSuggest extends EditorSuggest<PassageSuggestion> {
|
||||
private settings: LocalBibleRefSettings;
|
||||
|
||||
constructor(app: App, settings: LocalBibleRefSettings) {
|
||||
super(app);
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
onTrigger(
|
||||
cursor: EditorPosition,
|
||||
editor: Editor,
|
||||
_: any
|
||||
): EditorSuggestTriggerInfo | 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 = PassageReference.regExp.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 = PassageReference.parse(
|
||||
context.query,
|
||||
this.settings.defaultVersionShorthand,
|
||||
this.settings.defaultPassageFormat,
|
||||
);
|
||||
if (!passageRef) return [];
|
||||
|
||||
// grab all chapters in the range
|
||||
let texts = await this.getChapterTexts(passageRef);
|
||||
if (!texts) return [];
|
||||
|
||||
// split first chapter by start verse
|
||||
const textFromVerse = this.getTextFromStartVerse(
|
||||
texts[0],
|
||||
passageRef
|
||||
);
|
||||
if (!textFromVerse) return [];
|
||||
texts[0] = textFromVerse;
|
||||
|
||||
// split last chapter by end verse
|
||||
const lastIndex = texts.length - 1;
|
||||
const textToVerse = this.getTextToEndVerse(
|
||||
texts[lastIndex],
|
||||
passageRef
|
||||
);
|
||||
if (!textToVerse) return [];
|
||||
texts[lastIndex] = textToVerse;
|
||||
|
||||
// clean up chapter texts
|
||||
const multipleChapters = texts.length > 1;
|
||||
texts = texts.map((text, i) => {
|
||||
const chapterNumber = passageRef.startChapter + i;
|
||||
return this.cleanText(text, chapterNumber, multipleChapters);
|
||||
});
|
||||
|
||||
// suggest
|
||||
const excerpt = this.generateExcerpt(texts[0]);
|
||||
const text = this.formatTexts(texts, passageRef, context);
|
||||
return [{ excerpt, text }];
|
||||
}
|
||||
|
||||
renderSuggestion(item: PassageSuggestion, el: HTMLElement): void {
|
||||
el.innerText = item.excerpt;
|
||||
}
|
||||
|
||||
selectSuggestion(
|
||||
item: PassageSuggestion,
|
||||
_: MouseEvent | KeyboardEvent
|
||||
): void {
|
||||
if (!this.context) return;
|
||||
this.context.editor.replaceRange(
|
||||
item.text,
|
||||
this.context.start,
|
||||
this.context.end
|
||||
);
|
||||
}
|
||||
|
||||
/** Retrieves the texts of the chapters within a passage ref. */
|
||||
private async getChapterTexts(
|
||||
ref: PassageReference
|
||||
): Promise<string[] | null> {
|
||||
let basePath = "";
|
||||
for (let alias of [ref.book.name, ...ref.book.aliases]) {
|
||||
basePath = [this.settings.biblesPath, ref.version, alias].join("/");
|
||||
|
||||
// if the book exists at this alias, use the alias instead
|
||||
// and add the previous book name to the aliases
|
||||
if (await this.app.vault.adapter.exists(basePath)) {
|
||||
ref.book.aliases.push(ref.book.name);
|
||||
ref.book.aliases.remove(alias);
|
||||
ref.book.name = alias;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const texts: string[] = [];
|
||||
|
||||
// collect chapter texts
|
||||
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) return null;
|
||||
|
||||
texts.push(await this.app.vault.cachedRead(file));
|
||||
}
|
||||
|
||||
return texts;
|
||||
}
|
||||
|
||||
/** Extracts the text in the chapter from the start verse to the end. */
|
||||
private getTextFromStartVerse(
|
||||
text: string,
|
||||
ref: PassageReference
|
||||
): string | null {
|
||||
const regExp = new RegExp(
|
||||
`(?:[>-] )*(?:\\*\\*\\d{1,3}\\*\\* )?<sup>${ref.startVerse}</sup>`
|
||||
);
|
||||
const match = text.match(regExp);
|
||||
if (!match) return null;
|
||||
|
||||
const verseLabel = match[0];
|
||||
const parts = text.split(regExp);
|
||||
return verseLabel + parts[1];
|
||||
}
|
||||
|
||||
/** Extracts the text in the chapter from the start to the end verse. */
|
||||
private getTextToEndVerse(
|
||||
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();
|
||||
}
|
||||
|
||||
private cleanText(
|
||||
text: string,
|
||||
chapterNumber: number,
|
||||
multipleChapters: boolean
|
||||
): string {
|
||||
text = this.removeChapterNumbers(text);
|
||||
text = this.removeHeadings(text);
|
||||
text = this.removeFootnoteRefs(text);
|
||||
text = this.removeBOF(text);
|
||||
text = this.removeEOF(text);
|
||||
|
||||
const chapterMd = multipleChapters ? `**${chapterNumber}**` : "";
|
||||
if (text.startsWith("> ")) {
|
||||
const quoteMd = text.match(/^(?:> )+/)![0];
|
||||
return text.replace(quoteMd, `${quoteMd}${chapterMd} `);
|
||||
}
|
||||
|
||||
if (text.startsWith("- ")) {
|
||||
const listMd = text.match(/^(?:- )+/)![0];
|
||||
return text.replace(listMd, `${listMd}${chapterMd} `);
|
||||
}
|
||||
|
||||
return (text = `${chapterMd} ${text}`);
|
||||
}
|
||||
|
||||
/** Removes chapter numbers from the given text. */
|
||||
private removeChapterNumbers(text: string): string {
|
||||
return text.replace(/\*\*\d{1,3}\*\* /g, "");
|
||||
}
|
||||
|
||||
/** 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(/ \[\^\w{1,9}\]/g, "");
|
||||
}
|
||||
|
||||
/** Removes the beginning-of-file content from the given text. */
|
||||
private removeBOF(text: string): string {
|
||||
if (!text.startsWith("---")) return text;
|
||||
// split at YAML front matter
|
||||
let split = text.split(/^\-\-\-$/m);
|
||||
return split[2].trim();
|
||||
}
|
||||
|
||||
/** 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 an excerpt for the suggestion. */
|
||||
private generateExcerpt(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(/ {2,}/g, " ");
|
||||
return text.slice(0, 45) + "...";
|
||||
}
|
||||
|
||||
/** Formats the final text for suggestion. */
|
||||
private formatTexts(
|
||||
texts: string[],
|
||||
passageRef: PassageReference,
|
||||
context: EditorSuggestContext
|
||||
): string {
|
||||
let formatted = "";
|
||||
switch (passageRef.format) {
|
||||
case PassageFormat.Manuscript:
|
||||
formatted = texts.join(" ").trim();
|
||||
formatted = formatted.replace(/\n+/g, " ");
|
||||
formatted = formatted.replace(/\*\*\d{1,3}\*\*/g, "");
|
||||
formatted = formatted.replace(/<sup>\d{1,3}<\/sup> /g, "");
|
||||
formatted = formatted.replace(/(?:^[>-] | [>-] )/g, " ");
|
||||
formatted = formatted.trim() + "\n\n";
|
||||
break;
|
||||
case PassageFormat.Paragraph:
|
||||
formatted = texts.join("\n\n").trim();
|
||||
formatted += "\n\n";
|
||||
break;
|
||||
case PassageFormat.Quote:
|
||||
formatted = "> ";
|
||||
formatted += texts.join("\n\n").trim();
|
||||
formatted = formatted.replace(/\n/gm, "\n> ");
|
||||
formatted += "\n\n";
|
||||
break;
|
||||
case PassageFormat.Callout:
|
||||
const passageReference = passageRef.stringify();
|
||||
const passageLink = this.generatePassageLink(passageRef, context);
|
||||
formatted = `> [!quote] [${passageReference}](${passageLink})\n`;
|
||||
formatted += texts.join("\n\n").trim();
|
||||
formatted = formatted.replace(/\n/gm, "\n> ");
|
||||
formatted += "\n\n";
|
||||
break;
|
||||
}
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
/** Generates a link to the passage within the vault. */
|
||||
private generatePassageLink(
|
||||
ref: PassageReference,
|
||||
context: EditorSuggestContext
|
||||
): string {
|
||||
let link = "";
|
||||
let folder: TFolder | null = context.file.parent;
|
||||
while (folder?.parent) {
|
||||
link += "../";
|
||||
folder = folder.parent;
|
||||
}
|
||||
|
||||
const { version, book, startChapter } = ref;
|
||||
link += `${this.settings.biblesPath}/${version}/` +
|
||||
`${book.name}/${book.name} ${startChapter}.md`;
|
||||
return link.replace(/ /g, "%20");
|
||||
}
|
||||
}
|
||||
|
||||
interface PassageSuggestion {
|
||||
excerpt: string;
|
||||
text: string;
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { PassageFormat } from "./passage-reference";
|
||||
|
||||
export interface LocalBibleRefSettings {
|
||||
biblesPath: string;
|
||||
defaultVersionShorthand: string;
|
||||
defaultPassageFormat: PassageFormat;
|
||||
}
|
||||
Reference in New Issue
Block a user