mirror of
https://github.com/scotttomaszewski/obsidian-disciples-journal.git
synced 2026-07-22 05:42:13 +00:00
Base implementation
This commit is contained in:
parent
123e85369a
commit
dd195b911c
15 changed files with 3531 additions and 204 deletions
173
README.md
173
README.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
577
main.ts
|
|
@ -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);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
2288
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
7
rollup.config.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import json from "@rollup/plugin-json";
|
||||
|
||||
export default {
|
||||
plugins: [
|
||||
json()
|
||||
]
|
||||
};
|
||||
1
src/.obsidian/plugins/obsidian-disciples-journal/src/ESV.json
vendored
Normal file
1
src/.obsidian/plugins/obsidian-disciples-journal/src/ESV.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/ESV.json
Normal file
1
src/ESV.json
Normal file
File diff suppressed because one or more lines are too long
21
src/models/BibleReference.ts
Normal file
21
src/models/BibleReference.ts
Normal 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';
|
||||
333
src/services/BibleService.ts
Normal file
333
src/services/BibleService.ts
Normal 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
17
src/types.d.ts
vendored
Normal 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[];
|
||||
143
src/utils/BibleReferenceParser.ts
Normal file
143
src/utils/BibleReferenceParser.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
132
styles.css
132
styles.css
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue