@youversion/platform-react-ui 1.20.2 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -29,6 +29,247 @@ import {
29
29
  import { useControllableState } from "@radix-ui/react-use-controllable-state";
30
30
  import { useBooks, useTheme } from "@youversion/platform-react-hooks";
31
31
 
32
+ // ../core/dist/chunk-2Z2S2WY3.js
33
+ var NON_BREAKING_SPACE = "\xA0";
34
+ var FOOTNOTE_KEY_ATTR = "data-footnote-key";
35
+ var NEEDS_SPACE_BEFORE = /^[^\s.,;:!?)}\]'"'»›]/;
36
+ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
37
+ "DIV",
38
+ "P",
39
+ "SPAN",
40
+ "SUP",
41
+ "SUB",
42
+ "EM",
43
+ "STRONG",
44
+ "I",
45
+ "B",
46
+ "SMALL",
47
+ "BR",
48
+ "SECTION",
49
+ "TABLE",
50
+ "THEAD",
51
+ "TBODY",
52
+ "TR",
53
+ "TD",
54
+ "TH"
55
+ ]);
56
+ var DROP_ENTIRELY_TAGS = /* @__PURE__ */ new Set([
57
+ "SCRIPT",
58
+ "STYLE",
59
+ "IFRAME",
60
+ "OBJECT",
61
+ "EMBED",
62
+ "SVG",
63
+ "MATH",
64
+ "FORM",
65
+ "INPUT",
66
+ "BUTTON",
67
+ "TEXTAREA",
68
+ "SELECT",
69
+ "TEMPLATE",
70
+ "LINK",
71
+ "META",
72
+ "BASE",
73
+ "NOSCRIPT"
74
+ ]);
75
+ var ALLOWED_ATTRS = /* @__PURE__ */ new Set(["class", "v", "colspan", "rowspan", "dir", "usfm"]);
76
+ function sanitizeBibleHtmlDocument(doc) {
77
+ const root = doc.body ?? doc.documentElement;
78
+ for (const el of Array.from(root.querySelectorAll("*"))) {
79
+ const tag = el.tagName;
80
+ if (DROP_ENTIRELY_TAGS.has(tag)) {
81
+ el.remove();
82
+ continue;
83
+ }
84
+ if (!ALLOWED_TAGS.has(tag)) {
85
+ el.replaceWith(...Array.from(el.childNodes));
86
+ continue;
87
+ }
88
+ for (const attr of Array.from(el.attributes)) {
89
+ const name = attr.name.toLowerCase();
90
+ if (name.startsWith("on")) {
91
+ el.removeAttribute(attr.name);
92
+ continue;
93
+ }
94
+ if (!ALLOWED_ATTRS.has(name) && !name.startsWith("data-")) {
95
+ el.removeAttribute(attr.name);
96
+ }
97
+ }
98
+ }
99
+ }
100
+ function wrapVerseContent(doc) {
101
+ function wrapParagraphContent(doc2, paragraph, verseNum) {
102
+ const children = Array.from(paragraph.childNodes);
103
+ if (children.length === 0) return;
104
+ const wrapper = doc2.createElement("span");
105
+ wrapper.className = "yv-v";
106
+ wrapper.setAttribute("v", verseNum);
107
+ const firstChild = children[0];
108
+ if (firstChild) {
109
+ paragraph.insertBefore(wrapper, firstChild);
110
+ }
111
+ children.forEach((child) => {
112
+ wrapper.appendChild(child);
113
+ });
114
+ }
115
+ function wrapParagraphsUntilBoundary(doc2, verseNum, startParagraph, endParagraph) {
116
+ if (!startParagraph) return;
117
+ let currentParagraph = startParagraph.nextElementSibling;
118
+ while (currentParagraph && currentParagraph !== endParagraph) {
119
+ const isHeading = currentParagraph.classList.contains("yv-h") || currentParagraph.matches(
120
+ ".s1, .s2, .s3, .s4, .ms, .ms1, .ms2, .ms3, .ms4, .mr, .sp, .sr, .qa, .r"
121
+ );
122
+ if (isHeading) {
123
+ currentParagraph = currentParagraph.nextElementSibling;
124
+ continue;
125
+ }
126
+ if (currentParagraph.querySelector(".yv-v[v]")) break;
127
+ if (currentParagraph.classList.contains("p") || currentParagraph.tagName === "P") {
128
+ wrapParagraphContent(doc2, currentParagraph, verseNum);
129
+ }
130
+ currentParagraph = currentParagraph.nextElementSibling;
131
+ }
132
+ }
133
+ function handleParagraphWrapping(doc2, currentParagraph, nextParagraph, verseNum) {
134
+ if (!currentParagraph) return;
135
+ if (!nextParagraph) {
136
+ wrapParagraphsUntilBoundary(doc2, verseNum, currentParagraph);
137
+ return;
138
+ }
139
+ if (currentParagraph !== nextParagraph) {
140
+ wrapParagraphsUntilBoundary(doc2, verseNum, currentParagraph, nextParagraph);
141
+ }
142
+ }
143
+ function processVerseMarker(marker, index, markers) {
144
+ const verseNum = marker.getAttribute("v");
145
+ if (!verseNum) return;
146
+ const nextMarker = markers[index + 1];
147
+ const nodesToWrap = collectNodesBetweenMarkers(marker, nextMarker);
148
+ if (nodesToWrap.length === 0) return;
149
+ const currentParagraph = marker.closest(".p, p, div.p");
150
+ const nextParagraph = nextMarker?.closest(".p, p, div.p") || null;
151
+ const doc2 = marker.ownerDocument;
152
+ wrapNodesInVerse(marker, verseNum, nodesToWrap);
153
+ handleParagraphWrapping(doc2, currentParagraph, nextParagraph, verseNum);
154
+ }
155
+ function wrapNodesInVerse(marker, verseNum, nodes) {
156
+ const wrapper = marker.ownerDocument.createElement("span");
157
+ wrapper.className = "yv-v";
158
+ wrapper.setAttribute("v", verseNum);
159
+ const firstNode = nodes[0];
160
+ if (firstNode) {
161
+ marker.parentNode?.insertBefore(wrapper, firstNode);
162
+ }
163
+ nodes.forEach((node) => {
164
+ wrapper.appendChild(node);
165
+ });
166
+ marker.remove();
167
+ }
168
+ function shouldStopCollecting(node, endMarker) {
169
+ if (node === endMarker) return true;
170
+ if (endMarker && node.nodeType === 1 && node.contains(endMarker)) return true;
171
+ return false;
172
+ }
173
+ function shouldSkipNode(node) {
174
+ return node.nodeType === 1 && node.classList.contains("yv-h");
175
+ }
176
+ function collectNodesBetweenMarkers(startMarker, endMarker) {
177
+ const nodes = [];
178
+ let current = startMarker.nextSibling;
179
+ while (current && !shouldStopCollecting(current, endMarker)) {
180
+ if (shouldSkipNode(current)) {
181
+ current = current.nextSibling;
182
+ continue;
183
+ }
184
+ nodes.push(current);
185
+ current = current.nextSibling;
186
+ }
187
+ return nodes;
188
+ }
189
+ const verseMarkers = Array.from(doc.querySelectorAll(".yv-v[v]"));
190
+ verseMarkers.forEach(processVerseMarker);
191
+ }
192
+ function assignFootnoteKeys(doc) {
193
+ let introIdx = 0;
194
+ doc.querySelectorAll(".yv-n.f").forEach((fn) => {
195
+ const verseNum = fn.closest(".yv-v[v]")?.getAttribute("v");
196
+ fn.setAttribute(FOOTNOTE_KEY_ATTR, verseNum ?? `intro-${introIdx++}`);
197
+ });
198
+ }
199
+ function replaceFootnotesWithAnchors(doc, footnotes) {
200
+ for (const fn of footnotes) {
201
+ const key = fn.getAttribute(FOOTNOTE_KEY_ATTR);
202
+ if (!key) continue;
203
+ const prev = fn.previousSibling;
204
+ const next = fn.nextSibling;
205
+ const prevText = prev?.textContent ?? "";
206
+ const nextText = next?.textContent ?? "";
207
+ const prevNeedsSpace = prevText.length > 0 && !/\s$/.test(prevText);
208
+ const nextNeedsSpace = nextText.length > 0 && NEEDS_SPACE_BEFORE.test(nextText);
209
+ if (prevNeedsSpace && nextNeedsSpace && fn.parentNode) {
210
+ fn.parentNode.insertBefore(doc.createTextNode(" "), fn);
211
+ }
212
+ const anchor = doc.createElement("span");
213
+ anchor.setAttribute("data-verse-footnote", key);
214
+ anchor.setAttribute("data-verse-footnote-content", fn.innerHTML);
215
+ fn.replaceWith(anchor);
216
+ }
217
+ }
218
+ function addNbspToVerseLabels(doc) {
219
+ doc.querySelectorAll(".yv-vlbl").forEach((label) => {
220
+ const text = label.textContent || "";
221
+ if (!text.endsWith(NON_BREAKING_SPACE)) {
222
+ label.textContent = text + NON_BREAKING_SPACE;
223
+ }
224
+ });
225
+ }
226
+ function fixIrregularTables(doc) {
227
+ doc.querySelectorAll("table").forEach((table) => {
228
+ const rows = table.querySelectorAll("tr");
229
+ if (rows.length === 0) return;
230
+ let maxColumns = 0;
231
+ rows.forEach((row) => {
232
+ let count = 0;
233
+ row.querySelectorAll("td, th").forEach((cell) => {
234
+ count += parseInt(cell.getAttribute("colspan") || "1", 10);
235
+ });
236
+ maxColumns = Math.max(maxColumns, count);
237
+ });
238
+ if (maxColumns > 1) {
239
+ rows.forEach((row) => {
240
+ const cells = row.querySelectorAll("td, th");
241
+ if (cells.length === 1) {
242
+ const existing = parseInt(cells[0].getAttribute("colspan") || "1", 10);
243
+ if (existing < maxColumns) {
244
+ cells[0].setAttribute("colspan", maxColumns.toString());
245
+ }
246
+ }
247
+ });
248
+ }
249
+ });
250
+ }
251
+ function transformBibleHtml(html, options) {
252
+ const doc = options.parseHtml(html);
253
+ sanitizeBibleHtmlDocument(doc);
254
+ wrapVerseContent(doc);
255
+ assignFootnoteKeys(doc);
256
+ const footnotes = Array.from(doc.querySelectorAll(".yv-n.f"));
257
+ replaceFootnotesWithAnchors(doc, footnotes);
258
+ addNbspToVerseLabels(doc);
259
+ fixIrregularTables(doc);
260
+ const transformedHtml = options.serializeHtml(doc);
261
+ return { html: transformedHtml };
262
+ }
263
+ function transformBibleHtmlForBrowser(html) {
264
+ if (typeof globalThis.DOMParser === "undefined") {
265
+ throw new Error("DOMParser is required to transform Bible HTML in browser environments");
266
+ }
267
+ return transformBibleHtml(html, {
268
+ parseHtml: (h) => new DOMParser().parseFromString(h, "text/html"),
269
+ serializeHtml: (doc) => doc.body.innerHTML
270
+ });
271
+ }
272
+
32
273
  // ../../node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/external.js
33
274
  var external_exports = {};
34
275
  __export(external_exports, {
@@ -13251,12 +13492,18 @@ var BibleClient = (_a = class {
13251
13492
  /**
13252
13493
  * Fetches a passage (range of verses) from the Bible using the passages endpoint.
13253
13494
  * This is the new API format that returns HTML-formatted content.
13495
+ *
13496
+ * Note: The HTML returned from the API contains inline footnote content that should
13497
+ * be transformed before rendering. Use `transformBibleHtml()` or
13498
+ * `transformBibleHtmlForBrowser()` to clean up the HTML and extract footnotes.
13499
+ *
13254
13500
  * @param versionId The version ID.
13255
13501
  * @param usfm The USFM reference (e.g., "JHN.3.1-2", "GEN.1", "JHN.3.16").
13256
13502
  * @param format The format to return ("html" or "text", default: "html").
13257
13503
  * @param include_headings Whether to include headings in the content.
13258
13504
  * @param include_notes Whether to include notes in the content.
13259
13505
  * @returns The requested BiblePassage object with HTML content.
13506
+ *
13260
13507
  * @example
13261
13508
  * ```ts
13262
13509
  * // Get a single verse
@@ -13267,6 +13514,10 @@ var BibleClient = (_a = class {
13267
13514
  *
13268
13515
  * // Get an entire chapter
13269
13516
  * const chapter = await bibleClient.getPassage(3034, "GEN.1");
13517
+ *
13518
+ * // Transform HTML before rendering
13519
+ * const passage = await bibleClient.getPassage(3034, "JHN.3.16", "html", true, true);
13520
+ * const transformed = transformBibleHtmlForBrowser(passage.content);
13270
13521
  * ```
13271
13522
  */
13272
13523
  async getPassage(versionId, usfm, format = "html", include_headings, include_notes) {
@@ -15716,8 +15967,11 @@ var Footnote = (props) => /* @__PURE__ */ jsx21(
15716
15967
  );
15717
15968
 
15718
15969
  // src/lib/verse-html-utils.ts
15719
- import DOMPurify from "isomorphic-dompurify";
15720
- var NON_BREAKING_SPACE = "\xA0";
15970
+ var INTER_FONT = '"Inter", sans-serif';
15971
+ var SOURCE_SERIF_FONT = '"Source Serif 4", serif';
15972
+
15973
+ // src/components/verse.tsx
15974
+ import { Fragment as Fragment2, jsx as jsx22, jsxs as jsxs7 } from "react/jsx-runtime";
15721
15975
  var LETTERS = "abcdefghijklmnopqrstuvwxyz";
15722
15976
  function getFootnoteMarker(index) {
15723
15977
  const base = LETTERS.length;
@@ -15730,238 +15984,34 @@ function getFootnoteMarker(index) {
15730
15984
  } while (value >= 0);
15731
15985
  return marker;
15732
15986
  }
15733
- var INTER_FONT = '"Inter", sans-serif';
15734
- var SOURCE_SERIF_FONT = '"Source Serif 4", serif';
15735
- function wrapVerseContent(doc) {
15736
- function wrapParagraphContent(doc2, paragraph, verseNum) {
15737
- const children = Array.from(paragraph.childNodes);
15738
- if (children.length === 0) return;
15739
- const wrapper = doc2.createElement("span");
15740
- wrapper.className = "yv-v";
15741
- wrapper.setAttribute("v", verseNum);
15742
- const firstChild = children[0];
15743
- if (firstChild) {
15744
- paragraph.insertBefore(wrapper, firstChild);
15745
- }
15746
- children.forEach((child) => {
15747
- wrapper.appendChild(child);
15748
- });
15749
- }
15750
- function wrapParagraphsUntilBoundary(doc2, verseNum, startParagraph, endParagraph) {
15751
- if (!startParagraph) return;
15752
- let currentP = startParagraph.nextElementSibling;
15753
- while (currentP && currentP !== endParagraph) {
15754
- const isHeading = currentP.classList.contains("yv-h") || currentP.matches(".s1, .s2, .s3, .s4, .ms, .ms1, .ms2, .ms3, .ms4, .mr, .sp, .sr, .qa, .r");
15755
- if (isHeading) {
15756
- currentP = currentP.nextElementSibling;
15757
- continue;
15758
- }
15759
- if (currentP.querySelector(".yv-v[v]")) break;
15760
- if (currentP.classList.contains("p") || currentP.tagName === "P") {
15761
- wrapParagraphContent(doc2, currentP, verseNum);
15762
- }
15763
- currentP = currentP.nextElementSibling;
15764
- }
15765
- }
15766
- function handleParagraphWrapping(doc2, currentParagraph, nextParagraph, verseNum) {
15767
- if (!currentParagraph) return;
15768
- if (!nextParagraph) {
15769
- wrapParagraphsUntilBoundary(doc2, verseNum, currentParagraph);
15770
- return;
15771
- }
15772
- if (currentParagraph !== nextParagraph) {
15773
- wrapParagraphsUntilBoundary(doc2, verseNum, currentParagraph, nextParagraph);
15774
- }
15775
- }
15776
- function processVerseMarker(marker, index, markers) {
15777
- const verseNum = marker.getAttribute("v");
15778
- if (!verseNum) return;
15779
- const nextMarker = markers[index + 1];
15780
- const nodesToWrap = collectNodesBetweenMarkers(marker, nextMarker);
15781
- if (nodesToWrap.length === 0) return;
15782
- const currentParagraph = marker.closest(".p, p, div.p");
15783
- const nextParagraph = nextMarker?.closest(".p, p, div.p") || null;
15784
- const doc2 = marker.ownerDocument;
15785
- wrapNodesInVerse(marker, verseNum, nodesToWrap);
15786
- handleParagraphWrapping(doc2, currentParagraph, nextParagraph, verseNum);
15787
- }
15788
- function wrapNodesInVerse(marker, verseNum, nodes) {
15789
- const wrapper = marker.ownerDocument.createElement("span");
15790
- wrapper.className = "yv-v";
15791
- wrapper.setAttribute("v", verseNum);
15792
- const firstNode = nodes[0];
15793
- if (firstNode) {
15794
- marker.parentNode?.insertBefore(wrapper, firstNode);
15795
- }
15796
- nodes.forEach((node) => {
15797
- wrapper.appendChild(node);
15798
- });
15799
- marker.remove();
15800
- }
15801
- function shouldStopCollecting(node, endMarker) {
15802
- if (node === endMarker) return true;
15803
- if (endMarker && node instanceof Element && node.contains(endMarker)) return true;
15804
- return false;
15805
- }
15806
- function shouldSkipNode(node) {
15807
- return node instanceof Element && node.classList.contains("yv-h");
15808
- }
15809
- function collectNodesBetweenMarkers(startMarker, endMarker) {
15810
- const nodes = [];
15811
- let current = startMarker.nextSibling;
15812
- while (current && !shouldStopCollecting(current, endMarker)) {
15813
- if (shouldSkipNode(current)) {
15814
- current = current.nextSibling;
15815
- continue;
15816
- }
15817
- nodes.push(current);
15818
- current = current.nextSibling;
15819
- }
15820
- return nodes;
15821
- }
15822
- const verseMarkers = Array.from(doc.querySelectorAll(".yv-v[v]"));
15823
- verseMarkers.forEach(processVerseMarker);
15824
- }
15825
- var NEEDS_SPACE_BEFORE = /^[^\s.,;:!?)}\]'"»›]/;
15826
- function buildVerseHtml(wrappers) {
15987
+ function getVerseHtmlFromDom(container, verseNum) {
15988
+ const wrappers = container.querySelectorAll(`.yv-v[v="${verseNum}"]`);
15989
+ if (!wrappers.length) return "";
15827
15990
  const parts = [];
15828
15991
  let noteIdx = 0;
15829
- for (let i = 0; i < wrappers.length; i++) {
15992
+ wrappers.forEach((wrapper, i) => {
15830
15993
  if (i > 0) parts.push(" ");
15831
- const clone2 = wrappers[i].cloneNode(true);
15832
- const ownerDoc = wrappers[i].ownerDocument;
15994
+ const clone2 = wrapper.cloneNode(true);
15833
15995
  clone2.querySelectorAll(".yv-h, .yv-vlbl").forEach((el) => el.remove());
15834
- clone2.querySelectorAll(".yv-n.f").forEach((fn) => {
15835
- const marker = ownerDoc.createElement("sup");
15836
- marker.className = "yv:text-muted-foreground";
15837
- marker.textContent = getFootnoteMarker(noteIdx++);
15838
- fn.replaceWith(marker);
15996
+ clone2.querySelectorAll("[data-verse-footnote]").forEach((anchor) => {
15997
+ const sup = wrapper.ownerDocument.createElement("sup");
15998
+ sup.className = "yv:text-muted-foreground";
15999
+ sup.textContent = getFootnoteMarker(noteIdx++);
16000
+ anchor.replaceWith(sup);
15839
16001
  });
15840
16002
  parts.push(clone2.innerHTML);
15841
- }
15842
- return parts.join("");
15843
- }
15844
- var FOOTNOTE_KEY_ATTR = "data-footnote-key";
15845
- function assignFootnoteKeys(doc) {
15846
- let introIdx = 0;
15847
- doc.querySelectorAll(".yv-n.f").forEach((fn) => {
15848
- const verseNum = fn.closest(".yv-v[v]")?.getAttribute("v");
15849
- fn.setAttribute(FOOTNOTE_KEY_ATTR, verseNum ?? `intro-${introIdx++}`);
15850
16003
  });
16004
+ return parts.join("");
15851
16005
  }
15852
- function replaceFootnotesWithAnchors(doc, footnotes) {
15853
- for (const fn of footnotes) {
15854
- const key = fn.getAttribute(FOOTNOTE_KEY_ATTR);
15855
- const prev = fn.previousSibling;
15856
- const next = fn.nextSibling;
15857
- const prevText = prev?.textContent ?? "";
15858
- const nextText = next?.textContent ?? "";
15859
- const prevNeedsSpace = prevText.length > 0 && !/\s$/.test(prevText);
15860
- const nextNeedsSpace = nextText.length > 0 && NEEDS_SPACE_BEFORE.test(nextText);
15861
- if (prevNeedsSpace && nextNeedsSpace && fn.parentNode) {
15862
- fn.parentNode.insertBefore(doc.createTextNode(" "), fn);
15863
- }
15864
- const anchor = doc.createElement("span");
15865
- anchor.setAttribute("data-verse-footnote", key);
15866
- fn.replaceWith(anchor);
15867
- }
15868
- }
15869
- function extractNotesFromWrappedHtml(doc) {
15870
- const footnotes = Array.from(doc.querySelectorAll(".yv-n.f"));
15871
- if (!footnotes.length) return {};
15872
- const footnotesByKey = /* @__PURE__ */ new Map();
15873
- for (const fn of footnotes) {
15874
- const key = fn.getAttribute(FOOTNOTE_KEY_ATTR);
15875
- let arr = footnotesByKey.get(key);
15876
- if (!arr) {
15877
- arr = [];
15878
- footnotesByKey.set(key, arr);
15879
- }
15880
- arr.push(fn);
15881
- }
15882
- const wrappersByVerse = /* @__PURE__ */ new Map();
15883
- doc.querySelectorAll(".yv-v[v]").forEach((el) => {
15884
- const verseNum = el.getAttribute("v");
15885
- if (!verseNum) return;
15886
- const arr = wrappersByVerse.get(verseNum);
15887
- if (arr) arr.push(el);
15888
- else wrappersByVerse.set(verseNum, [el]);
15889
- });
15890
- const notes = {};
15891
- for (const [key, fns] of footnotesByKey) {
15892
- const wrappers = wrappersByVerse.get(key);
15893
- notes[key] = {
15894
- verseHtml: wrappers ? buildVerseHtml(wrappers) : "",
15895
- notes: fns.map((fn) => fn.innerHTML),
15896
- hasVerseContext: !!wrappers
15897
- };
15898
- }
15899
- replaceFootnotesWithAnchors(doc, footnotes);
15900
- return notes;
15901
- }
15902
- function addNbspToVerseLabels(doc) {
15903
- doc.querySelectorAll(".yv-vlbl").forEach((label) => {
15904
- const text = label.textContent || "";
15905
- if (!text.endsWith(NON_BREAKING_SPACE)) {
15906
- label.textContent = text + NON_BREAKING_SPACE;
15907
- }
15908
- });
15909
- }
15910
- function fixIrregularTables(doc) {
15911
- doc.querySelectorAll("table").forEach((table) => {
15912
- const rows = table.querySelectorAll("tr");
15913
- if (rows.length === 0) return;
15914
- let maxColumns = 0;
15915
- rows.forEach((row) => {
15916
- let count = 0;
15917
- row.querySelectorAll("td, th").forEach((cell) => {
15918
- count += cell instanceof HTMLTableCellElement ? parseInt(cell.getAttribute("colspan") || "1", 10) : 1;
15919
- });
15920
- maxColumns = Math.max(maxColumns, count);
15921
- });
15922
- if (maxColumns > 1) {
15923
- rows.forEach((row) => {
15924
- const cells = row.querySelectorAll("td, th");
15925
- if (cells.length === 1 && cells[0] instanceof HTMLTableCellElement) {
15926
- const existing = parseInt(cells[0].getAttribute("colspan") || "1", 10);
15927
- if (existing < maxColumns) {
15928
- cells[0].setAttribute("colspan", maxColumns.toString());
15929
- }
15930
- }
15931
- });
15932
- }
15933
- });
15934
- }
15935
- var DOMPURIFY_CONFIG = {
15936
- ALLOWED_ATTR: ["class", "style", "id", "v", "usfm"],
15937
- ALLOW_DATA_ATTR: true
15938
- };
15939
- function transformBibleHtml(html) {
15940
- if (typeof window === "undefined" || !("DOMParser" in window)) {
15941
- return { html, notes: {} };
15942
- }
15943
- const doc = new DOMParser().parseFromString(
15944
- DOMPurify.sanitize(html, DOMPURIFY_CONFIG),
15945
- "text/html"
15946
- );
15947
- wrapVerseContent(doc);
15948
- assignFootnoteKeys(doc);
15949
- const notes = extractNotesFromWrappedHtml(doc);
15950
- addNbspToVerseLabels(doc);
15951
- fixIrregularTables(doc);
15952
- return { html: doc.body.innerHTML, notes };
15953
- }
15954
-
15955
- // src/components/verse.tsx
15956
- import { Fragment as Fragment2, jsx as jsx22, jsxs as jsxs7 } from "react/jsx-runtime";
15957
16006
  var VerseFootnoteButton = memo(function VerseFootnoteButton2({
15958
16007
  verseNum,
15959
- verseNotes,
16008
+ notes,
16009
+ verseHtml,
16010
+ hasVerseContext,
15960
16011
  reference,
15961
16012
  fontSize,
15962
16013
  theme
15963
16014
  }) {
15964
- const { hasVerseContext } = verseNotes;
15965
16015
  const verseReference = reference ? `${reference}:${verseNum}` : `Verse ${verseNum}`;
15966
16016
  return /* @__PURE__ */ jsxs7(Popover, { children: [
15967
16017
  /* @__PURE__ */ jsx22(PopoverTrigger, { "data-yv-sdk": true, "data-yv-theme": theme, asChild: true, children: /* @__PURE__ */ jsx22(
@@ -15986,11 +16036,11 @@ var VerseFootnoteButton = memo(function VerseFootnoteButton2({
15986
16036
  {
15987
16037
  className: "yv:mb-3 yv:font-serif yv:*:font-serif",
15988
16038
  style: { fontSize: fontSize ? `${fontSize}px` : "1.25rem" },
15989
- dangerouslySetInnerHTML: { __html: verseNotes.verseHtml }
16039
+ dangerouslySetInnerHTML: { __html: verseHtml }
15990
16040
  }
15991
16041
  )
15992
16042
  ] }),
15993
- /* @__PURE__ */ jsx22("ul", { className: "yv:list-none yv:p-0 yv:m-0 yv:space-y-1", children: verseNotes.notes.map((note, index) => {
16043
+ /* @__PURE__ */ jsx22("ul", { className: "yv:list-none yv:p-0 yv:m-0 yv:space-y-1", children: notes.map((note, index) => {
15994
16044
  const marker = getFootnoteMarker(index);
15995
16045
  return /* @__PURE__ */ jsxs7(
15996
16046
  "li",
@@ -16029,7 +16079,6 @@ function VerseUnavailableMessage() {
16029
16079
  }
16030
16080
  function BibleTextHtml({
16031
16081
  html,
16032
- notes,
16033
16082
  reference,
16034
16083
  fontSize,
16035
16084
  theme,
@@ -16038,19 +16087,32 @@ function BibleTextHtml({
16038
16087
  highlightedVerses = {}
16039
16088
  }) {
16040
16089
  const contentRef = useRef3(null);
16041
- const [placeholders, setPlaceholders] = useState3([]);
16090
+ const [footnoteData, setFootnoteData] = useState3([]);
16042
16091
  const providerTheme = useTheme3();
16043
16092
  const currentTheme = theme || providerTheme;
16044
16093
  useLayoutEffect(() => {
16045
16094
  if (!contentRef.current) return;
16046
16095
  contentRef.current.innerHTML = html;
16047
16096
  const anchors = contentRef.current.querySelectorAll("[data-verse-footnote]");
16097
+ const notesByKey = /* @__PURE__ */ new Map();
16098
+ anchors.forEach((el) => {
16099
+ const verseNum = el.getAttribute("data-verse-footnote");
16100
+ if (!verseNum) return;
16101
+ const content = el.getAttribute("data-verse-footnote-content") || "";
16102
+ const existing = notesByKey.get(verseNum);
16103
+ if (existing) existing.push(content);
16104
+ else notesByKey.set(verseNum, [content]);
16105
+ });
16048
16106
  const result = [];
16049
16107
  anchors.forEach((el) => {
16050
16108
  const verseNum = el.getAttribute("data-verse-footnote");
16051
- if (verseNum) result.push({ verseNum, el });
16109
+ if (!verseNum) return;
16110
+ const allNotes = notesByKey.get(verseNum) || [];
16111
+ const hasVerseContext = el.closest(".yv-v[v]") !== null;
16112
+ const verseHtml = hasVerseContext ? getVerseHtmlFromDom(contentRef.current, verseNum) : "";
16113
+ result.push({ verseNum, el, notes: allNotes, verseHtml, hasVerseContext });
16052
16114
  });
16053
- setPlaceholders(result);
16115
+ setFootnoteData(result);
16054
16116
  }, [html]);
16055
16117
  useLayoutEffect(() => {
16056
16118
  if (!contentRef.current) return;
@@ -16070,15 +16132,15 @@ function BibleTextHtml({
16070
16132
  } : void 0;
16071
16133
  return /* @__PURE__ */ jsxs7(Fragment2, { children: [
16072
16134
  /* @__PURE__ */ jsx22("div", { ref: contentRef, onClick: handleClick }),
16073
- placeholders.map(({ verseNum, el }, index) => {
16074
- const verseNotes = notes[verseNum];
16075
- if (!verseNotes) return null;
16076
- return createPortal(
16135
+ footnoteData.map(
16136
+ ({ verseNum, el, notes, verseHtml, hasVerseContext }, index) => createPortal(
16077
16137
  /* @__PURE__ */ jsx22(
16078
16138
  VerseFootnoteButton,
16079
16139
  {
16080
16140
  verseNum,
16081
- verseNotes,
16141
+ notes,
16142
+ verseHtml,
16143
+ hasVerseContext,
16082
16144
  reference,
16083
16145
  fontSize,
16084
16146
  theme: currentTheme
@@ -16086,8 +16148,8 @@ function BibleTextHtml({
16086
16148
  ),
16087
16149
  el,
16088
16150
  `${verseNum}-${index}`
16089
- );
16090
- })
16151
+ )
16152
+ )
16091
16153
  ] });
16092
16154
  }
16093
16155
  var Verse = {
@@ -16130,7 +16192,10 @@ var Verse = {
16130
16192
  onVerseSelect,
16131
16193
  highlightedVerses
16132
16194
  }, ref) => {
16133
- const transformedData = useMemo3(() => transformBibleHtml(html), [html]);
16195
+ const transformedHtml = useMemo3(
16196
+ () => typeof window === "undefined" ? html : transformBibleHtmlForBrowser(html).html,
16197
+ [html]
16198
+ );
16134
16199
  const providerTheme = useTheme3();
16135
16200
  const currentTheme = theme || providerTheme;
16136
16201
  return /* @__PURE__ */ jsx22(
@@ -16149,8 +16214,7 @@ var Verse = {
16149
16214
  children: /* @__PURE__ */ jsx22(
16150
16215
  BibleTextHtml,
16151
16216
  {
16152
- html: transformedData.html,
16153
- notes: transformedData.notes,
16217
+ html: transformedHtml,
16154
16218
  reference,
16155
16219
  fontSize,
16156
16220
  theme: currentTheme,
@@ -17488,5 +17552,6 @@ export {
17488
17552
  YouVersionProvider,
17489
17553
  YouVersionUserInfo,
17490
17554
  getAdjacentChapter,
17555
+ transformBibleHtml,
17491
17556
  useYVAuth3 as useYVAuth
17492
17557
  };
@@ -1,32 +1,4 @@
1
- /**
2
- * Converts a 0-based footnote index into an alphabetic marker.
3
- *
4
- * Examples with LETTERS = "abcdefghijklmnopqrstuvwxyz":
5
- * 0 -> "a", 25 -> "z", 26 -> "aa", 27 -> "ab"
6
- *
7
- * This uses spreadsheet-style indexing and derives its base from
8
- * LETTERS.length so there are no hardcoded numeric assumptions.
9
- */
10
- export declare function getFootnoteMarker(index: number): string;
11
- export type VerseNotes = {
12
- verseHtml: string;
13
- notes: string[];
14
- hasVerseContext: boolean;
15
- };
16
1
  export declare const INTER_FONT: "\"Inter\", sans-serif";
17
2
  export declare const SOURCE_SERIF_FONT: "\"Source Serif 4\", serif";
18
3
  export type FontFamily = typeof INTER_FONT | typeof SOURCE_SERIF_FONT | (string & {});
19
- /**
20
- * Full transformation pipeline for Bible HTML from the API.
21
- *
22
- * 1. Sanitize (DOMPurify)
23
- * 2. Wrap verse content in selectable spans
24
- * 3. Extract footnotes and replace with portal anchors
25
- * 4. Add non-breaking spaces to verse labels
26
- * 5. Fix irregular table layouts
27
- */
28
- export declare function transformBibleHtml(html: string): {
29
- html: string;
30
- notes: Record<string, VerseNotes>;
31
- };
32
4
  //# sourceMappingURL=verse-html-utils.d.ts.map