@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/components/verse.d.ts.map +1 -1
- package/dist/index.cjs +302 -236
- package/dist/index.js +301 -236
- package/dist/lib/verse-html-utils.d.ts +0 -28
- package/dist/lib/verse-html-utils.d.ts.map +1 -1
- package/package.json +3 -4
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
|
-
|
|
15720
|
-
var
|
|
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
|
-
|
|
15734
|
-
|
|
15735
|
-
|
|
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
|
-
|
|
15992
|
+
wrappers.forEach((wrapper, i) => {
|
|
15830
15993
|
if (i > 0) parts.push(" ");
|
|
15831
|
-
const clone2 =
|
|
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("
|
|
15835
|
-
const
|
|
15836
|
-
|
|
15837
|
-
|
|
15838
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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 [
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
16074
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|