Formatting

This commit is contained in:
Scott Tomaszewski 2025-04-24 16:38:23 -04:00
parent 93c26d5a25
commit 95a34cf070
14 changed files with 1658 additions and 1653 deletions

View file

@ -1,4 +1,3 @@
import { Plugin } from 'obsidian';
import DisciplesJournalPlugin from './src/core/DisciplesJournalPlugin'; import DisciplesJournalPlugin from './src/core/DisciplesJournalPlugin';
export default DisciplesJournalPlugin; export default DisciplesJournalPlugin;

View file

@ -1,6 +1,6 @@
import { ButtonComponent, DropdownComponent, Notice } from "obsidian"; import {ButtonComponent, DropdownComponent, Notice} from "obsidian";
import { BibleReference } from "../core/BibleReference"; import {BibleReference} from "../core/BibleReference";
import { BookNames } from "../services/BookNames"; import {BookNames} from "../services/BookNames";
import {BibleBookFiles} from "../services/BibleBookFiles"; import {BibleBookFiles} from "../services/BibleBookFiles";
/** /**
@ -9,44 +9,44 @@ import {BibleBookFiles} from "../services/BibleBookFiles";
export class BibleNavigation { export class BibleNavigation {
private bibleBookFiles: BibleBookFiles; private bibleBookFiles: BibleBookFiles;
constructor(noteCreationService: BibleBookFiles) { constructor(noteCreationService: BibleBookFiles) {
this.bibleBookFiles = noteCreationService; this.bibleBookFiles = noteCreationService;
} }
/** /**
* Create navigation elements for a Bible chapter * Create navigation elements for a Bible chapter
*/ */
public createNavigationElements(containerEl: HTMLElement, reference: BibleReference): void { public createNavigationElements(containerEl: HTMLElement, reference: BibleReference): void {
const book = BookNames.normalize(reference.book) || reference.book; const book = BookNames.normalize(reference.book) || reference.book;
const chapter = reference.chapter; const chapter = reference.chapter;
// Get book chapter count // Get book chapter count
const chapterCount = BookNames.getChapterCount(book); const chapterCount = BookNames.getChapterCount(book);
// Determine if we're at the first or last chapter of the book // Determine if we're at the first or last chapter of the book
const isFirstChapter = chapter === 1; const isFirstChapter = chapter === 1;
const isLastChapter = chapter === chapterCount; const isLastChapter = chapter === chapterCount;
// Determine if we're at the first or last book of the Bible // Determine if we're at the first or last book of the Bible
const bookOrder = BookNames.getBookOrder(); const bookOrder = BookNames.getBookOrder();
const bookIndex = bookOrder.indexOf(book); const bookIndex = bookOrder.indexOf(book);
const isFirstBook = bookIndex === 0; const isFirstBook = bookIndex === 0;
const isLastBook = bookIndex === bookOrder.length - 1; const isLastBook = bookIndex === bookOrder.length - 1;
// Create the navigation container // Create the navigation container
const navEl = containerEl.createDiv({ cls: 'bible-navigation' }); const navEl = containerEl.createDiv({cls: 'bible-navigation'});
// Previous chapter button // Previous chapter button
if (isFirstChapter && isFirstBook) { if (isFirstChapter && isFirstBook) {
// No previous chapter if we're at Genesis 1 // No previous chapter if we're at Genesis 1
navEl.createSpan({ navEl.createSpan({
cls: 'nav-prev nav-disabled', cls: 'nav-prev nav-disabled',
text: '◄ Previous' text: '◄ Previous'
}); });
} else if (isFirstChapter) { } else if (isFirstChapter) {
// Previous book, last chapter // Previous book, last chapter
const prevBook = bookOrder[bookIndex - 1]; const prevBook = bookOrder[bookIndex - 1];
const prevChapter = BookNames.getChapterCount(prevBook); const prevChapter = BookNames.getChapterCount(prevBook);
new ButtonComponent(navEl) new ButtonComponent(navEl)
.setButtonText(`${prevBook} ${prevChapter}`) .setButtonText(`${prevBook} ${prevChapter}`)
.setClass('nav-prev') .setClass('nav-prev')
@ -70,38 +70,38 @@ export class BibleNavigation {
} }
// Book and chapter selector // Book and chapter selector
const selectorEl = navEl.createSpan({ const selectorEl = navEl.createSpan({
cls: 'nav-book-selector', cls: 'nav-book-selector',
text: `📖 ${book} ${chapter}` text: `📖 ${book} ${chapter}`
}); });
const selectorContainer = navEl.createDiv({ const selectorContainer = navEl.createDiv({
cls: 'nav-book-data dj-hidden' cls: 'nav-book-data dj-hidden'
}); });
// Create the book dropdown // Create the book dropdown
const bookDropdownContainer = selectorContainer.createDiv(); const bookDropdownContainer = selectorContainer.createDiv();
const bookDropdown = new DropdownComponent(bookDropdownContainer); const bookDropdown = new DropdownComponent(bookDropdownContainer);
// Add all books to the dropdown // Add all books to the dropdown
for (const bookName of bookOrder) { for (const bookName of bookOrder) {
bookDropdown.addOption(bookName, bookName); bookDropdown.addOption(bookName, bookName);
} }
bookDropdown.setValue(book); bookDropdown.setValue(book);
// Create the chapter dropdown // Create the chapter dropdown
const chapterDropdownContainer = selectorContainer.createDiv({ cls: 'nav-chapter-container' }); const chapterDropdownContainer = selectorContainer.createDiv({cls: 'nav-chapter-container'});
const chapterDropdown = new DropdownComponent(chapterDropdownContainer); const chapterDropdown = new DropdownComponent(chapterDropdownContainer);
// Add current book's chapters to the dropdown // Add current book's chapters to the dropdown
this.populateChapterDropdown(chapterDropdown, book, chapter); this.populateChapterDropdown(chapterDropdown, book, chapter);
// When book selection changes, update chapter dropdown // When book selection changes, update chapter dropdown
bookDropdown.onChange(value => { bookDropdown.onChange(value => {
this.populateChapterDropdown(chapterDropdown, value, 1); this.populateChapterDropdown(chapterDropdown, value, 1);
}); });
// Add Go button // Add Go button
new ButtonComponent(selectorContainer) new ButtonComponent(selectorContainer)
.setButtonText('Go') .setButtonText('Go')
.setClass('nav-go-button') .setClass('nav-go-button')
@ -113,20 +113,20 @@ export class BibleNavigation {
loadingNotice.hide(); loadingNotice.hide();
}); });
// Toggle selector on click // Toggle selector on click
selectorEl.addEventListener('click', () => { selectorEl.addEventListener('click', () => {
selectorContainer.classList.toggle('dj-hidden'); selectorContainer.classList.toggle('dj-hidden');
}); });
// Next chapter button // Next chapter button
if (isLastChapter && isLastBook) { if (isLastChapter && isLastBook) {
// No next chapter if we're at Revelation 22 // No next chapter if we're at Revelation 22
navEl.createSpan({ navEl.createSpan({
cls: 'nav-next nav-disabled', cls: 'nav-next nav-disabled',
text: 'Next ►' text: 'Next ►'
}); });
} else if (isLastChapter) { } else if (isLastChapter) {
// Next book, first chapter // Next book, first chapter
const nextBook = bookOrder[bookIndex + 1]; const nextBook = bookOrder[bookIndex + 1];
new ButtonComponent(navEl) new ButtonComponent(navEl)
.setButtonText(`${nextBook} 1`) .setButtonText(`${nextBook} 1`)
.setClass('nav-next') .setClass('nav-next')
@ -151,37 +151,37 @@ export class BibleNavigation {
} }
/** /**
* Populate the chapter dropdown for a specific book * Populate the chapter dropdown for a specific book
*/ */
private populateChapterDropdown( private populateChapterDropdown(
dropdown: DropdownComponent, dropdown: DropdownComponent,
book: string, book: string,
selectedChapter: number selectedChapter: number
): void { ): void {
// Clear existing options // Clear existing options
dropdown.selectEl.empty(); dropdown.selectEl.empty();
// Get the chapter count for this book // Get the chapter count for this book
const chapterCount = BookNames.getChapterCount(book); const chapterCount = BookNames.getChapterCount(book);
// Add chapter options // Add chapter options
for (let i = 1; i <= chapterCount; i++) { for (let i = 1; i <= chapterCount; i++) {
dropdown.addOption(i.toString(), i.toString()); dropdown.addOption(i.toString(), i.toString());
} }
// Set the selected chapter // Set the selected chapter
dropdown.setValue(selectedChapter.toString()); dropdown.setValue(selectedChapter.toString());
} }
/** /**
* Navigate to a specific chapter * Navigate to a specific chapter
*/ */
public async navigateToChapter(book: string, chapter: number): Promise<void> { public async navigateToChapter(book: string, chapter: number): Promise<void> {
try { try {
await this.bibleBookFiles.openChapterNote(new BibleReference(book, chapter).toString()); await this.bibleBookFiles.openChapterNote(new BibleReference(book, chapter).toString());
} catch (error) { } catch (error) {
console.error('Error navigating to chapter:', error); console.error('Error navigating to chapter:', error);
new Notice(`Error navigating to chapter: ${error.message}`); new Notice(`Error navigating to chapter: ${error.message}`);
} }
} }
} }

View file

@ -2,73 +2,73 @@
* Interface for Bible styling options * Interface for Bible styling options
*/ */
export interface BibleStyleOptions { export interface BibleStyleOptions {
fontSize: string; fontSize: string;
wordsOfChristColor: string; wordsOfChristColor: string;
verseNumberColor: string; verseNumberColor: string;
headingColor: string; headingColor: string;
blockIndentation: string; blockIndentation: string;
} }
/** /**
* Theme preset definitions * Theme preset definitions
*/ */
export interface ThemePreset { export interface ThemePreset {
light: BibleStyleOptions; light: BibleStyleOptions;
dark: BibleStyleOptions; dark: BibleStyleOptions;
} }
/** /**
* Available theme presets * Available theme presets
*/ */
export const THEME_PRESETS: Record<string, ThemePreset> = { export const THEME_PRESETS: Record<string, ThemePreset> = {
default: { default: {
light: { light: {
fontSize: '100%', fontSize: '100%',
wordsOfChristColor: 'var(--text-accent)', wordsOfChristColor: 'var(--text-accent)',
verseNumberColor: 'var(--text-accent)', verseNumberColor: 'var(--text-accent)',
headingColor: 'var(--text-accent)', headingColor: 'var(--text-accent)',
blockIndentation: '2em' blockIndentation: '2em'
}, },
dark: { dark: {
fontSize: '100%', fontSize: '100%',
wordsOfChristColor: 'var(--text-accent)', wordsOfChristColor: 'var(--text-accent)',
verseNumberColor: 'var(--text-accent)', verseNumberColor: 'var(--text-accent)',
headingColor: 'var(--text-accent)', headingColor: 'var(--text-accent)',
blockIndentation: '2em' blockIndentation: '2em'
} }
}, },
classic: { classic: {
light: { light: {
fontSize: '100%', fontSize: '100%',
wordsOfChristColor: '#b91c1c', wordsOfChristColor: '#b91c1c',
verseNumberColor: '#2563eb', verseNumberColor: '#2563eb',
headingColor: '#4b5563', headingColor: '#4b5563',
blockIndentation: '2em' blockIndentation: '2em'
}, },
dark: { dark: {
fontSize: '100%', fontSize: '100%',
wordsOfChristColor: '#ef4444', wordsOfChristColor: '#ef4444',
verseNumberColor: '#60a5fa', verseNumberColor: '#60a5fa',
headingColor: '#9ca3af', headingColor: '#9ca3af',
blockIndentation: '2em' blockIndentation: '2em'
} }
}, },
minimal: { minimal: {
light: { light: {
fontSize: '100%', fontSize: '100%',
wordsOfChristColor: '#000000', wordsOfChristColor: '#000000',
verseNumberColor: '#6b7280', verseNumberColor: '#6b7280',
headingColor: '#374151', headingColor: '#374151',
blockIndentation: '1.5em' blockIndentation: '1.5em'
}, },
dark: { dark: {
fontSize: '100%', fontSize: '100%',
wordsOfChristColor: '#ffffff', wordsOfChristColor: '#ffffff',
verseNumberColor: '#9ca3af', verseNumberColor: '#9ca3af',
headingColor: '#e5e7eb', headingColor: '#e5e7eb',
blockIndentation: '1.5em' blockIndentation: '1.5em'
} }
} }
}; };
/** /**
@ -77,60 +77,60 @@ export const THEME_PRESETS: Record<string, ThemePreset> = {
*/ */
export class BibleStyles { export class BibleStyles {
constructor() { constructor() {
// No initial style element creation needed // No initial style element creation needed
} }
/**
* Apply styles by updating CSS variables based on theme, preset, and settings.
*/
public applyStyles(doc: Document, theme: 'light' | 'dark', presetName: string = 'default', fontSize: string = '100%', settings?: {
wordsOfChristColor?: string;
verseNumberColor?: string;
headingColor?: string;
blockIndentation?: string;
}): void {
// Get the preset, defaulting to 'default' if not found
const preset = THEME_PRESETS[presetName] || THEME_PRESETS.default;
// Get the base options for the current theme
const baseOptions = preset[theme];
// Start with base options and apply the specific font size /**
const options: BibleStyleOptions = { * Apply styles by updating CSS variables based on theme, preset, and settings.
...baseOptions, */
fontSize: fontSize || '100%' // Ensure fontSize always has a value public applyStyles(doc: Document, theme: 'light' | 'dark', presetName: string = 'default', fontSize: string = '100%', settings?: {
}; wordsOfChristColor?: string;
verseNumberColor?: string;
// Apply custom settings from DisciplesJournalSettings if provided headingColor?: string;
// These will override the preset values blockIndentation?: string;
if (settings) { }): void {
if (settings.wordsOfChristColor) options.wordsOfChristColor = settings.wordsOfChristColor;
if (settings.verseNumberColor) options.verseNumberColor = settings.verseNumberColor;
if (settings.headingColor) options.headingColor = settings.headingColor;
if (settings.blockIndentation) options.blockIndentation = settings.blockIndentation;
}
// Update the CSS variables on the root element // Get the preset, defaulting to 'default' if not found
const rootStyle = doc.body.style; const preset = THEME_PRESETS[presetName] || THEME_PRESETS.default;
rootStyle.setProperty('--dj-font-size', options.fontSize);
rootStyle.setProperty('--dj-heading-color', options.headingColor);
rootStyle.setProperty('--dj-verse-number-color', options.verseNumberColor);
rootStyle.setProperty('--dj-woc-color', options.wordsOfChristColor);
rootStyle.setProperty('--dj-block-indentation', options.blockIndentation);
}
/** // Get the base options for the current theme
* Resets the custom styles potentially set by this class by removing the CSS variables. const baseOptions = preset[theme];
* This allows the default styles in styles.css to take effect.
*/ // Start with base options and apply the specific font size
public resetStyles(doc: Document): void { const options: BibleStyleOptions = {
const rootStyle = doc.body.style; ...baseOptions,
rootStyle.removeProperty('--dj-font-size'); fontSize: fontSize || '100%' // Ensure fontSize always has a value
rootStyle.removeProperty('--dj-heading-color'); };
rootStyle.removeProperty('--dj-verse-number-color');
rootStyle.removeProperty('--dj-woc-color'); // Apply custom settings from DisciplesJournalSettings if provided
rootStyle.removeProperty('--dj-block-indentation'); // These will override the preset values
} if (settings) {
} if (settings.wordsOfChristColor) options.wordsOfChristColor = settings.wordsOfChristColor;
if (settings.verseNumberColor) options.verseNumberColor = settings.verseNumberColor;
if (settings.headingColor) options.headingColor = settings.headingColor;
if (settings.blockIndentation) options.blockIndentation = settings.blockIndentation;
}
// Update the CSS variables on the root element
const rootStyle = doc.body.style;
rootStyle.setProperty('--dj-font-size', options.fontSize);
rootStyle.setProperty('--dj-heading-color', options.headingColor);
rootStyle.setProperty('--dj-verse-number-color', options.verseNumberColor);
rootStyle.setProperty('--dj-woc-color', options.wordsOfChristColor);
rootStyle.setProperty('--dj-block-indentation', options.blockIndentation);
}
/**
* Resets the custom styles potentially set by this class by removing the CSS variables.
* This allows the default styles in styles.css to take effect.
*/
public resetStyles(doc: Document): void {
const rootStyle = doc.body.style;
rootStyle.removeProperty('--dj-font-size');
rootStyle.removeProperty('--dj-heading-color');
rootStyle.removeProperty('--dj-verse-number-color');
rootStyle.removeProperty('--dj-woc-color');
rootStyle.removeProperty('--dj-block-indentation');
}
}

View file

@ -1,131 +1,132 @@
import { BibleReferenceRenderer } from '../components/BibleReferenceRenderer'; import {BibleReferenceRenderer} from '../components/BibleReferenceRenderer';
/** /**
* Handles all Bible reference-related DOM events * Handles all Bible reference-related DOM events
*/ */
export class BibleEventHandlers { export class BibleEventHandlers {
private bibleReferenceRenderer: BibleReferenceRenderer; private bibleReferenceRenderer: BibleReferenceRenderer;
private previewPopper: HTMLElement | null = null; private previewPopper: HTMLElement | null = null;
constructor(bibleReferenceRenderer: BibleReferenceRenderer) { constructor(bibleReferenceRenderer: BibleReferenceRenderer) {
this.bibleReferenceRenderer = bibleReferenceRenderer; this.bibleReferenceRenderer = bibleReferenceRenderer;
} }
/** /**
* Handle hover on Bible references * Handle hover on Bible references
*/ */
async handleBibleReferenceHover(event: MouseEvent) { async handleBibleReferenceHover(event: MouseEvent) {
// Don't create new preview if we already have one active // Don't create new preview if we already have one active
if (this.previewPopper) return; if (this.previewPopper) return;
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
if (!target || !target.closest) return; if (!target || !target.closest) return;
const referenceEl = target.closest('.bible-reference') as HTMLElement; const referenceEl = target.closest('.bible-reference') as HTMLElement;
if (!referenceEl) return; if (!referenceEl) return;
const referenceText = referenceEl.textContent; const referenceText = referenceEl.textContent;
if (!referenceText) return; if (!referenceText) return;
try { try {
// Create new preview // Create new preview
this.previewPopper = await this.bibleReferenceRenderer.showVersePreview( this.previewPopper = await this.bibleReferenceRenderer.showVersePreview(
referenceEl, referenceEl,
referenceText, referenceText,
event event
); );
// Add event listeners directly to the preview for better control // Add event listeners directly to the preview for better control
if (this.previewPopper) { if (this.previewPopper) {
// When mouse enters the popup, mark it as locked // When mouse enters the popup, mark it as locked
this.previewPopper.addEventListener('mouseenter', () => { this.previewPopper.addEventListener('mouseenter', () => {
this.previewPopper?.classList.add('popup-locked'); this.previewPopper?.classList.add('popup-locked');
}); });
// When mouse leaves the popup, check if we should close it // When mouse leaves the popup, check if we should close it
this.previewPopper.addEventListener('mouseleave', (e) => { this.previewPopper.addEventListener('mouseleave', (e) => {
// Only close if not moving to the reference or another part of the popup // Only close if not moving to the reference or another part of the popup
const relatedTarget = e.relatedTarget as HTMLElement; const relatedTarget = e.relatedTarget as HTMLElement;
if (relatedTarget && if (relatedTarget &&
!relatedTarget.classList.contains('bible-reference') && !relatedTarget.classList.contains('bible-reference') &&
!relatedTarget.closest('.bible-verse-preview')) { !relatedTarget.closest('.bible-verse-preview')) {
this.previewPopper?.classList.remove('popup-locked'); this.previewPopper?.classList.remove('popup-locked');
this.removePreviewPopper(relatedTarget.doc); this.removePreviewPopper(relatedTarget.doc);
} }
}); });
} }
// Also add listeners to the reference element // Also add listeners to the reference element
referenceEl.addEventListener('mouseleave', (e) => { referenceEl.addEventListener('mouseleave', (e) => {
// Don't close if the popup is locked (being hovered) or we're moving to the popup // Don't close if the popup is locked (being hovered) or we're moving to the popup
if (this.previewPopper) { if (this.previewPopper) {
const relatedTarget = e.relatedTarget as HTMLElement; const relatedTarget = e.relatedTarget as HTMLElement;
// If moving to the popup or if popup is locked, don't close // If moving to the popup or if popup is locked, don't close
if (relatedTarget && if (relatedTarget &&
(relatedTarget.classList.contains('bible-verse-preview') || (relatedTarget.classList.contains('bible-verse-preview') ||
relatedTarget.closest('.bible-verse-preview') || relatedTarget.closest('.bible-verse-preview') ||
this.previewPopper.classList.contains('popup-locked'))) { this.previewPopper.classList.contains('popup-locked'))) {
return; return;
} }
// Add a 100ms delay before closing to allow for cursor movement // Add a 100ms delay before closing to allow for cursor movement
setTimeout(() => { setTimeout(() => {
// If locked during this delay, don't close // If locked during this delay, don't close
if (!this.previewPopper || this.previewPopper.classList.contains('popup-locked')) { if (!this.previewPopper || this.previewPopper.classList.contains('popup-locked')) {
return; return;
} }
this.removePreviewPopper(relatedTarget.doc); this.removePreviewPopper(relatedTarget.doc);
}, 100); }, 100);
} }
}); });
} catch (error) { } catch (error) {
console.error('Error showing Bible reference preview:', error); console.error('Error showing Bible reference preview:', error);
} }
} }
/** /**
* Handle mouse out from Bible references * Handle mouse out from Bible references
*/ */
handleBibleReferenceMouseOut(event: MouseEvent) { handleBibleReferenceMouseOut(event: MouseEvent) {
// If we don't have a popup, nothing to do // If we don't have a popup, nothing to do
if (!this.previewPopper) return; if (!this.previewPopper) return;
// If the popup is locked (being hovered), don't close it // If the popup is locked (being hovered), don't close it
if (this.previewPopper.classList.contains('popup-locked')) { if (this.previewPopper.classList.contains('popup-locked')) {
return; return;
} }
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
const relatedTarget = event.relatedTarget as HTMLElement; const relatedTarget = event.relatedTarget as HTMLElement;
// If either target is missing, can't make a good decision // If either target is missing, can't make a good decision
if (!target || !relatedTarget) return; if (!target || !relatedTarget) return;
// If moving to/from the popup or reference, don't close // If moving to/from the popup or reference, don't close
if (target.classList.contains('bible-reference') || if (target.classList.contains('bible-reference') ||
target.classList.contains('bible-verse-preview') || target.classList.contains('bible-verse-preview') ||
target.closest('.bible-verse-preview') || target.closest('.bible-verse-preview') ||
relatedTarget.classList.contains('bible-reference') || relatedTarget.classList.contains('bible-reference') ||
relatedTarget.classList.contains('bible-verse-preview') || relatedTarget.classList.contains('bible-verse-preview') ||
relatedTarget.closest('.bible-verse-preview')) { relatedTarget.closest('.bible-verse-preview')) {
return; return;
} }
// In all other cases, remove the popup // In all other cases, remove the popup
this.removePreviewPopper(target.doc); this.removePreviewPopper(target.doc);
} }
/** /**
* Remove the preview popper if it exists * Remove the preview popper if it exists
*/ */
removePreviewPopper(doc: Document) { removePreviewPopper(doc: Document) {
if (this.previewPopper) { if (this.previewPopper) {
// Remove any hover gap elements // Remove any hover gap elements
const hoverGaps = doc.querySelectorAll('.bible-hover-gap'); const hoverGaps = doc.querySelectorAll('.bible-hover-gap');
hoverGaps.forEach(gap => gap.remove()); hoverGaps.forEach(gap => gap.remove());
this.previewPopper.remove(); this.previewPopper.remove();
this.previewPopper = null; this.previewPopper = null;
} }
} }
} }

View file

@ -1,55 +1,55 @@
import { MarkdownPostProcessorContext } from 'obsidian'; import {MarkdownPostProcessorContext} from 'obsidian';
import { BibleReferenceRenderer } from '../components/BibleReferenceRenderer'; import {BibleReferenceRenderer} from '../components/BibleReferenceRenderer';
import { DisciplesJournalSettings } from '../settings/DisciplesJournalSettings'; import {DisciplesJournalSettings} from '../settings/DisciplesJournalSettings';
/** /**
* Handles processing of Bible references in markdown text * Handles processing of Bible references in markdown text
*/ */
export class BibleMarkupProcessor { export class BibleMarkupProcessor {
private bibleReferenceRenderer: BibleReferenceRenderer; private bibleReferenceRenderer: BibleReferenceRenderer;
private settings: DisciplesJournalSettings; private settings: DisciplesJournalSettings;
constructor( constructor(
bibleReferenceRenderer: BibleReferenceRenderer, bibleReferenceRenderer: BibleReferenceRenderer,
settings: DisciplesJournalSettings settings: DisciplesJournalSettings
) { ) {
this.bibleReferenceRenderer = bibleReferenceRenderer; this.bibleReferenceRenderer = bibleReferenceRenderer;
this.settings = settings; this.settings = settings;
} }
/** /**
* Process Bible code blocks * Process Bible code blocks
*/ */
async processBibleCodeBlock(source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) { async processBibleCodeBlock(source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) {
// Check if display of full passages is enabled in settings // Check if display of full passages is enabled in settings
if (!this.settings.displayFullPassages) { if (!this.settings.displayFullPassages) {
return; return;
} }
try { try {
await this.bibleReferenceRenderer.processFullBiblePassage(source, el); await this.bibleReferenceRenderer.processFullBiblePassage(source, el);
} catch (error) { } catch (error) {
console.error('Error processing Bible code block:', error); console.error('Error processing Bible code block:', error);
el.createEl('div', { el.createEl('div', {
text: `Error processing Bible reference: ${error.message}`, text: `Error processing Bible reference: ${error.message}`,
cls: 'bible-reference-error' cls: 'bible-reference-error'
}); });
} }
} }
/** /**
* Process inline Bible references in text * Process inline Bible references in text
*/ */
async processInlineBibleReferences(el: HTMLElement, ctx: MarkdownPostProcessorContext): Promise<void> { async processInlineBibleReferences(el: HTMLElement, ctx: MarkdownPostProcessorContext): Promise<void> {
// Check if display of inline verses is enabled in settings // Check if display of inline verses is enabled in settings
if (!this.settings.displayInlineVerses) { if (!this.settings.displayInlineVerses) {
return; return;
} }
try { try {
await this.bibleReferenceRenderer.processInlineCodeBlocks(el, ctx); await this.bibleReferenceRenderer.processInlineCodeBlocks(el, ctx);
} catch (error) { } catch (error) {
console.error('Error processing inline Bible references:', error); console.error('Error processing inline Bible references:', error);
} }
} }
} }

View file

@ -4,22 +4,22 @@ import {BookNames} from "../services/BookNames";
* Represents a Bible Reference with book, chapter, and verse information * Represents a Bible Reference with book, chapter, and verse information
*/ */
export class BibleReference { export class BibleReference {
book: string; book: string;
chapter: number; chapter: number;
verse?: number; verse?: number;
endVerse?: number; endVerse?: number;
endChapter?: number; endChapter?: number;
/** /**
* Create a new Bible reference * Create a new Bible reference
*/ */
constructor(book: string, chapter: number, verse?: number, endVerse?: number, endChapter?: number) { constructor(book: string, chapter: number, verse?: number, endVerse?: number, endChapter?: number) {
this.book = book; this.book = book;
this.chapter = chapter; this.chapter = chapter;
this.verse = verse; this.verse = verse;
this.endVerse = endVerse; this.endVerse = endVerse;
this.endChapter = endChapter; this.endChapter = endChapter;
} }
/** /**
* Parse a string into a BibleReference object * Parse a string into a BibleReference object
@ -112,95 +112,95 @@ export class BibleReference {
} }
/** /**
* Get the formatted reference as a string * Get the formatted reference as a string
*/ */
public toString(): string { public toString(): string {
let reference = `${this.book} ${this.chapter}`; let reference = `${this.book} ${this.chapter}`;
// Add verse if present // Add verse if present
if (this.verse !== undefined) { if (this.verse !== undefined) {
reference += `:${this.verse}`; reference += `:${this.verse}`;
// Add end verse if present (for ranges within same chapter) // Add end verse if present (for ranges within same chapter)
if (this.endVerse !== undefined && this.endChapter === undefined) { if (this.endVerse !== undefined && this.endChapter === undefined) {
reference += `-${this.endVerse}`; reference += `-${this.endVerse}`;
} }
} }
// Handle cross-chapter reference // Handle cross-chapter reference
if (this.endChapter !== undefined) { if (this.endChapter !== undefined) {
reference += `-${this.endChapter}`; reference += `-${this.endChapter}`;
// Add end verse if present for a cross-chapter reference // Add end verse if present for a cross-chapter reference
if (this.endVerse !== undefined) { if (this.endVerse !== undefined) {
reference += `:${this.endVerse}`; reference += `:${this.endVerse}`;
} }
} }
return reference; return reference;
} }
/** /**
* Create a deep copy of the reference * Create a deep copy of the reference
*/ */
public clone(): BibleReference { public clone(): BibleReference {
return new BibleReference( return new BibleReference(
this.book, this.book,
this.chapter, this.chapter,
this.verse, this.verse,
this.endVerse, this.endVerse,
this.endChapter this.endChapter
); );
} }
/** /**
* Determine if this reference is a range (spans multiple verses) * Determine if this reference is a range (spans multiple verses)
*/ */
public isRange(): boolean { public isRange(): boolean {
return this.endVerse !== undefined || this.endChapter !== undefined; return this.endVerse !== undefined || this.endChapter !== undefined;
} }
/** /**
* Check if this is a chapter reference (no specific verse) * Check if this is a chapter reference (no specific verse)
*/ */
public isChapterReference(): boolean { public isChapterReference(): boolean {
return this.verse === undefined; return this.verse === undefined;
} }
/** /**
* Compare this reference with another to check if they refer to the same passage * Compare this reference with another to check if they refer to the same passage
*/ */
public equals(other: BibleReference): boolean { public equals(other: BibleReference): boolean {
return this.book === other.book && return this.book === other.book &&
this.chapter === other.chapter && this.chapter === other.chapter &&
this.verse === other.verse && this.verse === other.verse &&
this.endVerse === other.endVerse && this.endVerse === other.endVerse &&
this.endChapter === other.endChapter; this.endChapter === other.endChapter;
} }
/** /**
* Returns a reference string suitable for display in UI * Returns a reference string suitable for display in UI
*/ */
public getDisplayReference(): string { public getDisplayReference(): string {
// For UI display, we might want to abbreviate the book name // For UI display, we might want to abbreviate the book name
// or format the reference in a specific way // or format the reference in a specific way
return this.toString(); return this.toString();
} }
/** /**
* Returns a reference string suitable for use as a key in the Bible data * Returns a reference string suitable for use as a key in the Bible data
*/ */
public getDataKey(): string { public getDataKey(): string {
// For data keys, we often want to use a standardized format // For data keys, we often want to use a standardized format
// This assumes the book name is already standardized // This assumes the book name is already standardized
return `${this.book} ${this.chapter}${this.verse ? `:${this.verse}` : ''}`; return `${this.book} ${this.chapter}${this.verse ? `:${this.verse}` : ''}`;
} }
/** /**
* Creates a reference for the entire chapter containing this reference * Creates a reference for the entire chapter containing this reference
*/ */
public getChapterReference(): BibleReference { public getChapterReference(): BibleReference {
// Create a new reference with the same book and chapter, but no verse info // Create a new reference with the same book and chapter, but no verse info
return new BibleReference(this.book, this.chapter); return new BibleReference(this.book, this.chapter);
} }
} }

View file

@ -1,139 +1,143 @@
import { Plugin, MarkdownView, Notice } from 'obsidian'; import {Plugin, MarkdownView, Notice} from 'obsidian';
import { ESVApiService } from '../services/ESVApiService'; import {ESVApiService} from '../services/ESVApiService';
import { BibleContentService } from '../services/BibleContentService'; import {BibleContentService} from '../services/BibleContentService';
import { BibleReferenceRenderer } from '../components/BibleReferenceRenderer'; import {BibleReferenceRenderer} from '../components/BibleReferenceRenderer';
import { BibleStyles } from '../components/BibleStyles'; import {BibleStyles} from '../components/BibleStyles';
import { DisciplesJournalSettings, DEFAULT_SETTINGS, DisciplesJournalSettingsTab } from '../settings/DisciplesJournalSettings'; import {
import { BibleBookFiles } from 'src/services/BibleBookFiles'; DisciplesJournalSettings,
import { BibleMarkupProcessor } from './BibleMarkupProcessor'; DEFAULT_SETTINGS,
DisciplesJournalSettingsTab
} from '../settings/DisciplesJournalSettings';
import {BibleBookFiles} from 'src/services/BibleBookFiles';
import {BibleMarkupProcessor} from './BibleMarkupProcessor';
/** /**
* Disciples Journal Plugin for Obsidian * Disciples Journal Plugin for Obsidian
* Enhances Bible references with hover previews and in-note embedding * Enhances Bible references with hover previews and in-note embedding
*/ */
export default class DisciplesJournalPlugin extends Plugin { export default class DisciplesJournalPlugin extends Plugin {
settings: DisciplesJournalSettings; settings: DisciplesJournalSettings;
// Services // Services
private esvApiService: ESVApiService; private esvApiService: ESVApiService;
private bibleContentService: BibleContentService; private bibleContentService: BibleContentService;
private bibleBookFiles: BibleBookFiles; private bibleBookFiles: BibleBookFiles;
// Components // Components
private bibleStyles: BibleStyles; private bibleStyles: BibleStyles;
private bibleReferenceRenderer: BibleReferenceRenderer; private bibleReferenceRenderer: BibleReferenceRenderer;
private bibleMarkupProcessor: BibleMarkupProcessor; private bibleMarkupProcessor: BibleMarkupProcessor;
async onload() { async onload() {
console.log('Loading Disciples Journal plugin'); console.log('Loading Disciples Journal plugin');
// Initialize settings // Initialize settings
await this.loadSettings(); await this.loadSettings();
// Initialize services // Initialize services
this.esvApiService = new ESVApiService(this); this.esvApiService = new ESVApiService(this);
this.bibleContentService = new BibleContentService(this, this.esvApiService); this.bibleContentService = new BibleContentService(this, this.esvApiService);
// Check if ESV API token is set and show a notice if it's not // Check if ESV API token is set and show a notice if it's not
if (!this.settings.esvApiToken) { if (!this.settings.esvApiToken) {
new Notice('Disciples Journal: ESV API token not set. Bible content may not load correctly. Visit the plugin settings to add your API token.', 10000); new Notice('Disciples Journal: ESV API token not set. Bible content may not load correctly. Visit the plugin settings to add your API token.', 10000);
} }
// Initialize components // Initialize components
this.bibleStyles = new BibleStyles(); this.bibleStyles = new BibleStyles();
this.bibleBookFiles = new BibleBookFiles(this, this.bibleContentService); this.bibleBookFiles = new BibleBookFiles(this, this.bibleContentService);
this.bibleReferenceRenderer = new BibleReferenceRenderer( this.bibleReferenceRenderer = new BibleReferenceRenderer(
this.bibleContentService, this.bibleContentService,
this.bibleBookFiles, this.bibleBookFiles,
this this
); );
// Initialize markup processor // Initialize markup processor
this.bibleMarkupProcessor = new BibleMarkupProcessor(this.bibleReferenceRenderer, this.settings); this.bibleMarkupProcessor = new BibleMarkupProcessor(this.bibleReferenceRenderer, this.settings);
// Register bible reference processor // Register bible reference processor
this.registerMarkdownCodeBlockProcessor('bible', this.bibleMarkupProcessor.processBibleCodeBlock.bind(this.bibleMarkupProcessor)); this.registerMarkdownCodeBlockProcessor('bible', this.bibleMarkupProcessor.processBibleCodeBlock.bind(this.bibleMarkupProcessor));
// Register markdown post processor for inline references // Register markdown post processor for inline references
this.registerMarkdownPostProcessor(this.bibleMarkupProcessor.processInlineBibleReferences.bind(this.bibleMarkupProcessor)); this.registerMarkdownPostProcessor(this.bibleMarkupProcessor.processInlineBibleReferences.bind(this.bibleMarkupProcessor));
// Register settings tab // Register settings tab
this.addSettingTab(new DisciplesJournalSettingsTab(this.app, this)); this.addSettingTab(new DisciplesJournalSettingsTab(this.app, this));
// Register active leaf change to update styles // Register active leaf change to update styles
this.registerEvent(this.app.workspace.on('active-leaf-change', this.handleActiveLeafChange.bind(this))); this.registerEvent(this.app.workspace.on('active-leaf-change', this.handleActiveLeafChange.bind(this)));
// Load Bible data // Load Bible data
await this.loadBibleData(); await this.loadBibleData();
} }
onunload() { onunload() {
} }
async loadSettings() { async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
} }
async saveSettings() { async saveSettings() {
await this.saveData(this.settings); await this.saveData(this.settings);
} }
/** /**
* Load the Bible data from source * Load the Bible data from source
*/ */
async loadBibleData() { async loadBibleData() {
try { try {
console.log('Loading Bible data...'); console.log('Loading Bible data...');
await this.esvApiService.ensureBibleData(); await this.esvApiService.ensureBibleData();
console.log('Bible data loaded successfully'); console.log('Bible data loaded successfully');
} catch (error) { } catch (error) {
console.error('Error loading Bible data:', error); console.error('Error loading Bible data:', error);
} }
} }
/** /**
* Handle leaf change event to check for verse references in the URL * Handle leaf change event to check for verse references in the URL
*/ */
handleActiveLeafChange() { handleActiveLeafChange() {
// Refresh theme/styling when active leaf changes // Refresh theme/styling when active leaf changes
this.updateBibleStyles(); this.updateBibleStyles();
} }
/** /**
* Update Bible styles with current settings * Update Bible styles with current settings
*/ */
public updateBibleStyles(): void { public updateBibleStyles(): void {
const activeLeaf = this.app.workspace.getActiveViewOfType(MarkdownView); const activeLeaf = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!activeLeaf) return; if (!activeLeaf) return;
// Each popup window has a unique document, so we need to apply styles to each one // Each popup window has a unique document, so we need to apply styles to each one
// See https://discord.com/channels/686053708261228577/840286264964022302/1362059117190975519 // See https://discord.com/channels/686053708261228577/840286264964022302/1362059117190975519
// Note: this doesnt completely work. When a new popout is created the style cars arent ported over // Note: this doesnt completely work. When a new popout is created the style cars arent ported over
const docList: Document[] = []; const docList: Document[] = [];
this.app.workspace.iterateAllLeaves(leaf => docList.push(leaf.getContainer().doc)); this.app.workspace.iterateAllLeaves(leaf => docList.push(leaf.getContainer().doc));
docList.unique().forEach(doc => { docList.unique().forEach(doc => {
const isDarkMode = activeLeaf.containerEl.doc.body.classList.contains('theme-dark'); const isDarkMode = activeLeaf.containerEl.doc.body.classList.contains('theme-dark');
const theme = isDarkMode ? 'dark' : 'light'; const theme = isDarkMode ? 'dark' : 'light';
this.bibleStyles.applyStyles( this.bibleStyles.applyStyles(
doc, doc,
theme, theme,
this.settings.stylePreset, this.settings.stylePreset,
this.settings.bibleTextFontSize, this.settings.bibleTextFontSize,
{ {
wordsOfChristColor: this.settings.wordsOfChristColor, wordsOfChristColor: this.settings.wordsOfChristColor,
verseNumberColor: this.settings.verseNumberColor, verseNumberColor: this.settings.verseNumberColor,
headingColor: this.settings.headingColor, headingColor: this.settings.headingColor,
blockIndentation: this.settings.blockIndentation blockIndentation: this.settings.blockIndentation
} }
); );
}); });
} }
/** /**
* Open or create a chapter note (Public method for external access) * Open or create a chapter note (Public method for external access)
*/ */
public async openChapterNote(reference: string) { public async openChapterNote(reference: string) {
return this.bibleBookFiles.openChapterNote(reference); return this.bibleBookFiles.openChapterNote(reference);
} }
} }

View file

@ -1,110 +1,110 @@
import { TFile } from 'obsidian'; import {TFile} from 'obsidian';
import { BibleContentService } from './BibleContentService'; import {BibleContentService} from './BibleContentService';
import { BibleReference } from '../core/BibleReference'; import {BibleReference} from '../core/BibleReference';
import { BibleFormatter } from '../utils/BibleFormatter'; import {BibleFormatter} from '../utils/BibleFormatter';
import DisciplesJournalPlugin from "../core/DisciplesJournalPlugin"; import DisciplesJournalPlugin from "../core/DisciplesJournalPlugin";
/** /**
* Service for creating and opening Bible chapter notes * Service for creating and opening Bible chapter notes
*/ */
export class BibleBookFiles { export class BibleBookFiles {
private plugin: DisciplesJournalPlugin; private plugin: DisciplesJournalPlugin;
private bibleContentService: BibleContentService; private bibleContentService: BibleContentService;
constructor( constructor(
plugin: DisciplesJournalPlugin, plugin: DisciplesJournalPlugin,
bibleContentService: BibleContentService, bibleContentService: BibleContentService,
) { ) {
this.plugin = plugin; this.plugin = plugin;
this.bibleContentService = bibleContentService; this.bibleContentService = bibleContentService;
} }
/**
* Open or create a chapter note
*/
public async openChapterNote(reference: string) {
try {
// Parse the reference string to ensure it's valid
const parsedRef = BibleReference.parse(reference);
if (!parsedRef) {
console.error(`Invalid reference: ${reference}`);
return;
}
// Get full content path including version /**
const fullPath = this.getFullContentPath(); * Open or create a chapter note
const chapterPath = `${fullPath}/${parsedRef.book}/${parsedRef.book} ${parsedRef.chapter}.md`; */
public async openChapterNote(reference: string) {
try {
// Parse the reference string to ensure it's valid
const parsedRef = BibleReference.parse(reference);
if (!parsedRef) {
console.error(`Invalid reference: ${reference}`);
return;
}
// Check if note exists // Get full content path including version
const fileExists = await this.plugin.app.vault.adapter.exists(chapterPath); const fullPath = this.getFullContentPath();
const chapterPath = `${fullPath}/${parsedRef.book}/${parsedRef.book} ${parsedRef.chapter}.md`;
if (!fileExists && this.plugin.settings.downloadOnDemand) { // Check if note exists
// Create the note with content from the API const fileExists = await this.plugin.app.vault.adapter.exists(chapterPath);
await this.createChapterNote(parsedRef);
}
// Try opening the note if (!fileExists && this.plugin.settings.downloadOnDemand) {
const file = this.plugin.app.vault.getAbstractFileByPath(chapterPath); // Create the note with content from the API
await this.createChapterNote(parsedRef);
}
if (file && file instanceof TFile) { // Try opening the note
const leaf = this.plugin.app.workspace.getLeaf(false); const file = this.plugin.app.vault.getAbstractFileByPath(chapterPath);
await leaf.openFile(file);
// If there's a specific verse, scroll to it if (file && file instanceof TFile) {
if (parsedRef.verse) { const leaf = this.plugin.app.workspace.getLeaf(false);
setTimeout(() => { await leaf.openFile(file);
// Find the verse element and scroll to it
const verseEl = leaf.getContainer().doc.querySelector(`.verse-${parsedRef.verse}`);
if (verseEl) {
verseEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 300); // Give it a moment to load
}
} else {
console.error(`Could not find or create the chapter note: ${chapterPath}`);
}
} catch (error) {
console.error('Error opening chapter note:', error);
}
}
/** // If there's a specific verse, scroll to it
* Create a new chapter note if (parsedRef.verse) {
*/ setTimeout(() => {
// Find the verse element and scroll to it
const verseEl = leaf.getContainer().doc.querySelector(`.verse-${parsedRef.verse}`);
if (verseEl) {
verseEl.scrollIntoView({behavior: 'smooth', block: 'center'});
}
}, 300); // Give it a moment to load
}
} else {
console.error(`Could not find or create the chapter note: ${chapterPath}`);
}
} catch (error) {
console.error('Error opening chapter note:', error);
}
}
/**
* Create a new chapter note
*/
private async createChapterNote(reference: BibleReference) { private async createChapterNote(reference: BibleReference) {
try { try {
// Create a properly formatted reference string // Create a properly formatted reference string
const referenceStr = reference.toString(); const referenceStr = reference.toString();
// Get the content from the Bible API and format it for a note // Get the content from the Bible API and format it for a note
const passage = await this.bibleContentService.getBibleContent(referenceStr); const passage = await this.bibleContentService.getBibleContent(referenceStr);
// Check if passage is null // Check if passage is null
if (!passage) { if (!passage) {
console.error(`Failed to get Bible content for ${referenceStr}`); console.error(`Failed to get Bible content for ${referenceStr}`);
throw new Error(`Failed to get Bible content for ${referenceStr}`); throw new Error(`Failed to get Bible content for ${referenceStr}`);
} }
// Use the formatter utility to format the content // Use the formatter utility to format the content
const content = BibleFormatter.formatChapterContent(passage); const content = BibleFormatter.formatChapterContent(passage);
// Save the content to a note with the version path // Save the content to a note with the version path
const fullPath = this.getFullContentPath(); const fullPath = this.getFullContentPath();
const bookPath = `${fullPath}/${reference.book}`; const bookPath = `${fullPath}/${reference.book}`;
const chapterPath = `${fullPath}/${reference.book}/${reference.book} ${reference.chapter}.md`; const chapterPath = `${fullPath}/${reference.book}/${reference.book} ${reference.chapter}.md`;
// Ensure the directory exists // Ensure the directory exists
await this.plugin.app.vault.adapter.mkdir(bookPath); await this.plugin.app.vault.adapter.mkdir(bookPath);
// Create the note // Create the note
await this.plugin.app.vault.create(chapterPath, content); await this.plugin.app.vault.create(chapterPath, content);
return chapterPath; return chapterPath;
} catch (error) { } catch (error) {
console.error('Error creating chapter note:', error); console.error('Error creating chapter note:', error);
throw error; throw error;
} }
} }
/** /**
* Get the full path with version * Get the full path with version

View file

@ -1,26 +1,26 @@
import { BibleReference } from "../core/BibleReference"; import {BibleReference} from "../core/BibleReference";
import { BookNames } from "./BookNames"; import {BookNames} from "./BookNames";
import { ESVApiService } from "./ESVApiService"; import {ESVApiService} from "./ESVApiService";
import DisciplesJournalPlugin from "../core/DisciplesJournalPlugin"; import DisciplesJournalPlugin from "../core/DisciplesJournalPlugin";
/** /**
* Interface for a single Bible verse * Interface for a single Bible verse
*/ */
export interface BibleVerse { export interface BibleVerse {
book: string; book: string;
chapter: number; chapter: number;
verse: number; verse: number;
text: string; text: string;
} }
/** /**
* Interface for Bible passage content * Interface for Bible passage content
*/ */
export interface BiblePassage { export interface BiblePassage {
reference: string; reference: string;
verses: BibleVerse[]; verses: BibleVerse[];
htmlContent?: string; htmlContent?: string;
missingToken?: boolean; missingToken?: boolean;
} }
/** /**
@ -28,8 +28,8 @@ export interface BiblePassage {
*/ */
export class BibleContentService { export class BibleContentService {
private plugin: DisciplesJournalPlugin; private plugin: DisciplesJournalPlugin;
private bible: any = null; private bible: any = null;
private esvApiService: ESVApiService; private esvApiService: ESVApiService;
constructor(plugin: DisciplesJournalPlugin, esvApiService: ESVApiService) { constructor(plugin: DisciplesJournalPlugin, esvApiService: ESVApiService) {
this.plugin = plugin; this.plugin = plugin;
@ -37,127 +37,128 @@ export class BibleContentService {
} }
/** /**
* Get a single verse by reference * Get a single verse by reference
*/ */
public getVerse(book: string, chapter: number, verse: number): BibleVerse | null { public getVerse(book: string, chapter: number, verse: number): BibleVerse | null {
if (!this.bible) return null; if (!this.bible) return null;
try { try {
const normalizedBook = BookNames.normalize(book); const normalizedBook = BookNames.normalize(book);
if (!normalizedBook) return null; if (!normalizedBook) return null;
// Check if the book exists in the Bible data // Check if the book exists in the Bible data
if (!this.bible[normalizedBook]) return null; if (!this.bible[normalizedBook]) return null;
// Check if the chapter exists // Check if the chapter exists
const chapterStr = chapter.toString(); const chapterStr = chapter.toString();
if (!this.bible[normalizedBook][chapterStr]) return null; if (!this.bible[normalizedBook][chapterStr]) return null;
// Check if the verse exists // Check if the verse exists
const verseStr = verse.toString(); const verseStr = verse.toString();
if (!this.bible[normalizedBook][chapterStr][verseStr]) return null; if (!this.bible[normalizedBook][chapterStr][verseStr]) return null;
// Return the verse // Return the verse
return { return {
book: normalizedBook, book: normalizedBook,
chapter: chapter, chapter: chapter,
verse: verse, verse: verse,
text: this.bible[normalizedBook][chapterStr][verseStr] text: this.bible[normalizedBook][chapterStr][verseStr]
}; };
} catch (error) { } catch (error) {
console.error('Error getting verse:', error); console.error('Error getting verse:', error);
return null; return null;
} }
} }
/**
* Get a passage by reference
* This handles everything from single verses to full chapters
*/
public getPassage(reference: BibleReference): BiblePassage | null {
if (!this.bible) return null;
try {
const normalizedBook = BookNames.normalize(reference.book);
if (!normalizedBook) return null;
// Prepare the result
const result: BiblePassage = {
reference: reference.toString(),
verses: []
};
// Get the chapter
const chapterStr = reference.chapter.toString();
if (!this.bible[normalizedBook][chapterStr]) return null;
// If verse is specified, get just that verse or verse range
if (reference.verse) {
// Single verse
if (!reference.endVerse) {
const verse = this.getVerse(normalizedBook, reference.chapter, reference.verse);
if (verse) {
result.verses.push(verse);
}
}
// Verse range
else {
// Add all verses in the range
for (let v = reference.verse; v <= reference.endVerse; v++) {
const verse = this.getVerse(normalizedBook, reference.chapter, v);
if (verse) {
result.verses.push(verse);
}
}
}
}
// No verse specified, get the whole chapter
else {
// Get all verses in the chapter
const chapter = this.bible[normalizedBook][chapterStr];
if (chapter) {
const verseNumbers = Object.keys(chapter).map(v => parseInt(v)).sort((a, b) => a - b);
for (const verseNum of verseNumbers) {
const verse = this.getVerse(normalizedBook, reference.chapter, verseNum);
if (verse) {
result.verses.push(verse);
}
}
}
}
return result;
} catch (error) {
console.error('Error getting passage:', error);
return null;
}
}
/** /**
* Get Bible content from any source (local or API) * Get a passage by reference
*/ * This handles everything from single verses to full chapters
public async getBibleContent(referenceString: string): Promise<BiblePassage | null> { */
try { public getPassage(reference: BibleReference): BiblePassage | null {
// Parse the reference if (!this.bible) return null;
const parsedRef = BibleReference.parse(referenceString);
if (!parsedRef) { try {
console.error(`Invalid reference: ${referenceString}`); const normalizedBook = BookNames.normalize(reference.book);
return null; if (!normalizedBook) return null;
}
// Prepare the result
// Try to get from local Bible data first const result: BiblePassage = {
let passage = this.getPassage(parsedRef); reference: reference.toString(),
verses: []
// If not available locally and download on demand is enabled, try the API };
if (!passage && this.plugin.settings.downloadOnDemand) {
// Try to get from the ESV API // Get the chapter
passage = await this.esvApiService.downloadFromESVApi(referenceString); const chapterStr = reference.chapter.toString();
} if (!this.bible[normalizedBook][chapterStr]) return null;
return passage; // If verse is specified, get just that verse or verse range
} catch (error) { if (reference.verse) {
console.error('Error getting Bible content:', error); // Single verse
return null; if (!reference.endVerse) {
} const verse = this.getVerse(normalizedBook, reference.chapter, reference.verse);
} if (verse) {
result.verses.push(verse);
}
}
// Verse range
else {
// Add all verses in the range
for (let v = reference.verse; v <= reference.endVerse; v++) {
const verse = this.getVerse(normalizedBook, reference.chapter, v);
if (verse) {
result.verses.push(verse);
}
}
}
}
// No verse specified, get the whole chapter
else {
// Get all verses in the chapter
const chapter = this.bible[normalizedBook][chapterStr];
if (chapter) {
const verseNumbers = Object.keys(chapter).map(v => parseInt(v)).sort((a, b) => a - b);
for (const verseNum of verseNumbers) {
const verse = this.getVerse(normalizedBook, reference.chapter, verseNum);
if (verse) {
result.verses.push(verse);
}
}
}
}
return result;
} catch (error) {
console.error('Error getting passage:', error);
return null;
}
}
/**
* Get Bible content from any source (local or API)
*/
public async getBibleContent(referenceString: string): Promise<BiblePassage | null> {
try {
// Parse the reference
const parsedRef = BibleReference.parse(referenceString);
if (!parsedRef) {
console.error(`Invalid reference: ${referenceString}`);
return null;
}
// Try to get from local Bible data first
let passage = this.getPassage(parsedRef);
// If not available locally and download on demand is enabled, try the API
if (!passage && this.plugin.settings.downloadOnDemand) {
// Try to get from the ESV API
passage = await this.esvApiService.downloadFromESVApi(referenceString);
}
return passage;
} catch (error) {
console.error('Error getting Bible content:', error);
return null;
}
}
} }

View file

@ -2,245 +2,245 @@
* Service for handling Bible book name standardization and mapping * Service for handling Bible book name standardization and mapping
*/ */
export class BookNames { export class BookNames {
/** /**
* Standardize a book name to its canonical form * Standardize a book name to its canonical form
*/ */
public static normalize(bookName: string): string | null { public static normalize(bookName: string): string | null {
if (!bookName) { if (!bookName) {
return null; return null;
} }
// Clean up the book name
const cleanedName = bookName.trim();
// First try a direct lookup
if (BookNames._instance.bookNameMap.has(cleanedName.toLowerCase())) {
return BookNames._instance.bookNameMap.get(cleanedName.toLowerCase()) as string;
}
// Try to find a partial match
for (const [key, value] of BookNames._instance.bookNameMap.entries()) {
// Check if the key is a substring of the cleaned name
if (cleanedName.toLowerCase().startsWith(key)) {
return value;
}
}
return null;
}
/**
* Extract a book name from a reference string
*/
public static extractBookFromReference(reference: string): string {
if (!reference) {
return "";
}
// First try to match book names with numbers (e.g., "1 John")
const numberedBookRegex = /^(\d+\s*[A-Za-z]+)/;
const numberedMatch = reference.match(numberedBookRegex);
if (numberedMatch) {
return numberedMatch[1];
}
// If no numbered book was found, try to match a regular book name
// We iterate through all keys in the book name map to find the longest matching book name
let longestMatch = "";
let bookName = "";
// Start by extracting the first word (potential book name)
const firstWordMatch = reference.match(/^([A-Za-z]+)/);
if (firstWordMatch) {
bookName = firstWordMatch[1];
// Check if this is a valid book name
const standardizedName = BookNames.normalize(bookName);
if (standardizedName) {
longestMatch = bookName;
}
}
// Try matching with two words (for books like "Song of")
const twoWordsMatch = reference.match(/^([A-Za-z]+\s+[A-Za-z]+)/);
if (twoWordsMatch) {
bookName = twoWordsMatch[1];
// Check if this is a valid book name
const standardizedName = BookNames.normalize(bookName);
if (standardizedName) {
longestMatch = bookName;
}
}
// Try matching with three words (for books like "Song of Solomon")
const threeWordsMatch = reference.match(/^([A-Za-z]+\s+[A-Za-z]+\s+[A-Za-z]+)/);
if (threeWordsMatch) {
bookName = threeWordsMatch[1];
// Check if this is a valid book name
const standardizedName = BookNames.normalize(bookName);
if (standardizedName) {
longestMatch = bookName;
}
}
return longestMatch;
}
/**
* Get the chapter count for a given book name
*/
public static getChapterCount(bookName: string): number {
const standardizedName = BookNames.normalize(bookName);
if (standardizedName) {
return BookNames.bibleStructure[standardizedName] || 1;
}
return 1; // Default to 1 if book not found
}
/** // Clean up the book name
* Get the ordered list of book names const cleanedName = bookName.trim();
*/
public static getBookOrder(): string[] { // First try a direct lookup
return [...BookNames.bookOrder]; // Return a copy to prevent modification if (BookNames._instance.bookNameMap.has(cleanedName.toLowerCase())) {
} return BookNames._instance.bookNameMap.get(cleanedName.toLowerCase()) as string;
}
// Try to find a partial match
for (const [key, value] of BookNames._instance.bookNameMap.entries()) {
// Check if the key is a substring of the cleaned name
if (cleanedName.toLowerCase().startsWith(key)) {
return value;
}
}
return null;
}
/**
* Extract a book name from a reference string
*/
public static extractBookFromReference(reference: string): string {
if (!reference) {
return "";
}
// First try to match book names with numbers (e.g., "1 John")
const numberedBookRegex = /^(\d+\s*[A-Za-z]+)/;
const numberedMatch = reference.match(numberedBookRegex);
if (numberedMatch) {
return numberedMatch[1];
}
// If no numbered book was found, try to match a regular book name
// We iterate through all keys in the book name map to find the longest matching book name
let longestMatch = "";
let bookName = "";
// Start by extracting the first word (potential book name)
const firstWordMatch = reference.match(/^([A-Za-z]+)/);
if (firstWordMatch) {
bookName = firstWordMatch[1];
// Check if this is a valid book name
const standardizedName = BookNames.normalize(bookName);
if (standardizedName) {
longestMatch = bookName;
}
}
// Try matching with two words (for books like "Song of")
const twoWordsMatch = reference.match(/^([A-Za-z]+\s+[A-Za-z]+)/);
if (twoWordsMatch) {
bookName = twoWordsMatch[1];
// Check if this is a valid book name
const standardizedName = BookNames.normalize(bookName);
if (standardizedName) {
longestMatch = bookName;
}
}
// Try matching with three words (for books like "Song of Solomon")
const threeWordsMatch = reference.match(/^([A-Za-z]+\s+[A-Za-z]+\s+[A-Za-z]+)/);
if (threeWordsMatch) {
bookName = threeWordsMatch[1];
// Check if this is a valid book name
const standardizedName = BookNames.normalize(bookName);
if (standardizedName) {
longestMatch = bookName;
}
}
return longestMatch;
}
/**
* Get the chapter count for a given book name
*/
public static getChapterCount(bookName: string): number {
const standardizedName = BookNames.normalize(bookName);
if (standardizedName) {
return BookNames.bibleStructure[standardizedName] || 1;
}
return 1; // Default to 1 if book not found
}
/**
* Get the ordered list of book names
*/
public static getBookOrder(): string[] {
return [...BookNames.bookOrder]; // Return a copy to prevent modification
}
// Bible book structure (book name and chapter count) // Bible book structure (book name and chapter count)
private static bibleStructure: {[book: string]: number} = { private static bibleStructure: { [book: string]: number } = {
// Old Testament // Old Testament
"Genesis": 50,"Exodus": 40, "Leviticus": 27, "Numbers": 36, "Deuteronomy": 34, "Genesis": 50, "Exodus": 40, "Leviticus": 27, "Numbers": 36, "Deuteronomy": 34,
"Joshua": 24, "Judges": 21, "Ruth": 4, "1 Samuel": 31, "2 Samuel": 24, "Joshua": 24, "Judges": 21, "Ruth": 4, "1 Samuel": 31, "2 Samuel": 24,
"1 Kings": 22, "2 Kings": 25, "1 Chronicles": 29, "2 Chronicles": 36, "1 Kings": 22, "2 Kings": 25, "1 Chronicles": 29, "2 Chronicles": 36,
"Ezra": 10, "Nehemiah": 13, "Esther": 10, "Job": 42, "Psalms": 150, "Ezra": 10, "Nehemiah": 13, "Esther": 10, "Job": 42, "Psalms": 150,
"Proverbs": 31, "Ecclesiastes": 12, "Song of Solomon": 8, "Isaiah": 66, "Proverbs": 31, "Ecclesiastes": 12, "Song of Solomon": 8, "Isaiah": 66,
"Jeremiah": 52, "Lamentations": 5, "Ezekiel": 48, "Daniel": 12, "Hosea": 14, "Jeremiah": 52, "Lamentations": 5, "Ezekiel": 48, "Daniel": 12, "Hosea": 14,
"Joel": 3, "Amos": 9, "Obadiah": 1, "Jonah": 4, "Micah": 7, "Joel": 3, "Amos": 9, "Obadiah": 1, "Jonah": 4, "Micah": 7,
"Nahum": 3, "Habakkuk": 3, "Zephaniah": 3, "Haggai": 2, "Zechariah": 14, "Nahum": 3, "Habakkuk": 3, "Zephaniah": 3, "Haggai": 2, "Zechariah": 14,
"Malachi": 4, "Malachi": 4,
// New Testament // New Testament
"Matthew": 28, "Mark": 16, "Luke": 24, "John": 21, "Acts": 28, "Matthew": 28, "Mark": 16, "Luke": 24, "John": 21, "Acts": 28,
"Romans": 16, "1 Corinthians": 16, "2 Corinthians": 13, "Galatians": 6, "Romans": 16, "1 Corinthians": 16, "2 Corinthians": 13, "Galatians": 6,
"Ephesians": 6, "Philippians": 4, "Colossians": 4, "1 Thessalonians": 5, "Ephesians": 6, "Philippians": 4, "Colossians": 4, "1 Thessalonians": 5,
"2 Thessalonians": 3, "1 Timothy": 6, "2 Timothy": 4, "Titus": 3, "2 Thessalonians": 3, "1 Timothy": 6, "2 Timothy": 4, "Titus": 3,
"Philemon": 1, "Hebrews": 13, "James": 5, "1 Peter": 5, "2 Peter": 3, "Philemon": 1, "Hebrews": 13, "James": 5, "1 Peter": 5, "2 Peter": 3,
"1 John": 5, "2 John": 1, "3 John": 1, "Jude": 1, "Revelation": 22 "1 John": 5, "2 John": 1, "3 John": 1, "Jude": 1, "Revelation": 22
}; };
// Order of books in the Bible
private static bookOrder: string[] = [
// Old Testament
"Genesis", "Exodus", "Leviticus", "Numbers", "Deuteronomy",
"Joshua", "Judges", "Ruth", "1 Samuel", "2 Samuel",
"1 Kings", "2 Kings", "1 Chronicles", "2 Chronicles",
"Ezra", "Nehemiah", "Esther", "Job", "Psalms",
"Proverbs", "Ecclesiastes", "Song of Solomon", "Isaiah",
"Jeremiah", "Lamentations", "Ezekiel", "Daniel", "Hosea",
"Joel", "Amos", "Obadiah", "Jonah", "Micah",
"Nahum", "Habakkuk", "Zephaniah", "Haggai", "Zechariah",
"Malachi",
// New Testament
"Matthew", "Mark", "Luke", "John", "Acts",
"Romans", "1 Corinthians", "2 Corinthians", "Galatians",
"Ephesians", "Philippians", "Colossians", "1 Thessalonians",
"2 Thessalonians", "1 Timothy", "2 Timothy", "Titus",
"Philemon", "Hebrews", "James", "1 Peter", "2 Peter",
"1 John", "2 John", "3 John", "Jude", "Revelation"
];
private static _instance:BookNames = new BookNames(); // Order of books in the Bible
private static bookOrder: string[] = [
// Old Testament
"Genesis", "Exodus", "Leviticus", "Numbers", "Deuteronomy",
"Joshua", "Judges", "Ruth", "1 Samuel", "2 Samuel",
"1 Kings", "2 Kings", "1 Chronicles", "2 Chronicles",
"Ezra", "Nehemiah", "Esther", "Job", "Psalms",
"Proverbs", "Ecclesiastes", "Song of Solomon", "Isaiah",
"Jeremiah", "Lamentations", "Ezekiel", "Daniel", "Hosea",
"Joel", "Amos", "Obadiah", "Jonah", "Micah",
"Nahum", "Habakkuk", "Zephaniah", "Haggai", "Zechariah",
"Malachi",
// New Testament
"Matthew", "Mark", "Luke", "John", "Acts",
"Romans", "1 Corinthians", "2 Corinthians", "Galatians",
"Ephesians", "Philippians", "Colossians", "1 Thessalonians",
"2 Thessalonians", "1 Timothy", "2 Timothy", "Titus",
"Philemon", "Hebrews", "James", "1 Peter", "2 Peter",
"1 John", "2 John", "3 John", "Jude", "Revelation"
];
private bookNameMap: Map<string, string> = new Map(); private static _instance: BookNames = new BookNames();
private constructor() { private bookNameMap: Map<string, string> = new Map();
this.initializeBookNameMap();
}
/** private constructor() {
* Initialize the map of standardized book names this.initializeBookNameMap();
*/ }
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"]);
this.addBookMapping("1 Kings", ["1 Ki", "1 K", "1K", "I K", "1Ki", "1st Kings"]);
this.addBookMapping("2 Kings", ["2 Ki", "2 K", "2K", "II K", "2Ki", "2nd Kings"]);
this.addBookMapping("1 Chronicles", ["1 Ch", "1 Chr", "I Ch", "1Ch", "1st Chronicles"]);
this.addBookMapping("2 Chronicles", ["2 Ch", "2 Chr", "II Ch", "2Ch", "2nd Chronicles"]);
this.addBookMapping("Ezra", ["Ezr", "Ez"]);
this.addBookMapping("Nehemiah", ["Neh", "Ne"]);
this.addBookMapping("Esther", ["Est", "Es"]);
this.addBookMapping("Job", ["Jb"]);
this.addBookMapping("Psalms", ["Ps", "Psa", "Psalm", "Pslm"]);
this.addBookMapping("Proverbs", ["Prov", "Pro", "Pr"]);
this.addBookMapping("Ecclesiastes", ["Eccl", "Ecc", "Ec", "Qoh"]);
this.addBookMapping("Song of Solomon", ["Song", "SoS", "Canticles", "Song of Songs", "Cant"]);
this.addBookMapping("Isaiah", ["Isa", "Is"]);
this.addBookMapping("Jeremiah", ["Jer", "Je"]);
this.addBookMapping("Lamentations", ["Lam", "La"]);
this.addBookMapping("Ezekiel", ["Ezek", "Eze", "Ezk"]);
this.addBookMapping("Daniel", ["Dan", "Da", "Dn"]);
this.addBookMapping("Hosea", ["Hos", "Ho"]);
this.addBookMapping("Joel", ["Jl"]);
this.addBookMapping("Amos", ["Am"]);
this.addBookMapping("Obadiah", ["Obad", "Ob"]);
this.addBookMapping("Jonah", ["Jon", "Jnh"]);
this.addBookMapping("Micah", ["Mic", "Mi"]);
this.addBookMapping("Nahum", ["Nah", "Na"]);
this.addBookMapping("Habakkuk", ["Hab", "Hb"]);
this.addBookMapping("Zephaniah", ["Zeph", "Zep", "Zp"]);
this.addBookMapping("Haggai", ["Hag", "Hg"]);
this.addBookMapping("Zechariah", ["Zech", "Zec", "Zc"]);
this.addBookMapping("Malachi", ["Mal", "Ml"]);
// New Testament /**
this.addBookMapping("Matthew", ["Matt", "Mt"]); * Initialize the map of standardized book names
this.addBookMapping("Mark", ["Mk", "Mr"]); */
this.addBookMapping("Luke", ["Lk", "Lu"]); private initializeBookNameMap(): void {
this.addBookMapping("John", ["Jn", "Jhn"]); // Old Testament
this.addBookMapping("Acts", ["Act", "Ac"]); this.addBookMapping("Genesis", ["Gen", "Ge", "Gn"]);
this.addBookMapping("Romans", ["Rom", "Ro", "Rm"]); this.addBookMapping("Exodus", ["Exo", "Ex", "Exod"]);
this.addBookMapping("1 Corinthians", ["1 Cor", "1 Co", "I Co", "1Cor", "1st Corinthians"]); this.addBookMapping("Leviticus", ["Lev", "Le", "Lv"]);
this.addBookMapping("2 Corinthians", ["2 Cor", "2 Co", "II Co", "2Cor", "2nd Corinthians"]); this.addBookMapping("Numbers", ["Num", "Nu", "Nm", "Nb"]);
this.addBookMapping("Galatians", ["Gal", "Ga"]); this.addBookMapping("Deuteronomy", ["Deut", "De", "Dt"]);
this.addBookMapping("Ephesians", ["Eph", "Ep"]); this.addBookMapping("Joshua", ["Josh", "Jos", "Jsh"]);
this.addBookMapping("Philippians", ["Phil", "Php", "Pp"]); this.addBookMapping("Judges", ["Judg", "Jdg", "Jg"]);
this.addBookMapping("Colossians", ["Col", "Co"]); this.addBookMapping("Ruth", ["Rth", "Ru"]);
this.addBookMapping("1 Thessalonians", ["1 Thess", "1 Th", "I Th", "1Thess", "1st Thessalonians"]); this.addBookMapping("1 Samuel", ["1 Sam", "1 Sa", "1S", "I Sa", "1Sam", "1st Samuel"]);
this.addBookMapping("2 Thessalonians", ["2 Thess", "2 Th", "II Th", "2Thess", "2nd Thessalonians"]); this.addBookMapping("2 Samuel", ["2 Sam", "2 Sa", "2S", "II Sa", "2Sam", "2nd Samuel"]);
this.addBookMapping("1 Timothy", ["1 Tim", "1 Ti", "I Ti", "1Tim", "1st Timothy"]); this.addBookMapping("1 Kings", ["1 Ki", "1 K", "1K", "I K", "1Ki", "1st Kings"]);
this.addBookMapping("2 Timothy", ["2 Tim", "2 Ti", "II Ti", "2Tim", "2nd Timothy"]); this.addBookMapping("2 Kings", ["2 Ki", "2 K", "2K", "II K", "2Ki", "2nd Kings"]);
this.addBookMapping("Titus", ["Tit", "Ti"]); this.addBookMapping("1 Chronicles", ["1 Ch", "1 Chr", "I Ch", "1Ch", "1st Chronicles"]);
this.addBookMapping("Philemon", ["Phm", "Pm"]); this.addBookMapping("2 Chronicles", ["2 Ch", "2 Chr", "II Ch", "2Ch", "2nd Chronicles"]);
this.addBookMapping("Hebrews", ["Heb", "He"]); this.addBookMapping("Ezra", ["Ezr", "Ez"]);
this.addBookMapping("James", ["Jas", "Jm"]); this.addBookMapping("Nehemiah", ["Neh", "Ne"]);
this.addBookMapping("1 Peter", ["1 Pet", "1 Pe", "I Pe", "1Pet", "1st Peter"]); this.addBookMapping("Esther", ["Est", "Es"]);
this.addBookMapping("2 Peter", ["2 Pet", "2 Pe", "II Pe", "2Pet", "2nd Peter"]); this.addBookMapping("Job", ["Jb"]);
this.addBookMapping("1 John", ["1 Jn", "I Jn", "1Jn", "1st John"]); this.addBookMapping("Psalms", ["Ps", "Psa", "Psalm", "Pslm"]);
this.addBookMapping("2 John", ["2 Jn", "II Jn", "2Jn", "2nd John"]); this.addBookMapping("Proverbs", ["Prov", "Pro", "Pr"]);
this.addBookMapping("3 John", ["3 Jn", "III Jn", "3Jn", "3rd John"]); this.addBookMapping("Ecclesiastes", ["Eccl", "Ecc", "Ec", "Qoh"]);
this.addBookMapping("Jude", ["Jud", "Jd"]); this.addBookMapping("Song of Solomon", ["Song", "SoS", "Canticles", "Song of Songs", "Cant"]);
this.addBookMapping("Revelation", ["Rev", "Re", "The Revelation"]); this.addBookMapping("Isaiah", ["Isa", "Is"]);
} this.addBookMapping("Jeremiah", ["Jer", "Je"]);
this.addBookMapping("Lamentations", ["Lam", "La"]);
this.addBookMapping("Ezekiel", ["Ezek", "Eze", "Ezk"]);
this.addBookMapping("Daniel", ["Dan", "Da", "Dn"]);
this.addBookMapping("Hosea", ["Hos", "Ho"]);
this.addBookMapping("Joel", ["Jl"]);
this.addBookMapping("Amos", ["Am"]);
this.addBookMapping("Obadiah", ["Obad", "Ob"]);
this.addBookMapping("Jonah", ["Jon", "Jnh"]);
this.addBookMapping("Micah", ["Mic", "Mi"]);
this.addBookMapping("Nahum", ["Nah", "Na"]);
this.addBookMapping("Habakkuk", ["Hab", "Hb"]);
this.addBookMapping("Zephaniah", ["Zeph", "Zep", "Zp"]);
this.addBookMapping("Haggai", ["Hag", "Hg"]);
this.addBookMapping("Zechariah", ["Zech", "Zec", "Zc"]);
this.addBookMapping("Malachi", ["Mal", "Ml"]);
/** // New Testament
* Add a mapping for a book and its alternative names this.addBookMapping("Matthew", ["Matt", "Mt"]);
*/ this.addBookMapping("Mark", ["Mk", "Mr"]);
private addBookMapping(standardName: string, alternateNames: string[]): void { this.addBookMapping("Luke", ["Lk", "Lu"]);
// Add the standard name (lowercase for case-insensitive matching) this.addBookMapping("John", ["Jn", "Jhn"]);
this.bookNameMap.set(standardName.toLowerCase(), standardName); this.addBookMapping("Acts", ["Act", "Ac"]);
this.addBookMapping("Romans", ["Rom", "Ro", "Rm"]);
// Add alternate names this.addBookMapping("1 Corinthians", ["1 Cor", "1 Co", "I Co", "1Cor", "1st Corinthians"]);
for (const alternateName of alternateNames) { this.addBookMapping("2 Corinthians", ["2 Cor", "2 Co", "II Co", "2Cor", "2nd Corinthians"]);
this.bookNameMap.set(alternateName.toLowerCase(), standardName); this.addBookMapping("Galatians", ["Gal", "Ga"]);
} this.addBookMapping("Ephesians", ["Eph", "Ep"]);
} this.addBookMapping("Philippians", ["Phil", "Php", "Pp"]);
this.addBookMapping("Colossians", ["Col", "Co"]);
this.addBookMapping("1 Thessalonians", ["1 Thess", "1 Th", "I Th", "1Thess", "1st Thessalonians"]);
this.addBookMapping("2 Thessalonians", ["2 Thess", "2 Th", "II Th", "2Thess", "2nd Thessalonians"]);
this.addBookMapping("1 Timothy", ["1 Tim", "1 Ti", "I Ti", "1Tim", "1st Timothy"]);
this.addBookMapping("2 Timothy", ["2 Tim", "2 Ti", "II Ti", "2Tim", "2nd Timothy"]);
this.addBookMapping("Titus", ["Tit", "Ti"]);
this.addBookMapping("Philemon", ["Phm", "Pm"]);
this.addBookMapping("Hebrews", ["Heb", "He"]);
this.addBookMapping("James", ["Jas", "Jm"]);
this.addBookMapping("1 Peter", ["1 Pet", "1 Pe", "I Pe", "1Pet", "1st Peter"]);
this.addBookMapping("2 Peter", ["2 Pet", "2 Pe", "II Pe", "2Pet", "2nd Peter"]);
this.addBookMapping("1 John", ["1 Jn", "I Jn", "1Jn", "1st John"]);
this.addBookMapping("2 John", ["2 Jn", "II Jn", "2Jn", "2nd John"]);
this.addBookMapping("3 John", ["3 Jn", "III Jn", "3Jn", "3rd John"]);
this.addBookMapping("Jude", ["Jud", "Jd"]);
this.addBookMapping("Revelation", ["Rev", "Re", "The Revelation"]);
}
/**
* Add a mapping for a book and its alternative names
*/
private addBookMapping(standardName: string, alternateNames: string[]): void {
// Add the standard name (lowercase for case-insensitive matching)
this.bookNameMap.set(standardName.toLowerCase(), standardName);
// Add alternate names
for (const alternateName of alternateNames) {
this.bookNameMap.set(alternateName.toLowerCase(), standardName);
}
}
} }

View file

@ -1,329 +1,329 @@
import { requestUrl } from "obsidian"; import {requestUrl} from "obsidian";
import { BiblePassage } from "./BibleContentService"; import {BiblePassage} from "./BibleContentService";
import DisciplesJournalPlugin from "../core/DisciplesJournalPlugin"; import DisciplesJournalPlugin from "../core/DisciplesJournalPlugin";
/** /**
* Interface for ESV API Response * Interface for ESV API Response
*/ */
export interface ESVApiResponse { export interface ESVApiResponse {
query: string; query: string;
canonical: string; canonical: string;
parsed: number[][]; parsed: number[][];
passage_meta: ESVPassageMeta[]; passage_meta: ESVPassageMeta[];
passages: string[]; passages: string[];
} }
/** /**
* Interface for ESV Passage Metadata * Interface for ESV Passage Metadata
*/ */
export interface ESVPassageMeta { export interface ESVPassageMeta {
canonical: string; canonical: string;
chapter_start: number[]; chapter_start: number[];
chapter_end: number[]; chapter_end: number[];
prev_verse: number | null; prev_verse: number | null;
next_verse: number | null; next_verse: number | null;
prev_chapter: number | null; prev_chapter: number | null;
next_chapter: number[] | null; next_chapter: number[] | null;
} }
/** /**
* Service for interacting with the ESV API * Service for interacting with the ESV API
*/ */
export class ESVApiService { export class ESVApiService {
private plugin: DisciplesJournalPlugin; private plugin: DisciplesJournalPlugin;
// Store HTML formatted Bible chapters // Store HTML formatted Bible chapters
private htmlFormattedBible: { private htmlFormattedBible: {
[reference: string]: { [reference: string]: {
canonical: string; canonical: string;
htmlContent: string; htmlContent: string;
} }
} = {}; } = {};
constructor(plugin: DisciplesJournalPlugin) { constructor(plugin: DisciplesJournalPlugin) {
this.plugin = plugin; this.plugin = plugin;
} }
/** /**
* Get the full vault path including version subdirectory * Get the full vault path including version subdirectory
*/ */
private getFullContentPath(): string { private getFullContentPath(): string {
return `${this.plugin.settings.bibleContentVaultPath}/${this.plugin.settings.preferredBibleVersion}`; return `${this.plugin.settings.bibleContentVaultPath}/${this.plugin.settings.preferredBibleVersion}`;
} }
/** /**
* Get HTML formatted Bible content from in-memory storage * Get HTML formatted Bible content from in-memory storage
*/ */
public getHTMLContent(reference: string): BiblePassage | null { public getHTMLContent(reference: string): BiblePassage | null {
if (this.htmlFormattedBible[reference]) { if (this.htmlFormattedBible[reference]) {
return { return {
reference: this.htmlFormattedBible[reference].canonical, reference: this.htmlFormattedBible[reference].canonical,
verses: [], // Empty since we're using HTML verses: [], // Empty since we're using HTML
htmlContent: this.htmlFormattedBible[reference].htmlContent htmlContent: this.htmlFormattedBible[reference].htmlContent
}; };
} }
return null; return null;
} }
/** /**
* Load a collection of HTML formatted Bible chapters from files in the vault * Load a collection of HTML formatted Bible chapters from files in the vault
*/ */
public async loadBibleChaptersFromVault(): Promise<void> { public async loadBibleChaptersFromVault(): Promise<void> {
try { try {
// Get the path where Bible content is stored // Get the path where Bible content is stored
const fullPath = this.getFullContentPath(); const fullPath = this.getFullContentPath();
// Check if path exists
if (!(await this.plugin.app.vault.adapter.exists(fullPath))) {
console.log(`Bible content directory ${fullPath} does not exist yet`);
return;
}
// Get all book directories
const bookDirs = await this.plugin.app.vault.adapter.list(fullPath);
// Process each book directory
for (const bookDir of bookDirs.folders) {
// Get all chapter files
const files = await this.plugin.app.vault.adapter.list(bookDir);
// Process each chapter file
for (const file of files.files) {
if (file.endsWith('.json')) {
try {
// Read and parse the file
const content = await this.plugin.app.vault.adapter.read(file);
const data = JSON.parse(content);
// Process the data if it's in the expected format
if (this.isESVApiFormat(data)) {
// Store the chapter content
const canonical = data.canonical;
const htmlContent = data.passages[0];
this.htmlFormattedBible[canonical] = {
canonical,
htmlContent
};
}
} catch (error) {
console.error(`Error processing file ${file}:`, error);
}
}
}
}
console.log(`Loaded ${Object.keys(this.htmlFormattedBible).length} Bible chapters from vault`);
} catch (error) {
console.error('Error loading Bible chapters from vault:', error);
}
}
/** // Check if path exists
* Check if data is in ESV API format if (!(await this.plugin.app.vault.adapter.exists(fullPath))) {
*/ console.log(`Bible content directory ${fullPath} does not exist yet`);
public isESVApiFormat(data: any): boolean { return;
return data && }
typeof data === 'object' &&
data.canonical !== undefined &&
data.passages !== undefined &&
Array.isArray(data.passages) &&
data.passages.length > 0;
}
/** // Get all book directories
* Create a DOM-friendly error message for missing token or API errors const bookDirs = await this.plugin.app.vault.adapter.list(fullPath);
*/
private createErrorMessageContent(doc: Document, type: 'missing-token' | 'api-error', message: string = '', details: string = ''): HTMLElement {
// We're required to return a string here because it's part of the ESV API response structure
// This method creates the content in a DOM-safe way then serializes to a string
const container = doc.createElement('div');
container.className = type === 'missing-token' ? 'bible-missing-token-warning' : 'bible-api-error';
const titleEl = doc.createElement('p');
const titleStrong = doc.createElement('strong');
titleStrong.textContent = type === 'missing-token' ? 'ESV API Token Not Set' : 'Error Loading Bible Passage';
titleEl.appendChild(titleStrong);
container.appendChild(titleEl);
if (message) {
const messageEl = doc.createElement('p');
messageEl.textContent = message;
container.appendChild(messageEl);
}
if (details) {
const detailsEl = doc.createElement('p');
detailsEl.textContent = details;
container.appendChild(detailsEl);
}
// For the API token link
if (type === 'missing-token') {
const linkPara = doc.createElement('p');
linkPara.textContent = 'You can request a free token from ';
const link = doc.createElement('a');
link.textContent = 'api.esv.org';
link.href = 'https://api.esv.org/docs/';
link.target = '_blank';
linkPara.appendChild(link);
linkPara.appendChild(doc.createTextNode('.'));
container.appendChild(linkPara);
}
return container;
}
/** // Process each book directory
* Download Bible content from the ESV API for (const bookDir of bookDirs.folders) {
*/ // Get all chapter files
public async downloadFromESVApi(reference: string): Promise<BiblePassage | null> { const files = await this.plugin.app.vault.adapter.list(bookDir);
if (!this.plugin.settings.esvApiToken) {
console.warn('ESV API token not set. Cannot download content.');
return {
reference: reference,
verses: [],
htmlContent: this.createErrorMessageContent(
document,
'missing-token',
'To display Bible passages, you need to set up an ESV API token in the plugin settings.'
).outerHTML,
missingToken: true
};
}
try {
// Encode the reference for the URL
const encodedRef = encodeURIComponent(reference);
// Build the API URL with parameters
const apiUrl = `https://api.esv.org/v3/passage/html/?q=${encodedRef}&include-passage-references=false&include-verse-numbers=true&include-first-verse-numbers=true&include-footnotes=true&include-headings=true`;
// Make the request
const response = await requestUrl({
url: apiUrl,
method: 'GET',
headers: {
'Authorization': `Token ${this.plugin.settings.esvApiToken}`
}
});
// Check if the request was successful
if (response.status === 200) {
const data = response.json as ESVApiResponse;
// Save the response to a file
await this.saveESVApiResponse(reference, data);
// Return the content
return {
reference: data.canonical,
verses: [], // Empty since we're using HTML
htmlContent: data.passages[0]
};
} else {
console.error(`ESV API request failed with status ${response.status}: ${response.text}`);
return {
reference: reference,
verses: [],
htmlContent: this.createErrorMessageContent(
document,
'api-error',
'Failed to load the passage from the ESV API.',
`Status: ${response.status}. Please check your API token in the plugin settings.`
).outerHTML
};
}
} catch (error) {
console.error('Error downloading from ESV API:', error);
return {
reference: reference,
verses: [],
htmlContent: this.createErrorMessageContent(
document,
'api-error',
'An error occurred when trying to access the ESV API.',
'Please check your internet connection and API token.'
).outerHTML
};
}
}
/** // Process each chapter file
* Save ESV API response to a file for (const file of files.files) {
*/ if (file.endsWith('.json')) {
private async saveESVApiResponse(reference: string, data: ESVApiResponse): Promise<void> { try {
try { // Read and parse the file
// Extract book and chapter from the canonical reference const content = await this.plugin.app.vault.adapter.read(file);
const parts = data.canonical.split(' '); const data = JSON.parse(content);
if (parts.length < 2) return;
// Process the data if it's in the expected format
if (this.isESVApiFormat(data)) {
// Store the chapter content
const canonical = data.canonical;
const htmlContent = data.passages[0];
this.htmlFormattedBible[canonical] = {
canonical,
htmlContent
};
}
} catch (error) {
console.error(`Error processing file ${file}:`, error);
}
}
}
}
console.log(`Loaded ${Object.keys(this.htmlFormattedBible).length} Bible chapters from vault`);
} catch (error) {
console.error('Error loading Bible chapters from vault:', error);
}
}
/**
* Check if data is in ESV API format
*/
public isESVApiFormat(data: any): boolean {
return data &&
typeof data === 'object' &&
data.canonical !== undefined &&
data.passages !== undefined &&
Array.isArray(data.passages) &&
data.passages.length > 0;
}
/**
* Create a DOM-friendly error message for missing token or API errors
*/
private createErrorMessageContent(doc: Document, type: 'missing-token' | 'api-error', message: string = '', details: string = ''): HTMLElement {
// We're required to return a string here because it's part of the ESV API response structure
// This method creates the content in a DOM-safe way then serializes to a string
const container = doc.createElement('div');
container.className = type === 'missing-token' ? 'bible-missing-token-warning' : 'bible-api-error';
const titleEl = doc.createElement('p');
const titleStrong = doc.createElement('strong');
titleStrong.textContent = type === 'missing-token' ? 'ESV API Token Not Set' : 'Error Loading Bible Passage';
titleEl.appendChild(titleStrong);
container.appendChild(titleEl);
if (message) {
const messageEl = doc.createElement('p');
messageEl.textContent = message;
container.appendChild(messageEl);
}
if (details) {
const detailsEl = doc.createElement('p');
detailsEl.textContent = details;
container.appendChild(detailsEl);
}
// For the API token link
if (type === 'missing-token') {
const linkPara = doc.createElement('p');
linkPara.textContent = 'You can request a free token from ';
const link = doc.createElement('a');
link.textContent = 'api.esv.org';
link.href = 'https://api.esv.org/docs/';
link.target = '_blank';
linkPara.appendChild(link);
linkPara.appendChild(doc.createTextNode('.'));
container.appendChild(linkPara);
}
return container;
}
/**
* Download Bible content from the ESV API
*/
public async downloadFromESVApi(reference: string): Promise<BiblePassage | null> {
if (!this.plugin.settings.esvApiToken) {
console.warn('ESV API token not set. Cannot download content.');
return {
reference: reference,
verses: [],
htmlContent: this.createErrorMessageContent(
document,
'missing-token',
'To display Bible passages, you need to set up an ESV API token in the plugin settings.'
).outerHTML,
missingToken: true
};
}
try {
// Encode the reference for the URL
const encodedRef = encodeURIComponent(reference);
// Build the API URL with parameters
const apiUrl = `https://api.esv.org/v3/passage/html/?q=${encodedRef}&include-passage-references=false&include-verse-numbers=true&include-first-verse-numbers=true&include-footnotes=true&include-headings=true`;
// Make the request
const response = await requestUrl({
url: apiUrl,
method: 'GET',
headers: {
'Authorization': `Token ${this.plugin.settings.esvApiToken}`
}
});
// Check if the request was successful
if (response.status === 200) {
const data = response.json as ESVApiResponse;
// Save the response to a file
await this.saveESVApiResponse(reference, data);
// Return the content
return {
reference: data.canonical,
verses: [], // Empty since we're using HTML
htmlContent: data.passages[0]
};
} else {
console.error(`ESV API request failed with status ${response.status}: ${response.text}`);
return {
reference: reference,
verses: [],
htmlContent: this.createErrorMessageContent(
document,
'api-error',
'Failed to load the passage from the ESV API.',
`Status: ${response.status}. Please check your API token in the plugin settings.`
).outerHTML
};
}
} catch (error) {
console.error('Error downloading from ESV API:', error);
return {
reference: reference,
verses: [],
htmlContent: this.createErrorMessageContent(
document,
'api-error',
'An error occurred when trying to access the ESV API.',
'Please check your internet connection and API token.'
).outerHTML
};
}
}
/**
* Save ESV API response to a file
*/
private async saveESVApiResponse(reference: string, data: ESVApiResponse): Promise<void> {
try {
// Extract book and chapter from the canonical reference
const parts = data.canonical.split(' ');
if (parts.length < 2) return;
const book = parts.slice(0, -1).join(' '); const book = parts.slice(0, -1).join(' ');
// Create the directory structure with version subdirectory
const fullPath = this.getFullContentPath();
const bookPath = `${fullPath}/${book}`;
await this.ensureVaultDirectoryExists(fullPath);
await this.ensureVaultDirectoryExists(bookPath);
// Save the raw API response as JSON
const jsonPath = `${bookPath}/${data.canonical}.json`;
await this.plugin.app.vault.adapter.write(jsonPath, JSON.stringify(data));
} catch (error) {
console.error('Error saving ESV API response:', error);
}
}
/** // Create the directory structure with version subdirectory
* Ensure vault directory exists, creating it if necessary const fullPath = this.getFullContentPath();
*/ const bookPath = `${fullPath}/${book}`;
private async ensureVaultDirectoryExists(path: string): Promise<void> { await this.ensureVaultDirectoryExists(fullPath);
const parts = path.split('/').filter(p => p.length > 0); await this.ensureVaultDirectoryExists(bookPath);
let currentPath = '';
for (const part of parts) {
currentPath += (currentPath ? '/' : '') + part;
if (!(await this.plugin.app.vault.adapter.exists(currentPath))) {
await this.plugin.app.vault.adapter.mkdir(currentPath);
}
}
}
/** // Save the raw API response as JSON
* Ensures Bible data is loaded, downloading if necessary const jsonPath = `${bookPath}/${data.canonical}.json`;
*/ await this.plugin.app.vault.adapter.write(jsonPath, JSON.stringify(data));
public async ensureBibleData(): Promise<void> { } catch (error) {
try { console.error('Error saving ESV API response:', error);
// Check if we have data by looking for specific files in the vault }
const hasData = await this.checkBibleDataExists(); }
if (!hasData) { /**
console.log('Bible data not found, loading from vault...'); * Ensure vault directory exists, creating it if necessary
await this.loadBibleChaptersFromVault(); */
} else { private async ensureVaultDirectoryExists(path: string): Promise<void> {
console.log('Bible data already exists'); const parts = path.split('/').filter(p => p.length > 0);
} let currentPath = '';
} catch (error) {
console.error('Error ensuring Bible data:', error); for (const part of parts) {
throw error; currentPath += (currentPath ? '/' : '') + part;
} if (!(await this.plugin.app.vault.adapter.exists(currentPath))) {
} await this.plugin.app.vault.adapter.mkdir(currentPath);
}
/** }
* Check if Bible data exists in the vault }
*/
private async checkBibleDataExists(): Promise<boolean> { /**
try { * Ensures Bible data is loaded, downloading if necessary
// Check for a common book like Genesis */
const genesisPath = `${this.getFullContentPath()}/Genesis/Genesis 1.md`; public async ensureBibleData(): Promise<void> {
return await this.plugin.app.vault.adapter.exists(genesisPath); try {
} catch (error) { // Check if we have data by looking for specific files in the vault
console.error('Error checking if Bible data exists:', error); const hasData = await this.checkBibleDataExists();
return false;
} if (!hasData) {
} console.log('Bible data not found, loading from vault...');
await this.loadBibleChaptersFromVault();
} else {
console.log('Bible data already exists');
}
} catch (error) {
console.error('Error ensuring Bible data:', error);
throw error;
}
}
/**
* Check if Bible data exists in the vault
*/
private async checkBibleDataExists(): Promise<boolean> {
try {
// Check for a common book like Genesis
const genesisPath = `${this.getFullContentPath()}/Genesis/Genesis 1.md`;
return await this.plugin.app.vault.adapter.exists(genesisPath);
} catch (error) {
console.error('Error checking if Bible data exists:', error);
return false;
}
}
} }

View file

@ -1,272 +1,272 @@
import { App, Notice, PluginSettingTab, Setting } from 'obsidian'; import {App, Notice, PluginSettingTab, Setting} from 'obsidian';
import DisciplesJournalPlugin from '../core/DisciplesJournalPlugin'; import DisciplesJournalPlugin from '../core/DisciplesJournalPlugin';
import { THEME_PRESETS } from '../components/BibleStyles'; import {THEME_PRESETS} from '../components/BibleStyles';
export interface DisciplesJournalSettings { export interface DisciplesJournalSettings {
displayInlineVerses: boolean; displayInlineVerses: boolean;
displayFullPassages: boolean; displayFullPassages: boolean;
bibleTextFontSize: string; bibleTextFontSize: string;
wordsOfChristColor: string; wordsOfChristColor: string;
verseNumberColor: string; verseNumberColor: string;
headingColor: string; headingColor: string;
blockIndentation: string; blockIndentation: string;
preferredBibleVersion: string; preferredBibleVersion: string;
esvApiToken: string; esvApiToken: string;
downloadOnDemand: boolean; downloadOnDemand: boolean;
bibleContentVaultPath: string; bibleContentVaultPath: string;
stylePreset: string; stylePreset: string;
showNavigationForVerses: boolean; showNavigationForVerses: boolean;
} }
export const DEFAULT_SETTINGS: DisciplesJournalSettings = { export const DEFAULT_SETTINGS: DisciplesJournalSettings = {
displayInlineVerses: true, displayInlineVerses: true,
displayFullPassages: true, displayFullPassages: true,
bibleTextFontSize: '100%', bibleTextFontSize: '100%',
wordsOfChristColor: 'var(--text-accent)', wordsOfChristColor: 'var(--text-accent)',
verseNumberColor: 'var(--text-accent)', verseNumberColor: 'var(--text-accent)',
headingColor: 'var(--text-accent)', headingColor: 'var(--text-accent)',
blockIndentation: '2em', blockIndentation: '2em',
preferredBibleVersion: 'ESV', preferredBibleVersion: 'ESV',
esvApiToken: '', esvApiToken: '',
downloadOnDemand: true, downloadOnDemand: true,
bibleContentVaultPath: 'Bible', bibleContentVaultPath: 'Bible',
stylePreset: 'default', stylePreset: 'default',
showNavigationForVerses: false showNavigationForVerses: false
}; };
export class DisciplesJournalSettingsTab extends PluginSettingTab { export class DisciplesJournalSettingsTab extends PluginSettingTab {
plugin: DisciplesJournalPlugin; plugin: DisciplesJournalPlugin;
constructor(app: App, plugin: DisciplesJournalPlugin) { constructor(app: App, plugin: DisciplesJournalPlugin) {
super(app, plugin); super(app, plugin);
this.plugin = plugin; this.plugin = plugin;
} }
display(): void { display(): void {
const { containerEl } = this; const {containerEl} = this;
containerEl.empty(); containerEl.empty();
new Setting(containerEl).setName('Display Customization').setHeading(); new Setting(containerEl).setName('Display Customization').setHeading();
new Setting(containerEl) new Setting(containerEl)
.setName('Display Inline Verses') .setName('Display Inline Verses')
.setDesc('Enable rendering of inline Bible references (in `code blocks`).') .setDesc('Enable rendering of inline Bible references (in `code blocks`).')
.addToggle(toggle => toggle .addToggle(toggle => toggle
.setValue(this.plugin.settings.displayInlineVerses) .setValue(this.plugin.settings.displayInlineVerses)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.displayInlineVerses = value; this.plugin.settings.displayInlineVerses = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
new Setting(containerEl) new Setting(containerEl)
.setName('Display Full Passages') .setName('Display Full Passages')
.setDesc('Enable rendering of full Bible passages (in ```bible code blocks).') .setDesc('Enable rendering of full Bible passages (in ```bible code blocks).')
.addToggle(toggle => toggle .addToggle(toggle => toggle
.setValue(this.plugin.settings.displayFullPassages) .setValue(this.plugin.settings.displayFullPassages)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.displayFullPassages = value; this.plugin.settings.displayFullPassages = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
new Setting(containerEl) new Setting(containerEl)
.setName('Show Navigation for Verses') .setName('Show Navigation for Verses')
.setDesc('Show chapter navigation when displaying specific verses (by default, navigation only shows for full chapters).') .setDesc('Show chapter navigation when displaying specific verses (by default, navigation only shows for full chapters).')
.addToggle(toggle => toggle .addToggle(toggle => toggle
.setValue(this.plugin.settings.showNavigationForVerses) .setValue(this.plugin.settings.showNavigationForVerses)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.showNavigationForVerses = value; this.plugin.settings.showNavigationForVerses = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
new Setting(containerEl)
.setName('Style Preset')
.setDesc('Choose a predefined style preset for Bible content.')
.addDropdown(dropdown => {
// Add all theme presets
Object.keys(THEME_PRESETS).forEach(preset => {
dropdown.addOption(preset, preset.charAt(0).toUpperCase() + preset.slice(1));
});
dropdown.setValue(this.plugin.settings.stylePreset);
dropdown.onChange(async (value) => {
this.plugin.settings.stylePreset = value;
await this.plugin.saveSettings();
this.plugin.updateBibleStyles();
});
});
new Setting(containerEl) new Setting(containerEl)
.setName('Bible Text Font Size') .setName('Style Preset')
.setDesc('Set the font size for Bible verses and passages.') .setDesc('Choose a predefined style preset for Bible content.')
.addDropdown(dropdown => dropdown .addDropdown(dropdown => {
.addOption('80%', 'Smaller (80%)') // Add all theme presets
.addOption('90%', 'Small (90%)') Object.keys(THEME_PRESETS).forEach(preset => {
.addOption('100%', 'Normal (100%)') dropdown.addOption(preset, preset.charAt(0).toUpperCase() + preset.slice(1));
.addOption('110%', 'Large (110%)') });
.addOption('120%', 'Larger (120%)')
.setValue(this.plugin.settings.bibleTextFontSize)
.onChange(async (value) => {
this.plugin.settings.bibleTextFontSize = value;
await this.plugin.saveSettings();
this.plugin.updateBibleStyles();
})
);
new Setting(containerEl).setName('Text Styling').setHeading();
new Setting(containerEl) dropdown.setValue(this.plugin.settings.stylePreset);
.setName('Words of Christ Color') dropdown.onChange(async (value) => {
.setDesc('Set the color for Words of Christ (use `var(--text-normal)` for no special color).') this.plugin.settings.stylePreset = value;
.addText(text => text await this.plugin.saveSettings();
.setPlaceholder('var(--text-accent)') this.plugin.updateBibleStyles();
.setValue(this.plugin.settings.wordsOfChristColor) });
.onChange(async (value) => { });
this.plugin.settings.wordsOfChristColor = value || 'var(--text-accent)';
await this.plugin.saveSettings(); new Setting(containerEl)
this.plugin.updateBibleStyles(); .setName('Bible Text Font Size')
})); .setDesc('Set the font size for Bible verses and passages.')
.addDropdown(dropdown => dropdown
new Setting(containerEl) .addOption('80%', 'Smaller (80%)')
.setName('Verse Number Color') .addOption('90%', 'Small (90%)')
.setDesc('Set the color for verse numbers.') .addOption('100%', 'Normal (100%)')
.addText(text => text .addOption('110%', 'Large (110%)')
.setPlaceholder('var(--text-accent)') .addOption('120%', 'Larger (120%)')
.setValue(this.plugin.settings.verseNumberColor) .setValue(this.plugin.settings.bibleTextFontSize)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.verseNumberColor = value || 'var(--text-accent)'; this.plugin.settings.bibleTextFontSize = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
this.plugin.updateBibleStyles(); this.plugin.updateBibleStyles();
})); })
);
new Setting(containerEl)
.setName('Heading Color') new Setting(containerEl).setName('Text Styling').setHeading();
.setDesc('Set the color for Bible passage headings.')
.addText(text => text new Setting(containerEl)
.setPlaceholder('var(--text-accent)') .setName('Words of Christ Color')
.setValue(this.plugin.settings.headingColor) .setDesc('Set the color for Words of Christ (use `var(--text-normal)` for no special color).')
.onChange(async (value) => { .addText(text => text
this.plugin.settings.headingColor = value || 'var(--text-accent)'; .setPlaceholder('var(--text-accent)')
await this.plugin.saveSettings(); .setValue(this.plugin.settings.wordsOfChristColor)
this.plugin.updateBibleStyles(); .onChange(async (value) => {
})); this.plugin.settings.wordsOfChristColor = value || 'var(--text-accent)';
await this.plugin.saveSettings();
new Setting(containerEl) this.plugin.updateBibleStyles();
.setName('Block Indentation') }));
.setDesc('Set the indentation for block sections.')
.addText(text => text new Setting(containerEl)
.setPlaceholder('2em') .setName('Verse Number Color')
.setValue(this.plugin.settings.blockIndentation) .setDesc('Set the color for verse numbers.')
.onChange(async (value) => { .addText(text => text
this.plugin.settings.blockIndentation = value || '2em'; .setPlaceholder('var(--text-accent)')
await this.plugin.saveSettings(); .setValue(this.plugin.settings.verseNumberColor)
this.plugin.updateBibleStyles(); .onChange(async (value) => {
})); this.plugin.settings.verseNumberColor = value || 'var(--text-accent)';
await this.plugin.saveSettings();
new Setting(containerEl).setName('Bible').setHeading(); this.plugin.updateBibleStyles();
}));
new Setting(containerEl)
.setName('Preferred Bible Version') new Setting(containerEl)
.setDesc('Select your preferred Bible version (only ESV currently supported).') .setName('Heading Color')
.addDropdown(dropdown => dropdown .setDesc('Set the color for Bible passage headings.')
.addOption('ESV', 'English Standard Version (ESV)') .addText(text => text
.setValue(this.plugin.settings.preferredBibleVersion) .setPlaceholder('var(--text-accent)')
.onChange(async (value) => { .setValue(this.plugin.settings.headingColor)
this.plugin.settings.preferredBibleVersion = value; .onChange(async (value) => {
await this.plugin.saveSettings(); this.plugin.settings.headingColor = value || 'var(--text-accent)';
}) await this.plugin.saveSettings();
); this.plugin.updateBibleStyles();
}));
new Setting(containerEl)
.setName('Bible Content Vault Path') new Setting(containerEl)
.setDesc('Root path in your vault where Bible content will be stored. Each translation will be stored in a subdirectory.') .setName('Block Indentation')
.addText(text => text .setDesc('Set the indentation for block sections.')
.setPlaceholder('Bible') .addText(text => text
.setValue(this.plugin.settings.bibleContentVaultPath) .setPlaceholder('2em')
.onChange(async (value) => { .setValue(this.plugin.settings.blockIndentation)
this.plugin.settings.bibleContentVaultPath = value || 'Bible'; .onChange(async (value) => {
await this.plugin.saveSettings(); this.plugin.settings.blockIndentation = value || '2em';
})); await this.plugin.saveSettings();
this.plugin.updateBibleStyles();
new Setting(containerEl).setName('ESV API').setHeading(); }));
const apiInfoDiv = containerEl.createDiv({ cls: 'disciples-journal-api-info' }); new Setting(containerEl).setName('Bible').setHeading();
apiInfoDiv.createEl('p', { new Setting(containerEl)
text: 'The ESV API allows this plugin to download and display Bible passages from the ESV translation.' .setName('Preferred Bible Version')
}); .setDesc('Select your preferred Bible version (only ESV currently supported).')
.addDropdown(dropdown => dropdown
apiInfoDiv.createEl('p', { .addOption('ESV', 'English Standard Version (ESV)')
text: 'To get a free ESV API token:', .setValue(this.plugin.settings.preferredBibleVersion)
attr: { style: 'font-weight: bold;' } .onChange(async (value) => {
}); this.plugin.settings.preferredBibleVersion = value;
await this.plugin.saveSettings();
const instructionsList = apiInfoDiv.createEl('ol'); })
);
const steps = [
'Visit api.esv.org', new Setting(containerEl)
'Sign up for a free account', .setName('Bible Content Vault Path')
'After logging in, go to "API Keys" in your account', .setDesc('Root path in your vault where Bible content will be stored. Each translation will be stored in a subdirectory.')
'Create a new token and copy it here' .addText(text => text
]; .setPlaceholder('Bible')
.setValue(this.plugin.settings.bibleContentVaultPath)
steps.forEach(step => { .onChange(async (value) => {
if (step === 'Visit api.esv.org') { this.plugin.settings.bibleContentVaultPath = value || 'Bible';
const listItem = instructionsList.createEl('li'); await this.plugin.saveSettings();
listItem.createEl('span', { text: 'Visit ' }); }));
listItem.createEl('a', {
text: 'api.esv.org', new Setting(containerEl).setName('ESV API').setHeading();
href: 'https://api.esv.org/docs/',
attr: { target: '_blank' } const apiInfoDiv = containerEl.createDiv({cls: 'disciples-journal-api-info'});
});
} else { apiInfoDiv.createEl('p', {
instructionsList.createEl('li', { text: step }); text: 'The ESV API allows this plugin to download and display Bible passages from the ESV translation.'
} });
});
apiInfoDiv.createEl('p', {
apiInfoDiv.createEl('p', { text: 'To get a free ESV API token:',
text: 'With a valid token, the plugin can download and display Bible passages directly in your notes.' attr: {style: 'font-weight: bold;'}
}); });
new Setting(containerEl) const instructionsList = apiInfoDiv.createEl('ol');
.setName('ESV API Token')
.setDesc('Enter your ESV API token from api.esv.org.') const steps = [
.addText(text => text 'Visit api.esv.org',
.setPlaceholder('Enter your ESV API token') 'Sign up for a free account',
.setValue(this.plugin.settings.esvApiToken) 'After logging in, go to "API Keys" in your account',
.onChange(async (value) => { 'Create a new token and copy it here'
this.plugin.settings.esvApiToken = value; ];
await this.plugin.saveSettings();
if (value) { steps.forEach(step => {
new Notice('ESV API token updated', 2000); if (step === 'Visit api.esv.org') {
} const listItem = instructionsList.createEl('li');
})); listItem.createEl('span', {text: 'Visit '});
listItem.createEl('a', {
new Setting(containerEl) text: 'api.esv.org',
.setName('Download on Demand') href: 'https://api.esv.org/docs/',
.setDesc('Enable automatic downloading of Bible content when requested.') attr: {target: '_blank'}
.addToggle(toggle => toggle });
.setValue(this.plugin.settings.downloadOnDemand) } else {
.onChange(async (value) => { instructionsList.createEl('li', {text: step});
this.plugin.settings.downloadOnDemand = value; }
await this.plugin.saveSettings(); });
}));
apiInfoDiv.createEl('p', {
text: 'With a valid token, the plugin can download and display Bible passages directly in your notes.'
new Setting(containerEl) });
.setName('Bible Data Status')
.setDesc('Click to reload Bible data from source') new Setting(containerEl)
.addButton(button => button .setName('ESV API Token')
.setButtonText('Reload Bible Data') .setDesc('Enter your ESV API token from api.esv.org.')
.onClick(async () => { .addText(text => text
button.setButtonText('Loading...'); .setPlaceholder('Enter your ESV API token')
button.setDisabled(true); .setValue(this.plugin.settings.esvApiToken)
await this.plugin.loadBibleData(); .onChange(async (value) => {
button.setButtonText('Reload Bible Data'); this.plugin.settings.esvApiToken = value;
button.setDisabled(false); await this.plugin.saveSettings();
this.display(); // Refresh the settings panel if (value) {
})); new Notice('ESV API token updated', 2000);
} }
}));
new Setting(containerEl)
.setName('Download on Demand')
.setDesc('Enable automatic downloading of Bible content when requested.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.downloadOnDemand)
.onChange(async (value) => {
this.plugin.settings.downloadOnDemand = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Bible Data Status')
.setDesc('Click to reload Bible data from source')
.addButton(button => button
.setButtonText('Reload Bible Data')
.onClick(async () => {
button.setButtonText('Loading...');
button.setDisabled(true);
await this.plugin.loadBibleData();
button.setButtonText('Reload Bible Data');
button.setDisabled(false);
this.display(); // Refresh the settings panel
}));
}
} }

20
src/types.d.ts vendored
View file

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

View file

@ -1,29 +1,29 @@
import { BiblePassage } from "../services/BibleContentService"; import {BiblePassage} from "../services/BibleContentService";
/** /**
* Utility for formatting Bible content consistently across the application * Utility for formatting Bible content consistently across the application
*/ */
export class BibleFormatter { export class BibleFormatter {
/** /**
* Format chapter content as Markdown * Format chapter content as Markdown
*/ */
public static formatChapterContent(passage: BiblePassage): string { public static formatChapterContent(passage: BiblePassage): string {
if (!passage) return "# Error: No passage content\n\nThe requested passage could not be loaded."; if (!passage) return "# Error: No passage content\n\nThe requested passage could not be loaded.";
let content = ""; let content = "";
// Add code block for rendering // Add code block for rendering
content += "```bible\n"; content += "```bible\n";
content += passage.reference; content += passage.reference;
content += "\n```\n\n"; content += "\n```\n\n";
// Alternatively, add each verse separately // Alternatively, add each verse separately
if (passage.verses && passage.verses.length > 0) { if (passage.verses && passage.verses.length > 0) {
for (const verse of passage.verses) { for (const verse of passage.verses) {
content += `**${verse.verse}** ${verse.text}\n\n`; content += `**${verse.verse}** ${verse.text}\n\n`;
} }
} }
return content; return content;
} }
} }