Render ESV HTML via sanitizeHTMLToDom instead of innerHTML

Resolves FOLLOWUP.md #3. All four sites in BibleReferenceRenderer that
assigned the ESV API response to innerHTML now run it through Obsidian's
sanitizeHTMLToDom() and append the resulting DocumentFragment, dropping
the inline no-unsanitized/property and no-inner-html eslint-disable
comments. The verse-preview extraction queries/clones paragraphs off the
sanitized fragment instead of a temporary innerHTML div.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scott Tomaszewski 2026-05-31 11:47:03 -04:00
parent d01fc5f202
commit 5813205586
3 changed files with 21 additions and 20 deletions

View file

@ -1,5 +1,9 @@
# Changelog
## Unreleased
- Renders ESV passage HTML through Obsidian's `sanitizeHTMLToDom()` and appends the resulting fragment, instead of assigning the raw API response to `innerHTML`
## 0.13.0
- Brings the plugin in line with current Obsidian community standards

View file

@ -65,18 +65,20 @@ callbacks reuse the shared instance instead of constructing new ones. The file-l
---
## 3. Rendering ESV HTML via `innerHTML`
## 3. Rendering ESV HTML via `innerHTML` — ✅ RESOLVED
**File:** `src/components/BibleReferenceRenderer.ts` (4 sites)
**What's wrong:** passage HTML returned by the ESV API is injected with
**What was wrong:** passage HTML returned by the ESV API was 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.
`@microsoft/sdl/no-inner-html`.
**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.
**Fix applied:** all four sites now run the passage HTML through Obsidian's
`sanitizeHTMLToDom()` and append the returned `DocumentFragment`, so no raw HTML
string is written to the DOM. The verse-preview extraction (`showVersePreview`)
parses into the sanitized fragment and queries/clones paragraphs off of it
instead of a temporary `innerHTML` div. The inline `eslint-disable` comments at
each site were removed.
---

View file

@ -1,4 +1,4 @@
import {Notice} from "obsidian";
import {Notice, sanitizeHTMLToDom} from "obsidian";
import {BibleContentService} from "../services/BibleContentService";
import {BibleNavigation} from "./BibleNavigation";
import DisciplesJournalPlugin from "src/core/DisciplesJournalPlugin";
@ -133,8 +133,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;
passageEl.appendChild(sanitizeHTMLToDom(response.passage.html));
containerEl.appendChild(passageEl);
el.appendChild(containerEl);
@ -187,18 +186,16 @@ export class BibleReferenceRenderer {
// Use the HTML content directly, but try to extract just the portion we need
// for the preview (to avoid showing footnotes, chapter headings, etc.)
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;
// Parse and sanitize the HTML into a detached fragment we can inspect
const parsed = sanitizeHTMLToDom(passage.html);
// Remove footnote sections before extracting paragraphs
if (this.plugin.settings.hideFootnotesInPreview) {
tempEl.querySelectorAll('.footnotes, .extra_text').forEach(el => el.remove());
parsed.querySelectorAll('.footnotes, .extra_text').forEach(el => el.remove());
}
// Find and extract the main verse content (paragraphs)
const paragraphs = tempEl.querySelectorAll('p:not(.extra_text)');
const paragraphs = parsed.querySelectorAll('p:not(.extra_text)');
if (paragraphs.length > 0) {
for (let i = 0; i < paragraphs.length; i++) {
const cloned = paragraphs[i].cloneNode(true);
@ -213,13 +210,11 @@ 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;
contentEl.appendChild(sanitizeHTMLToDom(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;
contentEl.appendChild(sanitizeHTMLToDom(passage.html));
}
versePreviewEl.appendChild(contentEl);