@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.
@@ -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(["old_testament", "new_testament", "deuterocanon"]);
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
  });