From 6fb1b08ea3eca0b2c5afa47a624b5bb5b4ff759f Mon Sep 17 00:00:00 2001 From: Scott Tomaszewski Date: Tue, 2 Jun 2026 22:13:53 -0400 Subject: [PATCH] feat: wrapPassageVerses wraps each verse in a .dj-verse span --- src/components/VerseWrapper.ts | 41 ++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/components/VerseWrapper.ts diff --git a/src/components/VerseWrapper.ts b/src/components/VerseWrapper.ts new file mode 100644 index 0000000..866280d --- /dev/null +++ b/src/components/VerseWrapper.ts @@ -0,0 +1,41 @@ +import { parseVerseId } from "../utils/VerseId"; + +/** + * Post-process a rendered ESV passage so each verse's inline text is wrapped in a + * ``, making verses selectable and + * highlightable. Idempotent: skips a passage that's already wrapped. + * + * ESV HTML marks verses only with ``; + * a verse's text runs as loose nodes until the next marker, sometimes across

+ * boundaries — so we wrap per block element and tag spans with the verse number. + */ +export function wrapPassageVerses(passageEl: HTMLElement): void { + if (passageEl.querySelector(".dj-verse")) return; + const doc = passageEl.doc; + + const blocks = passageEl.querySelectorAll("p, li"); + blocks.forEach((block) => { + let current: HTMLSpanElement | null = null; + // Snapshot child nodes first; we re-parent them as we go. + const nodes = Array.from(block.childNodes); + for (const node of nodes) { + const marker = + node.instanceOf(HTMLElement) && (node.hasClass("verse-num") || node.hasClass("chapter-num")) + ? parseVerseId(node.id) + : null; + + if (marker) { + current = doc.createElement("span"); + current.addClass("dj-verse"); + current.dataset.chapter = String(marker.chapter); + current.dataset.verse = String(marker.verse); + block.insertBefore(current, node); + } + + if (current) { + current.appendChild(node); // moves node out of block into the span + } + // Nodes before the first marker (rare) stay where they are. + } + }); +}