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';
export default DisciplesJournalPlugin;
export default DisciplesJournalPlugin;

View file

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

View file

@ -2,73 +2,73 @@
* Interface for Bible styling options
*/
export interface BibleStyleOptions {
fontSize: string;
wordsOfChristColor: string;
verseNumberColor: string;
headingColor: string;
blockIndentation: string;
fontSize: string;
wordsOfChristColor: string;
verseNumberColor: string;
headingColor: string;
blockIndentation: string;
}
/**
* Theme preset definitions
*/
export interface ThemePreset {
light: BibleStyleOptions;
dark: BibleStyleOptions;
light: BibleStyleOptions;
dark: BibleStyleOptions;
}
/**
* Available theme presets
*/
export const THEME_PRESETS: Record<string, ThemePreset> = {
default: {
light: {
fontSize: '100%',
wordsOfChristColor: 'var(--text-accent)',
verseNumberColor: 'var(--text-accent)',
headingColor: 'var(--text-accent)',
blockIndentation: '2em'
},
dark: {
fontSize: '100%',
wordsOfChristColor: 'var(--text-accent)',
verseNumberColor: 'var(--text-accent)',
headingColor: 'var(--text-accent)',
blockIndentation: '2em'
}
},
classic: {
light: {
fontSize: '100%',
wordsOfChristColor: '#b91c1c',
verseNumberColor: '#2563eb',
headingColor: '#4b5563',
blockIndentation: '2em'
},
dark: {
fontSize: '100%',
wordsOfChristColor: '#ef4444',
verseNumberColor: '#60a5fa',
headingColor: '#9ca3af',
blockIndentation: '2em'
}
},
minimal: {
light: {
fontSize: '100%',
wordsOfChristColor: '#000000',
verseNumberColor: '#6b7280',
headingColor: '#374151',
blockIndentation: '1.5em'
},
dark: {
fontSize: '100%',
wordsOfChristColor: '#ffffff',
verseNumberColor: '#9ca3af',
headingColor: '#e5e7eb',
blockIndentation: '1.5em'
}
}
default: {
light: {
fontSize: '100%',
wordsOfChristColor: 'var(--text-accent)',
verseNumberColor: 'var(--text-accent)',
headingColor: 'var(--text-accent)',
blockIndentation: '2em'
},
dark: {
fontSize: '100%',
wordsOfChristColor: 'var(--text-accent)',
verseNumberColor: 'var(--text-accent)',
headingColor: 'var(--text-accent)',
blockIndentation: '2em'
}
},
classic: {
light: {
fontSize: '100%',
wordsOfChristColor: '#b91c1c',
verseNumberColor: '#2563eb',
headingColor: '#4b5563',
blockIndentation: '2em'
},
dark: {
fontSize: '100%',
wordsOfChristColor: '#ef4444',
verseNumberColor: '#60a5fa',
headingColor: '#9ca3af',
blockIndentation: '2em'
}
},
minimal: {
light: {
fontSize: '100%',
wordsOfChristColor: '#000000',
verseNumberColor: '#6b7280',
headingColor: '#374151',
blockIndentation: '1.5em'
},
dark: {
fontSize: '100%',
wordsOfChristColor: '#ffffff',
verseNumberColor: '#9ca3af',
headingColor: '#e5e7eb',
blockIndentation: '1.5em'
}
}
};
/**
@ -77,60 +77,60 @@ export const THEME_PRESETS: Record<string, ThemePreset> = {
*/
export class BibleStyles {
constructor() {
// 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];
constructor() {
// No initial style element creation needed
}
// Start with base options and apply the specific font size
const options: BibleStyleOptions = {
...baseOptions,
fontSize: fontSize || '100%' // Ensure fontSize always has a value
};
// Apply custom settings from DisciplesJournalSettings if provided
// 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;
}
/**
* 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 {
// 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);
}
// Get the preset, defaulting to 'default' if not found
const preset = THEME_PRESETS[presetName] || THEME_PRESETS.default;
/**
* 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');
}
}
// 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 = {
...baseOptions,
fontSize: fontSize || '100%' // Ensure fontSize always has a value
};
// Apply custom settings from DisciplesJournalSettings if provided
// 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
*/
export class BibleEventHandlers {
private bibleReferenceRenderer: BibleReferenceRenderer;
private previewPopper: HTMLElement | null = null;
constructor(bibleReferenceRenderer: BibleReferenceRenderer) {
this.bibleReferenceRenderer = bibleReferenceRenderer;
}
private bibleReferenceRenderer: BibleReferenceRenderer;
private previewPopper: HTMLElement | null = null;
constructor(bibleReferenceRenderer: BibleReferenceRenderer) {
this.bibleReferenceRenderer = bibleReferenceRenderer;
}
/**
* Handle hover on Bible references
*/
async handleBibleReferenceHover(event: MouseEvent) {
// Don't create new preview if we already have one active
if (this.previewPopper) return;
const target = event.target as HTMLElement;
if (!target || !target.closest) return;
const referenceEl = target.closest('.bible-reference') as HTMLElement;
if (!referenceEl) return;
const referenceText = referenceEl.textContent;
if (!referenceText) return;
try {
// Create new preview
this.previewPopper = await this.bibleReferenceRenderer.showVersePreview(
referenceEl,
referenceText,
event
);
// Add event listeners directly to the preview for better control
if (this.previewPopper) {
// When mouse enters the popup, mark it as locked
this.previewPopper.addEventListener('mouseenter', () => {
this.previewPopper?.classList.add('popup-locked');
});
// When mouse leaves the popup, check if we should close it
this.previewPopper.addEventListener('mouseleave', (e) => {
// Only close if not moving to the reference or another part of the popup
const relatedTarget = e.relatedTarget as HTMLElement;
if (relatedTarget &&
!relatedTarget.classList.contains('bible-reference') &&
!relatedTarget.closest('.bible-verse-preview')) {
this.previewPopper?.classList.remove('popup-locked');
this.removePreviewPopper(relatedTarget.doc);
}
});
}
// Also add listeners to the reference element
referenceEl.addEventListener('mouseleave', (e) => {
// Don't close if the popup is locked (being hovered) or we're moving to the popup
if (this.previewPopper) {
const relatedTarget = e.relatedTarget as HTMLElement;
// If moving to the popup or if popup is locked, don't close
if (relatedTarget &&
(relatedTarget.classList.contains('bible-verse-preview') ||
relatedTarget.closest('.bible-verse-preview') ||
this.previewPopper.classList.contains('popup-locked'))) {
return;
}
// Add a 100ms delay before closing to allow for cursor movement
setTimeout(() => {
// If locked during this delay, don't close
if (!this.previewPopper || this.previewPopper.classList.contains('popup-locked')) {
return;
}
this.removePreviewPopper(relatedTarget.doc);
}, 100);
}
});
} catch (error) {
console.error('Error showing Bible reference preview:', error);
}
}
/**
* Handle mouse out from Bible references
*/
handleBibleReferenceMouseOut(event: MouseEvent) {
// If we don't have a popup, nothing to do
if (!this.previewPopper) return;
// If the popup is locked (being hovered), don't close it
if (this.previewPopper.classList.contains('popup-locked')) {
return;
}
const target = event.target as HTMLElement;
const relatedTarget = event.relatedTarget as HTMLElement;
// If either target is missing, can't make a good decision
if (!target || !relatedTarget) return;
// If moving to/from the popup or reference, don't close
if (target.classList.contains('bible-reference') ||
target.classList.contains('bible-verse-preview') ||
target.closest('.bible-verse-preview') ||
relatedTarget.classList.contains('bible-reference') ||
relatedTarget.classList.contains('bible-verse-preview') ||
relatedTarget.closest('.bible-verse-preview')) {
return;
}
// In all other cases, remove the popup
this.removePreviewPopper(target.doc);
}
/**
* Remove the preview popper if it exists
*/
removePreviewPopper(doc: Document) {
if (this.previewPopper) {
// Remove any hover gap elements
const hoverGaps = doc.querySelectorAll('.bible-hover-gap');
hoverGaps.forEach(gap => gap.remove());
this.previewPopper.remove();
this.previewPopper = null;
}
}
* Handle hover on Bible references
*/
async handleBibleReferenceHover(event: MouseEvent) {
// Don't create new preview if we already have one active
if (this.previewPopper) return;
const target = event.target as HTMLElement;
if (!target || !target.closest) return;
const referenceEl = target.closest('.bible-reference') as HTMLElement;
if (!referenceEl) return;
const referenceText = referenceEl.textContent;
if (!referenceText) return;
try {
// Create new preview
this.previewPopper = await this.bibleReferenceRenderer.showVersePreview(
referenceEl,
referenceText,
event
);
// Add event listeners directly to the preview for better control
if (this.previewPopper) {
// When mouse enters the popup, mark it as locked
this.previewPopper.addEventListener('mouseenter', () => {
this.previewPopper?.classList.add('popup-locked');
});
// When mouse leaves the popup, check if we should close it
this.previewPopper.addEventListener('mouseleave', (e) => {
// Only close if not moving to the reference or another part of the popup
const relatedTarget = e.relatedTarget as HTMLElement;
if (relatedTarget &&
!relatedTarget.classList.contains('bible-reference') &&
!relatedTarget.closest('.bible-verse-preview')) {
this.previewPopper?.classList.remove('popup-locked');
this.removePreviewPopper(relatedTarget.doc);
}
});
}
// Also add listeners to the reference element
referenceEl.addEventListener('mouseleave', (e) => {
// Don't close if the popup is locked (being hovered) or we're moving to the popup
if (this.previewPopper) {
const relatedTarget = e.relatedTarget as HTMLElement;
// If moving to the popup or if popup is locked, don't close
if (relatedTarget &&
(relatedTarget.classList.contains('bible-verse-preview') ||
relatedTarget.closest('.bible-verse-preview') ||
this.previewPopper.classList.contains('popup-locked'))) {
return;
}
// Add a 100ms delay before closing to allow for cursor movement
setTimeout(() => {
// If locked during this delay, don't close
if (!this.previewPopper || this.previewPopper.classList.contains('popup-locked')) {
return;
}
this.removePreviewPopper(relatedTarget.doc);
}, 100);
}
});
} catch (error) {
console.error('Error showing Bible reference preview:', error);
}
}
/**
* Handle mouse out from Bible references
*/
handleBibleReferenceMouseOut(event: MouseEvent) {
// If we don't have a popup, nothing to do
if (!this.previewPopper) return;
// If the popup is locked (being hovered), don't close it
if (this.previewPopper.classList.contains('popup-locked')) {
return;
}
const target = event.target as HTMLElement;
const relatedTarget = event.relatedTarget as HTMLElement;
// If either target is missing, can't make a good decision
if (!target || !relatedTarget) return;
// If moving to/from the popup or reference, don't close
if (target.classList.contains('bible-reference') ||
target.classList.contains('bible-verse-preview') ||
target.closest('.bible-verse-preview') ||
relatedTarget.classList.contains('bible-reference') ||
relatedTarget.classList.contains('bible-verse-preview') ||
relatedTarget.closest('.bible-verse-preview')) {
return;
}
// In all other cases, remove the popup
this.removePreviewPopper(target.doc);
}
/**
* Remove the preview popper if it exists
*/
removePreviewPopper(doc: Document) {
if (this.previewPopper) {
// Remove any hover gap elements
const hoverGaps = doc.querySelectorAll('.bible-hover-gap');
hoverGaps.forEach(gap => gap.remove());
this.previewPopper.remove();
this.previewPopper = null;
}
}
}

View file

@ -1,55 +1,55 @@
import { MarkdownPostProcessorContext } from 'obsidian';
import { BibleReferenceRenderer } from '../components/BibleReferenceRenderer';
import { DisciplesJournalSettings } from '../settings/DisciplesJournalSettings';
import {MarkdownPostProcessorContext} from 'obsidian';
import {BibleReferenceRenderer} from '../components/BibleReferenceRenderer';
import {DisciplesJournalSettings} from '../settings/DisciplesJournalSettings';
/**
* Handles processing of Bible references in markdown text
*/
export class BibleMarkupProcessor {
private bibleReferenceRenderer: BibleReferenceRenderer;
private settings: DisciplesJournalSettings;
constructor(
bibleReferenceRenderer: BibleReferenceRenderer,
settings: DisciplesJournalSettings
) {
this.bibleReferenceRenderer = bibleReferenceRenderer;
this.settings = settings;
}
/**
* Process Bible code blocks
*/
async processBibleCodeBlock(source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) {
// Check if display of full passages is enabled in settings
if (!this.settings.displayFullPassages) {
return;
}
try {
await this.bibleReferenceRenderer.processFullBiblePassage(source, el);
} catch (error) {
console.error('Error processing Bible code block:', error);
el.createEl('div', {
text: `Error processing Bible reference: ${error.message}`,
cls: 'bible-reference-error'
});
}
}
/**
* Process inline Bible references in text
*/
async processInlineBibleReferences(el: HTMLElement, ctx: MarkdownPostProcessorContext): Promise<void> {
// Check if display of inline verses is enabled in settings
if (!this.settings.displayInlineVerses) {
return;
}
try {
await this.bibleReferenceRenderer.processInlineCodeBlocks(el, ctx);
} catch (error) {
console.error('Error processing inline Bible references:', error);
}
}
}
private bibleReferenceRenderer: BibleReferenceRenderer;
private settings: DisciplesJournalSettings;
constructor(
bibleReferenceRenderer: BibleReferenceRenderer,
settings: DisciplesJournalSettings
) {
this.bibleReferenceRenderer = bibleReferenceRenderer;
this.settings = settings;
}
/**
* Process Bible code blocks
*/
async processBibleCodeBlock(source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) {
// Check if display of full passages is enabled in settings
if (!this.settings.displayFullPassages) {
return;
}
try {
await this.bibleReferenceRenderer.processFullBiblePassage(source, el);
} catch (error) {
console.error('Error processing Bible code block:', error);
el.createEl('div', {
text: `Error processing Bible reference: ${error.message}`,
cls: 'bible-reference-error'
});
}
}
/**
* Process inline Bible references in text
*/
async processInlineBibleReferences(el: HTMLElement, ctx: MarkdownPostProcessorContext): Promise<void> {
// Check if display of inline verses is enabled in settings
if (!this.settings.displayInlineVerses) {
return;
}
try {
await this.bibleReferenceRenderer.processInlineCodeBlocks(el, ctx);
} catch (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
*/
export class BibleReference {
book: string;
chapter: number;
verse?: number;
endVerse?: number;
endChapter?: number;
/**
* Create a new Bible reference
*/
constructor(book: string, chapter: number, verse?: number, endVerse?: number, endChapter?: number) {
this.book = book;
this.chapter = chapter;
this.verse = verse;
this.endVerse = endVerse;
this.endChapter = endChapter;
}
book: string;
chapter: number;
verse?: number;
endVerse?: number;
endChapter?: number;
/**
* Create a new Bible reference
*/
constructor(book: string, chapter: number, verse?: number, endVerse?: number, endChapter?: number) {
this.book = book;
this.chapter = chapter;
this.verse = verse;
this.endVerse = endVerse;
this.endChapter = endChapter;
}
/**
* Parse a string into a BibleReference object
@ -112,95 +112,95 @@ export class BibleReference {
}
/**
* Get the formatted reference as a string
*/
public toString(): string {
let reference = `${this.book} ${this.chapter}`;
// Add verse if present
if (this.verse !== undefined) {
reference += `:${this.verse}`;
// Add end verse if present (for ranges within same chapter)
if (this.endVerse !== undefined && this.endChapter === undefined) {
reference += `-${this.endVerse}`;
}
}
// Handle cross-chapter reference
if (this.endChapter !== undefined) {
reference += `-${this.endChapter}`;
// Add end verse if present for a cross-chapter reference
if (this.endVerse !== undefined) {
reference += `:${this.endVerse}`;
}
}
return reference;
}
/**
* Create a deep copy of the reference
*/
public clone(): BibleReference {
return new BibleReference(
this.book,
this.chapter,
this.verse,
this.endVerse,
this.endChapter
);
}
/**
* Determine if this reference is a range (spans multiple verses)
*/
public isRange(): boolean {
return this.endVerse !== undefined || this.endChapter !== undefined;
}
/**
* Check if this is a chapter reference (no specific verse)
*/
public isChapterReference(): boolean {
return this.verse === undefined;
}
/**
* Compare this reference with another to check if they refer to the same passage
*/
public equals(other: BibleReference): boolean {
return this.book === other.book &&
this.chapter === other.chapter &&
this.verse === other.verse &&
this.endVerse === other.endVerse &&
this.endChapter === other.endChapter;
}
/**
* Returns a reference string suitable for display in UI
*/
public getDisplayReference(): string {
// For UI display, we might want to abbreviate the book name
// or format the reference in a specific way
return this.toString();
}
/**
* Returns a reference string suitable for use as a key in the Bible data
*/
public getDataKey(): string {
// For data keys, we often want to use a standardized format
// This assumes the book name is already standardized
return `${this.book} ${this.chapter}${this.verse ? `:${this.verse}` : ''}`;
}
/**
* Creates a reference for the entire chapter containing this reference
*/
public getChapterReference(): BibleReference {
// Create a new reference with the same book and chapter, but no verse info
return new BibleReference(this.book, this.chapter);
}
* Get the formatted reference as a string
*/
public toString(): string {
let reference = `${this.book} ${this.chapter}`;
// Add verse if present
if (this.verse !== undefined) {
reference += `:${this.verse}`;
// Add end verse if present (for ranges within same chapter)
if (this.endVerse !== undefined && this.endChapter === undefined) {
reference += `-${this.endVerse}`;
}
}
// Handle cross-chapter reference
if (this.endChapter !== undefined) {
reference += `-${this.endChapter}`;
// Add end verse if present for a cross-chapter reference
if (this.endVerse !== undefined) {
reference += `:${this.endVerse}`;
}
}
return reference;
}
/**
* Create a deep copy of the reference
*/
public clone(): BibleReference {
return new BibleReference(
this.book,
this.chapter,
this.verse,
this.endVerse,
this.endChapter
);
}
/**
* Determine if this reference is a range (spans multiple verses)
*/
public isRange(): boolean {
return this.endVerse !== undefined || this.endChapter !== undefined;
}
/**
* Check if this is a chapter reference (no specific verse)
*/
public isChapterReference(): boolean {
return this.verse === undefined;
}
/**
* Compare this reference with another to check if they refer to the same passage
*/
public equals(other: BibleReference): boolean {
return this.book === other.book &&
this.chapter === other.chapter &&
this.verse === other.verse &&
this.endVerse === other.endVerse &&
this.endChapter === other.endChapter;
}
/**
* Returns a reference string suitable for display in UI
*/
public getDisplayReference(): string {
// For UI display, we might want to abbreviate the book name
// or format the reference in a specific way
return this.toString();
}
/**
* Returns a reference string suitable for use as a key in the Bible data
*/
public getDataKey(): string {
// For data keys, we often want to use a standardized format
// This assumes the book name is already standardized
return `${this.book} ${this.chapter}${this.verse ? `:${this.verse}` : ''}`;
}
/**
* Creates a reference for the entire chapter containing this reference
*/
public getChapterReference(): BibleReference {
// Create a new reference with the same book and chapter, but no verse info
return new BibleReference(this.book, this.chapter);
}
}

View file

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

View file

@ -1,110 +1,110 @@
import { TFile } from 'obsidian';
import { BibleContentService } from './BibleContentService';
import { BibleReference } from '../core/BibleReference';
import { BibleFormatter } from '../utils/BibleFormatter';
import {TFile} from 'obsidian';
import {BibleContentService} from './BibleContentService';
import {BibleReference} from '../core/BibleReference';
import {BibleFormatter} from '../utils/BibleFormatter';
import DisciplesJournalPlugin from "../core/DisciplesJournalPlugin";
/**
* Service for creating and opening Bible chapter notes
*/
export class BibleBookFiles {
private plugin: DisciplesJournalPlugin;
private bibleContentService: BibleContentService;
private plugin: DisciplesJournalPlugin;
private bibleContentService: BibleContentService;
constructor(
constructor(
plugin: DisciplesJournalPlugin,
bibleContentService: BibleContentService,
) {
this.plugin = plugin;
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;
}
bibleContentService: BibleContentService,
) {
this.plugin = plugin;
this.bibleContentService = bibleContentService;
}
// Get full content path including version
const fullPath = this.getFullContentPath();
const chapterPath = `${fullPath}/${parsedRef.book}/${parsedRef.book} ${parsedRef.chapter}.md`;
/**
* 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;
}
// Check if note exists
const fileExists = await this.plugin.app.vault.adapter.exists(chapterPath);
// Get full content path including version
const fullPath = this.getFullContentPath();
const chapterPath = `${fullPath}/${parsedRef.book}/${parsedRef.book} ${parsedRef.chapter}.md`;
if (!fileExists && this.plugin.settings.downloadOnDemand) {
// Create the note with content from the API
await this.createChapterNote(parsedRef);
}
// Check if note exists
const fileExists = await this.plugin.app.vault.adapter.exists(chapterPath);
// Try opening the note
const file = this.plugin.app.vault.getAbstractFileByPath(chapterPath);
if (!fileExists && this.plugin.settings.downloadOnDemand) {
// Create the note with content from the API
await this.createChapterNote(parsedRef);
}
if (file && file instanceof TFile) {
const leaf = this.plugin.app.workspace.getLeaf(false);
await leaf.openFile(file);
// Try opening the note
const file = this.plugin.app.vault.getAbstractFileByPath(chapterPath);
// If there's a specific verse, scroll to it
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);
}
}
if (file && file instanceof TFile) {
const leaf = this.plugin.app.workspace.getLeaf(false);
await leaf.openFile(file);
/**
* Create a new chapter note
*/
// If there's a specific verse, scroll to it
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) {
try {
// Create a properly formatted reference string
const referenceStr = reference.toString();
try {
// Create a properly formatted reference string
const referenceStr = reference.toString();
// Get the content from the Bible API and format it for a note
const passage = await this.bibleContentService.getBibleContent(referenceStr);
// Get the content from the Bible API and format it for a note
const passage = await this.bibleContentService.getBibleContent(referenceStr);
// Check if passage is null
if (!passage) {
console.error(`Failed to get Bible content for ${referenceStr}`);
throw new Error(`Failed to get Bible content for ${referenceStr}`);
}
// Check if passage is null
if (!passage) {
console.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
const content = BibleFormatter.formatChapterContent(passage);
// Use the formatter utility to format the content
const content = BibleFormatter.formatChapterContent(passage);
// Save the content to a note with the version path
const fullPath = this.getFullContentPath();
const bookPath = `${fullPath}/${reference.book}`;
const chapterPath = `${fullPath}/${reference.book}/${reference.book} ${reference.chapter}.md`;
// Save the content to a note with the version path
const fullPath = this.getFullContentPath();
const bookPath = `${fullPath}/${reference.book}`;
const chapterPath = `${fullPath}/${reference.book}/${reference.book} ${reference.chapter}.md`;
// Ensure the directory exists
await this.plugin.app.vault.adapter.mkdir(bookPath);
// Ensure the directory exists
await this.plugin.app.vault.adapter.mkdir(bookPath);
// Create the note
await this.plugin.app.vault.create(chapterPath, content);
// Create the note
await this.plugin.app.vault.create(chapterPath, content);
return chapterPath;
} catch (error) {
console.error('Error creating chapter note:', error);
throw error;
}
}
return chapterPath;
} catch (error) {
console.error('Error creating chapter note:', error);
throw error;
}
}
/**
* Get the full path with version

View file

@ -1,26 +1,26 @@
import { BibleReference } from "../core/BibleReference";
import { BookNames } from "./BookNames";
import { ESVApiService } from "./ESVApiService";
import {BibleReference} from "../core/BibleReference";
import {BookNames} from "./BookNames";
import {ESVApiService} from "./ESVApiService";
import DisciplesJournalPlugin from "../core/DisciplesJournalPlugin";
/**
* Interface for a single Bible verse
*/
export interface BibleVerse {
book: string;
chapter: number;
verse: number;
text: string;
book: string;
chapter: number;
verse: number;
text: string;
}
/**
* Interface for Bible passage content
*/
export interface BiblePassage {
reference: string;
verses: BibleVerse[];
htmlContent?: string;
missingToken?: boolean;
reference: string;
verses: BibleVerse[];
htmlContent?: string;
missingToken?: boolean;
}
/**
@ -28,8 +28,8 @@ export interface BiblePassage {
*/
export class BibleContentService {
private plugin: DisciplesJournalPlugin;
private bible: any = null;
private esvApiService: ESVApiService;
private bible: any = null;
private esvApiService: ESVApiService;
constructor(plugin: DisciplesJournalPlugin, esvApiService: ESVApiService) {
this.plugin = plugin;
@ -37,127 +37,128 @@ export class BibleContentService {
}
/**
* Get a single verse by reference
*/
public getVerse(book: string, chapter: number, verse: number): BibleVerse | null {
if (!this.bible) return null;
try {
const normalizedBook = BookNames.normalize(book);
if (!normalizedBook) return null;
// Check if the book exists in the Bible data
if (!this.bible[normalizedBook]) return null;
// Check if the chapter exists
const chapterStr = chapter.toString();
if (!this.bible[normalizedBook][chapterStr]) return null;
// Check if the verse exists
const verseStr = verse.toString();
if (!this.bible[normalizedBook][chapterStr][verseStr]) return null;
// Return the verse
return {
book: normalizedBook,
chapter: chapter,
verse: verse,
text: this.bible[normalizedBook][chapterStr][verseStr]
};
} catch (error) {
console.error('Error getting verse:', error);
return null;
}
}
* Get a single verse by reference
*/
public getVerse(book: string, chapter: number, verse: number): BibleVerse | null {
if (!this.bible) return null;
try {
const normalizedBook = BookNames.normalize(book);
if (!normalizedBook) return null;
// Check if the book exists in the Bible data
if (!this.bible[normalizedBook]) return null;
// Check if the chapter exists
const chapterStr = chapter.toString();
if (!this.bible[normalizedBook][chapterStr]) return null;
// Check if the verse exists
const verseStr = verse.toString();
if (!this.bible[normalizedBook][chapterStr][verseStr]) return null;
// Return the verse
return {
book: normalizedBook,
chapter: chapter,
verse: verse,
text: this.bible[normalizedBook][chapterStr][verseStr]
};
} catch (error) {
console.error('Error getting verse:', error);
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)
*/
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;
}
}
* 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)
*/
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
*/
export class BookNames {
/**
* Standardize a book name to its canonical form
*/
public static normalize(bookName: string): string | null {
if (!bookName) {
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
}
/**
* Standardize a book name to its canonical form
*/
public static normalize(bookName: string): string | null {
if (!bookName) {
return null;
}
/**
* Get the ordered list of book names
*/
public static getBookOrder(): string[] {
return [...BookNames.bookOrder]; // Return a copy to prevent modification
}
// 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
}
/**
* 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)
private static bibleStructure: {[book: string]: number} = {
// Old Testament
"Genesis": 50,"Exodus": 40, "Leviticus": 27, "Numbers": 36, "Deuteronomy": 34,
"Joshua": 24, "Judges": 21, "Ruth": 4, "1 Samuel": 31, "2 Samuel": 24,
"1 Kings": 22, "2 Kings": 25, "1 Chronicles": 29, "2 Chronicles": 36,
"Ezra": 10, "Nehemiah": 13, "Esther": 10, "Job": 42, "Psalms": 150,
"Proverbs": 31, "Ecclesiastes": 12, "Song of Solomon": 8, "Isaiah": 66,
"Jeremiah": 52, "Lamentations": 5, "Ezekiel": 48, "Daniel": 12, "Hosea": 14,
"Joel": 3, "Amos": 9, "Obadiah": 1, "Jonah": 4, "Micah": 7,
"Nahum": 3, "Habakkuk": 3, "Zephaniah": 3, "Haggai": 2, "Zechariah": 14,
"Malachi": 4,
// New Testament
"Matthew": 28, "Mark": 16, "Luke": 24, "John": 21, "Acts": 28,
"Romans": 16, "1 Corinthians": 16, "2 Corinthians": 13, "Galatians": 6,
"Ephesians": 6, "Philippians": 4, "Colossians": 4, "1 Thessalonians": 5,
"2 Thessalonians": 3, "1 Timothy": 6, "2 Timothy": 4, "Titus": 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
};
// 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 bibleStructure: { [book: string]: number } = {
// Old Testament
"Genesis": 50, "Exodus": 40, "Leviticus": 27, "Numbers": 36, "Deuteronomy": 34,
"Joshua": 24, "Judges": 21, "Ruth": 4, "1 Samuel": 31, "2 Samuel": 24,
"1 Kings": 22, "2 Kings": 25, "1 Chronicles": 29, "2 Chronicles": 36,
"Ezra": 10, "Nehemiah": 13, "Esther": 10, "Job": 42, "Psalms": 150,
"Proverbs": 31, "Ecclesiastes": 12, "Song of Solomon": 8, "Isaiah": 66,
"Jeremiah": 52, "Lamentations": 5, "Ezekiel": 48, "Daniel": 12, "Hosea": 14,
"Joel": 3, "Amos": 9, "Obadiah": 1, "Jonah": 4, "Micah": 7,
"Nahum": 3, "Habakkuk": 3, "Zephaniah": 3, "Haggai": 2, "Zechariah": 14,
"Malachi": 4,
// New Testament
"Matthew": 28, "Mark": 16, "Luke": 24, "John": 21, "Acts": 28,
"Romans": 16, "1 Corinthians": 16, "2 Corinthians": 13, "Galatians": 6,
"Ephesians": 6, "Philippians": 4, "Colossians": 4, "1 Thessalonians": 5,
"2 Thessalonians": 3, "1 Timothy": 6, "2 Timothy": 4, "Titus": 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
};
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() {
this.initializeBookNameMap();
}
private bookNameMap: Map<string, string> = new Map();
/**
* Initialize the map of standardized book names
*/
private initializeBookNameMap(): void {
// Old Testament
this.addBookMapping("Genesis", ["Gen", "Ge", "Gn"]);
this.addBookMapping("Exodus", ["Exo", "Ex", "Exod"]);
this.addBookMapping("Leviticus", ["Lev", "Le", "Lv"]);
this.addBookMapping("Numbers", ["Num", "Nu", "Nm", "Nb"]);
this.addBookMapping("Deuteronomy", ["Deut", "De", "Dt"]);
this.addBookMapping("Joshua", ["Josh", "Jos", "Jsh"]);
this.addBookMapping("Judges", ["Judg", "Jdg", "Jg"]);
this.addBookMapping("Ruth", ["Rth", "Ru"]);
this.addBookMapping("1 Samuel", ["1 Sam", "1 Sa", "1S", "I Sa", "1Sam", "1st Samuel"]);
this.addBookMapping("2 Samuel", ["2 Sam", "2 Sa", "2S", "II Sa", "2Sam", "2nd Samuel"]);
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"]);
private constructor() {
this.initializeBookNameMap();
}
// New Testament
this.addBookMapping("Matthew", ["Matt", "Mt"]);
this.addBookMapping("Mark", ["Mk", "Mr"]);
this.addBookMapping("Luke", ["Lk", "Lu"]);
this.addBookMapping("John", ["Jn", "Jhn"]);
this.addBookMapping("Acts", ["Act", "Ac"]);
this.addBookMapping("Romans", ["Rom", "Ro", "Rm"]);
this.addBookMapping("1 Corinthians", ["1 Cor", "1 Co", "I Co", "1Cor", "1st Corinthians"]);
this.addBookMapping("2 Corinthians", ["2 Cor", "2 Co", "II Co", "2Cor", "2nd Corinthians"]);
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"]);
}
/**
* Initialize the map of standardized book names
*/
private initializeBookNameMap(): void {
// Old Testament
this.addBookMapping("Genesis", ["Gen", "Ge", "Gn"]);
this.addBookMapping("Exodus", ["Exo", "Ex", "Exod"]);
this.addBookMapping("Leviticus", ["Lev", "Le", "Lv"]);
this.addBookMapping("Numbers", ["Num", "Nu", "Nm", "Nb"]);
this.addBookMapping("Deuteronomy", ["Deut", "De", "Dt"]);
this.addBookMapping("Joshua", ["Josh", "Jos", "Jsh"]);
this.addBookMapping("Judges", ["Judg", "Jdg", "Jg"]);
this.addBookMapping("Ruth", ["Rth", "Ru"]);
this.addBookMapping("1 Samuel", ["1 Sam", "1 Sa", "1S", "I Sa", "1Sam", "1st Samuel"]);
this.addBookMapping("2 Samuel", ["2 Sam", "2 Sa", "2S", "II Sa", "2Sam", "2nd Samuel"]);
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"]);
/**
* 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);
}
}
// New Testament
this.addBookMapping("Matthew", ["Matt", "Mt"]);
this.addBookMapping("Mark", ["Mk", "Mr"]);
this.addBookMapping("Luke", ["Lk", "Lu"]);
this.addBookMapping("John", ["Jn", "Jhn"]);
this.addBookMapping("Acts", ["Act", "Ac"]);
this.addBookMapping("Romans", ["Rom", "Ro", "Rm"]);
this.addBookMapping("1 Corinthians", ["1 Cor", "1 Co", "I Co", "1Cor", "1st Corinthians"]);
this.addBookMapping("2 Corinthians", ["2 Cor", "2 Co", "II Co", "2Cor", "2nd Corinthians"]);
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 { BiblePassage } from "./BibleContentService";
import {requestUrl} from "obsidian";
import {BiblePassage} from "./BibleContentService";
import DisciplesJournalPlugin from "../core/DisciplesJournalPlugin";
/**
* Interface for ESV API Response
*/
export interface ESVApiResponse {
query: string;
canonical: string;
parsed: number[][];
passage_meta: ESVPassageMeta[];
passages: string[];
query: string;
canonical: string;
parsed: number[][];
passage_meta: ESVPassageMeta[];
passages: string[];
}
/**
* Interface for ESV Passage Metadata
*/
export interface ESVPassageMeta {
canonical: string;
chapter_start: number[];
chapter_end: number[];
prev_verse: number | null;
next_verse: number | null;
prev_chapter: number | null;
next_chapter: number[] | null;
canonical: string;
chapter_start: number[];
chapter_end: number[];
prev_verse: number | null;
next_verse: number | null;
prev_chapter: number | null;
next_chapter: number[] | null;
}
/**
* Service for interacting with the ESV API
*/
export class ESVApiService {
private plugin: DisciplesJournalPlugin;
private plugin: DisciplesJournalPlugin;
// Store HTML formatted Bible chapters
private htmlFormattedBible: {
[reference: string]: {
canonical: string;
htmlContent: string;
}
} = {};
// Store HTML formatted Bible chapters
private htmlFormattedBible: {
[reference: string]: {
canonical: string;
htmlContent: string;
}
} = {};
constructor(plugin: DisciplesJournalPlugin) {
this.plugin = plugin;
}
constructor(plugin: DisciplesJournalPlugin) {
this.plugin = plugin;
}
/**
* Get the full vault path including version subdirectory
*/
private getFullContentPath(): string {
return `${this.plugin.settings.bibleContentVaultPath}/${this.plugin.settings.preferredBibleVersion}`;
}
/**
* Get the full vault path including version subdirectory
*/
private getFullContentPath(): string {
return `${this.plugin.settings.bibleContentVaultPath}/${this.plugin.settings.preferredBibleVersion}`;
}
/**
* Get HTML formatted Bible content from in-memory storage
*/
public getHTMLContent(reference: string): BiblePassage | null {
if (this.htmlFormattedBible[reference]) {
return {
reference: this.htmlFormattedBible[reference].canonical,
verses: [], // Empty since we're using HTML
htmlContent: this.htmlFormattedBible[reference].htmlContent
};
}
return null;
}
/**
* Get HTML formatted Bible content from in-memory storage
*/
public getHTMLContent(reference: string): BiblePassage | null {
if (this.htmlFormattedBible[reference]) {
return {
reference: this.htmlFormattedBible[reference].canonical,
verses: [], // Empty since we're using HTML
htmlContent: this.htmlFormattedBible[reference].htmlContent
};
}
return null;
}
/**
* Load a collection of HTML formatted Bible chapters from files in the vault
*/
public async loadBibleChaptersFromVault(): Promise<void> {
try {
// Get the path where Bible content is stored
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);
}
}
/**
* Load a collection of HTML formatted Bible chapters from files in the vault
*/
public async loadBibleChaptersFromVault(): Promise<void> {
try {
// Get the path where Bible content is stored
const fullPath = this.getFullContentPath();
/**
* 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;
}
// Check if path exists
if (!(await this.plugin.app.vault.adapter.exists(fullPath))) {
console.log(`Bible content directory ${fullPath} does not exist yet`);
return;
}
/**
* 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;
}
// Get all book directories
const bookDirs = await this.plugin.app.vault.adapter.list(fullPath);
/**
* 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
};
}
}
// Process each book directory
for (const bookDir of bookDirs.folders) {
// Get all chapter files
const files = await this.plugin.app.vault.adapter.list(bookDir);
/**
* 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;
// 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 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(' ');
// 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);
}
}
/**
* Ensure vault directory exists, creating it if necessary
*/
private async ensureVaultDirectoryExists(path: string): Promise<void> {
const parts = path.split('/').filter(p => p.length > 0);
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);
}
}
}
// Create the directory structure with version subdirectory
const fullPath = this.getFullContentPath();
const bookPath = `${fullPath}/${book}`;
await this.ensureVaultDirectoryExists(fullPath);
await this.ensureVaultDirectoryExists(bookPath);
/**
* Ensures Bible data is loaded, downloading if necessary
*/
public async ensureBibleData(): Promise<void> {
try {
// 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...');
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;
}
}
// 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);
}
}
/**
* Ensure vault directory exists, creating it if necessary
*/
private async ensureVaultDirectoryExists(path: string): Promise<void> {
const parts = path.split('/').filter(p => p.length > 0);
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);
}
}
}
/**
* Ensures Bible data is loaded, downloading if necessary
*/
public async ensureBibleData(): Promise<void> {
try {
// 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...');
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 { THEME_PRESETS } from '../components/BibleStyles';
import {THEME_PRESETS} from '../components/BibleStyles';
export interface DisciplesJournalSettings {
displayInlineVerses: boolean;
displayFullPassages: boolean;
bibleTextFontSize: string;
wordsOfChristColor: string;
verseNumberColor: string;
headingColor: string;
blockIndentation: string;
preferredBibleVersion: string;
esvApiToken: string;
downloadOnDemand: boolean;
bibleContentVaultPath: string;
stylePreset: string;
showNavigationForVerses: boolean;
displayInlineVerses: boolean;
displayFullPassages: boolean;
bibleTextFontSize: string;
wordsOfChristColor: string;
verseNumberColor: string;
headingColor: string;
blockIndentation: string;
preferredBibleVersion: string;
esvApiToken: string;
downloadOnDemand: boolean;
bibleContentVaultPath: string;
stylePreset: string;
showNavigationForVerses: boolean;
}
export const DEFAULT_SETTINGS: DisciplesJournalSettings = {
displayInlineVerses: true,
displayFullPassages: true,
bibleTextFontSize: '100%',
wordsOfChristColor: 'var(--text-accent)',
verseNumberColor: 'var(--text-accent)',
headingColor: 'var(--text-accent)',
blockIndentation: '2em',
preferredBibleVersion: 'ESV',
esvApiToken: '',
downloadOnDemand: true,
bibleContentVaultPath: 'Bible',
stylePreset: 'default',
showNavigationForVerses: false
displayInlineVerses: true,
displayFullPassages: true,
bibleTextFontSize: '100%',
wordsOfChristColor: 'var(--text-accent)',
verseNumberColor: 'var(--text-accent)',
headingColor: 'var(--text-accent)',
blockIndentation: '2em',
preferredBibleVersion: 'ESV',
esvApiToken: '',
downloadOnDemand: true,
bibleContentVaultPath: 'Bible',
stylePreset: 'default',
showNavigationForVerses: false
};
export class DisciplesJournalSettingsTab extends PluginSettingTab {
plugin: DisciplesJournalPlugin;
plugin: DisciplesJournalPlugin;
constructor(app: App, plugin: DisciplesJournalPlugin) {
super(app, plugin);
this.plugin = plugin;
}
constructor(app: App, plugin: DisciplesJournalPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
display(): void {
const {containerEl} = this;
containerEl.empty();
new Setting(containerEl).setName('Display Customization').setHeading();
new Setting(containerEl).setName('Display Customization').setHeading();
new Setting(containerEl)
.setName('Display Inline Verses')
.setDesc('Enable rendering of inline Bible references (in `code blocks`).')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.displayInlineVerses)
.onChange(async (value) => {
this.plugin.settings.displayInlineVerses = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Display Inline Verses')
.setDesc('Enable rendering of inline Bible references (in `code blocks`).')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.displayInlineVerses)
.onChange(async (value) => {
this.plugin.settings.displayInlineVerses = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Display Full Passages')
.setDesc('Enable rendering of full Bible passages (in ```bible code blocks).')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.displayFullPassages)
.onChange(async (value) => {
this.plugin.settings.displayFullPassages = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Display Full Passages')
.setDesc('Enable rendering of full Bible passages (in ```bible code blocks).')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.displayFullPassages)
.onChange(async (value) => {
this.plugin.settings.displayFullPassages = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Show Navigation for Verses')
.setDesc('Show chapter navigation when displaying specific verses (by default, navigation only shows for full chapters).')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showNavigationForVerses)
.onChange(async (value) => {
this.plugin.settings.showNavigationForVerses = value;
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)
.setName('Show Navigation for Verses')
.setDesc('Show chapter navigation when displaying specific verses (by default, navigation only shows for full chapters).')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showNavigationForVerses)
.onChange(async (value) => {
this.plugin.settings.showNavigationForVerses = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Bible Text Font Size')
.setDesc('Set the font size for Bible verses and passages.')
.addDropdown(dropdown => dropdown
.addOption('80%', 'Smaller (80%)')
.addOption('90%', 'Small (90%)')
.addOption('100%', 'Normal (100%)')
.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)
.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));
});
new Setting(containerEl)
.setName('Words of Christ Color')
.setDesc('Set the color for Words of Christ (use `var(--text-normal)` for no special color).')
.addText(text => text
.setPlaceholder('var(--text-accent)')
.setValue(this.plugin.settings.wordsOfChristColor)
.onChange(async (value) => {
this.plugin.settings.wordsOfChristColor = value || 'var(--text-accent)';
await this.plugin.saveSettings();
this.plugin.updateBibleStyles();
}));
new Setting(containerEl)
.setName('Verse Number Color')
.setDesc('Set the color for verse numbers.')
.addText(text => text
.setPlaceholder('var(--text-accent)')
.setValue(this.plugin.settings.verseNumberColor)
.onChange(async (value) => {
this.plugin.settings.verseNumberColor = value || 'var(--text-accent)';
await this.plugin.saveSettings();
this.plugin.updateBibleStyles();
}));
new Setting(containerEl)
.setName('Heading Color')
.setDesc('Set the color for Bible passage headings.')
.addText(text => text
.setPlaceholder('var(--text-accent)')
.setValue(this.plugin.settings.headingColor)
.onChange(async (value) => {
this.plugin.settings.headingColor = value || 'var(--text-accent)';
await this.plugin.saveSettings();
this.plugin.updateBibleStyles();
}));
new Setting(containerEl)
.setName('Block Indentation')
.setDesc('Set the indentation for block sections.')
.addText(text => text
.setPlaceholder('2em')
.setValue(this.plugin.settings.blockIndentation)
.onChange(async (value) => {
this.plugin.settings.blockIndentation = value || '2em';
await this.plugin.saveSettings();
this.plugin.updateBibleStyles();
}));
new Setting(containerEl).setName('Bible').setHeading();
new Setting(containerEl)
.setName('Preferred Bible Version')
.setDesc('Select your preferred Bible version (only ESV currently supported).')
.addDropdown(dropdown => dropdown
.addOption('ESV', 'English Standard Version (ESV)')
.setValue(this.plugin.settings.preferredBibleVersion)
.onChange(async (value) => {
this.plugin.settings.preferredBibleVersion = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Bible Content Vault Path')
.setDesc('Root path in your vault where Bible content will be stored. Each translation will be stored in a subdirectory.')
.addText(text => text
.setPlaceholder('Bible')
.setValue(this.plugin.settings.bibleContentVaultPath)
.onChange(async (value) => {
this.plugin.settings.bibleContentVaultPath = value || 'Bible';
await this.plugin.saveSettings();
}));
new Setting(containerEl).setName('ESV API').setHeading();
const apiInfoDiv = containerEl.createDiv({ cls: 'disciples-journal-api-info' });
apiInfoDiv.createEl('p', {
text: 'The ESV API allows this plugin to download and display Bible passages from the ESV translation.'
});
apiInfoDiv.createEl('p', {
text: 'To get a free ESV API token:',
attr: { style: 'font-weight: bold;' }
});
const instructionsList = apiInfoDiv.createEl('ol');
const steps = [
'Visit api.esv.org',
'Sign up for a free account',
'After logging in, go to "API Keys" in your account',
'Create a new token and copy it here'
];
steps.forEach(step => {
if (step === 'Visit api.esv.org') {
const listItem = instructionsList.createEl('li');
listItem.createEl('span', { text: 'Visit ' });
listItem.createEl('a', {
text: 'api.esv.org',
href: 'https://api.esv.org/docs/',
attr: { target: '_blank' }
});
} else {
instructionsList.createEl('li', { text: step });
}
});
apiInfoDiv.createEl('p', {
text: 'With a valid token, the plugin can download and display Bible passages directly in your notes.'
});
new Setting(containerEl)
.setName('ESV API Token')
.setDesc('Enter your ESV API token from api.esv.org.')
.addText(text => text
.setPlaceholder('Enter your ESV API token')
.setValue(this.plugin.settings.esvApiToken)
.onChange(async (value) => {
this.plugin.settings.esvApiToken = value;
await this.plugin.saveSettings();
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
}));
}
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)
.setName('Bible Text Font Size')
.setDesc('Set the font size for Bible verses and passages.')
.addDropdown(dropdown => dropdown
.addOption('80%', 'Smaller (80%)')
.addOption('90%', 'Small (90%)')
.addOption('100%', 'Normal (100%)')
.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)
.setName('Words of Christ Color')
.setDesc('Set the color for Words of Christ (use `var(--text-normal)` for no special color).')
.addText(text => text
.setPlaceholder('var(--text-accent)')
.setValue(this.plugin.settings.wordsOfChristColor)
.onChange(async (value) => {
this.plugin.settings.wordsOfChristColor = value || 'var(--text-accent)';
await this.plugin.saveSettings();
this.plugin.updateBibleStyles();
}));
new Setting(containerEl)
.setName('Verse Number Color')
.setDesc('Set the color for verse numbers.')
.addText(text => text
.setPlaceholder('var(--text-accent)')
.setValue(this.plugin.settings.verseNumberColor)
.onChange(async (value) => {
this.plugin.settings.verseNumberColor = value || 'var(--text-accent)';
await this.plugin.saveSettings();
this.plugin.updateBibleStyles();
}));
new Setting(containerEl)
.setName('Heading Color')
.setDesc('Set the color for Bible passage headings.')
.addText(text => text
.setPlaceholder('var(--text-accent)')
.setValue(this.plugin.settings.headingColor)
.onChange(async (value) => {
this.plugin.settings.headingColor = value || 'var(--text-accent)';
await this.plugin.saveSettings();
this.plugin.updateBibleStyles();
}));
new Setting(containerEl)
.setName('Block Indentation')
.setDesc('Set the indentation for block sections.')
.addText(text => text
.setPlaceholder('2em')
.setValue(this.plugin.settings.blockIndentation)
.onChange(async (value) => {
this.plugin.settings.blockIndentation = value || '2em';
await this.plugin.saveSettings();
this.plugin.updateBibleStyles();
}));
new Setting(containerEl).setName('Bible').setHeading();
new Setting(containerEl)
.setName('Preferred Bible Version')
.setDesc('Select your preferred Bible version (only ESV currently supported).')
.addDropdown(dropdown => dropdown
.addOption('ESV', 'English Standard Version (ESV)')
.setValue(this.plugin.settings.preferredBibleVersion)
.onChange(async (value) => {
this.plugin.settings.preferredBibleVersion = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Bible Content Vault Path')
.setDesc('Root path in your vault where Bible content will be stored. Each translation will be stored in a subdirectory.')
.addText(text => text
.setPlaceholder('Bible')
.setValue(this.plugin.settings.bibleContentVaultPath)
.onChange(async (value) => {
this.plugin.settings.bibleContentVaultPath = value || 'Bible';
await this.plugin.saveSettings();
}));
new Setting(containerEl).setName('ESV API').setHeading();
const apiInfoDiv = containerEl.createDiv({cls: 'disciples-journal-api-info'});
apiInfoDiv.createEl('p', {
text: 'The ESV API allows this plugin to download and display Bible passages from the ESV translation.'
});
apiInfoDiv.createEl('p', {
text: 'To get a free ESV API token:',
attr: {style: 'font-weight: bold;'}
});
const instructionsList = apiInfoDiv.createEl('ol');
const steps = [
'Visit api.esv.org',
'Sign up for a free account',
'After logging in, go to "API Keys" in your account',
'Create a new token and copy it here'
];
steps.forEach(step => {
if (step === 'Visit api.esv.org') {
const listItem = instructionsList.createEl('li');
listItem.createEl('span', {text: 'Visit '});
listItem.createEl('a', {
text: 'api.esv.org',
href: 'https://api.esv.org/docs/',
attr: {target: '_blank'}
});
} else {
instructionsList.createEl('li', {text: step});
}
});
apiInfoDiv.createEl('p', {
text: 'With a valid token, the plugin can download and display Bible passages directly in your notes.'
});
new Setting(containerEl)
.setName('ESV API Token')
.setDesc('Enter your ESV API token from api.esv.org.')
.addText(text => text
.setPlaceholder('Enter your ESV API token')
.setValue(this.plugin.settings.esvApiToken)
.onChange(async (value) => {
this.plugin.settings.esvApiToken = value;
await this.plugin.saveSettings();
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' {
const value: any;
export default value;
const value: any;
export default value;
}
// Define the structure of the Bible data for better type checking
interface ESVVerse {
pk: number;
translation: string;
book: number | string;
chapter: number;
verse: number;
text: string;
comment?: string;
pk: number;
translation: string;
book: number | string;
chapter: number;
verse: number;
text: 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
*/
export class BibleFormatter {
/**
* Format chapter content as Markdown
*/
public static formatChapterContent(passage: BiblePassage): string {
if (!passage) return "# Error: No passage content\n\nThe requested passage could not be loaded.";
let content = "";
// Add code block for rendering
content += "```bible\n";
content += passage.reference;
content += "\n```\n\n";
// Alternatively, add each verse separately
if (passage.verses && passage.verses.length > 0) {
for (const verse of passage.verses) {
content += `**${verse.verse}** ${verse.text}\n\n`;
}
}
return content;
}
/**
* Format chapter content as Markdown
*/
public static formatChapterContent(passage: BiblePassage): string {
if (!passage) return "# Error: No passage content\n\nThe requested passage could not be loaded.";
let content = "";
// Add code block for rendering
content += "```bible\n";
content += passage.reference;
content += "\n```\n\n";
// Alternatively, add each verse separately
if (passage.verses && passage.verses.length > 0) {
for (const verse of passage.verses) {
content += `**${verse.verse}** ${verse.text}\n\n`;
}
}
return content;
}
}