@youversion/platform-core 1.20.1 → 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/.turbo/turbo-build.log +21 -10
- package/AGENTS.md +47 -2
- package/CHANGELOG.md +22 -0
- package/dist/browser-DzQ1yOHv.d.cts +69 -0
- package/dist/browser-DzQ1yOHv.d.ts +69 -0
- package/dist/browser.cjs +270 -0
- package/dist/browser.d.cts +1 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +6 -0
- package/dist/chunk-2Z2S2WY3.js +245 -0
- package/dist/index.cjs +250 -3
- package/dist/index.d.cts +45 -41
- package/dist/index.d.ts +45 -41
- package/dist/index.js +19 -2
- package/dist/server.cjs +275 -0
- package/dist/server.d.cts +25 -0
- package/dist/server.d.ts +25 -0
- package/dist/server.js +18 -0
- package/package.json +21 -3
- package/src/bible-html-transformer-server.ts +37 -0
- package/src/bible-html-transformer.server.test.ts +151 -0
- package/src/bible-html-transformer.test.ts +403 -0
- package/src/bible-html-transformer.ts +378 -0
- package/src/bible.ts +10 -0
- package/src/browser.ts +4 -0
- package/src/index.ts +5 -0
- package/src/schemas/book.ts +3 -5
- package/src/server.ts +2 -0
- package/src/types/index.ts +32 -8
- package/src/utils/constants.ts +5 -3
- package/src/__tests__/mocks/configuration.ts +0 -53
- package/src/__tests__/mocks/jwt.ts +0 -93
- package/src/__tests__/mocks/tokens.ts +0 -69
- package/src/highlight.ts +0 -16
- package/src/types/api-config.ts +0 -7
- package/src/types/auth.ts +0 -18
- package/src/types/highlight.ts +0 -9
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// src/bible-html-transformer.ts
|
|
2
|
+
var NON_BREAKING_SPACE = "\xA0";
|
|
3
|
+
var FOOTNOTE_KEY_ATTR = "data-footnote-key";
|
|
4
|
+
var NEEDS_SPACE_BEFORE = /^[^\s.,;:!?)}\]'"'»›]/;
|
|
5
|
+
var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
6
|
+
"DIV",
|
|
7
|
+
"P",
|
|
8
|
+
"SPAN",
|
|
9
|
+
"SUP",
|
|
10
|
+
"SUB",
|
|
11
|
+
"EM",
|
|
12
|
+
"STRONG",
|
|
13
|
+
"I",
|
|
14
|
+
"B",
|
|
15
|
+
"SMALL",
|
|
16
|
+
"BR",
|
|
17
|
+
"SECTION",
|
|
18
|
+
"TABLE",
|
|
19
|
+
"THEAD",
|
|
20
|
+
"TBODY",
|
|
21
|
+
"TR",
|
|
22
|
+
"TD",
|
|
23
|
+
"TH"
|
|
24
|
+
]);
|
|
25
|
+
var DROP_ENTIRELY_TAGS = /* @__PURE__ */ new Set([
|
|
26
|
+
"SCRIPT",
|
|
27
|
+
"STYLE",
|
|
28
|
+
"IFRAME",
|
|
29
|
+
"OBJECT",
|
|
30
|
+
"EMBED",
|
|
31
|
+
"SVG",
|
|
32
|
+
"MATH",
|
|
33
|
+
"FORM",
|
|
34
|
+
"INPUT",
|
|
35
|
+
"BUTTON",
|
|
36
|
+
"TEXTAREA",
|
|
37
|
+
"SELECT",
|
|
38
|
+
"TEMPLATE",
|
|
39
|
+
"LINK",
|
|
40
|
+
"META",
|
|
41
|
+
"BASE",
|
|
42
|
+
"NOSCRIPT"
|
|
43
|
+
]);
|
|
44
|
+
var ALLOWED_ATTRS = /* @__PURE__ */ new Set(["class", "v", "colspan", "rowspan", "dir", "usfm"]);
|
|
45
|
+
function sanitizeBibleHtmlDocument(doc) {
|
|
46
|
+
const root = doc.body ?? doc.documentElement;
|
|
47
|
+
for (const el of Array.from(root.querySelectorAll("*"))) {
|
|
48
|
+
const tag = el.tagName;
|
|
49
|
+
if (DROP_ENTIRELY_TAGS.has(tag)) {
|
|
50
|
+
el.remove();
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!ALLOWED_TAGS.has(tag)) {
|
|
54
|
+
el.replaceWith(...Array.from(el.childNodes));
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
for (const attr of Array.from(el.attributes)) {
|
|
58
|
+
const name = attr.name.toLowerCase();
|
|
59
|
+
if (name.startsWith("on")) {
|
|
60
|
+
el.removeAttribute(attr.name);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!ALLOWED_ATTRS.has(name) && !name.startsWith("data-")) {
|
|
64
|
+
el.removeAttribute(attr.name);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function wrapVerseContent(doc) {
|
|
70
|
+
function wrapParagraphContent(doc2, paragraph, verseNum) {
|
|
71
|
+
const children = Array.from(paragraph.childNodes);
|
|
72
|
+
if (children.length === 0) return;
|
|
73
|
+
const wrapper = doc2.createElement("span");
|
|
74
|
+
wrapper.className = "yv-v";
|
|
75
|
+
wrapper.setAttribute("v", verseNum);
|
|
76
|
+
const firstChild = children[0];
|
|
77
|
+
if (firstChild) {
|
|
78
|
+
paragraph.insertBefore(wrapper, firstChild);
|
|
79
|
+
}
|
|
80
|
+
children.forEach((child) => {
|
|
81
|
+
wrapper.appendChild(child);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function wrapParagraphsUntilBoundary(doc2, verseNum, startParagraph, endParagraph) {
|
|
85
|
+
if (!startParagraph) return;
|
|
86
|
+
let currentParagraph = startParagraph.nextElementSibling;
|
|
87
|
+
while (currentParagraph && currentParagraph !== endParagraph) {
|
|
88
|
+
const isHeading = currentParagraph.classList.contains("yv-h") || currentParagraph.matches(
|
|
89
|
+
".s1, .s2, .s3, .s4, .ms, .ms1, .ms2, .ms3, .ms4, .mr, .sp, .sr, .qa, .r"
|
|
90
|
+
);
|
|
91
|
+
if (isHeading) {
|
|
92
|
+
currentParagraph = currentParagraph.nextElementSibling;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (currentParagraph.querySelector(".yv-v[v]")) break;
|
|
96
|
+
if (currentParagraph.classList.contains("p") || currentParagraph.tagName === "P") {
|
|
97
|
+
wrapParagraphContent(doc2, currentParagraph, verseNum);
|
|
98
|
+
}
|
|
99
|
+
currentParagraph = currentParagraph.nextElementSibling;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function handleParagraphWrapping(doc2, currentParagraph, nextParagraph, verseNum) {
|
|
103
|
+
if (!currentParagraph) return;
|
|
104
|
+
if (!nextParagraph) {
|
|
105
|
+
wrapParagraphsUntilBoundary(doc2, verseNum, currentParagraph);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (currentParagraph !== nextParagraph) {
|
|
109
|
+
wrapParagraphsUntilBoundary(doc2, verseNum, currentParagraph, nextParagraph);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function processVerseMarker(marker, index, markers) {
|
|
113
|
+
const verseNum = marker.getAttribute("v");
|
|
114
|
+
if (!verseNum) return;
|
|
115
|
+
const nextMarker = markers[index + 1];
|
|
116
|
+
const nodesToWrap = collectNodesBetweenMarkers(marker, nextMarker);
|
|
117
|
+
if (nodesToWrap.length === 0) return;
|
|
118
|
+
const currentParagraph = marker.closest(".p, p, div.p");
|
|
119
|
+
const nextParagraph = nextMarker?.closest(".p, p, div.p") || null;
|
|
120
|
+
const doc2 = marker.ownerDocument;
|
|
121
|
+
wrapNodesInVerse(marker, verseNum, nodesToWrap);
|
|
122
|
+
handleParagraphWrapping(doc2, currentParagraph, nextParagraph, verseNum);
|
|
123
|
+
}
|
|
124
|
+
function wrapNodesInVerse(marker, verseNum, nodes) {
|
|
125
|
+
const wrapper = marker.ownerDocument.createElement("span");
|
|
126
|
+
wrapper.className = "yv-v";
|
|
127
|
+
wrapper.setAttribute("v", verseNum);
|
|
128
|
+
const firstNode = nodes[0];
|
|
129
|
+
if (firstNode) {
|
|
130
|
+
marker.parentNode?.insertBefore(wrapper, firstNode);
|
|
131
|
+
}
|
|
132
|
+
nodes.forEach((node) => {
|
|
133
|
+
wrapper.appendChild(node);
|
|
134
|
+
});
|
|
135
|
+
marker.remove();
|
|
136
|
+
}
|
|
137
|
+
function shouldStopCollecting(node, endMarker) {
|
|
138
|
+
if (node === endMarker) return true;
|
|
139
|
+
if (endMarker && node.nodeType === 1 && node.contains(endMarker)) return true;
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
function shouldSkipNode(node) {
|
|
143
|
+
return node.nodeType === 1 && node.classList.contains("yv-h");
|
|
144
|
+
}
|
|
145
|
+
function collectNodesBetweenMarkers(startMarker, endMarker) {
|
|
146
|
+
const nodes = [];
|
|
147
|
+
let current = startMarker.nextSibling;
|
|
148
|
+
while (current && !shouldStopCollecting(current, endMarker)) {
|
|
149
|
+
if (shouldSkipNode(current)) {
|
|
150
|
+
current = current.nextSibling;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
nodes.push(current);
|
|
154
|
+
current = current.nextSibling;
|
|
155
|
+
}
|
|
156
|
+
return nodes;
|
|
157
|
+
}
|
|
158
|
+
const verseMarkers = Array.from(doc.querySelectorAll(".yv-v[v]"));
|
|
159
|
+
verseMarkers.forEach(processVerseMarker);
|
|
160
|
+
}
|
|
161
|
+
function assignFootnoteKeys(doc) {
|
|
162
|
+
let introIdx = 0;
|
|
163
|
+
doc.querySelectorAll(".yv-n.f").forEach((fn) => {
|
|
164
|
+
const verseNum = fn.closest(".yv-v[v]")?.getAttribute("v");
|
|
165
|
+
fn.setAttribute(FOOTNOTE_KEY_ATTR, verseNum ?? `intro-${introIdx++}`);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function replaceFootnotesWithAnchors(doc, footnotes) {
|
|
169
|
+
for (const fn of footnotes) {
|
|
170
|
+
const key = fn.getAttribute(FOOTNOTE_KEY_ATTR);
|
|
171
|
+
if (!key) continue;
|
|
172
|
+
const prev = fn.previousSibling;
|
|
173
|
+
const next = fn.nextSibling;
|
|
174
|
+
const prevText = prev?.textContent ?? "";
|
|
175
|
+
const nextText = next?.textContent ?? "";
|
|
176
|
+
const prevNeedsSpace = prevText.length > 0 && !/\s$/.test(prevText);
|
|
177
|
+
const nextNeedsSpace = nextText.length > 0 && NEEDS_SPACE_BEFORE.test(nextText);
|
|
178
|
+
if (prevNeedsSpace && nextNeedsSpace && fn.parentNode) {
|
|
179
|
+
fn.parentNode.insertBefore(doc.createTextNode(" "), fn);
|
|
180
|
+
}
|
|
181
|
+
const anchor = doc.createElement("span");
|
|
182
|
+
anchor.setAttribute("data-verse-footnote", key);
|
|
183
|
+
anchor.setAttribute("data-verse-footnote-content", fn.innerHTML);
|
|
184
|
+
fn.replaceWith(anchor);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function addNbspToVerseLabels(doc) {
|
|
188
|
+
doc.querySelectorAll(".yv-vlbl").forEach((label) => {
|
|
189
|
+
const text = label.textContent || "";
|
|
190
|
+
if (!text.endsWith(NON_BREAKING_SPACE)) {
|
|
191
|
+
label.textContent = text + NON_BREAKING_SPACE;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function fixIrregularTables(doc) {
|
|
196
|
+
doc.querySelectorAll("table").forEach((table) => {
|
|
197
|
+
const rows = table.querySelectorAll("tr");
|
|
198
|
+
if (rows.length === 0) return;
|
|
199
|
+
let maxColumns = 0;
|
|
200
|
+
rows.forEach((row) => {
|
|
201
|
+
let count = 0;
|
|
202
|
+
row.querySelectorAll("td, th").forEach((cell) => {
|
|
203
|
+
count += parseInt(cell.getAttribute("colspan") || "1", 10);
|
|
204
|
+
});
|
|
205
|
+
maxColumns = Math.max(maxColumns, count);
|
|
206
|
+
});
|
|
207
|
+
if (maxColumns > 1) {
|
|
208
|
+
rows.forEach((row) => {
|
|
209
|
+
const cells = row.querySelectorAll("td, th");
|
|
210
|
+
if (cells.length === 1) {
|
|
211
|
+
const existing = parseInt(cells[0].getAttribute("colspan") || "1", 10);
|
|
212
|
+
if (existing < maxColumns) {
|
|
213
|
+
cells[0].setAttribute("colspan", maxColumns.toString());
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function transformBibleHtml(html, options) {
|
|
221
|
+
const doc = options.parseHtml(html);
|
|
222
|
+
sanitizeBibleHtmlDocument(doc);
|
|
223
|
+
wrapVerseContent(doc);
|
|
224
|
+
assignFootnoteKeys(doc);
|
|
225
|
+
const footnotes = Array.from(doc.querySelectorAll(".yv-n.f"));
|
|
226
|
+
replaceFootnotesWithAnchors(doc, footnotes);
|
|
227
|
+
addNbspToVerseLabels(doc);
|
|
228
|
+
fixIrregularTables(doc);
|
|
229
|
+
const transformedHtml = options.serializeHtml(doc);
|
|
230
|
+
return { html: transformedHtml };
|
|
231
|
+
}
|
|
232
|
+
function transformBibleHtmlForBrowser(html) {
|
|
233
|
+
if (typeof globalThis.DOMParser === "undefined") {
|
|
234
|
+
throw new Error("DOMParser is required to transform Bible HTML in browser environments");
|
|
235
|
+
}
|
|
236
|
+
return transformBibleHtml(html, {
|
|
237
|
+
parseHtml: (h) => new DOMParser().parseFromString(h, "text/html"),
|
|
238
|
+
serializeHtml: (doc) => doc.body.innerHTML
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export {
|
|
243
|
+
transformBibleHtml,
|
|
244
|
+
transformBibleHtmlForBrowser
|
|
245
|
+
};
|
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
BOOK_CANON: () => BOOK_CANON,
|
|
25
25
|
BOOK_IDS: () => BOOK_IDS,
|
|
26
26
|
BibleClient: () => BibleClient,
|
|
27
|
+
CANON_IDS: () => CANON_IDS,
|
|
27
28
|
DEFAULT_LICENSE_FREE_BIBLE_VERSION: () => DEFAULT_LICENSE_FREE_BIBLE_VERSION,
|
|
28
29
|
HighlightsClient: () => HighlightsClient,
|
|
29
30
|
LanguagesClient: () => LanguagesClient,
|
|
@@ -35,7 +36,8 @@ __export(index_exports, {
|
|
|
35
36
|
YouVersionAPIUsers: () => YouVersionAPIUsers,
|
|
36
37
|
YouVersionPlatformConfiguration: () => YouVersionPlatformConfiguration,
|
|
37
38
|
YouVersionUserInfo: () => YouVersionUserInfo,
|
|
38
|
-
getAdjacentChapter: () => getAdjacentChapter
|
|
39
|
+
getAdjacentChapter: () => getAdjacentChapter,
|
|
40
|
+
transformBibleHtml: () => transformBibleHtml
|
|
39
41
|
});
|
|
40
42
|
module.exports = __toCommonJS(index_exports);
|
|
41
43
|
|
|
@@ -203,6 +205,7 @@ var import_zod3 = require("zod");
|
|
|
203
205
|
|
|
204
206
|
// src/utils/constants.ts
|
|
205
207
|
var DEFAULT_LICENSE_FREE_BIBLE_VERSION = 3034;
|
|
208
|
+
var CANON_IDS = ["old_testament", "new_testament", "deuterocanon"];
|
|
206
209
|
var BOOK_IDS = [
|
|
207
210
|
"GEN",
|
|
208
211
|
"EXO",
|
|
@@ -444,7 +447,7 @@ var BibleChapterSchema = import_zod2.z.object({
|
|
|
444
447
|
});
|
|
445
448
|
|
|
446
449
|
// src/schemas/book.ts
|
|
447
|
-
var CanonSchema = import_zod3.z.enum(
|
|
450
|
+
var CanonSchema = import_zod3.z.enum(CANON_IDS);
|
|
448
451
|
var BibleBookIntroSchema = import_zod3.z.object({
|
|
449
452
|
/** Intro identifier */
|
|
450
453
|
id: import_zod3.z.string(),
|
|
@@ -787,12 +790,18 @@ var BibleClient = class _BibleClient {
|
|
|
787
790
|
/**
|
|
788
791
|
* Fetches a passage (range of verses) from the Bible using the passages endpoint.
|
|
789
792
|
* This is the new API format that returns HTML-formatted content.
|
|
793
|
+
*
|
|
794
|
+
* Note: The HTML returned from the API contains inline footnote content that should
|
|
795
|
+
* be transformed before rendering. Use `transformBibleHtml()` or
|
|
796
|
+
* `transformBibleHtmlForBrowser()` to clean up the HTML and extract footnotes.
|
|
797
|
+
*
|
|
790
798
|
* @param versionId The version ID.
|
|
791
799
|
* @param usfm The USFM reference (e.g., "JHN.3.1-2", "GEN.1", "JHN.3.16").
|
|
792
800
|
* @param format The format to return ("html" or "text", default: "html").
|
|
793
801
|
* @param include_headings Whether to include headings in the content.
|
|
794
802
|
* @param include_notes Whether to include notes in the content.
|
|
795
803
|
* @returns The requested BiblePassage object with HTML content.
|
|
804
|
+
*
|
|
796
805
|
* @example
|
|
797
806
|
* ```ts
|
|
798
807
|
* // Get a single verse
|
|
@@ -803,6 +812,10 @@ var BibleClient = class _BibleClient {
|
|
|
803
812
|
*
|
|
804
813
|
* // Get an entire chapter
|
|
805
814
|
* const chapter = await bibleClient.getPassage(3034, "GEN.1");
|
|
815
|
+
*
|
|
816
|
+
* // Transform HTML before rendering
|
|
817
|
+
* const passage = await bibleClient.getPassage(3034, "JHN.3.16", "html", true, true);
|
|
818
|
+
* const transformed = transformBibleHtmlForBrowser(passage.content);
|
|
806
819
|
* ```
|
|
807
820
|
*/
|
|
808
821
|
async getPassage(versionId, usfm, format = "html", include_headings, include_notes) {
|
|
@@ -1730,12 +1743,245 @@ function lastCanonicalOfPreviousBook(books, bookIndex) {
|
|
|
1730
1743
|
}
|
|
1731
1744
|
return null;
|
|
1732
1745
|
}
|
|
1746
|
+
|
|
1747
|
+
// src/bible-html-transformer.ts
|
|
1748
|
+
var NON_BREAKING_SPACE = "\xA0";
|
|
1749
|
+
var FOOTNOTE_KEY_ATTR = "data-footnote-key";
|
|
1750
|
+
var NEEDS_SPACE_BEFORE = /^[^\s.,;:!?)}\]'"'»›]/;
|
|
1751
|
+
var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
1752
|
+
"DIV",
|
|
1753
|
+
"P",
|
|
1754
|
+
"SPAN",
|
|
1755
|
+
"SUP",
|
|
1756
|
+
"SUB",
|
|
1757
|
+
"EM",
|
|
1758
|
+
"STRONG",
|
|
1759
|
+
"I",
|
|
1760
|
+
"B",
|
|
1761
|
+
"SMALL",
|
|
1762
|
+
"BR",
|
|
1763
|
+
"SECTION",
|
|
1764
|
+
"TABLE",
|
|
1765
|
+
"THEAD",
|
|
1766
|
+
"TBODY",
|
|
1767
|
+
"TR",
|
|
1768
|
+
"TD",
|
|
1769
|
+
"TH"
|
|
1770
|
+
]);
|
|
1771
|
+
var DROP_ENTIRELY_TAGS = /* @__PURE__ */ new Set([
|
|
1772
|
+
"SCRIPT",
|
|
1773
|
+
"STYLE",
|
|
1774
|
+
"IFRAME",
|
|
1775
|
+
"OBJECT",
|
|
1776
|
+
"EMBED",
|
|
1777
|
+
"SVG",
|
|
1778
|
+
"MATH",
|
|
1779
|
+
"FORM",
|
|
1780
|
+
"INPUT",
|
|
1781
|
+
"BUTTON",
|
|
1782
|
+
"TEXTAREA",
|
|
1783
|
+
"SELECT",
|
|
1784
|
+
"TEMPLATE",
|
|
1785
|
+
"LINK",
|
|
1786
|
+
"META",
|
|
1787
|
+
"BASE",
|
|
1788
|
+
"NOSCRIPT"
|
|
1789
|
+
]);
|
|
1790
|
+
var ALLOWED_ATTRS = /* @__PURE__ */ new Set(["class", "v", "colspan", "rowspan", "dir", "usfm"]);
|
|
1791
|
+
function sanitizeBibleHtmlDocument(doc) {
|
|
1792
|
+
const root = doc.body ?? doc.documentElement;
|
|
1793
|
+
for (const el of Array.from(root.querySelectorAll("*"))) {
|
|
1794
|
+
const tag = el.tagName;
|
|
1795
|
+
if (DROP_ENTIRELY_TAGS.has(tag)) {
|
|
1796
|
+
el.remove();
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
if (!ALLOWED_TAGS.has(tag)) {
|
|
1800
|
+
el.replaceWith(...Array.from(el.childNodes));
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
for (const attr of Array.from(el.attributes)) {
|
|
1804
|
+
const name = attr.name.toLowerCase();
|
|
1805
|
+
if (name.startsWith("on")) {
|
|
1806
|
+
el.removeAttribute(attr.name);
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1809
|
+
if (!ALLOWED_ATTRS.has(name) && !name.startsWith("data-")) {
|
|
1810
|
+
el.removeAttribute(attr.name);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
function wrapVerseContent(doc) {
|
|
1816
|
+
function wrapParagraphContent(doc2, paragraph, verseNum) {
|
|
1817
|
+
const children = Array.from(paragraph.childNodes);
|
|
1818
|
+
if (children.length === 0) return;
|
|
1819
|
+
const wrapper = doc2.createElement("span");
|
|
1820
|
+
wrapper.className = "yv-v";
|
|
1821
|
+
wrapper.setAttribute("v", verseNum);
|
|
1822
|
+
const firstChild = children[0];
|
|
1823
|
+
if (firstChild) {
|
|
1824
|
+
paragraph.insertBefore(wrapper, firstChild);
|
|
1825
|
+
}
|
|
1826
|
+
children.forEach((child) => {
|
|
1827
|
+
wrapper.appendChild(child);
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
function wrapParagraphsUntilBoundary(doc2, verseNum, startParagraph, endParagraph) {
|
|
1831
|
+
if (!startParagraph) return;
|
|
1832
|
+
let currentParagraph = startParagraph.nextElementSibling;
|
|
1833
|
+
while (currentParagraph && currentParagraph !== endParagraph) {
|
|
1834
|
+
const isHeading = currentParagraph.classList.contains("yv-h") || currentParagraph.matches(
|
|
1835
|
+
".s1, .s2, .s3, .s4, .ms, .ms1, .ms2, .ms3, .ms4, .mr, .sp, .sr, .qa, .r"
|
|
1836
|
+
);
|
|
1837
|
+
if (isHeading) {
|
|
1838
|
+
currentParagraph = currentParagraph.nextElementSibling;
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
if (currentParagraph.querySelector(".yv-v[v]")) break;
|
|
1842
|
+
if (currentParagraph.classList.contains("p") || currentParagraph.tagName === "P") {
|
|
1843
|
+
wrapParagraphContent(doc2, currentParagraph, verseNum);
|
|
1844
|
+
}
|
|
1845
|
+
currentParagraph = currentParagraph.nextElementSibling;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
function handleParagraphWrapping(doc2, currentParagraph, nextParagraph, verseNum) {
|
|
1849
|
+
if (!currentParagraph) return;
|
|
1850
|
+
if (!nextParagraph) {
|
|
1851
|
+
wrapParagraphsUntilBoundary(doc2, verseNum, currentParagraph);
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
if (currentParagraph !== nextParagraph) {
|
|
1855
|
+
wrapParagraphsUntilBoundary(doc2, verseNum, currentParagraph, nextParagraph);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
function processVerseMarker(marker, index, markers) {
|
|
1859
|
+
const verseNum = marker.getAttribute("v");
|
|
1860
|
+
if (!verseNum) return;
|
|
1861
|
+
const nextMarker = markers[index + 1];
|
|
1862
|
+
const nodesToWrap = collectNodesBetweenMarkers(marker, nextMarker);
|
|
1863
|
+
if (nodesToWrap.length === 0) return;
|
|
1864
|
+
const currentParagraph = marker.closest(".p, p, div.p");
|
|
1865
|
+
const nextParagraph = nextMarker?.closest(".p, p, div.p") || null;
|
|
1866
|
+
const doc2 = marker.ownerDocument;
|
|
1867
|
+
wrapNodesInVerse(marker, verseNum, nodesToWrap);
|
|
1868
|
+
handleParagraphWrapping(doc2, currentParagraph, nextParagraph, verseNum);
|
|
1869
|
+
}
|
|
1870
|
+
function wrapNodesInVerse(marker, verseNum, nodes) {
|
|
1871
|
+
const wrapper = marker.ownerDocument.createElement("span");
|
|
1872
|
+
wrapper.className = "yv-v";
|
|
1873
|
+
wrapper.setAttribute("v", verseNum);
|
|
1874
|
+
const firstNode = nodes[0];
|
|
1875
|
+
if (firstNode) {
|
|
1876
|
+
marker.parentNode?.insertBefore(wrapper, firstNode);
|
|
1877
|
+
}
|
|
1878
|
+
nodes.forEach((node) => {
|
|
1879
|
+
wrapper.appendChild(node);
|
|
1880
|
+
});
|
|
1881
|
+
marker.remove();
|
|
1882
|
+
}
|
|
1883
|
+
function shouldStopCollecting(node, endMarker) {
|
|
1884
|
+
if (node === endMarker) return true;
|
|
1885
|
+
if (endMarker && node.nodeType === 1 && node.contains(endMarker)) return true;
|
|
1886
|
+
return false;
|
|
1887
|
+
}
|
|
1888
|
+
function shouldSkipNode(node) {
|
|
1889
|
+
return node.nodeType === 1 && node.classList.contains("yv-h");
|
|
1890
|
+
}
|
|
1891
|
+
function collectNodesBetweenMarkers(startMarker, endMarker) {
|
|
1892
|
+
const nodes = [];
|
|
1893
|
+
let current = startMarker.nextSibling;
|
|
1894
|
+
while (current && !shouldStopCollecting(current, endMarker)) {
|
|
1895
|
+
if (shouldSkipNode(current)) {
|
|
1896
|
+
current = current.nextSibling;
|
|
1897
|
+
continue;
|
|
1898
|
+
}
|
|
1899
|
+
nodes.push(current);
|
|
1900
|
+
current = current.nextSibling;
|
|
1901
|
+
}
|
|
1902
|
+
return nodes;
|
|
1903
|
+
}
|
|
1904
|
+
const verseMarkers = Array.from(doc.querySelectorAll(".yv-v[v]"));
|
|
1905
|
+
verseMarkers.forEach(processVerseMarker);
|
|
1906
|
+
}
|
|
1907
|
+
function assignFootnoteKeys(doc) {
|
|
1908
|
+
let introIdx = 0;
|
|
1909
|
+
doc.querySelectorAll(".yv-n.f").forEach((fn) => {
|
|
1910
|
+
const verseNum = fn.closest(".yv-v[v]")?.getAttribute("v");
|
|
1911
|
+
fn.setAttribute(FOOTNOTE_KEY_ATTR, verseNum ?? `intro-${introIdx++}`);
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
function replaceFootnotesWithAnchors(doc, footnotes) {
|
|
1915
|
+
for (const fn of footnotes) {
|
|
1916
|
+
const key = fn.getAttribute(FOOTNOTE_KEY_ATTR);
|
|
1917
|
+
if (!key) continue;
|
|
1918
|
+
const prev = fn.previousSibling;
|
|
1919
|
+
const next = fn.nextSibling;
|
|
1920
|
+
const prevText = prev?.textContent ?? "";
|
|
1921
|
+
const nextText = next?.textContent ?? "";
|
|
1922
|
+
const prevNeedsSpace = prevText.length > 0 && !/\s$/.test(prevText);
|
|
1923
|
+
const nextNeedsSpace = nextText.length > 0 && NEEDS_SPACE_BEFORE.test(nextText);
|
|
1924
|
+
if (prevNeedsSpace && nextNeedsSpace && fn.parentNode) {
|
|
1925
|
+
fn.parentNode.insertBefore(doc.createTextNode(" "), fn);
|
|
1926
|
+
}
|
|
1927
|
+
const anchor = doc.createElement("span");
|
|
1928
|
+
anchor.setAttribute("data-verse-footnote", key);
|
|
1929
|
+
anchor.setAttribute("data-verse-footnote-content", fn.innerHTML);
|
|
1930
|
+
fn.replaceWith(anchor);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
function addNbspToVerseLabels(doc) {
|
|
1934
|
+
doc.querySelectorAll(".yv-vlbl").forEach((label) => {
|
|
1935
|
+
const text = label.textContent || "";
|
|
1936
|
+
if (!text.endsWith(NON_BREAKING_SPACE)) {
|
|
1937
|
+
label.textContent = text + NON_BREAKING_SPACE;
|
|
1938
|
+
}
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
function fixIrregularTables(doc) {
|
|
1942
|
+
doc.querySelectorAll("table").forEach((table) => {
|
|
1943
|
+
const rows = table.querySelectorAll("tr");
|
|
1944
|
+
if (rows.length === 0) return;
|
|
1945
|
+
let maxColumns = 0;
|
|
1946
|
+
rows.forEach((row) => {
|
|
1947
|
+
let count = 0;
|
|
1948
|
+
row.querySelectorAll("td, th").forEach((cell) => {
|
|
1949
|
+
count += parseInt(cell.getAttribute("colspan") || "1", 10);
|
|
1950
|
+
});
|
|
1951
|
+
maxColumns = Math.max(maxColumns, count);
|
|
1952
|
+
});
|
|
1953
|
+
if (maxColumns > 1) {
|
|
1954
|
+
rows.forEach((row) => {
|
|
1955
|
+
const cells = row.querySelectorAll("td, th");
|
|
1956
|
+
if (cells.length === 1) {
|
|
1957
|
+
const existing = parseInt(cells[0].getAttribute("colspan") || "1", 10);
|
|
1958
|
+
if (existing < maxColumns) {
|
|
1959
|
+
cells[0].setAttribute("colspan", maxColumns.toString());
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
function transformBibleHtml(html, options) {
|
|
1967
|
+
const doc = options.parseHtml(html);
|
|
1968
|
+
sanitizeBibleHtmlDocument(doc);
|
|
1969
|
+
wrapVerseContent(doc);
|
|
1970
|
+
assignFootnoteKeys(doc);
|
|
1971
|
+
const footnotes = Array.from(doc.querySelectorAll(".yv-n.f"));
|
|
1972
|
+
replaceFootnotesWithAnchors(doc, footnotes);
|
|
1973
|
+
addNbspToVerseLabels(doc);
|
|
1974
|
+
fixIrregularTables(doc);
|
|
1975
|
+
const transformedHtml = options.serializeHtml(doc);
|
|
1976
|
+
return { html: transformedHtml };
|
|
1977
|
+
}
|
|
1733
1978
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1734
1979
|
0 && (module.exports = {
|
|
1735
1980
|
ApiClient,
|
|
1736
1981
|
BOOK_CANON,
|
|
1737
1982
|
BOOK_IDS,
|
|
1738
1983
|
BibleClient,
|
|
1984
|
+
CANON_IDS,
|
|
1739
1985
|
DEFAULT_LICENSE_FREE_BIBLE_VERSION,
|
|
1740
1986
|
HighlightsClient,
|
|
1741
1987
|
LanguagesClient,
|
|
@@ -1747,5 +1993,6 @@ function lastCanonicalOfPreviousBook(books, bookIndex) {
|
|
|
1747
1993
|
YouVersionAPIUsers,
|
|
1748
1994
|
YouVersionPlatformConfiguration,
|
|
1749
1995
|
YouVersionUserInfo,
|
|
1750
|
-
getAdjacentChapter
|
|
1996
|
+
getAdjacentChapter,
|
|
1997
|
+
transformBibleHtml
|
|
1751
1998
|
});
|