@youversion/platform-react-ui 1.14.3 → 1.14.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"verse.d.ts","sourceRoot":"","sources":["../../src/components/verse.tsx"],"names":[],"mappings":"AAeA,OAAO,EAIL,KAAK,UAAU,EAGhB,MAAM,wBAAwB,CAAC;AAuPhC;;GAEG;AACH,KAAK,UAAU,GAAG;IAChB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;OAEG;IACH,IAAI,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;CACzB,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC3C,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC7C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,KAAK;IAChB;;;;;;;;OAQG;mCACwC,UAAU,KAAG,KAAK,CAAC,YAAY;;CA6E3E,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC3C,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC7C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,GAAI,qJAY3B,kBAAkB,KAAG,KAAK,CAAC,YA2D7B,CAAC"}
1
+ {"version":3,"file":"verse.d.ts","sourceRoot":"","sources":["../../src/components/verse.tsx"],"names":[],"mappings":"AAcA,OAAO,EAGL,KAAK,UAAU,EAEhB,MAAM,wBAAwB,CAAC;AA6JhC;;GAEG;AACH,KAAK,UAAU,GAAG;IAChB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;OAEG;IACH,IAAI,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;CACzB,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC3C,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC7C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,KAAK;IAChB;;;;;;;;OAQG;mCACwC,UAAU,KAAG,KAAK,CAAC,YAAY;;CAwE3E,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC3C,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC7C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,GAAI,qJAY3B,kBAAkB,KAAG,KAAK,CAAC,YA2D7B,CAAC"}
package/dist/index.cjs CHANGED
@@ -15569,13 +15569,24 @@ function PersonIcon(props) {
15569
15569
 
15570
15570
  // src/components/verse.tsx
15571
15571
  var import_platform_react_hooks3 = require("@youversion/platform-react-hooks");
15572
- var import_isomorphic_dompurify = __toESM(require("isomorphic-dompurify"), 1);
15573
15572
  var import_react3 = require("react");
15574
15573
  var import_react_dom = require("react-dom");
15575
15574
 
15576
15575
  // src/lib/verse-html-utils.ts
15576
+ var import_isomorphic_dompurify = __toESM(require("isomorphic-dompurify"), 1);
15577
15577
  var NON_BREAKING_SPACE = "\xA0";
15578
15578
  var LETTERS = "abcdefghijklmnopqrstuvwxyz";
15579
+ function getFootnoteMarker(index) {
15580
+ const base = LETTERS.length;
15581
+ if (base === 0) return String(index + 1);
15582
+ let value = index;
15583
+ let marker = "";
15584
+ do {
15585
+ marker = LETTERS[value % base] + marker;
15586
+ value = Math.floor(value / base) - 1;
15587
+ } while (value >= 0);
15588
+ return marker;
15589
+ }
15579
15590
  var INTER_FONT = '"Inter", sans-serif';
15580
15591
  var SOURCE_SERIF_FONT = '"Source Serif 4", serif';
15581
15592
  function wrapVerseContent(doc) {
@@ -15668,73 +15679,125 @@ function wrapVerseContent(doc) {
15668
15679
  const verseMarkers = Array.from(doc.querySelectorAll(".yv-v[v]"));
15669
15680
  verseMarkers.forEach(processVerseMarker);
15670
15681
  }
15682
+ var NEEDS_SPACE_BEFORE = /^[^\s.,;:!?)}\]'"»›]/;
15683
+ function buildVerseHtml(wrappers) {
15684
+ const parts = [];
15685
+ let noteIdx = 0;
15686
+ for (let i = 0; i < wrappers.length; i++) {
15687
+ if (i > 0) parts.push(" ");
15688
+ const clone2 = wrappers[i].cloneNode(true);
15689
+ const ownerDoc = wrappers[i].ownerDocument;
15690
+ clone2.querySelectorAll(".yv-h, .yv-vlbl").forEach((el) => el.remove());
15691
+ clone2.querySelectorAll(".yv-n.f").forEach((fn) => {
15692
+ const marker = ownerDoc.createElement("sup");
15693
+ marker.className = "yv:text-muted-foreground";
15694
+ marker.textContent = getFootnoteMarker(noteIdx++);
15695
+ fn.replaceWith(marker);
15696
+ });
15697
+ parts.push(clone2.innerHTML);
15698
+ }
15699
+ return parts.join("");
15700
+ }
15701
+ function replaceFootnotesWithAnchors(doc, footnotes) {
15702
+ for (const fn of footnotes) {
15703
+ const verseNum = fn.closest(".yv-v[v]")?.getAttribute("v");
15704
+ if (!verseNum) continue;
15705
+ const prev = fn.previousSibling;
15706
+ const next = fn.nextSibling;
15707
+ const prevText = prev?.textContent ?? "";
15708
+ const nextText = next?.textContent ?? "";
15709
+ const prevNeedsSpace = prevText.length > 0 && !/\s$/.test(prevText);
15710
+ const nextNeedsSpace = nextText.length > 0 && NEEDS_SPACE_BEFORE.test(nextText);
15711
+ if (prevNeedsSpace && nextNeedsSpace && fn.parentNode) {
15712
+ fn.parentNode.insertBefore(doc.createTextNode(" "), fn);
15713
+ }
15714
+ const anchor = doc.createElement("span");
15715
+ anchor.setAttribute("data-verse-footnote", verseNum);
15716
+ fn.replaceWith(anchor);
15717
+ }
15718
+ }
15671
15719
  function extractNotesFromWrappedHtml(doc) {
15672
15720
  const footnotes = Array.from(doc.querySelectorAll(".yv-n.f"));
15673
15721
  if (!footnotes.length) return {};
15674
15722
  const footnotesByVerse = /* @__PURE__ */ new Map();
15675
- footnotes.forEach((fn) => {
15723
+ for (const fn of footnotes) {
15676
15724
  const verseNum = fn.closest(".yv-v[v]")?.getAttribute("v");
15677
- if (verseNum) {
15678
- let arr = footnotesByVerse.get(verseNum);
15679
- if (!arr) {
15680
- arr = [];
15681
- footnotesByVerse.set(verseNum, arr);
15682
- }
15683
- arr.push(fn);
15725
+ if (!verseNum) continue;
15726
+ let arr = footnotesByVerse.get(verseNum);
15727
+ if (!arr) {
15728
+ arr = [];
15729
+ footnotesByVerse.set(verseNum, arr);
15684
15730
  }
15731
+ arr.push(fn);
15732
+ }
15733
+ const wrappersByVerse = /* @__PURE__ */ new Map();
15734
+ doc.querySelectorAll(".yv-v[v]").forEach((el) => {
15735
+ const verseNum = el.getAttribute("v");
15736
+ if (!verseNum) return;
15737
+ const arr = wrappersByVerse.get(verseNum);
15738
+ if (arr) arr.push(el);
15739
+ else wrappersByVerse.set(verseNum, [el]);
15685
15740
  });
15686
15741
  const notes = {};
15687
- const NEEDS_SPACE_BEFORE = /^[^\s.,;:!?)}\]'"»›]/;
15688
- footnotesByVerse.forEach((fns, verseNum) => {
15689
- const verseWrappers = Array.from(doc.querySelectorAll(`.yv-v[v="${verseNum}"]`));
15690
- let verseHtml = "";
15691
- let noteIdx = 0;
15692
- verseWrappers.forEach((wrapper, wrapperIdx) => {
15693
- if (wrapperIdx > 0) verseHtml += " ";
15694
- const walker = doc.createTreeWalker(wrapper, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
15695
- let lastWasFootnote = false;
15696
- while (walker.nextNode()) {
15697
- const node = walker.currentNode;
15698
- if (node instanceof Element) {
15699
- if (node.classList.contains("yv-n") && node.classList.contains("f")) {
15700
- verseHtml += `<sup class="yv:text-muted-foreground">${LETTERS[noteIdx++] || noteIdx}</sup>`;
15701
- lastWasFootnote = true;
15702
- }
15703
- } else if (node.nodeType === Node.TEXT_NODE) {
15704
- const parent = node.parentElement;
15705
- if (parent?.closest(".yv-n.f") || parent?.closest(".yv-h")) continue;
15706
- if (parent?.classList.contains("yv-vlbl")) continue;
15707
- let text = node.textContent || "";
15708
- if (lastWasFootnote && text && NEEDS_SPACE_BEFORE.test(text)) {
15709
- text = " " + text;
15710
- }
15711
- verseHtml += text;
15712
- lastWasFootnote = false;
15713
- }
15714
- }
15715
- });
15742
+ for (const [verseNum, fns] of footnotesByVerse) {
15716
15743
  notes[verseNum] = {
15717
- verseHtml,
15744
+ verseHtml: buildVerseHtml(wrappersByVerse.get(verseNum) ?? []),
15718
15745
  notes: fns.map((fn) => fn.innerHTML)
15719
15746
  };
15720
- const lastWrapper = verseWrappers[verseWrappers.length - 1];
15721
- if (lastWrapper?.parentNode) {
15722
- const placeholder = doc.createElement("span");
15723
- placeholder.setAttribute("data-verse-footnote", verseNum);
15724
- lastWrapper.parentNode.insertBefore(placeholder, lastWrapper.nextSibling);
15747
+ }
15748
+ replaceFootnotesWithAnchors(doc, footnotes);
15749
+ return notes;
15750
+ }
15751
+ function addNbspToVerseLabels(doc) {
15752
+ doc.querySelectorAll(".yv-vlbl").forEach((label) => {
15753
+ const text = label.textContent || "";
15754
+ if (!text.endsWith(NON_BREAKING_SPACE)) {
15755
+ label.textContent = text + NON_BREAKING_SPACE;
15725
15756
  }
15726
15757
  });
15727
- footnotes.forEach((fn) => {
15728
- const prev = fn.previousSibling;
15729
- const next = fn.nextSibling;
15730
- const prevNeedsSpace = prev?.nodeType === Node.TEXT_NODE && prev.textContent && !/\s$/.test(prev.textContent);
15731
- const nextNeedsSpace = next?.nodeType === Node.TEXT_NODE && next.textContent && NEEDS_SPACE_BEFORE.test(next.textContent);
15732
- if (prevNeedsSpace && nextNeedsSpace) {
15733
- fn.parentNode?.insertBefore(doc.createTextNode(" "), next);
15758
+ }
15759
+ function fixIrregularTables(doc) {
15760
+ doc.querySelectorAll("table").forEach((table) => {
15761
+ const rows = table.querySelectorAll("tr");
15762
+ if (rows.length === 0) return;
15763
+ let maxColumns = 0;
15764
+ rows.forEach((row) => {
15765
+ let count = 0;
15766
+ row.querySelectorAll("td, th").forEach((cell) => {
15767
+ count += cell instanceof HTMLTableCellElement ? parseInt(cell.getAttribute("colspan") || "1", 10) : 1;
15768
+ });
15769
+ maxColumns = Math.max(maxColumns, count);
15770
+ });
15771
+ if (maxColumns > 1) {
15772
+ rows.forEach((row) => {
15773
+ const cells = row.querySelectorAll("td, th");
15774
+ if (cells.length === 1 && cells[0] instanceof HTMLTableCellElement) {
15775
+ const existing = parseInt(cells[0].getAttribute("colspan") || "1", 10);
15776
+ if (existing < maxColumns) {
15777
+ cells[0].setAttribute("colspan", maxColumns.toString());
15778
+ }
15779
+ }
15780
+ });
15734
15781
  }
15735
- fn.remove();
15736
15782
  });
15737
- return notes;
15783
+ }
15784
+ var DOMPURIFY_CONFIG = {
15785
+ ALLOWED_ATTR: ["class", "style", "id", "v", "usfm"],
15786
+ ALLOW_DATA_ATTR: true
15787
+ };
15788
+ function transformBibleHtml(html) {
15789
+ if (typeof window === "undefined" || !("DOMParser" in window)) {
15790
+ return { html, notes: {} };
15791
+ }
15792
+ const doc = new DOMParser().parseFromString(
15793
+ import_isomorphic_dompurify.default.sanitize(html, DOMPURIFY_CONFIG),
15794
+ "text/html"
15795
+ );
15796
+ wrapVerseContent(doc);
15797
+ const notes = extractNotesFromWrappedHtml(doc);
15798
+ addNbspToVerseLabels(doc);
15799
+ fixIrregularTables(doc);
15800
+ return { html: doc.body.innerHTML, notes };
15738
15801
  }
15739
15802
 
15740
15803
  // src/components/icons/footnote.tsx
@@ -15795,20 +15858,23 @@ var VerseFootnoteButton = (0, import_react3.memo)(function VerseFootnoteButton2(
15795
15858
  dangerouslySetInnerHTML: { __html: verseNotes.verseHtml }
15796
15859
  }
15797
15860
  ),
15798
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("ul", { className: "yv:list-none yv:p-0 yv:m-0 yv:space-y-1", children: verseNotes.notes.map((note, index) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
15799
- "li",
15800
- {
15801
- className: "yv:flex yv:gap-2 yv:text-xs yv:border-b yv:border-border yv:py-2",
15802
- children: [
15803
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("span", { className: "", children: [
15804
- LETTERS[index] || index + 1,
15805
- "."
15806
- ] }),
15807
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { dangerouslySetInnerHTML: { __html: note } })
15808
- ]
15809
- },
15810
- LETTERS[index]
15811
- )) })
15861
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("ul", { className: "yv:list-none yv:p-0 yv:m-0 yv:space-y-1", children: verseNotes.notes.map((note, index) => {
15862
+ const marker = getFootnoteMarker(index);
15863
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
15864
+ "li",
15865
+ {
15866
+ className: "yv:flex yv:gap-2 yv:text-xs yv:border-b yv:border-border yv:py-2",
15867
+ children: [
15868
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("span", { className: "", children: [
15869
+ marker,
15870
+ "."
15871
+ ] }),
15872
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { dangerouslySetInnerHTML: { __html: note } })
15873
+ ]
15874
+ },
15875
+ marker
15876
+ );
15877
+ }) })
15812
15878
  ] })
15813
15879
  }
15814
15880
  )
@@ -15825,57 +15891,39 @@ function BibleTextHtml({
15825
15891
  highlightedVerses = {}
15826
15892
  }) {
15827
15893
  const contentRef = (0, import_react3.useRef)(null);
15828
- const [placeholders, setPlaceholders] = (0, import_react3.useState)(/* @__PURE__ */ new Map());
15894
+ const [placeholders, setPlaceholders] = (0, import_react3.useState)([]);
15829
15895
  const providerTheme = (0, import_platform_react_hooks3.useTheme)();
15830
15896
  const currentTheme = theme || providerTheme;
15831
15897
  (0, import_react3.useLayoutEffect)(() => {
15832
15898
  if (!contentRef.current) return;
15833
15899
  contentRef.current.innerHTML = html;
15834
- const map2 = /* @__PURE__ */ new Map();
15835
- Object.keys(notes).forEach((verseNum) => {
15836
- const el = contentRef.current?.querySelector(`[data-verse-footnote="${verseNum}"]`);
15837
- if (el) map2.set(verseNum, el);
15900
+ const anchors = contentRef.current.querySelectorAll("[data-verse-footnote]");
15901
+ const result = [];
15902
+ anchors.forEach((el) => {
15903
+ const verseNum = el.getAttribute("data-verse-footnote");
15904
+ if (verseNum) result.push({ verseNum, el });
15838
15905
  });
15839
- setPlaceholders(map2);
15840
- }, [html, notes]);
15906
+ setPlaceholders(result);
15907
+ }, [html]);
15841
15908
  (0, import_react3.useLayoutEffect)(() => {
15842
15909
  if (!contentRef.current) return;
15843
- const verseElements = contentRef.current.querySelectorAll(".yv-v[v]");
15844
- verseElements.forEach((el) => {
15910
+ contentRef.current.querySelectorAll(".yv-v[v]").forEach((el) => {
15845
15911
  const verseNum = parseInt(el.getAttribute("v") || "0", 10);
15846
- if (selectedVerses.includes(verseNum)) {
15847
- el.classList.add("yv-v-selected");
15848
- } else {
15849
- el.classList.remove("yv-v-selected");
15850
- }
15851
- if (highlightedVerses[verseNum]) {
15852
- el.classList.add("yv-v-highlighted");
15853
- } else {
15854
- el.classList.remove("yv-v-highlighted");
15855
- }
15912
+ el.classList.toggle("yv-v-selected", selectedVerses.includes(verseNum));
15913
+ el.classList.toggle("yv-v-highlighted", !!highlightedVerses[verseNum]);
15856
15914
  });
15857
15915
  }, [html, selectedVerses, highlightedVerses]);
15858
- const selectedVersesRef = (0, import_react3.useRef)(selectedVerses);
15859
- selectedVersesRef.current = selectedVerses;
15860
- (0, import_react3.useLayoutEffect)(() => {
15861
- const element = contentRef.current;
15862
- if (!element || !onVerseSelect) return;
15863
- const handleClick = (e) => {
15864
- const target = e.target;
15865
- const verseEl = target.closest(".yv-v[v]");
15866
- if (!verseEl) return;
15867
- const verseNum = parseInt(verseEl.getAttribute("v") || "0", 10);
15868
- if (verseNum === 0) return;
15869
- const current = selectedVersesRef.current;
15870
- const newSelected = current.includes(verseNum) ? current.filter((v) => v !== verseNum) : [...current, verseNum].sort((a, b) => a - b);
15871
- onVerseSelect(newSelected);
15872
- };
15873
- element.addEventListener("click", handleClick);
15874
- return () => element.removeEventListener("click", handleClick);
15875
- }, [onVerseSelect]);
15916
+ const handleClick = onVerseSelect ? (e) => {
15917
+ const verseEl = e.target.closest(".yv-v[v]");
15918
+ if (!verseEl) return;
15919
+ const verseNum = parseInt(verseEl.getAttribute("v") || "0", 10);
15920
+ if (!verseNum) return;
15921
+ const newSelected = selectedVerses.includes(verseNum) ? selectedVerses.filter((v) => v !== verseNum) : [...selectedVerses, verseNum].sort((a, b) => a - b);
15922
+ onVerseSelect(newSelected);
15923
+ } : void 0;
15876
15924
  return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
15877
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { ref: contentRef }),
15878
- Array.from(placeholders.entries()).map(([verseNum, el]) => {
15925
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { ref: contentRef, onClick: handleClick }),
15926
+ placeholders.map(({ verseNum, el }, index) => {
15879
15927
  const verseNotes = notes[verseNum];
15880
15928
  if (!verseNotes) return null;
15881
15929
  return (0, import_react_dom.createPortal)(
@@ -15889,68 +15937,12 @@ function BibleTextHtml({
15889
15937
  theme: currentTheme
15890
15938
  }
15891
15939
  ),
15892
- el
15940
+ el,
15941
+ `${verseNum}-${index}`
15893
15942
  );
15894
15943
  })
15895
15944
  ] });
15896
15945
  }
15897
- var DOMPURIFY_CONFIG = {
15898
- ALLOWED_ATTR: ["class", "style", "id", "v", "usfm"],
15899
- ALLOW_DATA_ATTR: true
15900
- };
15901
- function yvDomTransformer(html, extractNotes = false) {
15902
- if (typeof window === "undefined" || !("DOMParser" in window)) {
15903
- return { html, notes: {} };
15904
- }
15905
- const doc = new DOMParser().parseFromString(
15906
- import_isomorphic_dompurify.default.sanitize(html, DOMPURIFY_CONFIG),
15907
- "text/html"
15908
- );
15909
- wrapVerseContent(doc);
15910
- const extractedNotes = extractNotes ? extractNotesFromWrappedHtml(doc) : {};
15911
- const verseLabels = doc.querySelectorAll(".yv-vlbl");
15912
- verseLabels.forEach((label) => {
15913
- const text = label.textContent || "";
15914
- if (!text.endsWith(NON_BREAKING_SPACE)) {
15915
- label.textContent = text + NON_BREAKING_SPACE;
15916
- }
15917
- });
15918
- const tables = doc.querySelectorAll("table");
15919
- tables.forEach((table) => {
15920
- const rows = table.querySelectorAll("tr");
15921
- if (rows.length === 0) return;
15922
- let maxColumns = 0;
15923
- rows.forEach((row) => {
15924
- const cells = row.querySelectorAll("td, th");
15925
- let rowColumnCount = 0;
15926
- cells.forEach((cell) => {
15927
- if (cell instanceof HTMLTableCellElement) {
15928
- const colspan = parseInt(cell.getAttribute("colspan") || "1", 10);
15929
- rowColumnCount += colspan;
15930
- } else {
15931
- rowColumnCount += 1;
15932
- }
15933
- });
15934
- maxColumns = Math.max(maxColumns, rowColumnCount);
15935
- });
15936
- if (maxColumns > 1) {
15937
- rows.forEach((row) => {
15938
- const cells = row.querySelectorAll("td, th");
15939
- if (cells.length === 1) {
15940
- const cell = cells[0];
15941
- if (cell instanceof HTMLTableCellElement) {
15942
- const existingColspan = parseInt(cell.getAttribute("colspan") || "1", 10);
15943
- if (existingColspan < maxColumns) {
15944
- cell.setAttribute("colspan", maxColumns.toString());
15945
- }
15946
- }
15947
- }
15948
- });
15949
- }
15950
- });
15951
- const modifiedHtml = doc.body.innerHTML;
15952
- return { html: modifiedHtml, notes: extractedNotes };
15953
- }
15954
15946
  var Verse = {
15955
15947
  /**
15956
15948
  * Renders a single verse with superscript number and text.
@@ -15991,12 +15983,9 @@ var Verse = {
15991
15983
  onVerseSelect,
15992
15984
  highlightedVerses
15993
15985
  }, ref) => {
15994
- const [transformedData, setTransformedData] = (0, import_react3.useState)({ html, notes: {} });
15986
+ const transformedData = (0, import_react3.useMemo)(() => transformBibleHtml(html), [html]);
15995
15987
  const providerTheme = (0, import_platform_react_hooks3.useTheme)();
15996
15988
  const currentTheme = theme || providerTheme;
15997
- (0, import_react3.useEffect)(() => {
15998
- setTransformedData(yvDomTransformer(html, true));
15999
- }, [html]);
16000
15989
  return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
16001
15990
  "section",
16002
15991
  {
@@ -16260,7 +16249,7 @@ function Content5() {
16260
16249
  function UserMenu() {
16261
16250
  const { auth, signIn, signOut, userInfo } = (0, import_platform_react_hooks4.useYVAuth)();
16262
16251
  return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Popover, { children: [
16263
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PopoverTrigger, { "data-testid": "user-menu-trigger", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Button, { size: "icon", variant: "secondary", children: auth.isAuthenticated && userInfo?.avatarUrlFormat ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
16252
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PopoverTrigger, { asChild: true, "data-testid": "user-menu-trigger", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Button, { size: "icon", variant: "secondary", children: auth.isAuthenticated && userInfo?.avatarUrlFormat ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
16264
16253
  "img",
16265
16254
  {
16266
16255
  src: userInfo.getAvatarUrl(32, 32)?.toString(),
@@ -16353,7 +16342,7 @@ function Toolbar({ border = "top" }) {
16353
16342
  )
16354
16343
  ] }),
16355
16344
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Popover, { children: [
16356
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PopoverTrigger, { "aria-label": "Settings", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Button, { size: "icon", variant: "secondary", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(GearIcon, { className: "yv:text-foreground" }) }) }),
16345
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PopoverTrigger, { asChild: true, "aria-label": "Settings", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Button, { size: "icon", variant: "secondary", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(GearIcon, { className: "yv:text-foreground" }) }) }),
16357
16346
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(PopoverContent, { sideOffset: 16, heading: "Reader Settings", theme: background, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "yv:flex yv:flex-col yv:gap-4 yv:p-4", children: [
16358
16347
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "yv:grid yv:grid-cols-2", children: [
16359
16348
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
package/dist/index.js CHANGED
@@ -14726,9 +14726,9 @@ import {
14726
14726
  import {
14727
14727
  createContext as createContext3,
14728
14728
  useContext as useContext3,
14729
- useEffect as useEffect4,
14729
+ useEffect as useEffect3,
14730
14730
  useLayoutEffect as useLayoutEffect2,
14731
- useMemo as useMemo3,
14731
+ useMemo as useMemo4,
14732
14732
  useState as useState4
14733
14733
  } from "react";
14734
14734
 
@@ -15551,11 +15551,10 @@ function PersonIcon(props) {
15551
15551
 
15552
15552
  // src/components/verse.tsx
15553
15553
  import { usePassage, useTheme as useTheme3 } from "@youversion/platform-react-hooks";
15554
- import DOMPurify from "isomorphic-dompurify";
15555
15554
  import {
15556
15555
  forwardRef as forwardRef2,
15557
15556
  memo,
15558
- useEffect as useEffect3,
15557
+ useMemo as useMemo3,
15559
15558
  useLayoutEffect,
15560
15559
  useRef as useRef3,
15561
15560
  useState as useState3
@@ -15563,8 +15562,20 @@ import {
15563
15562
  import { createPortal } from "react-dom";
15564
15563
 
15565
15564
  // src/lib/verse-html-utils.ts
15565
+ import DOMPurify from "isomorphic-dompurify";
15566
15566
  var NON_BREAKING_SPACE = "\xA0";
15567
15567
  var LETTERS = "abcdefghijklmnopqrstuvwxyz";
15568
+ function getFootnoteMarker(index) {
15569
+ const base = LETTERS.length;
15570
+ if (base === 0) return String(index + 1);
15571
+ let value = index;
15572
+ let marker = "";
15573
+ do {
15574
+ marker = LETTERS[value % base] + marker;
15575
+ value = Math.floor(value / base) - 1;
15576
+ } while (value >= 0);
15577
+ return marker;
15578
+ }
15568
15579
  var INTER_FONT = '"Inter", sans-serif';
15569
15580
  var SOURCE_SERIF_FONT = '"Source Serif 4", serif';
15570
15581
  function wrapVerseContent(doc) {
@@ -15657,73 +15668,125 @@ function wrapVerseContent(doc) {
15657
15668
  const verseMarkers = Array.from(doc.querySelectorAll(".yv-v[v]"));
15658
15669
  verseMarkers.forEach(processVerseMarker);
15659
15670
  }
15671
+ var NEEDS_SPACE_BEFORE = /^[^\s.,;:!?)}\]'"»›]/;
15672
+ function buildVerseHtml(wrappers) {
15673
+ const parts = [];
15674
+ let noteIdx = 0;
15675
+ for (let i = 0; i < wrappers.length; i++) {
15676
+ if (i > 0) parts.push(" ");
15677
+ const clone2 = wrappers[i].cloneNode(true);
15678
+ const ownerDoc = wrappers[i].ownerDocument;
15679
+ clone2.querySelectorAll(".yv-h, .yv-vlbl").forEach((el) => el.remove());
15680
+ clone2.querySelectorAll(".yv-n.f").forEach((fn) => {
15681
+ const marker = ownerDoc.createElement("sup");
15682
+ marker.className = "yv:text-muted-foreground";
15683
+ marker.textContent = getFootnoteMarker(noteIdx++);
15684
+ fn.replaceWith(marker);
15685
+ });
15686
+ parts.push(clone2.innerHTML);
15687
+ }
15688
+ return parts.join("");
15689
+ }
15690
+ function replaceFootnotesWithAnchors(doc, footnotes) {
15691
+ for (const fn of footnotes) {
15692
+ const verseNum = fn.closest(".yv-v[v]")?.getAttribute("v");
15693
+ if (!verseNum) continue;
15694
+ const prev = fn.previousSibling;
15695
+ const next = fn.nextSibling;
15696
+ const prevText = prev?.textContent ?? "";
15697
+ const nextText = next?.textContent ?? "";
15698
+ const prevNeedsSpace = prevText.length > 0 && !/\s$/.test(prevText);
15699
+ const nextNeedsSpace = nextText.length > 0 && NEEDS_SPACE_BEFORE.test(nextText);
15700
+ if (prevNeedsSpace && nextNeedsSpace && fn.parentNode) {
15701
+ fn.parentNode.insertBefore(doc.createTextNode(" "), fn);
15702
+ }
15703
+ const anchor = doc.createElement("span");
15704
+ anchor.setAttribute("data-verse-footnote", verseNum);
15705
+ fn.replaceWith(anchor);
15706
+ }
15707
+ }
15660
15708
  function extractNotesFromWrappedHtml(doc) {
15661
15709
  const footnotes = Array.from(doc.querySelectorAll(".yv-n.f"));
15662
15710
  if (!footnotes.length) return {};
15663
15711
  const footnotesByVerse = /* @__PURE__ */ new Map();
15664
- footnotes.forEach((fn) => {
15712
+ for (const fn of footnotes) {
15665
15713
  const verseNum = fn.closest(".yv-v[v]")?.getAttribute("v");
15666
- if (verseNum) {
15667
- let arr = footnotesByVerse.get(verseNum);
15668
- if (!arr) {
15669
- arr = [];
15670
- footnotesByVerse.set(verseNum, arr);
15671
- }
15672
- arr.push(fn);
15714
+ if (!verseNum) continue;
15715
+ let arr = footnotesByVerse.get(verseNum);
15716
+ if (!arr) {
15717
+ arr = [];
15718
+ footnotesByVerse.set(verseNum, arr);
15673
15719
  }
15720
+ arr.push(fn);
15721
+ }
15722
+ const wrappersByVerse = /* @__PURE__ */ new Map();
15723
+ doc.querySelectorAll(".yv-v[v]").forEach((el) => {
15724
+ const verseNum = el.getAttribute("v");
15725
+ if (!verseNum) return;
15726
+ const arr = wrappersByVerse.get(verseNum);
15727
+ if (arr) arr.push(el);
15728
+ else wrappersByVerse.set(verseNum, [el]);
15674
15729
  });
15675
15730
  const notes = {};
15676
- const NEEDS_SPACE_BEFORE = /^[^\s.,;:!?)}\]'"»›]/;
15677
- footnotesByVerse.forEach((fns, verseNum) => {
15678
- const verseWrappers = Array.from(doc.querySelectorAll(`.yv-v[v="${verseNum}"]`));
15679
- let verseHtml = "";
15680
- let noteIdx = 0;
15681
- verseWrappers.forEach((wrapper, wrapperIdx) => {
15682
- if (wrapperIdx > 0) verseHtml += " ";
15683
- const walker = doc.createTreeWalker(wrapper, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
15684
- let lastWasFootnote = false;
15685
- while (walker.nextNode()) {
15686
- const node = walker.currentNode;
15687
- if (node instanceof Element) {
15688
- if (node.classList.contains("yv-n") && node.classList.contains("f")) {
15689
- verseHtml += `<sup class="yv:text-muted-foreground">${LETTERS[noteIdx++] || noteIdx}</sup>`;
15690
- lastWasFootnote = true;
15691
- }
15692
- } else if (node.nodeType === Node.TEXT_NODE) {
15693
- const parent = node.parentElement;
15694
- if (parent?.closest(".yv-n.f") || parent?.closest(".yv-h")) continue;
15695
- if (parent?.classList.contains("yv-vlbl")) continue;
15696
- let text = node.textContent || "";
15697
- if (lastWasFootnote && text && NEEDS_SPACE_BEFORE.test(text)) {
15698
- text = " " + text;
15699
- }
15700
- verseHtml += text;
15701
- lastWasFootnote = false;
15702
- }
15703
- }
15704
- });
15731
+ for (const [verseNum, fns] of footnotesByVerse) {
15705
15732
  notes[verseNum] = {
15706
- verseHtml,
15733
+ verseHtml: buildVerseHtml(wrappersByVerse.get(verseNum) ?? []),
15707
15734
  notes: fns.map((fn) => fn.innerHTML)
15708
15735
  };
15709
- const lastWrapper = verseWrappers[verseWrappers.length - 1];
15710
- if (lastWrapper?.parentNode) {
15711
- const placeholder = doc.createElement("span");
15712
- placeholder.setAttribute("data-verse-footnote", verseNum);
15713
- lastWrapper.parentNode.insertBefore(placeholder, lastWrapper.nextSibling);
15736
+ }
15737
+ replaceFootnotesWithAnchors(doc, footnotes);
15738
+ return notes;
15739
+ }
15740
+ function addNbspToVerseLabels(doc) {
15741
+ doc.querySelectorAll(".yv-vlbl").forEach((label) => {
15742
+ const text = label.textContent || "";
15743
+ if (!text.endsWith(NON_BREAKING_SPACE)) {
15744
+ label.textContent = text + NON_BREAKING_SPACE;
15714
15745
  }
15715
15746
  });
15716
- footnotes.forEach((fn) => {
15717
- const prev = fn.previousSibling;
15718
- const next = fn.nextSibling;
15719
- const prevNeedsSpace = prev?.nodeType === Node.TEXT_NODE && prev.textContent && !/\s$/.test(prev.textContent);
15720
- const nextNeedsSpace = next?.nodeType === Node.TEXT_NODE && next.textContent && NEEDS_SPACE_BEFORE.test(next.textContent);
15721
- if (prevNeedsSpace && nextNeedsSpace) {
15722
- fn.parentNode?.insertBefore(doc.createTextNode(" "), next);
15747
+ }
15748
+ function fixIrregularTables(doc) {
15749
+ doc.querySelectorAll("table").forEach((table) => {
15750
+ const rows = table.querySelectorAll("tr");
15751
+ if (rows.length === 0) return;
15752
+ let maxColumns = 0;
15753
+ rows.forEach((row) => {
15754
+ let count = 0;
15755
+ row.querySelectorAll("td, th").forEach((cell) => {
15756
+ count += cell instanceof HTMLTableCellElement ? parseInt(cell.getAttribute("colspan") || "1", 10) : 1;
15757
+ });
15758
+ maxColumns = Math.max(maxColumns, count);
15759
+ });
15760
+ if (maxColumns > 1) {
15761
+ rows.forEach((row) => {
15762
+ const cells = row.querySelectorAll("td, th");
15763
+ if (cells.length === 1 && cells[0] instanceof HTMLTableCellElement) {
15764
+ const existing = parseInt(cells[0].getAttribute("colspan") || "1", 10);
15765
+ if (existing < maxColumns) {
15766
+ cells[0].setAttribute("colspan", maxColumns.toString());
15767
+ }
15768
+ }
15769
+ });
15723
15770
  }
15724
- fn.remove();
15725
15771
  });
15726
- return notes;
15772
+ }
15773
+ var DOMPURIFY_CONFIG = {
15774
+ ALLOWED_ATTR: ["class", "style", "id", "v", "usfm"],
15775
+ ALLOW_DATA_ATTR: true
15776
+ };
15777
+ function transformBibleHtml(html) {
15778
+ if (typeof window === "undefined" || !("DOMParser" in window)) {
15779
+ return { html, notes: {} };
15780
+ }
15781
+ const doc = new DOMParser().parseFromString(
15782
+ DOMPurify.sanitize(html, DOMPURIFY_CONFIG),
15783
+ "text/html"
15784
+ );
15785
+ wrapVerseContent(doc);
15786
+ const notes = extractNotesFromWrappedHtml(doc);
15787
+ addNbspToVerseLabels(doc);
15788
+ fixIrregularTables(doc);
15789
+ return { html: doc.body.innerHTML, notes };
15727
15790
  }
15728
15791
 
15729
15792
  // src/components/icons/footnote.tsx
@@ -15784,20 +15847,23 @@ var VerseFootnoteButton = memo(function VerseFootnoteButton2({
15784
15847
  dangerouslySetInnerHTML: { __html: verseNotes.verseHtml }
15785
15848
  }
15786
15849
  ),
15787
- /* @__PURE__ */ jsx20("ul", { className: "yv:list-none yv:p-0 yv:m-0 yv:space-y-1", children: verseNotes.notes.map((note, index) => /* @__PURE__ */ jsxs6(
15788
- "li",
15789
- {
15790
- className: "yv:flex yv:gap-2 yv:text-xs yv:border-b yv:border-border yv:py-2",
15791
- children: [
15792
- /* @__PURE__ */ jsxs6("span", { className: "", children: [
15793
- LETTERS[index] || index + 1,
15794
- "."
15795
- ] }),
15796
- /* @__PURE__ */ jsx20("span", { dangerouslySetInnerHTML: { __html: note } })
15797
- ]
15798
- },
15799
- LETTERS[index]
15800
- )) })
15850
+ /* @__PURE__ */ jsx20("ul", { className: "yv:list-none yv:p-0 yv:m-0 yv:space-y-1", children: verseNotes.notes.map((note, index) => {
15851
+ const marker = getFootnoteMarker(index);
15852
+ return /* @__PURE__ */ jsxs6(
15853
+ "li",
15854
+ {
15855
+ className: "yv:flex yv:gap-2 yv:text-xs yv:border-b yv:border-border yv:py-2",
15856
+ children: [
15857
+ /* @__PURE__ */ jsxs6("span", { className: "", children: [
15858
+ marker,
15859
+ "."
15860
+ ] }),
15861
+ /* @__PURE__ */ jsx20("span", { dangerouslySetInnerHTML: { __html: note } })
15862
+ ]
15863
+ },
15864
+ marker
15865
+ );
15866
+ }) })
15801
15867
  ] })
15802
15868
  }
15803
15869
  )
@@ -15814,57 +15880,39 @@ function BibleTextHtml({
15814
15880
  highlightedVerses = {}
15815
15881
  }) {
15816
15882
  const contentRef = useRef3(null);
15817
- const [placeholders, setPlaceholders] = useState3(/* @__PURE__ */ new Map());
15883
+ const [placeholders, setPlaceholders] = useState3([]);
15818
15884
  const providerTheme = useTheme3();
15819
15885
  const currentTheme = theme || providerTheme;
15820
15886
  useLayoutEffect(() => {
15821
15887
  if (!contentRef.current) return;
15822
15888
  contentRef.current.innerHTML = html;
15823
- const map2 = /* @__PURE__ */ new Map();
15824
- Object.keys(notes).forEach((verseNum) => {
15825
- const el = contentRef.current?.querySelector(`[data-verse-footnote="${verseNum}"]`);
15826
- if (el) map2.set(verseNum, el);
15889
+ const anchors = contentRef.current.querySelectorAll("[data-verse-footnote]");
15890
+ const result = [];
15891
+ anchors.forEach((el) => {
15892
+ const verseNum = el.getAttribute("data-verse-footnote");
15893
+ if (verseNum) result.push({ verseNum, el });
15827
15894
  });
15828
- setPlaceholders(map2);
15829
- }, [html, notes]);
15895
+ setPlaceholders(result);
15896
+ }, [html]);
15830
15897
  useLayoutEffect(() => {
15831
15898
  if (!contentRef.current) return;
15832
- const verseElements = contentRef.current.querySelectorAll(".yv-v[v]");
15833
- verseElements.forEach((el) => {
15899
+ contentRef.current.querySelectorAll(".yv-v[v]").forEach((el) => {
15834
15900
  const verseNum = parseInt(el.getAttribute("v") || "0", 10);
15835
- if (selectedVerses.includes(verseNum)) {
15836
- el.classList.add("yv-v-selected");
15837
- } else {
15838
- el.classList.remove("yv-v-selected");
15839
- }
15840
- if (highlightedVerses[verseNum]) {
15841
- el.classList.add("yv-v-highlighted");
15842
- } else {
15843
- el.classList.remove("yv-v-highlighted");
15844
- }
15901
+ el.classList.toggle("yv-v-selected", selectedVerses.includes(verseNum));
15902
+ el.classList.toggle("yv-v-highlighted", !!highlightedVerses[verseNum]);
15845
15903
  });
15846
15904
  }, [html, selectedVerses, highlightedVerses]);
15847
- const selectedVersesRef = useRef3(selectedVerses);
15848
- selectedVersesRef.current = selectedVerses;
15849
- useLayoutEffect(() => {
15850
- const element = contentRef.current;
15851
- if (!element || !onVerseSelect) return;
15852
- const handleClick = (e) => {
15853
- const target = e.target;
15854
- const verseEl = target.closest(".yv-v[v]");
15855
- if (!verseEl) return;
15856
- const verseNum = parseInt(verseEl.getAttribute("v") || "0", 10);
15857
- if (verseNum === 0) return;
15858
- const current = selectedVersesRef.current;
15859
- const newSelected = current.includes(verseNum) ? current.filter((v) => v !== verseNum) : [...current, verseNum].sort((a, b) => a - b);
15860
- onVerseSelect(newSelected);
15861
- };
15862
- element.addEventListener("click", handleClick);
15863
- return () => element.removeEventListener("click", handleClick);
15864
- }, [onVerseSelect]);
15905
+ const handleClick = onVerseSelect ? (e) => {
15906
+ const verseEl = e.target.closest(".yv-v[v]");
15907
+ if (!verseEl) return;
15908
+ const verseNum = parseInt(verseEl.getAttribute("v") || "0", 10);
15909
+ if (!verseNum) return;
15910
+ const newSelected = selectedVerses.includes(verseNum) ? selectedVerses.filter((v) => v !== verseNum) : [...selectedVerses, verseNum].sort((a, b) => a - b);
15911
+ onVerseSelect(newSelected);
15912
+ } : void 0;
15865
15913
  return /* @__PURE__ */ jsxs6(Fragment2, { children: [
15866
- /* @__PURE__ */ jsx20("div", { ref: contentRef }),
15867
- Array.from(placeholders.entries()).map(([verseNum, el]) => {
15914
+ /* @__PURE__ */ jsx20("div", { ref: contentRef, onClick: handleClick }),
15915
+ placeholders.map(({ verseNum, el }, index) => {
15868
15916
  const verseNotes = notes[verseNum];
15869
15917
  if (!verseNotes) return null;
15870
15918
  return createPortal(
@@ -15878,68 +15926,12 @@ function BibleTextHtml({
15878
15926
  theme: currentTheme
15879
15927
  }
15880
15928
  ),
15881
- el
15929
+ el,
15930
+ `${verseNum}-${index}`
15882
15931
  );
15883
15932
  })
15884
15933
  ] });
15885
15934
  }
15886
- var DOMPURIFY_CONFIG = {
15887
- ALLOWED_ATTR: ["class", "style", "id", "v", "usfm"],
15888
- ALLOW_DATA_ATTR: true
15889
- };
15890
- function yvDomTransformer(html, extractNotes = false) {
15891
- if (typeof window === "undefined" || !("DOMParser" in window)) {
15892
- return { html, notes: {} };
15893
- }
15894
- const doc = new DOMParser().parseFromString(
15895
- DOMPurify.sanitize(html, DOMPURIFY_CONFIG),
15896
- "text/html"
15897
- );
15898
- wrapVerseContent(doc);
15899
- const extractedNotes = extractNotes ? extractNotesFromWrappedHtml(doc) : {};
15900
- const verseLabels = doc.querySelectorAll(".yv-vlbl");
15901
- verseLabels.forEach((label) => {
15902
- const text = label.textContent || "";
15903
- if (!text.endsWith(NON_BREAKING_SPACE)) {
15904
- label.textContent = text + NON_BREAKING_SPACE;
15905
- }
15906
- });
15907
- const tables = doc.querySelectorAll("table");
15908
- tables.forEach((table) => {
15909
- const rows = table.querySelectorAll("tr");
15910
- if (rows.length === 0) return;
15911
- let maxColumns = 0;
15912
- rows.forEach((row) => {
15913
- const cells = row.querySelectorAll("td, th");
15914
- let rowColumnCount = 0;
15915
- cells.forEach((cell) => {
15916
- if (cell instanceof HTMLTableCellElement) {
15917
- const colspan = parseInt(cell.getAttribute("colspan") || "1", 10);
15918
- rowColumnCount += colspan;
15919
- } else {
15920
- rowColumnCount += 1;
15921
- }
15922
- });
15923
- maxColumns = Math.max(maxColumns, rowColumnCount);
15924
- });
15925
- if (maxColumns > 1) {
15926
- rows.forEach((row) => {
15927
- const cells = row.querySelectorAll("td, th");
15928
- if (cells.length === 1) {
15929
- const cell = cells[0];
15930
- if (cell instanceof HTMLTableCellElement) {
15931
- const existingColspan = parseInt(cell.getAttribute("colspan") || "1", 10);
15932
- if (existingColspan < maxColumns) {
15933
- cell.setAttribute("colspan", maxColumns.toString());
15934
- }
15935
- }
15936
- }
15937
- });
15938
- }
15939
- });
15940
- const modifiedHtml = doc.body.innerHTML;
15941
- return { html: modifiedHtml, notes: extractedNotes };
15942
- }
15943
15935
  var Verse = {
15944
15936
  /**
15945
15937
  * Renders a single verse with superscript number and text.
@@ -15980,12 +15972,9 @@ var Verse = {
15980
15972
  onVerseSelect,
15981
15973
  highlightedVerses
15982
15974
  }, ref) => {
15983
- const [transformedData, setTransformedData] = useState3({ html, notes: {} });
15975
+ const transformedData = useMemo3(() => transformBibleHtml(html), [html]);
15984
15976
  const providerTheme = useTheme3();
15985
15977
  const currentTheme = theme || providerTheme;
15986
- useEffect3(() => {
15987
- setTransformedData(yvDomTransformer(html, true));
15988
- }, [html]);
15989
15978
  return /* @__PURE__ */ jsx20(
15990
15979
  "section",
15991
15980
  {
@@ -16145,10 +16134,10 @@ function Root6({
16145
16134
  setCurrentFontFamily(savedFontFamily);
16146
16135
  }
16147
16136
  }, []);
16148
- useEffect4(() => {
16137
+ useEffect3(() => {
16149
16138
  localStorage.setItem("youversion-platform:reader:font-size", currentFontSize.toString());
16150
16139
  }, [currentFontSize]);
16151
- useEffect4(() => {
16140
+ useEffect3(() => {
16152
16141
  localStorage.setItem("youversion-platform:reader:font-family", currentFontFamily);
16153
16142
  }, [currentFontFamily]);
16154
16143
  const providerTheme = useTheme4();
@@ -16191,7 +16180,7 @@ function Content5() {
16191
16180
  } = useBibleReaderContext();
16192
16181
  const { books } = useBooks2(versionId);
16193
16182
  const { version: version2 } = useVersion2(versionId);
16194
- const bookData = useMemo3(() => {
16183
+ const bookData = useMemo4(() => {
16195
16184
  return books?.data?.find((b) => b.id === book);
16196
16185
  }, [books?.data, book]);
16197
16186
  const usfmReference = `${book}.${chapter}`;
@@ -16249,7 +16238,7 @@ function Content5() {
16249
16238
  function UserMenu() {
16250
16239
  const { auth, signIn, signOut, userInfo } = useYVAuth();
16251
16240
  return /* @__PURE__ */ jsxs7(Popover, { children: [
16252
- /* @__PURE__ */ jsx21(PopoverTrigger, { "data-testid": "user-menu-trigger", children: /* @__PURE__ */ jsx21(Button, { size: "icon", variant: "secondary", children: auth.isAuthenticated && userInfo?.avatarUrlFormat ? /* @__PURE__ */ jsx21(
16241
+ /* @__PURE__ */ jsx21(PopoverTrigger, { asChild: true, "data-testid": "user-menu-trigger", children: /* @__PURE__ */ jsx21(Button, { size: "icon", variant: "secondary", children: auth.isAuthenticated && userInfo?.avatarUrlFormat ? /* @__PURE__ */ jsx21(
16253
16242
  "img",
16254
16243
  {
16255
16244
  src: userInfo.getAvatarUrl(32, 32)?.toString(),
@@ -16342,7 +16331,7 @@ function Toolbar({ border = "top" }) {
16342
16331
  )
16343
16332
  ] }),
16344
16333
  /* @__PURE__ */ jsxs7(Popover, { children: [
16345
- /* @__PURE__ */ jsx21(PopoverTrigger, { "aria-label": "Settings", children: /* @__PURE__ */ jsx21(Button, { size: "icon", variant: "secondary", children: /* @__PURE__ */ jsx21(GearIcon, { className: "yv:text-foreground" }) }) }),
16334
+ /* @__PURE__ */ jsx21(PopoverTrigger, { asChild: true, "aria-label": "Settings", children: /* @__PURE__ */ jsx21(Button, { size: "icon", variant: "secondary", children: /* @__PURE__ */ jsx21(GearIcon, { className: "yv:text-foreground" }) }) }),
16346
16335
  /* @__PURE__ */ jsx21(PopoverContent, { sideOffset: 16, heading: "Reader Settings", theme: background, children: /* @__PURE__ */ jsxs7("div", { className: "yv:flex yv:flex-col yv:gap-4 yv:p-4", children: [
16347
16336
  /* @__PURE__ */ jsxs7("div", { className: "yv:grid yv:grid-cols-2", children: [
16348
16337
  /* @__PURE__ */ jsx21(
@@ -16433,7 +16422,7 @@ function Toolbar({ border = "top" }) {
16433
16422
  var BibleReader = Object.assign({}, { Root: Root6, Content: Content5, Toolbar });
16434
16423
 
16435
16424
  // src/components/YouVersionAuthButton.tsx
16436
- import React10, { useMemo as useMemo4 } from "react";
16425
+ import React10, { useMemo as useMemo5 } from "react";
16437
16426
 
16438
16427
  // src/components/icons/loader.tsx
16439
16428
  import { jsx as jsx22 } from "react/jsx-runtime";
@@ -16595,7 +16584,7 @@ var YouVersionAuthButton = React10.forwardRef(
16595
16584
  }
16596
16585
  };
16597
16586
  const buttonLoading = auth.isLoading;
16598
- const buttonText = useMemo4(() => {
16587
+ const buttonText = useMemo5(() => {
16599
16588
  if (text) return text;
16600
16589
  const isSignOut = mode === "signOut" || mode === "auto" && auth.isAuthenticated;
16601
16590
  if (size === "short") {
@@ -1,5 +1,13 @@
1
- export declare const NON_BREAKING_SPACE = "\u00A0";
2
- export declare const LETTERS = "abcdefghijklmnopqrstuvwxyz";
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;
3
11
  export type VerseNotes = {
4
12
  verseHtml: string;
5
13
  notes: string[];
@@ -8,24 +16,16 @@ export declare const INTER_FONT: "\"Inter\", sans-serif";
8
16
  export declare const SOURCE_SERIF_FONT: "\"Source Serif 4\", serif";
9
17
  export type FontFamily = typeof INTER_FONT | typeof SOURCE_SERIF_FONT | (string & {});
10
18
  /**
11
- * Wraps verse content in `yv-v` elements for easier CSS targeting.
12
- *
13
- * Transforms empty verse markers into wrapping containers. When a verse spans
14
- * multiple paragraphs, creates duplicate wrappers in each paragraph (Bible.com pattern).
15
- *
16
- * Before: <span class="yv-v" v="1"></span><span class="yv-vlbl">1</span>Text...
17
- * After: <span class="yv-v" v="1"><span class="yv-vlbl">1</span>Text...</span>
19
+ * Full transformation pipeline for Bible HTML from the API.
18
20
  *
19
- * This enables simple CSS selectors like `.yv-v[v="1"] { background: yellow; }`
21
+ * 1. Sanitize (DOMPurify)
22
+ * 2. Wrap verse content in selectable spans
23
+ * 3. Extract footnotes and replace with portal anchors
24
+ * 4. Add non-breaking spaces to verse labels
25
+ * 5. Fix irregular table layouts
20
26
  */
21
- export declare function wrapVerseContent(doc: Document): void;
22
- /**
23
- * Extracts footnotes from wrapped verse HTML and prepares data for footnote popovers.
24
- *
25
- * This function assumes verses are already wrapped in `.yv-v[v]` elements (by wrapVerseContent).
26
- * It uses `.closest('.yv-v[v]')` to find which verse each footnote belongs to.
27
- *
28
- * @returns Notes data for popovers, keyed by verse number
29
- */
30
- export declare function extractNotesFromWrappedHtml(doc: Document): Record<string, VerseNotes>;
27
+ export declare function transformBibleHtml(html: string): {
28
+ html: string;
29
+ notes: Record<string, VerseNotes>;
30
+ };
31
31
  //# sourceMappingURL=verse-html-utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"verse-html-utils.d.ts","sourceRoot":"","sources":["../../src/lib/verse-html-utils.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAE3C,eAAO,MAAM,OAAO,+BAA+B,CAAC;AAEpD,MAAM,MAAM,UAAU,GAAG;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,UAAU,EAAG,uBAA8B,CAAC;AACzD,eAAO,MAAM,iBAAiB,EAAG,2BAAkC,CAAC;AACpE,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,GAAG,OAAO,iBAAiB,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAEtF;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CA0IpD;AAGD;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAkGrF"}
1
+ {"version":3,"file":"verse-html-utils.d.ts","sourceRoot":"","sources":["../../src/lib/verse-html-utils.ts"],"names":[],"mappings":"AAMA;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAavD;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,UAAU,EAAG,uBAA8B,CAAC;AACzD,eAAO,MAAM,iBAAiB,EAAG,2BAAkC,CAAC;AACpE,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,GAAG,OAAO,iBAAiB,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AA0UtF;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;CAAE,CAgBpG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youversion/platform-react-ui",
3
- "version": "1.14.3",
3
+ "version": "1.14.4",
4
4
  "description": "React SDK for YouVersion Platform",
5
5
  "license": "TBD",
6
6
  "type": "module",
@@ -38,8 +38,8 @@
38
38
  "isomorphic-dompurify": "2.23.0",
39
39
  "tailwind-merge": "3.3.1",
40
40
  "tw-animate-css": "1.4.0",
41
- "@youversion/platform-core": "1.14.3",
42
- "@youversion/platform-react-hooks": "1.14.3"
41
+ "@youversion/platform-core": "1.14.4",
42
+ "@youversion/platform-react-hooks": "1.14.4"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "react": ">=19.1.0 <20.0.0",
@@ -76,8 +76,8 @@
76
76
  "vite": "7.1.11",
77
77
  "vitest": "4.0.4",
78
78
  "vitest-browser-react": "2.0.2",
79
- "@internal/eslint-config": "0.0.0",
80
- "@internal/tsconfig": "0.0.0"
79
+ "@internal/tsconfig": "0.0.0",
80
+ "@internal/eslint-config": "0.0.0"
81
81
  },
82
82
  "publishConfig": {
83
83
  "access": "public",