Base implementation

This commit is contained in:
Scott Tomaszewski 2025-03-09 17:32:25 -04:00
parent 123e85369a
commit dd195b911c
15 changed files with 3531 additions and 204 deletions

173
README.md
View file

@ -1,94 +1,101 @@
# Obsidian Sample Plugin
# Disciples Journal - Bible Verse Reference Plugin for Obsidian
This is a sample plugin for Obsidian (https://obsidian.md).
This plugin enhances your Bible study and journaling in Obsidian by rendering Bible verse references and passages directly in your notes.
This project uses TypeScript to provide type checking and documentation.
The repo depends on the latest plugin API (obsidian.d.ts) in TypeScript Definition format, which contains TSDoc comments describing what it does.
## Features
This sample plugin demonstrates some of the basic functionality the plugin API can do.
- Adds a ribbon icon, which shows a Notice when clicked.
- Adds a command "Open Sample Modal" which opens a Modal.
- Adds a plugin setting tab to the settings page.
- Registers a global click event and output 'click' to the console.
- Registers a global interval which logs 'setInterval' to the console.
### Inline Verse References
## First time developing plugins?
Type a Bible reference in an inline code block like this:
Quick starting guide for new plugin devs:
- Check if [someone already developed a plugin for what you want](https://obsidian.md/plugins)! There might be an existing plugin similar enough that you can partner up with.
- Make a copy of this repo as a template with the "Use this template" button (login to GitHub if you don't see it).
- Clone your repo to a local development folder. For convenience, you can place this folder in your `.obsidian/plugins/your-plugin-name` folder.
- Install NodeJS, then run `npm i` in the command line under your repo folder.
- Run `npm run dev` to compile your plugin from `main.ts` to `main.js`.
- Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`.
- Reload Obsidian to load the new version of your plugin.
- Enable plugin in settings window.
- For updates to the Obsidian API run `npm update` in the command line under your repo folder.
## Releasing new releases
- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release.
- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible.
- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases
- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release.
- Publish the release.
> You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`.
> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json`
## Adding your plugin to the community plugin list
- Check the [plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines).
- Publish an initial version.
- Make sure you have a `README.md` file in the root of your repo.
- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin.
## How to use
- Clone this repo.
- Make sure your NodeJS is at least v16 (`node --version`).
- `npm i` or `yarn` to install dependencies.
- `npm run dev` to start compilation in watch mode.
## Manually installing the plugin
- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
## Improve code quality with eslint (optional)
- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code.
- To use eslint with this project, make sure to install eslint from terminal:
- `npm install -g eslint`
- To use eslint to analyze this project use this command:
- `eslint main.ts`
- eslint will then create a report with suggestions for code improvement by file and line number.
- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder:
- `eslint .\src\`
## Funding URL
You can include funding URLs where people who use your plugin can financially support it.
The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file:
```json
{
"fundingUrl": "https://buymeacoffee.com"
}
```
`Genesis 1:1`
```
If you have multiple URLs, you can also do:
The plugin will transform it into a clickable link. When you:
```json
{
"fundingUrl": {
"Buy Me a Coffee": "https://buymeacoffee.com",
"GitHub Sponsor": "https://github.com/sponsors",
"Patreon": "https://www.patreon.com/"
}
}
- **Hover** over the reference: Shows a preview of the verse text
- **Click** on the reference: Opens or creates a note for that chapter
### Full Passage Rendering
For longer passages, use a code block with the "bible" language specifier:
````
```bible
Genesis 1:1-10
```
````
## API Documentation
This will render the entire passage directly in your note, displaying all verses in the range.
See https://github.com/obsidianmd/obsidian-api
## Supported Reference Formats
The plugin supports various Bible reference formats:
- Single verses: `Genesis 1:1`, `John 3:16`
- Verse ranges in the same chapter: `Genesis 1:1-10`, `John 3:16-18`
- Entire chapters: `Genesis 1`, `Psalm 23`
- Multi-chapter passages: `Matthew 5:3-7:29`
- Books with spaces: `1 Corinthians 13:4-7`, `Song of Solomon 2:1`
## Settings
Access plugin settings in Settings → Disciples Journal:
- **Display Settings**:
- **Display Inline Verses**: Toggle inline verse reference rendering
- **Display Full Passages**: Toggle full passage code block rendering
- **Verse Font Size**: Customize the font size for displayed verses
- **Bible Version Settings**:
- Currently only supports the English Standard Version (ESV)
- **Diagnostics**:
- Check Bible data loading status
- Reload Bible data if needed
## Installation
1. In Obsidian, go to Settings → Community plugins → Browse
2. Search for "Disciples Journal"
3. Click Install, then Enable
## Manual Installation
1. Download the latest release from the releases page
2. Extract the zip file to your Obsidian plugins folder: `.obsidian/plugins/`
3. Make sure the `ESV.json` file is in the `src` directory within the plugin folder
4. Enable the plugin in Obsidian's community plugins settings
## Troubleshooting
### Bible Data Not Loading
This plugin requires the `ESV.json` file to be properly loaded. If you see an error about Bible data not loading, try these steps:
1. Go to Settings → Disciples Journal → Diagnostics
2. Click the "Reload Bible Data" button
3. If that doesn't work, verify that the `ESV.json` file exists in the plugin's `src` directory
4. Restart Obsidian and try again
### Verse References Not Working
If your verse references aren't being processed correctly:
1. Make sure the plugin is enabled
2. Check that you're using the correct format (e.g., `Genesis 1:1` inside backticks)
3. For code blocks, ensure you're using the "bible" language specification
4. Try reloading the plugin through Settings → Disciples Journal → Diagnostics
## Scripture Copyright
Scripture quotations marked "ESV" are from the ESV® Bible (The Holy Bible, English Standard Version®), copyright © 2001 by Crossway, a publishing ministry of Good News Publishers. Used by permission. All rights reserved.
## Feedback and Contributions
If you encounter any issues or have suggestions for improvements, please open an issue on [GitHub](https://github.com/scottTomaszewski/obsidian-disciples-journal).
## License
This project is licensed under the MIT License - see the LICENSE file for details.

View file

@ -1,6 +1,8 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import fs from "fs";
import path from "path";
const banner =
`/*
@ -11,6 +13,21 @@ if you want to view the source, please visit the github repository of this plugi
const prod = (process.argv[2] === "production");
// Ensure ESV.json exists and is copied to the output directory
const esvJsonPath = path.resolve("./src/ESV.json");
const destDir = "./";
// Make sure we have the ESV.json file
if (fs.existsSync(esvJsonPath)) {
console.log("ESV.json found, including in build");
// Create the directory if it doesn't exist
fs.mkdirSync(path.dirname(path.resolve(destDir, "src/ESV.json")), { recursive: true });
// Copy the file
fs.copyFileSync(esvJsonPath, path.resolve(destDir, "src/ESV.json"));
} else {
console.error("ESV.json not found at:", esvJsonPath);
}
const context = await esbuild.context({
banner: {
js: banner,
@ -39,6 +56,7 @@ const context = await esbuild.context({
treeShaking: true,
outfile: "main.js",
minify: prod,
loader: { ".json": "json" },
});
if (prod) {

577
main.ts
View file

@ -1,134 +1,489 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
import { App, MarkdownPostProcessorContext, Plugin, PluginSettingTab, Setting, TFile, MarkdownView, Notice, requestUrl } from 'obsidian';
import { BibleService } from './src/services/BibleService';
import { BibleReferenceParser } from './src/utils/BibleReferenceParser';
import { BiblePassage } from './src/models/BibleReference';
// Remember to rename these classes and interfaces!
interface MyPluginSettings {
mySetting: string;
// Import the Bible data
// We'll try different approaches to ensure it loads properly
let ESV: any = null;
try {
// Approach 1: Direct import (works if ESV.json is properly bundled)
// @ts-ignore
ESV = require('./src/ESV.json');
} catch (e) {
console.error("Failed to load ESV.json using require:", e);
// The actual file loading will be handled in the plugin's onload method
}
const DEFAULT_SETTINGS: MyPluginSettings = {
mySetting: 'default'
interface DisciplesJournalSettings {
displayInlineVerses: boolean;
displayFullPassages: boolean;
fontSizeForVerses: string;
preferredBibleVersion: string;
}
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
const DEFAULT_SETTINGS: DisciplesJournalSettings = {
displayInlineVerses: true,
displayFullPassages: true,
fontSizeForVerses: '100%',
preferredBibleVersion: 'ESV'
};
async onload() {
await this.loadSettings();
export default class DisciplesJournalPlugin extends Plugin {
settings: DisciplesJournalSettings;
bibleService: BibleService;
bibleDataLoaded: boolean = false;
loadingNotice: Notice | null = null;
// This creates an icon in the left ribbon.
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
// Called when the user clicks the icon.
new Notice('This is a notice!');
});
// Perform additional things with the ribbon
ribbonIconEl.addClass('my-plugin-ribbon-class');
async onload() {
console.log("Loading Disciples Journal plugin...");
await this.loadSettings();
// Initialize Bible service
this.bibleService = new BibleService();
// Show loading notice
this.loadingNotice = new Notice("Loading Bible data...", 0);
// Try to load the Bible data
try {
await this.loadBibleData();
// Register Markdown post processor for inline code (e.g., `Genesis 1:1`)
this.registerMarkdownPostProcessor((element, context) => {
this.processInlineCodeBlocks(element, context);
});
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
const statusBarItemEl = this.addStatusBarItem();
statusBarItemEl.setText('Status Bar Text');
// Register Markdown code block processor for multiline passages (e.g., ```bible Genesis 1:1-10 ```)
this.registerMarkdownCodeBlockProcessor('bible', (source, el, ctx) => {
this.processFullBiblePassage(source, el, ctx);
});
// This adds a simple command that can be triggered anywhere
this.addCommand({
id: 'open-sample-modal-simple',
name: 'Open sample modal (simple)',
callback: () => {
new SampleModal(this.app).open();
}
});
// This adds an editor command that can perform some operation on the current editor instance
this.addCommand({
id: 'sample-editor-command',
name: 'Sample editor command',
editorCallback: (editor: Editor, view: MarkdownView) => {
console.log(editor.getSelection());
editor.replaceSelection('Sample Editor Command');
}
});
// This adds a complex command that can check whether the current state of the app allows execution of the command
this.addCommand({
id: 'open-sample-modal-complex',
name: 'Open sample modal (complex)',
checkCallback: (checking: boolean) => {
// Conditions to check
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (markdownView) {
// If checking is true, we're simply "checking" if the command can be run.
// If checking is false, then we want to actually perform the operation.
if (!checking) {
new SampleModal(this.app).open();
}
// Register event handlers
this.registerEvent(
this.app.workspace.on('layout-change', () => {
// Reprocess any open files if Bible data was loaded after they were opened
if (this.bibleDataLoaded) {
this.app.workspace.iterateAllLeaves((leaf) => {
const view = leaf.view;
if (view instanceof MarkdownView) {
view.previewMode.rerender(true);
}
});
}
})
);
// This command will only show up in Command Palette when the check function returns true
return true;
}
}
});
// Add settings tab
this.addSettingTab(new DisciplesJournalSettingsTab(this.app, this));
// Clear the loading notice and show success
if (this.loadingNotice) {
this.loadingNotice.hide();
this.loadingNotice = null;
}
new Notice('Disciples Journal Bible plugin loaded successfully!', 3000);
} catch (error) {
console.error("Failed to initialize Bible service:", error);
// Clear the loading notice and show error
if (this.loadingNotice) {
this.loadingNotice.hide();
this.loadingNotice = null;
}
new Notice("Failed to load Bible data. Please check the console for details.", 5000);
// Still register the settings tab so the user can change settings
this.addSettingTab(new DisciplesJournalSettingsTab(this.app, this));
}
}
/**
* Load the Bible data from various possible sources
*/
public async loadBibleData(): Promise<void> {
try {
// Try approach 1: Use the data from require if available
if (ESV) {
console.log("Loading Bible data from require (memory)");
this.bibleService.loadBible(ESV);
this.bibleDataLoaded = true;
return;
}
console.log("Attempting to load Bible data from file...");
// Try approach 2: Load from multiple possible file locations
await this.tryLoadingBibleData();
} catch (error) {
console.error("All Bible data loading methods failed:", error);
this.bibleDataLoaded = false;
throw new Error("Could not load Bible data from any source");
}
}
/**
* Try multiple approaches to load the Bible data
*/
private async tryLoadingBibleData(): Promise<void> {
const pluginDir = this.manifest.dir;
const possiblePaths = [
`${pluginDir}/src/ESV.json`,
'./src/ESV.json',
'../../disciples-journal-vault/ESV.json', // Check the vault root
'../ESV.json',
];
let lastError: Error | null = null;
// Try each path in order
for (const path of possiblePaths) {
try {
console.log(`Attempting to load ESV.json from: ${path}`);
// Try using requestUrl from Obsidian API
const response = await requestUrl({
url: path,
method: 'GET',
});
if (response.status === 200 && response.json) {
console.log(`ESV.json loaded successfully from ${path}`);
this.bibleService.loadBible(response.json);
this.bibleDataLoaded = true;
return;
}
} catch (error) {
console.log(`Failed to load from ${path}:`, error);
lastError = error as Error;
// Continue to the next path
}
}
// If we got here, all attempts failed
if (lastError) {
throw new Error(`Failed to load Bible data: ${lastError.message}`);
} else {
throw new Error('Failed to load Bible data from any location');
}
}
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new SampleSettingTab(this.app, this));
/**
* Process inline code blocks that might contain Bible references
*/
private processInlineCodeBlocks(element: HTMLElement, context: MarkdownPostProcessorContext): void {
if (!this.settings.displayInlineVerses) return;
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
// Using this function will automatically remove the event listener when this plugin is disabled.
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
console.log('click', evt);
});
const codeBlocks = element.querySelectorAll('code:not(.block-language-bible)');
codeBlocks.forEach(codeBlock => {
const text = codeBlock.textContent?.trim() || '';
const reference = BibleReferenceParser.parseReference(text);
if (reference) {
// Check if it's a valid Bible reference
const referenceType = BibleReferenceParser.getReferenceType(reference);
// Convert inline code to a Bible reference link
const linkEl = document.createElement('a');
linkEl.classList.add('bible-reference');
linkEl.textContent = text;
linkEl.dataset.referenceText = text;
// Show verse text on hover
this.registerDomEvent(linkEl, 'mouseover', (e) => {
this.showVersePreview(linkEl, text, e);
});
// Open chapter note when clicked
this.registerDomEvent(linkEl, 'click', (e) => {
e.preventDefault();
this.openChapterNote(reference.book, reference.startChapter);
});
// Replace the code element with our custom link
codeBlock.replaceWith(linkEl);
}
});
}
/**
* Process full Bible passage code blocks
*/
private async processFullBiblePassage(source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext): Promise<void> {
if (!this.settings.displayFullPassages) return;
const reference = source.trim();
const passage = this.bibleService.getBibleContent(reference);
if (passage) {
const containerEl = document.createElement('div');
containerEl.classList.add('bible-passage-container');
// Add reference heading
const headingEl = document.createElement('h3');
headingEl.classList.add('bible-passage-heading');
headingEl.textContent = passage.reference;
containerEl.appendChild(headingEl);
// Add verses
const passageEl = document.createElement('div');
passageEl.classList.add('bible-passage-text');
passageEl.style.fontSize = this.settings.fontSizeForVerses;
for (const verse of passage.verses) {
const verseEl = document.createElement('p');
verseEl.classList.add('bible-verse');
const verseNumEl = document.createElement('span');
verseNumEl.classList.add('bible-verse-number');
verseNumEl.textContent = `${verse.verse} `;
const verseTextEl = document.createElement('span');
verseTextEl.classList.add('bible-verse-text');
verseTextEl.textContent = verse.text;
verseEl.appendChild(verseNumEl);
verseEl.appendChild(verseTextEl);
passageEl.appendChild(verseEl);
}
containerEl.appendChild(passageEl);
el.appendChild(containerEl);
} else {
// If reference not found, show error
const errorEl = document.createElement('div');
errorEl.classList.add('bible-reference-error');
errorEl.textContent = `Bible reference "${reference}" not found.`;
el.appendChild(errorEl);
}
}
/**
* Show a verse preview in a hover popup
*/
private async showVersePreview(element: HTMLElement, referenceText: string, event: MouseEvent): Promise<void> {
const passage = this.bibleService.getBibleContent(referenceText);
if (!passage) return;
// Create verse preview element
const versePreviewEl = document.createElement('div');
versePreviewEl.classList.add('bible-verse-preview');
// Add reference heading
const headingEl = document.createElement('div');
headingEl.classList.add('bible-verse-preview-heading');
headingEl.textContent = passage.reference;
versePreviewEl.appendChild(headingEl);
// Add verse content
const contentEl = document.createElement('div');
contentEl.classList.add('bible-verse-preview-content');
for (const verse of passage.verses) {
const verseEl = document.createElement('p');
if (passage.verses.length > 1) {
const verseNumEl = document.createElement('span');
verseNumEl.classList.add('bible-verse-number');
verseNumEl.textContent = `${verse.verse} `;
verseEl.appendChild(verseNumEl);
}
const verseTextEl = document.createElement('span');
verseTextEl.textContent = verse.text;
verseEl.appendChild(verseTextEl);
contentEl.appendChild(verseEl);
}
versePreviewEl.appendChild(contentEl);
// Show the verse preview as a tooltip
// We'll position it manually near the element
const rect = element.getBoundingClientRect();
versePreviewEl.style.position = 'absolute';
versePreviewEl.style.left = `${rect.left}px`;
versePreviewEl.style.top = `${rect.bottom + 10}px`;
versePreviewEl.style.zIndex = '1000';
versePreviewEl.style.backgroundColor = 'var(--background-secondary)';
versePreviewEl.style.padding = '10px';
versePreviewEl.style.borderRadius = '5px';
versePreviewEl.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.3)';
versePreviewEl.style.maxWidth = '400px';
document.body.appendChild(versePreviewEl);
// Remove the preview when mouse leaves the element or clicks elsewhere
const removePreview = () => {
document.body.removeChild(versePreviewEl);
element.removeEventListener('mouseleave', removePreview);
document.removeEventListener('click', removePreview);
};
element.addEventListener('mouseleave', removePreview);
document.addEventListener('click', removePreview);
}
/**
* Open a note for the specified Bible chapter
*/
private async openChapterNote(book: string, chapter: number): Promise<void> {
// Format the filename for the chapter note
const fileName = `Bible/${book}/${book} ${chapter}.md`;
// Check if the note exists
const file = this.app.vault.getAbstractFileByPath(fileName);
if (file instanceof TFile) {
// If it exists, open it
const leaf = this.app.workspace.getLeaf();
await leaf.openFile(file);
} else {
// If it doesn't exist, create it
try {
// Make sure directories exist
await this.app.vault.createFolder(`Bible/${book}`).catch(() => {
// Folder might already exist, just continue
});
// Get the chapter content
const bibleChapter = this.bibleService.getChapter(book, chapter);
if (!bibleChapter) {
new Notice(`Chapter ${book} ${chapter} not found.`);
return;
}
// Format the note content
let content = `# ${book} ${chapter}\n\n`;
for (const verse of bibleChapter.verses) {
content += `**${verse.verse}** ${verse.text}\n\n`;
}
// Create the file
const newFile = await this.app.vault.create(fileName, content);
// Open the new file
const leaf = this.app.workspace.getLeaf();
await leaf.openFile(newFile);
} catch (error) {
console.error('Error creating chapter note:', error);
new Notice(`Error creating note for ${book} ${chapter}`);
}
}
}
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
}
onunload() {
// Clean up
}
onunload() {
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
async saveSettings() {
await this.saveData(this.settings);
}
}
class SampleModal extends Modal {
constructor(app: App) {
super(app);
}
class DisciplesJournalSettingsTab extends PluginSettingTab {
plugin: DisciplesJournalPlugin;
onOpen() {
const {contentEl} = this;
contentEl.setText('Woah!');
}
constructor(app: App, plugin: DisciplesJournalPlugin) {
super(app, plugin);
this.plugin = plugin;
}
onClose() {
const {contentEl} = this;
contentEl.empty();
}
}
display(): void {
const { containerEl } = this;
containerEl.empty();
class SampleSettingTab extends PluginSettingTab {
plugin: MyPlugin;
containerEl.createEl('h2', { text: 'Disciples Journal Settings' });
containerEl.createEl('h3', { text: 'Display Settings' });
constructor(app: App, plugin: MyPlugin) {
super(app, plugin);
this.plugin = plugin;
}
new Setting(containerEl)
.setName('Display Inline Verses')
.setDesc('Enable inline Bible verse references using `Genesis 1:1` syntax')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.displayInlineVerses)
.onChange(async (value) => {
this.plugin.settings.displayInlineVerses = value;
await this.plugin.saveSettings();
}));
display(): void {
const {containerEl} = this;
new Setting(containerEl)
.setName('Display Full Passages')
.setDesc('Enable full Bible passage blocks using ```bible Genesis 1:1-10``` syntax')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.displayFullPassages)
.onChange(async (value) => {
this.plugin.settings.displayFullPassages = value;
await this.plugin.saveSettings();
}));
containerEl.empty();
new Setting(containerEl)
.setName('Setting #1')
.setDesc('It\'s a secret')
.addText(text => text
.setPlaceholder('Enter your secret')
.setValue(this.plugin.settings.mySetting)
.onChange(async (value) => {
this.plugin.settings.mySetting = value;
await this.plugin.saveSettings();
}));
}
}
new Setting(containerEl)
.setName('Verse Font Size')
.setDesc('Set the font size for displayed verses (e.g., 100%, 14px, 1.2em)')
.addText(text => text
.setPlaceholder('100%')
.setValue(this.plugin.settings.fontSizeForVerses)
.onChange(async (value) => {
this.plugin.settings.fontSizeForVerses = value;
await this.plugin.saveSettings();
}));
containerEl.createEl('h3', { text: 'Bible Version Settings' });
new Setting(containerEl)
.setName('Preferred Bible Version')
.setDesc('Select the preferred Bible version (currently only ESV is supported)')
.addDropdown(dropdown => dropdown
.addOption('ESV', 'English Standard Version (ESV)')
.setValue(this.plugin.settings.preferredBibleVersion)
.onChange(async (value) => {
this.plugin.settings.preferredBibleVersion = value;
await this.plugin.saveSettings();
})
);
containerEl.createEl('h3', { text: 'About' });
const aboutDiv = containerEl.createDiv();
aboutDiv.addClass('disciples-journal-about');
aboutDiv.innerHTML = `
<p>Disciples Journal Bible plugin for Obsidian</p>
<p>Version: ${this.plugin.manifest.version}</p>
<p>Transform Bible references into interactive elements in your notes.</p>
<p><small>ESV® Bible copyright information: Scripture quotations marked "ESV" are from the ESV® Bible
(The Holy Bible, English Standard Version®), copyright © 2001 by Crossway, a publishing ministry of Good News Publishers.
Used by permission. All rights reserved.</small></p>
`;
// Add a button to check Bible data status
containerEl.createEl('h3', { text: 'Diagnostics' });
new Setting(containerEl)
.setName('Bible Data Status')
.setDesc(this.plugin.bibleDataLoaded ? 'Bible data loaded successfully' : 'Bible data not loaded')
.addButton(button => button
.setButtonText('Reload Bible Data')
.onClick(async () => {
try {
button.setButtonText('Loading...');
button.setDisabled(true);
await this.plugin.loadBibleData();
new Notice('Bible data reloaded successfully!');
this.display(); // Refresh settings to show updated status
} catch (error) {
console.error('Failed to reload Bible data:', error);
new Notice('Failed to reload Bible data. Check console for details.');
} finally {
button.setButtonText('Reload Bible Data');
button.setDisabled(false);
}
})
);
}
}

View file

@ -1,11 +1,11 @@
{
"id": "sample-plugin",
"name": "Sample Plugin",
"version": "1.0.0",
"id": "disciples-journal",
"name": "Disciples Journal",
"version": "0.1.0",
"minAppVersion": "0.15.0",
"description": "Demonstrates some of the capabilities of the Obsidian API.",
"author": "Obsidian",
"authorUrl": "https://obsidian.md",
"fundingUrl": "https://obsidian.md/pricing",
"isDesktopOnly": false
"description": "Bible verse reference and passage renderer for Obsidian. Transforms inline code references like `Genesis 1:1` into interactive verses and renders full passages with ```bible``` code blocks.",
"author": "Scott Tomaszewski (Xentis)",
"authorUrl": "https://github.com/scottTomaszewski",
"isDesktopOnly": false,
"fundingUrl": ""
}

2288
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -12,13 +12,14 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@rollup/plugin-json": "^6.1.0",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"tslib": "2.4.0",
"tslib": "^2.4.0",
"typescript": "4.7.4"
}
}

7
rollup.config.js Normal file
View file

@ -0,0 +1,7 @@
import json from "@rollup/plugin-json";
export default {
plugins: [
json()
]
};

File diff suppressed because one or more lines are too long

1
src/ESV.json Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,21 @@
export interface BibleVerse {
book: string;
chapter: number;
verse: number;
text: string;
}
export interface BiblePassage {
reference: string;
verses: BibleVerse[];
}
export interface BibleReferenceRange {
book: string;
startChapter: number;
startVerse: number;
endChapter?: number;
endVerse?: number;
}
export type BibleReferenceType = 'verse' | 'passage' | 'chapter';

View file

@ -0,0 +1,333 @@
import { App } from "obsidian";
import { BiblePassage, BibleReferenceRange, BibleVerse } from "../models/BibleReference";
import { BibleReferenceParser } from "../utils/BibleReferenceParser";
interface Bible {
[book: string]: {
[chapter: string]: {
[verse: string]: string;
}
}
}
export class BibleService {
private bible: Bible | null = null;
private bookNameMap: Map<string, string> = new Map();
constructor() {
this.initializeBookNameMap();
}
/**
* Initialize the map of standardized book names
*/
private initializeBookNameMap(): void {
// Old Testament
this.addBookMapping("Genesis", ["Gen", "Ge", "Gn"]);
this.addBookMapping("Exodus", ["Exo", "Ex", "Exod"]);
this.addBookMapping("Leviticus", ["Lev", "Le", "Lv"]);
this.addBookMapping("Numbers", ["Num", "Nu", "Nm", "Nb"]);
this.addBookMapping("Deuteronomy", ["Deut", "De", "Dt"]);
this.addBookMapping("Joshua", ["Josh", "Jos", "Jsh"]);
this.addBookMapping("Judges", ["Judg", "Jdg", "Jg"]);
this.addBookMapping("Ruth", ["Rth", "Ru"]);
this.addBookMapping("1 Samuel", ["1 Sam", "1 Sa", "1S", "I Sa", "1Sam", "1st Samuel"]);
this.addBookMapping("2 Samuel", ["2 Sam", "2 Sa", "2S", "II Sa", "2Sam", "2nd Samuel"]);
// ... Add more books as needed
// New Testament
this.addBookMapping("Matthew", ["Matt", "Mt"]);
this.addBookMapping("Mark", ["Mrk", "Mk", "Mr"]);
this.addBookMapping("Luke", ["Luk", "Lk"]);
this.addBookMapping("John", ["Jn", "Jhn"]);
this.addBookMapping("Acts", ["Act", "Ac"]);
this.addBookMapping("Romans", ["Rom", "Ro", "Rm"]);
this.addBookMapping("1 Corinthians", ["1 Cor", "1 Co", "I Co", "1Cor", "1st Corinthians"]);
this.addBookMapping("2 Corinthians", ["2 Cor", "2 Co", "II Co", "2Cor", "2nd Corinthians"]);
// ... Add more books as needed
}
/**
* Helper method to add book name mappings
*/
private addBookMapping(standardName: string, alternateNames: string[]): void {
this.bookNameMap.set(standardName.toLowerCase(), standardName);
alternateNames.forEach(name => {
this.bookNameMap.set(name.toLowerCase(), standardName);
});
}
/**
* Load the Bible data directly from a provided Bible object
* This method is called from the main plugin with the imported Bible data
*/
public loadBible(bibleData: any): void {
// Process the raw data into our expected format
try {
// Check if we have valid data
if (!bibleData) {
throw new Error("No Bible data provided");
}
// Convert the data into our required format if needed
const formattedData: Bible = {};
// If the data is already in our expected format, use it directly
if (typeof bibleData === 'object' &&
bibleData.hasOwnProperty('Genesis') &&
bibleData.Genesis.hasOwnProperty('1')) {
this.bible = bibleData as Bible;
return;
}
// Map numeric book IDs to names
const bookIdMap: {[key: string]: 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"
};
// Process the ESV.json format
if (Array.isArray(bibleData)) {
console.log(`Processing ${bibleData.length} verses from ESV data`);
// Track our progress
let processedVerses = 0;
const totalVerses = bibleData.length;
const progressInterval = Math.floor(totalVerses / 10); // Report progress at 10% intervals
bibleData.forEach(verse => {
// Increment processed count
processedVerses++;
// Log progress
if (processedVerses % progressInterval === 0) {
const percentComplete = Math.floor((processedVerses / totalVerses) * 100);
console.log(`Bible data processing: ${percentComplete}% complete`);
}
// Get proper book name from ID or use as is if it's a string
let bookName: string;
if (typeof verse.book === 'number') {
bookName = bookIdMap[verse.book.toString()] || `Book ${verse.book}`;
} else {
bookName = verse.book.toString();
}
const chapter = verse.chapter.toString();
const verseNum = verse.verse.toString();
const text = verse.text;
// Create the nested objects if they don't exist
if (!formattedData[bookName]) {
formattedData[bookName] = {};
}
if (!formattedData[bookName][chapter]) {
formattedData[bookName][chapter] = {};
}
// Add the verse text
formattedData[bookName][chapter][verseNum] = text;
});
console.log(`Bible data processing complete: ${processedVerses} verses processed`);
} else {
console.error("Unsupported Bible data format:", typeof bibleData);
throw new Error("Unsupported Bible data format");
}
this.bible = formattedData;
console.log("Bible data loaded successfully into structured format");
} catch (error) {
console.error("Failed to process Bible data:", error);
throw new Error("Failed to process Bible data");
}
}
/**
* Get a single verse by reference
*/
public getVerse(book: string, chapter: number, verse: number): BibleVerse | null {
if (!this.bible) {
throw new Error("Bible data not loaded");
}
const standardBook = this.standardizeBookName(book);
if (!standardBook || !this.bible[standardBook]) {
return null;
}
const chapterData = this.bible[standardBook][chapter.toString()];
if (!chapterData) {
return null;
}
const verseText = chapterData[verse.toString()];
if (!verseText) {
return null;
}
return {
book: standardBook,
chapter,
verse,
text: verseText
};
}
/**
* Get a passage by reference range
*/
public getPassage(reference: BibleReferenceRange): BiblePassage | null {
if (!this.bible) {
throw new Error("Bible data not loaded");
}
const verses: BibleVerse[] = [];
const standardBook = this.standardizeBookName(reference.book);
if (!standardBook || !this.bible[standardBook]) {
return null;
}
const startChapter = reference.startChapter;
const startVerse = reference.startVerse;
const endChapter = reference.endChapter || startChapter;
const endVerse = reference.endVerse || startVerse;
// Loop through chapters and verses in the range
for (let chapter = startChapter; chapter <= endChapter; chapter++) {
const chapterData = this.bible[standardBook][chapter.toString()];
if (!chapterData) continue;
const firstVerse = chapter === startChapter ? startVerse : 1;
const lastVerse = chapter === endChapter ? endVerse : Object.keys(chapterData).length;
for (let verse = firstVerse; verse <= lastVerse; verse++) {
const verseText = chapterData[verse.toString()];
if (verseText) {
verses.push({
book: standardBook,
chapter,
verse,
text: verseText
});
}
}
}
if (verses.length === 0) {
return null;
}
// Format the reference string
let referenceString = `${standardBook} ${startChapter}`;
if (startVerse > 1 || endVerse) {
referenceString += `:${startVerse}`;
if (endChapter > startChapter) {
referenceString += `-${endChapter}:${endVerse}`;
} else if (endVerse && endVerse > startVerse) {
referenceString += `-${endVerse}`;
}
}
return {
reference: referenceString,
verses
};
}
/**
* Get an entire chapter
*/
public getChapter(book: string, chapter: number): BiblePassage | null {
if (!this.bible) {
throw new Error("Bible data not loaded");
}
const standardBook = this.standardizeBookName(book);
if (!standardBook || !this.bible[standardBook]) {
return null;
}
const chapterData = this.bible[standardBook][chapter.toString()];
if (!chapterData) {
return null;
}
const verses: BibleVerse[] = [];
const verseNumbers = Object.keys(chapterData).map(v => parseInt(v)).sort((a, b) => a - b);
for (const verse of verseNumbers) {
verses.push({
book: standardBook,
chapter,
verse,
text: chapterData[verse.toString()]
});
}
return {
reference: `${standardBook} ${chapter}`,
verses
};
}
/**
* Get Bible content by parsing a reference string
*/
public getBibleContent(referenceString: string): BiblePassage | null {
const reference = BibleReferenceParser.parseReference(referenceString);
if (!reference) {
return null;
}
const referenceType = BibleReferenceParser.getReferenceType(reference);
if (referenceType === 'verse') {
const verse = this.getVerse(reference.book, reference.startChapter, reference.startVerse);
if (!verse) {
return null;
}
return {
reference: `${verse.book} ${verse.chapter}:${verse.verse}`,
verses: [verse]
};
} else if (referenceType === 'chapter') {
return this.getChapter(reference.book, reference.startChapter);
} else {
return this.getPassage(reference);
}
}
/**
* Standardize book name to match the format in the Bible data
*/
private standardizeBookName(bookName: string): string | null {
const lowercaseBook = bookName.toLowerCase().trim();
// Try direct match
if (this.bookNameMap.has(lowercaseBook)) {
return this.bookNameMap.get(lowercaseBook)!;
}
// Try partial match
for (const [key, value] of this.bookNameMap.entries()) {
if (lowercaseBook.includes(key) || key.includes(lowercaseBook)) {
return value;
}
}
return null;
}
}

17
src/types.d.ts vendored Normal file
View file

@ -0,0 +1,17 @@
declare module '*.json' {
const value: any;
export default value;
}
// Define the structure of the Bible data for better type checking
interface ESVVerse {
pk: number;
translation: string;
book: number | string;
chapter: number;
verse: number;
text: string;
comment?: string;
}
declare const ESV: ESVVerse[];

View file

@ -0,0 +1,143 @@
import { BibleReferenceRange, BibleReferenceType } from "../models/BibleReference";
export class BibleReferenceParser {
// Regular expression to match Bible references
// This pattern handles various formats like:
// Genesis 1:1
// Gen 1:1
// Genesis 1:1-10
// Genesis 1:1-2:10
// Genesis 1
// 1 John 2:3
private static referenceRegex =
/(\d?\s*[A-Za-z]+(?:\s+[A-Za-z]+)?)\s*(\d+)(?::(\d+)(?:-(\d+))?)?(?:-(\d+)(?::(\d+))?)?/;
/**
* Parse a Bible reference string into its components
* Examples: "Genesis 1:1", "John 3:16-18", "Psalm 23", "Matthew 5:3-7:29"
*/
public static parseReference(text: string): BibleReferenceRange | null {
// Clean up the text to normalize whitespace
const cleanText = text.trim().replace(/\s+/g, ' ');
const match = this.referenceRegex.exec(cleanText);
if (!match) {
console.log(`Failed to parse reference: "${text}"`);
return null;
}
try {
const [_, book, chapter, startVerse, endVerseInSameChapter, endChapter, endVerse] = match;
// Extract book, ensuring it's properly formatted
const bookName = book.trim();
// Parse the chapter number
const chapterNum = parseInt(chapter);
if (isNaN(chapterNum)) {
console.log(`Invalid chapter number in "${text}"`);
return null;
}
// Handle case where reference is to a whole chapter (e.g., "Genesis 1")
if (!startVerse) {
return {
book: bookName,
startChapter: chapterNum,
startVerse: 1,
};
}
// Parse the starting verse
const startVerseNum = parseInt(startVerse);
if (isNaN(startVerseNum)) {
console.log(`Invalid start verse number in "${text}"`);
return null;
}
// Handle case where reference is to a single verse (e.g., "Genesis 1:1")
if (!endVerseInSameChapter && !endChapter) {
return {
book: bookName,
startChapter: chapterNum,
startVerse: startVerseNum,
};
}
// Handle case where reference is to a range within the same chapter (e.g., "Genesis 1:1-10")
if (endVerseInSameChapter) {
const endVerseNum = parseInt(endVerseInSameChapter);
if (isNaN(endVerseNum)) {
console.log(`Invalid end verse number in "${text}"`);
return null;
}
return {
book: bookName,
startChapter: chapterNum,
startVerse: startVerseNum,
endChapter: chapterNum,
endVerse: endVerseNum,
};
}
// Handle case where reference spans multiple chapters (e.g., "Matthew 5:3-7:29")
if (endChapter) {
const endChapterNum = parseInt(endChapter);
if (isNaN(endChapterNum)) {
console.log(`Invalid end chapter number in "${text}"`);
return null;
}
// If there's no end verse specified, assume the entire chapter
let endVerseNum = 1;
if (endVerse) {
endVerseNum = parseInt(endVerse);
if (isNaN(endVerseNum)) {
console.log(`Invalid end verse number in "${text}"`);
return null;
}
}
return {
book: bookName,
startChapter: chapterNum,
startVerse: startVerseNum,
endChapter: endChapterNum,
endVerse: endVerseNum,
};
}
// This shouldn't happen with our regex, but just in case
console.log(`Unable to parse reference format: "${text}"`);
return null;
} catch (error) {
console.error(`Error parsing reference "${text}":`, error);
return null;
}
}
/**
* Determine the type of reference (verse, passage, chapter)
*/
public static getReferenceType(reference: BibleReferenceRange): BibleReferenceType {
// If no end chapter or verse is specified, it's a single verse
if (!reference.endChapter && !reference.endVerse) {
return 'verse';
}
// If the end chapter is different from the start chapter, it's a multi-chapter passage
if (reference.endChapter && reference.endChapter !== reference.startChapter) {
return 'passage';
}
// If the start verse is 1 and no end verse is specified, it's a whole chapter
if (reference.startVerse === 1 && !reference.endVerse) {
return 'chapter';
}
// Otherwise, it's a passage within a chapter
return 'passage';
}
}

View file

@ -6,3 +6,135 @@ available in the app when your plugin is enabled.
If your plugin does not need CSS, delete this file.
*/
/* Bible reference styles */
.bible-reference {
color: var(--text-accent);
text-decoration: none;
border-bottom: 1px dashed var(--text-accent);
cursor: pointer;
padding: 0 2px;
font-weight: 500;
border-radius: 2px;
transition: all 0.2s ease;
}
.bible-reference:hover {
color: var(--text-accent-hover);
background-color: var(--background-modifier-hover);
border-bottom: 1px solid var(--text-accent-hover);
}
/* Bible verse preview */
.bible-verse-preview {
font-size: 14px;
line-height: 1.5;
max-width: 450px;
max-height: 300px;
overflow-y: auto;
padding: 12px;
border-radius: 5px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-border);
}
.bible-verse-preview-heading {
font-weight: bold;
font-size: 16px;
margin-bottom: 10px;
color: var(--text-accent);
border-bottom: 1px solid var(--background-modifier-border);
padding-bottom: 6px;
}
.bible-verse-preview-content {
color: var(--text-normal);
}
.bible-verse-preview-content p {
margin: 0 0 8px 0;
text-indent: -18px;
padding-left: 18px;
}
.bible-verse-number {
font-weight: bold;
color: var(--text-muted);
font-size: 0.8em;
vertical-align: super;
margin-right: 4px;
padding: 0 2px;
}
/* Bible passage styles */
.bible-passage-container {
margin: 1.5em 0;
padding: 1.5em;
background-color: var(--background-secondary);
border-radius: 8px;
border-left: 4px solid var(--text-accent);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.bible-passage-heading {
margin: 0 0 15px 0;
color: var(--text-accent);
font-size: 1.2em;
font-weight: 600;
padding-bottom: 8px;
border-bottom: 1px solid var(--background-modifier-border);
}
.bible-passage-text {
line-height: 1.8;
color: var(--text-normal);
}
.bible-verse {
margin-bottom: 0.7em;
text-indent: -20px;
padding-left: 20px;
}
.bible-verse-text {
display: inline;
}
/* Chapter headings in passages */
.bible-chapter-heading {
font-weight: bold;
font-size: 1.1em;
margin: 1em 0 0.5em;
color: var(--text-accent);
}
/* Error message */
.bible-reference-error {
color: var(--text-error);
font-style: italic;
padding: 10px;
border-left: 3px solid var(--text-error);
background-color: var(--background-modifier-error-hover);
border-radius: 3px;
}
/* About section in settings */
.disciples-journal-about {
padding: 10px;
margin: 10px 0;
background-color: var(--background-secondary);
border-radius: 5px;
font-size: 0.9em;
}
.disciples-journal-about p {
margin: 8px 0;
}
.disciples-journal-about small {
color: var(--text-muted);
font-size: 0.85em;
line-height: 1.4;
display: block;
}

View file

@ -11,6 +11,8 @@
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": [
"DOM",
"ES5",
@ -19,6 +21,7 @@
]
},
"include": [
"**/*.ts"
"**/*.ts",
"src/**/*.json"
]
}