This commit is contained in:
Scott Tomaszewski 2026-05-30 21:57:09 -04:00
parent e08f577102
commit d93b00bc12
26 changed files with 5162 additions and 2425 deletions

View file

@ -1,3 +0,0 @@
node_modules/
main.js

View file

@ -1,23 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
}
}

View file

@ -1,5 +1,14 @@
# Changelog
## 0.13.0
- Brings the plugin in line with current Obsidian community standards
- Modernizes the lint toolchain (flat config, `eslint-plugin-obsidianmd`, type-aware `typescript-eslint`); the plugin now passes `eslint` and `tsc` cleanly
- Settings UI text uses sentence case
- Corrects the manifest description and raises `minAppVersion` to `1.6.6` to match the APIs actually used
- Tightens type safety (removes `any`, narrows YAML/error handling) and moves remaining inline styles to `styles.css`
- Known follow-ups (tracked in `FOLLOWUP.md`): the hover-preview event listeners are still attached globally and should move to the registered-listener lifecycle, and file reads/writes should migrate to the `Vault`/`FileManager` APIs
## 0.12.0
- Adds "Open Bible" command and ribbon icon to browse books and chapters without a reference

106
FOLLOWUP.md Normal file
View file

@ -0,0 +1,106 @@
# Follow-up work
This file tracks known issues that were **intentionally deferred** during the
0.13.0 standards/compliance pass. The codebase currently passes `npx eslint .`
and `npm run build` with zero errors/warnings; the items below are silenced with
scoped `eslint-disable` comments (each referencing this file) so the deferral is
explicit rather than hidden.
Scope decision at the time: do the "safe subset" of standards fixes and leave the
file-API internals and the event-handler internals **functionally unchanged**.
---
## 1. Hover-preview event listeners leak (highest priority)
**Files:** `src/core/BibleEventHandlers.ts`, `src/components/BibleReferenceRenderer.ts`,
`src/components/BibleReferenceInlineExtension.ts`
**What's wrong:**
- `BibleReferenceRenderer.processInlineCodeBlocks` and the CodeMirror
`eventHandlers` in `BibleReferenceInlineExtension` create a **new
`BibleEventHandlers` instance on every `mouseover`/`mouseout`**.
- Each `BibleEventHandlers` constructor calls `document.addEventListener('mousemove', …)`
and `document.addEventListener('click', …)` on the **global `document`**.
- These global listeners are only removed by `cleanup()`, which is **never called**
`DisciplesJournalPlugin.onunload()` is empty. So listeners accumulate for the
lifetime of the app session and are never released on plugin unload.
- It also uses `window.setInterval` / `clearInterval` and the global `document`
rather than `activeDocument`, so hover previews don't behave correctly in
pop-out windows.
**Rules currently disabled for this:** `obsidianmd/prefer-active-doc`,
`obsidianmd/prefer-window-timers`, `@typescript-eslint/no-deprecated`
(file-level disable at the top of `BibleEventHandlers.ts`).
**Suggested fix:**
- Make `BibleEventHandlers` a single, long-lived instance owned by the plugin
(or make it extend `Component`), created once in `onload`.
- Register all DOM listeners with `this.registerDomEvent(...)` and the interval
with `this.registerInterval(...)` so Obsidian tears them down automatically.
- Use `activeDocument` / `activeWindow` instead of the globals for pop-out support.
- Stop instantiating `new BibleEventHandlers(...)` inside the hover callbacks.
---
## 2. File access via `vault.adapter` instead of the Vault/FileManager APIs
**Files:** `src/services/ESVApiService.ts`, `src/services/BibleFiles.ts`,
`src/core/DisciplesJournalPlugin.ts`
**What's wrong (Obsidian guideline rules 1922):**
- `vault.adapter.write(...)` is used to create/overwrite notes
(`ESVApiService.saveESVApiResponseAsMdNote`, `DisciplesJournalPlugin.updateAllBibleNoteFrontmatter`).
Prefer `Vault.create()` for new files and `Vault.process()` for background
edits of existing files. For frontmatter specifically, `FileManager.processFrontMatter()`
is the intended API and would remove most of the manual YAML string handling
in `FrontmatterUtil.ts`.
- `vault.adapter.exists` / `vault.adapter.mkdir` (directory creation) should use
`Vault.getAbstractFileByPath()` / `Vault.createFolder()`.
- `BibleFiles.clearData` uses `vault.adapter.rmdir(...)`; deletion should go
through `FileManager.trashFile()` so it respects the user's trash settings.
- Paths are built by string concatenation; run user-configurable paths through
`normalizePath()`.
> Note: these are **not** currently flagged by the lint config (the obsidianmd
> rules target `Vault.trash/delete`, not `adapter.*`), so there are no disable
> comments for them — but they are still non-conformant and worth migrating.
---
## 3. Rendering ESV HTML via `innerHTML`
**File:** `src/components/BibleReferenceRenderer.ts` (4 sites)
**What's wrong:** passage HTML returned by the ESV API is injected with
`element.innerHTML = passage.html`. Flagged by `no-unsanitized/property` and
`@microsoft/sdl/no-inner-html`. The content is trusted (comes from the ESV API
over HTTPS), so this is currently accepted and disabled inline at each site.
**Suggested fix:** parse the HTML and rebuild it with Obsidian DOM helpers
(`createEl`/`createDiv`), or sanitize before insertion, so no raw HTML string is
written to the DOM.
---
## 4. `setTimeout` for scroll-to-verse
**File:** `src/services/BibleChapterFiles.ts`
The scroll-to-verse uses a fixed `window.setTimeout(..., 300)` to wait for the
note to render before querying for the verse element. This was left as-is (only
prefixed with `window.` for pop-out compatibility). A more robust approach would
key off a render/layout event rather than a magic delay.
---
## 5. Smaller cleanups noticed in passing
- `src/services/BibleContentService.ts` and `src/services/ESVApiService.ts` both
contain `// TODO` notes about duplicated ESV-response → `BiblePassage`
conversion logic that could be unified.
- `src/services/BibleChapterFiles.ts` has a `// TODO - logic in this class needs
to move to BibleFiles` note; `BibleFiles.ts` has TODOs for `openChapterNote` /
`openPassageNote` helpers.
- `openChapterNote` takes a `string` and re-parses it; several call sites already
hold a `BibleReference` and could pass it directly (existing TODO).

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 Scott Tomaszewski
Copyright (c) 2024-2026 Scott Tomaszewski
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,6 +1,6 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import { builtinModules as builtins } from "node:module";
const banner =
`/*

54
eslint.config.mjs Normal file
View file

@ -0,0 +1,54 @@
import { defineConfig } from "eslint/config";
import obsidianmd from "eslint-plugin-obsidianmd";
import tseslint from "typescript-eslint";
export default defineConfig([
{
// obsidianmd only wires up JSON parsing for package.json; other JSON
// files would be parsed as JS and error. Build artifacts/configs too.
ignores: [
"main.js",
"node_modules/**",
"**/*.mjs",
"manifest.json",
"versions.json",
"tsconfig.json",
"devbox.json",
"devbox.lock",
"package-lock.json",
],
},
...obsidianmd.configs.recommended,
{
files: ["**/*.ts"],
languageOptions: {
parser: tseslint.parser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
// Obsidian/CodeMirror callback signatures often include unused args.
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
"@typescript-eslint/ban-ts-comment": "off",
// Disabled upstream as "not working as intended" (v0.3.0); it also
// lower-cases proper nouns (Bible, ESV, Christ). Sentence case is
// applied manually instead.
"obsidianmd/ui/sentence-case": "off",
},
},
{
// obsidianmd's recommended set applies several type-aware rules globally
// (no file filter). They require TS parser services and crash on the JSON
// files. They are only meaningful for source, so disable them for JSON.
files: ["**/*.json"],
rules: {
"obsidianmd/no-plugin-as-component": "off",
"obsidianmd/no-view-references-in-plugin": "off",
"obsidianmd/prefer-file-manager-trash-file": "off",
"obsidianmd/prefer-instanceof": "off",
"obsidianmd/no-unsupported-api": "off",
},
},
]);

View file

@ -1,11 +1,10 @@
{
"id": "disciples-journal",
"name": "Disciples Journal",
"version": "0.12.0",
"minAppVersion": "0.15.0",
"description": "Embed Bible references and passages in your notes and read the Bible in Obsidian.",
"version": "0.13.0",
"minAppVersion": "1.6.6",
"description": "Embed Bible references and passages in your notes and read the Bible.",
"author": "Scott Tomaszewski (Xentis)",
"authorUrl": "https://github.com/scottTomaszewski",
"isDesktopOnly": false,
"fundingUrl": ""
"isDesktopOnly": false
}

7098
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,26 +1,36 @@
{
"name": "obsidian-disciples-journal",
"version": "0.12.0",
"description": "Obsidian plugin for Bible verse references and passage display",
"version": "0.13.0",
"description": "Embed Bible references and passages in your notes and read the Bible.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"lint": "eslint .",
"version": "node version-bump.mjs && git add manifest.json versions.json",
"build-no-check": "node esbuild.config.mjs production"
},
"keywords": [],
"author": "",
"keywords": [
"obsidian",
"bible",
"scripture",
"esv",
"verse"
],
"author": "Scott Tomaszewski (Xentis)",
"license": "MIT",
"devDependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@rollup/plugin-json": "^6.1.0",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"eslint": "^9.18.0",
"eslint-plugin-obsidianmd": "^0.3.0",
"obsidian": "latest",
"tslib": "^2.4.0",
"typescript": "4.7.4"
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
}
}

View file

@ -189,7 +189,8 @@ export class BibleNavigation {
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}`);
const message = error instanceof Error ? error.message : String(error);
new Notice(`Error navigating to chapter: ${message}`);
}
}
}

View file

@ -1,5 +1,5 @@
import { ViewUpdate, EditorView, ViewPlugin, Decoration, DecorationSet } from "@codemirror/view";
import {RangeSet, RangeSetBuilder} from "@codemirror/state";
import {RangeSetBuilder} from "@codemirror/state";
import { syntaxTree } from "@codemirror/language";
import { BibleReference } from "src/core/BibleReference";
import { BibleEventHandlers } from "src/core/BibleEventHandlers";
@ -26,7 +26,7 @@ export function createInlineReferenceExtension(renderer: BibleReferenceRenderer,
buildDecorations(view: EditorView): DecorationSet {
if (!view.state.field(editorLivePreviewField)) {
return RangeSet.empty;
return Decoration.none;
}
const builder = new RangeSetBuilder<Decoration>();
@ -34,7 +34,7 @@ export function createInlineReferenceExtension(renderer: BibleReferenceRenderer,
syntaxTree(view.state).iterate({
from,
to,
enter: (node: any) => {
enter: (node) => {
if (node.type.name.contains("inline-code")) {
const content = view.state.doc.sliceString(node.from, node.to);
// Try to parse as a Bible reference
@ -74,12 +74,12 @@ export function createInlineReferenceExtension(renderer: BibleReferenceRenderer,
return;
}
contentService.getBibleContent(reference).then(response => {
void contentService.getBibleContent(reference).then(response => {
if (response.isError()) {
new Notice(response.errorMessage, 10000);
return;
}
new BibleEventHandlers(renderer).handleBibleReferenceHover(e, response.passage);
void new BibleEventHandlers(renderer).handleBibleReferenceHover(e, response.passage);
});
}
},

View file

@ -61,7 +61,7 @@ export class BibleReferenceRenderer {
}
referenceEl.addEventListener('mouseover', (e) => {
new BibleEventHandlers(this).handleBibleReferenceHover(e, response.passage);
void new BibleEventHandlers(this).handleBibleReferenceHover(e, response.passage);
});
referenceEl.addEventListener('mouseout', (e) => {
new BibleEventHandlers(this).handleBibleReferenceMouseOut(e);
@ -124,6 +124,7 @@ export class BibleReferenceRenderer {
// Add verses
const passageEl = el.doc.createElement('div');
passageEl.classList.add('bible-passage-text');
// eslint-disable-next-line no-unsanitized/property, @microsoft/sdl/no-inner-html -- deferred: ESV API returns trusted formatted HTML; see FOLLOWUP.md
passageEl.innerHTML = response.passage.html;
containerEl.appendChild(passageEl);
@ -152,7 +153,7 @@ export class BibleReferenceRenderer {
try {
// Call the method to open the chapter note
// TODO - openChapterNote should take in a BibleReference instead of string
this.plugin.openChapterNote(passage.reference.toString());
void this.plugin.openChapterNote(passage.reference.toString());
// Close the preview - using a method available in BibleEventHandlers
// Remove the popup directly instead of trying to access the private property
@ -164,7 +165,7 @@ export class BibleReferenceRenderer {
console.error('Error opening chapter note from popup:', error);
// Show user feedback if there's an error
new Notice(`Disciples Journal: Unable to open chapter note for ${passage.reference}`, 10000);
new Notice(`Disciples Journal: Unable to open chapter note for ${passage.reference.toString()}`, 10000);
}
});
@ -179,6 +180,7 @@ export class BibleReferenceRenderer {
try {
// Create a temporary element to parse the HTML
const tempEl = element.doc.createElement('div');
// eslint-disable-next-line no-unsanitized/property, @microsoft/sdl/no-inner-html -- deferred: ESV API returns trusted formatted HTML; see FOLLOWUP.md
tempEl.innerHTML = passage.html;
// Remove footnote sections before extracting paragraphs
@ -190,7 +192,10 @@ export class BibleReferenceRenderer {
const paragraphs = tempEl.querySelectorAll('p:not(.extra_text)');
if (paragraphs.length > 0) {
for (let i = 0; i < paragraphs.length; i++) {
const cloned = paragraphs[i].cloneNode(true) as HTMLElement;
const cloned = paragraphs[i].cloneNode(true);
if (!cloned.instanceOf(HTMLElement)) {
continue;
}
// Strip inline footnote markers if the setting is enabled
if (this.plugin.settings.hideFootnotesInPreview) {
cloned.querySelectorAll('.footnote').forEach(fn => fn.remove());
@ -199,10 +204,12 @@ export class BibleReferenceRenderer {
}
} else {
// Fallback if we can't extract the verses properly
// eslint-disable-next-line no-unsanitized/property, @microsoft/sdl/no-inner-html -- deferred: ESV API returns trusted formatted HTML; see FOLLOWUP.md
contentEl.innerHTML = passage.html;
}
} catch (error) {
console.error("Error extracting verse content from HTML:", error);
// eslint-disable-next-line no-unsanitized/property, @microsoft/sdl/no-inner-html -- deferred: ESV API returns trusted formatted HTML; see FOLLOWUP.md
contentEl.innerHTML = passage.html;
}
@ -213,8 +220,11 @@ export class BibleReferenceRenderer {
// Position the main popup with slight overlap to the reference
// This creates an easier hover target when moving from reference to popup
versePreviewEl.style.left = `${rect.left}px`;
versePreviewEl.style.top = `${rect.bottom - 3}px`;
// (dynamic positioning depends on runtime geometry)
versePreviewEl.setCssStyles({
left: `${rect.left}px`,
top: `${rect.bottom - 3}px`,
});
// Add popup to document
element.doc.body.appendChild(versePreviewEl);

View file

@ -15,11 +15,10 @@ export class BookSuggest extends AbstractInputSuggest<string> {
this.onBookSelected = onBookSelected;
const wrapper = inputEl.parentElement!;
wrapper.style.position = 'relative';
wrapper.style.display = 'inline-block'; // shrinkwrap to input width
wrapper.addClass('disciples-journal-book-suggest-wrapper');
// make room for the clear “×”
inputEl.style.paddingRight = '1.5em';
inputEl.addClass('disciples-journal-book-suggest-input');
const clearBtn = wrapper.createEl('button', {
cls: 'clear-input-button',
text: '×',
@ -58,22 +57,23 @@ export class BookSuggest extends AbstractInputSuggest<string> {
super.open();
// give Obsidian time to insert & position the popover…
requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
// 1) grab the suggestion container
// this is the <div> Obsidian just appended after your input
const container = this.inputEl.doc
?.querySelector('.suggestion-container') as HTMLElement;
if (!container) return;
const container = this.inputEl.doc.querySelector('.suggestion-container');
if (!(container instanceof HTMLElement)) return;
// 2) compute your input's screen-coords
const rect = this.inputEl.getBoundingClientRect();
// 3) force the dropdown to sit flush with your input's left edge
container.style.position = 'absolute';
container.style.left = `${rect.left}px`;
container.style.top = `${rect.bottom}px`; // drop below
container.style.minWidth = `${rect.width}px`; // match widths
// (dynamic positioning depends on runtime geometry)
container.setCssStyles({
position: 'absolute',
left: `${rect.left}px`,
top: `${rect.bottom}px`,
minWidth: `${rect.width}px`,
});
});
}
}

View file

@ -56,7 +56,11 @@ export class OpenBibleModal extends SuggestModal<BibleTarget> {
el.setText(item.label);
}
async onChooseSuggestion(item: BibleTarget, _evt: MouseEvent | KeyboardEvent): Promise<void> {
onChooseSuggestion(item: BibleTarget, _evt: MouseEvent | KeyboardEvent): void {
void this.openTarget(item);
}
private async openTarget(item: BibleTarget): Promise<void> {
if (item.chapter === 0) {
// Book selected -- reopen modal for chapter selection
const chapterCount = BookNames.getChapterCount(item.book);
@ -113,7 +117,11 @@ class OpenBibleChapterModal extends SuggestModal<BibleTarget> {
el.setText(item.label);
}
async onChooseSuggestion(item: BibleTarget, _evt: MouseEvent | KeyboardEvent): Promise<void> {
onChooseSuggestion(item: BibleTarget, _evt: MouseEvent | KeyboardEvent): void {
void this.openChapter(item);
}
private async openChapter(item: BibleTarget): Promise<void> {
const loadingNotice = new Notice("Loading chapter...", 0);
await this.bibleBookFiles.openChapterNote(`${item.book} ${item.chapter}`);
loadingNotice.hide();

View file

@ -1,3 +1,11 @@
/*
* DEFERRED: this class attaches hover-preview listeners to the global `document`
* and is created per hover event, leaking listeners. It is slated for a
* lifecycle rewrite (registerDomEvent / registerInterval / activeDocument)
* see FOLLOWUP.md. Until then the popout-window, timer, and deprecation rules
* tied to that code are disabled here rather than partially reworked.
*/
/* eslint-disable obsidianmd/prefer-active-doc, obsidianmd/prefer-window-timers, @typescript-eslint/no-deprecated */
import { BiblePassage } from 'src/utils/BiblePassage';
import { BibleReferenceRenderer } from '../components/BibleReferenceRenderer';
@ -134,8 +142,7 @@ export class BibleEventHandlers {
// If no preview active, nothing to do
if (!this.previewPopper) return;
const target = event.target as HTMLElement;
const relatedTarget = event.relatedTarget as HTMLElement;
// If moving directly to the preview element, don't close
@ -158,7 +165,7 @@ export class BibleEventHandlers {
try {
const hoverGaps = doc.querySelectorAll('.bible-hover-gap');
hoverGaps.forEach(gap => gap.remove());
} catch (e) {
} catch {
// Ignore any errors during cleanup
}
}

View file

@ -30,8 +30,9 @@ export class BibleMarkupProcessor {
await this.bibleReferenceRenderer.processFullBiblePassage(source, el);
} catch (error) {
console.error('Error processing Bible code block:', error);
const message = error instanceof Error ? error.message : String(error);
el.createEl('div', {
text: `Error processing Bible reference: ${error.message}`,
text: `Error processing Bible reference: ${message}`,
cls: 'bible-reference-error'
});
}

View file

@ -33,8 +33,6 @@ export default class DisciplesJournalPlugin extends Plugin {
private bibleMarkupProcessor: BibleMarkupProcessor;
async onload() {
console.log('Loading Disciples Journal plugin');
// Initialize settings
await this.loadSettings();
@ -61,10 +59,12 @@ export default class DisciplesJournalPlugin extends Plugin {
this.bibleMarkupProcessor = new BibleMarkupProcessor(this.bibleReferenceRenderer, this.settings);
// Register bible reference processor
this.registerMarkdownCodeBlockProcessor('bible', this.bibleMarkupProcessor.processBibleCodeBlock.bind(this.bibleMarkupProcessor));
this.registerMarkdownCodeBlockProcessor('bible', (source, el, ctx) =>
this.bibleMarkupProcessor.processBibleCodeBlock(source, el, ctx));
// Register markdown post processor for inline references
this.registerMarkdownPostProcessor(this.bibleMarkupProcessor.processInlineBibleReferences.bind(this.bibleMarkupProcessor));
this.registerMarkdownPostProcessor((el, ctx) =>
this.bibleMarkupProcessor.processInlineBibleReferences(el, ctx));
// Register editor extension for Live Preview
if (this.settings.displayInlineVerses) {
@ -93,14 +93,15 @@ export default class DisciplesJournalPlugin extends Plugin {
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)));
this.registerEvent(this.app.workspace.on('active-leaf-change', () => this.handleActiveLeafChange()));
}
onunload() {
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
const savedData = (await this.loadData()) as Partial<DisciplesJournalSettings> | null;
this.settings = Object.assign({}, DEFAULT_SETTINGS, savedData);
}
async saveSettings() {
@ -180,8 +181,8 @@ export default class DisciplesJournalPlugin extends Plugin {
continue;
}
const fmData = parseYaml(fmInfo.frontmatter);
if (!fmData || !fmData.canonical) {
const fmData = parseYaml(fmInfo.frontmatter) as { canonical?: unknown } | null;
if (!fmData || typeof fmData.canonical !== 'string') {
skippedCount++;
continue;
}

View file

@ -1,7 +1,6 @@
import {TFile} from 'obsidian';
import {BibleContentService} from './BibleContentService';
import {BibleReference} from '../core/BibleReference';
import {BibleCodeblockFormatter} from '../utils/BibleCodeblockFormatter';
import DisciplesJournalPlugin from "../core/DisciplesJournalPlugin";
import {BibleApiResponse} from "../utils/BibleApiResponse";
import {BibleFiles} from "./BibleFiles";
@ -52,7 +51,7 @@ export class BibleChapterFiles {
// If there's a specific verse, scroll to it
if (parsedRef.verse) {
setTimeout(() => {
window.setTimeout(() => {
// Find the verse element and scroll to it
const verseEl = leaf.getContainer().doc.querySelector(`.verse-${parsedRef.verse}`);
if (verseEl) {
@ -82,8 +81,8 @@ export class BibleChapterFiles {
// Check if passage is null
if (response.isError()) {
// TODO - handle this better with a dialog box or Notice or something.
console.error(`Failed to get Bible content for ${bookChapter}`);
throw new Error(`Failed to get Bible content for ${bookChapter}`);
console.error(`Failed to get Bible content for ${bookChapter.toString()}`);
throw new Error(`Failed to get Bible content for ${bookChapter.toString()}`);
}
} catch (error) {
console.error('Error creating chapter note:', error);

View file

@ -27,14 +27,14 @@ export class BibleContentService {
// Return cached result if available
const passage = this.getCachedRef(ref);
if (passage) {
console.debug(`Returning cached passage(s) (${ref})`)
return BibleApiResponse.success(passage);
}
// Load from local file
if (await BibleFiles.fileExistsForPassage(ref, this.plugin)) {
console.debug(`Pulling passage(s) (${ref}) from local file`)
const passageMdFile = BibleFiles.getFileForPassage(ref, this.plugin);
const passageMdFile = await BibleFiles.fileExistsForPassage(ref, this.plugin)
? BibleFiles.getFileForPassage(ref, this.plugin)
: null;
if (passageMdFile) {
try {
const fileContent = await this.plugin.app.vault.read(passageMdFile);
const frontmatter = getFrontMatterInfo(fileContent);
@ -42,12 +42,13 @@ export class BibleContentService {
if (frontmatter && frontmatter.frontmatter) {
return this.convertEsvApiResponseToGeneric(parseYaml(frontmatter.frontmatter), ref);
} else {
const message = `No frontmatter found in file for reference ${ref}. Loading from API instead.`;
const message = `No frontmatter found in file for reference ${ref.toString()}. Loading from API instead.`;
console.error(message);
// return BibleApiResponse.error(message, ErrorType.BadApiResponse);
}
} catch (error) {
const message = `Error reading file for reference ${ref}: ${error}`;
const detail = error instanceof Error ? error.message : String(error);
const message = `Error reading file for reference ${ref.toString()}: ${detail}`;
console.error(message);
return BibleApiResponse.error(message, ErrorType.BadApiResponse);
}
@ -55,7 +56,6 @@ export class BibleContentService {
// Otherwise, grab from the API
if (this.plugin.settings.downloadOnDemand) {
console.debug(`Pulling passage(s) (${ref}) from ESV API`)
const response = await this.esvApiService.downloadFromESVApi(ref);
if (response.isError()) {
return response
@ -64,20 +64,24 @@ export class BibleContentService {
return response;
}
} else {
console.error(`Settings "downloadOnDemand" is false. Skipping request to get passage ${ref}`);
return BibleApiResponse.error(`Settings prevent download of passage ${ref}`, ErrorType.RequestsForbidden)
console.error(`Settings "downloadOnDemand" is false. Skipping request to get passage ${ref.toString()}`);
return BibleApiResponse.error(`Settings prevent download of passage ${ref.toString()}`, ErrorType.RequestsForbidden)
}
}
// TODO - I would like this to go away once I normalize things a bit. This code is duplicated within ESVAPIService
private convertEsvApiResponseToGeneric(frontmatter: any, ref: BibleReference) {
const canonicalRef = BibleReference.parse(frontmatter.canonical);
private convertEsvApiResponseToGeneric(frontmatter: unknown, ref: BibleReference) {
const fm = frontmatter as { canonical?: unknown; passages?: unknown } | null;
const canonical = fm && typeof fm.canonical === 'string' ? fm.canonical : null;
const canonicalRef = canonical ? BibleReference.parse(canonical) : null;
if (!canonicalRef) {
const message = `Failed to parse canonical reference (${frontmatter.canonical}) from ESV API for ${ref.toString()}`;
const message = `Failed to parse canonical reference (${String(canonical)}) from ESV API for ${ref.toString()}`;
console.error(message);
return BibleApiResponse.error(message, ErrorType.BadApiResponse);
}
return BibleApiResponse.success(new BiblePassage(canonicalRef, frontmatter.passages[0]));
const passages = fm && Array.isArray(fm.passages) ? (fm.passages as unknown[]) : [];
const html = typeof passages[0] === 'string' ? passages[0] : '';
return BibleApiResponse.success(new BiblePassage(canonicalRef, html));
}
private getCachedRef(ref: BibleReference): BiblePassage | undefined {

View file

@ -10,7 +10,7 @@ import {buildFrontmatterString, getCustomFrontmatterForReference} from "../utils
/**
* Interface for ESV API Response
*/
export interface ESVApiResponse {
export type ESVApiResponse = {
query: string;
canonical: string;
parsed: number[][];
@ -126,6 +126,10 @@ export class ESVApiService {
// Save the raw API response as a markdown note with frontmatter
const passage = BibleReference.parse(data.canonical);
if (!passage) {
console.error(`Failed to parse canonical reference from ESV API: ${data.canonical}`);
return;
}
const filePath = BibleFiles.pathForPassage(passage, this.plugin);
const customYaml = getCustomFrontmatterForReference(passage, this.plugin.settings);
const frontmatterBody = buildFrontmatterString(data, customYaml);

View file

@ -56,10 +56,10 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
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')
.setName('Display inline verses')
.setDesc('Enable rendering of inline Bible references (in `code blocks`).')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.displayInlineVerses)
@ -70,7 +70,7 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName('Display Full Passages')
.setName('Display full passages')
.setDesc('Enable rendering of full Bible passages (in ```bible code blocks).')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.displayFullPassages)
@ -81,7 +81,7 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName('Show Navigation for Verses')
.setName('Show navigation for verses')
.setDesc('Show chapter navigation when displaying specific verses (by default, navigation only shows for full chapters).')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showNavigationForVerses)
@ -92,7 +92,7 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName('Hide Footnotes')
.setName('Hide footnotes')
.setDesc('Hide footnotes in the displayed scripture.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.hideFootnotes)
@ -104,7 +104,7 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName('Hide Footnotes in Hover Previews')
.setName('Hide footnotes in hover previews')
.setDesc('Hide footnotes in hover preview popups.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.hideFootnotesInPreview)
@ -116,7 +116,7 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName('Style Preset')
.setName('Style preset')
.setDesc('Choose a predefined style preset for Bible content.')
.addDropdown(dropdown => {
// Add all theme presets
@ -133,7 +133,7 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
});
new Setting(containerEl)
.setName('Bible Text Font Size')
.setName('Bible text font size')
.setDesc('Set the font size for Bible verses and passages.')
.addDropdown(dropdown => dropdown
.addOption('80%', 'Smaller (80%)')
@ -149,10 +149,10 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
})
);
new Setting(containerEl).setName('Text Styling').setHeading();
new Setting(containerEl).setName('Text styling').setHeading();
new Setting(containerEl)
.setName('Words of Christ Color')
.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)')
@ -164,7 +164,7 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Verse Number Color')
.setName('Verse number color')
.setDesc('Set the color for verse numbers.')
.addText(text => text
.setPlaceholder('var(--text-accent)')
@ -176,7 +176,7 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Heading Color')
.setName('Heading color')
.setDesc('Set the color for Bible passage headings.')
.addText(text => text
.setPlaceholder('var(--text-accent)')
@ -188,7 +188,7 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Block Indentation')
.setName('Block indentation')
.setDesc('Set the indentation for block sections.')
.addText(text => text
.setPlaceholder('2em')
@ -202,7 +202,7 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
new Setting(containerEl).setName('Bible').setHeading();
new Setting(containerEl)
.setName('Preferred Bible Version')
.setName('Preferred Bible version')
.setDesc('Select your preferred Bible version (only ESV currently supported).')
.addDropdown(dropdown => dropdown
.addOption('ESV', 'English Standard Version (ESV)')
@ -214,7 +214,7 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName('Bible Content Vault Path')
.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')
@ -224,7 +224,7 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));
new Setting(containerEl).setName('Note Frontmatter').setHeading();
new Setting(containerEl).setName('Note frontmatter').setHeading();
const fmInfoDiv = containerEl.createDiv({cls: 'disciples-journal-frontmatter-info'});
@ -234,10 +234,10 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
fmInfoDiv.createEl('p', {
text: 'Template variables -- use these in values to inject reference data:',
attr: {style: 'font-weight: bold; margin-bottom: 4px;'}
cls: 'disciples-journal-info-label'
});
const varList = fmInfoDiv.createEl('ul', {attr: {style: 'margin-top: 0; padding-left: 1.5em;'}});
const varList = fmInfoDiv.createEl('ul', {cls: 'disciples-journal-var-list'});
for (const v of TEMPLATE_VARIABLES) {
const li = varList.createEl('li');
li.createEl('code', {text: v.variable});
@ -245,7 +245,7 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
}
new Setting(containerEl)
.setName('Chapter Note Frontmatter')
.setName('Chapter note frontmatter')
.setDesc('Custom YAML frontmatter added to chapter-level notes (e.g., Genesis 1).')
.addTextArea(text => text
.setPlaceholder('tags:\n - bible/{{book}}\ntype: chapter')
@ -256,7 +256,7 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Passage Note Frontmatter')
.setName('Passage note frontmatter')
.setDesc('Custom YAML frontmatter added to passage-level notes (e.g., Genesis 1:5 or Genesis 1:5-10).')
.addTextArea(text => text
.setPlaceholder('tags:\n - bible/{{book}}\npassage: "{{reference}}"')
@ -276,7 +276,7 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
apiInfoDiv.createEl('p', {
text: 'To get a free ESV API token:',
attr: {style: 'font-weight: bold;'}
cls: 'disciples-journal-info-label'
});
const instructionsList = apiInfoDiv.createEl('ol');
@ -307,7 +307,7 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
});
new Setting(containerEl)
.setName('ESV API Token')
.setName('ESV API token')
.setDesc('Enter your ESV API token from api.esv.org.')
.addText(text => text
.setPlaceholder('Enter your ESV API token')
@ -321,7 +321,7 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Download on Demand')
.setName('Download on demand')
.setDesc('Enable automatic downloading of Bible content when requested.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.downloadOnDemand)
@ -333,7 +333,7 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
new Setting(containerEl)
.setDesc("Click this button to clear the cached bible files. Click this after upgrading from 0.7.0 or earlier. Data will be redownloaded on demand")
.addButton(btn => btn
.setButtonText("Clear Bible Data")
.setButtonText("Clear Bible data")
.onClick(async evt => {
await BibleFiles.clearData(this.plugin);
}));

2
src/types.d.ts vendored
View file

@ -1,5 +1,5 @@
declare module '*.json' {
const value: any;
const value: unknown;
export default value;
}

View file

@ -47,7 +47,7 @@ export function renderCustomFrontmatter(
if (!context) {
return rawYaml;
}
return rawYaml.replace(/\{\{(\w+)}}/g, (match, key) => {
return rawYaml.replace(/\{\{(\w+)}}/g, (match: string, key: string) => {
return key in context ? context[key] : match;
});
}
@ -56,25 +56,26 @@ export function renderCustomFrontmatter(
* Parse user-provided YAML, filtering out API-reserved keys.
* Returns null on empty input or parse failure.
*/
function parseCustomYaml(customYaml: string): Record<string, any> | null {
function parseCustomYaml(customYaml: string): Record<string, unknown> | null {
const trimmed = customYaml.trim();
if (!trimmed) {
return null;
}
try {
const parsed = parseYaml(trimmed);
const parsed = parseYaml(trimmed) as unknown;
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
console.warn('Disciples Journal: custom frontmatter must be a YAML mapping, ignoring.');
return null;
}
// Filter out API-reserved keys
const filtered: Record<string, any> = {};
for (const key of Object.keys(parsed)) {
const source = parsed as Record<string, unknown>;
const filtered: Record<string, unknown> = {};
for (const key of Object.keys(source)) {
if (API_KEYS.has(key)) {
console.warn(`Disciples Journal: custom frontmatter key "${key}" collides with API data, skipping.`);
continue;
}
filtered[key] = parsed[key];
filtered[key] = source[key];
}
return Object.keys(filtered).length > 0 ? filtered : null;
} catch (e) {
@ -87,7 +88,7 @@ function parseCustomYaml(customYaml: string): Record<string, any> | null {
* Merge cssclasses, ensuring PLUGIN_CSS_CLASS is always present.
* Handles string, array, or undefined inputs from both sides.
*/
function mergeCssClasses(existing: any, custom: any): string[] {
function mergeCssClasses(existing: unknown, custom: unknown): string[] {
const classes = new Set<string>();
classes.add(PLUGIN_CSS_CLASS);
@ -107,7 +108,7 @@ function mergeCssClasses(existing: any, custom: any): string[] {
* Returns the YAML content without the --- delimiters.
*/
export function buildFrontmatterString(
apiData: Record<string, any>,
apiData: Record<string, unknown>,
customYaml: string
): string {
// Separate cssclasses from API data so we can place it last
@ -115,8 +116,8 @@ export function buildFrontmatterString(
const customData = parseCustomYaml(customYaml);
// Extract cssclasses from custom data if present
let customCss: any = undefined;
const customFields: Record<string, any> = {};
let customCss: unknown = undefined;
const customFields: Record<string, unknown> = {};
if (customData) {
for (const [key, value] of Object.entries(customData)) {
if (key === 'cssclasses') {
@ -128,7 +129,7 @@ export function buildFrontmatterString(
}
// Build the combined object: API fields, then custom fields, then cssclasses last
const combined: Record<string, any> = {...apiFields};
const combined: Record<string, unknown> = {...apiFields};
for (const [key, value] of Object.entries(customFields)) {
combined[key] = value;
}
@ -150,21 +151,22 @@ export function mergeCustomFrontmatterIntoExisting(
return null;
}
let existing: Record<string, any>;
let existing: Record<string, unknown>;
try {
existing = parseYaml(existingFrontmatter);
if (existing === null || typeof existing !== 'object' || Array.isArray(existing)) {
const parsedExisting = parseYaml(existingFrontmatter) as unknown;
if (parsedExisting === null || typeof parsedExisting !== 'object' || Array.isArray(parsedExisting)) {
console.warn('Disciples Journal: existing frontmatter is not a valid YAML mapping, skipping merge.');
return null;
}
existing = parsedExisting as Record<string, unknown>;
} catch (e) {
console.warn('Disciples Journal: failed to parse existing frontmatter, skipping merge.', e);
return null;
}
// Extract cssclasses from custom data if present
let customCss: any = undefined;
const customFields: Record<string, any> = {};
let customCss: unknown = undefined;
const customFields: Record<string, unknown> = {};
for (const [key, value] of Object.entries(customData)) {
if (key === 'cssclasses') {
customCss = value;

View file

@ -504,3 +504,24 @@ button.clear-input-button {
.inline-bible-reference .inline-bible-reference-text { /* Target the text part */
font-size: var(--dj-font-size); /* Keep original font size variable for now, or switch to --dj-font-size if desired */
}
/* Settings: frontmatter/API info blocks */
.disciples-journal-info-label {
font-weight: var(--font-bold);
margin-bottom: var(--size-4-1);
}
.disciples-journal-var-list {
margin-top: 0;
padding-left: var(--size-4-6);
}
/* Book search input (navigation) */
.disciples-journal-book-suggest-wrapper {
position: relative;
display: inline-block;
}
.disciples-journal-book-suggest-input {
padding-right: 1.5em;
}

View file

@ -1,3 +1,4 @@
{
"1.0.0": "0.15.0"
"0.12.0": "0.15.0",
"0.13.0": "1.6.6"
}