Skip to content

Commit 3023085

Browse files
committed
refactor(wrapVerseContent): consolidate into single function
These functions lack meaningful context when isolated, so consolidating them improves code clarity and maintainability.
1 parent 7bd3d0f commit 3023085

File tree

1 file changed

+109
-108
lines changed

1 file changed

+109
-108
lines changed

packages/ui/src/lib/verse-html-utils.ts

Lines changed: 109 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -19,141 +19,142 @@ export type VerseNotes = {
1919
* This enables simple CSS selectors like `.yv-v[v="1"] { background: yellow; }`
2020
*/
2121
export function wrapVerseContent(doc: Document): void {
22-
const verseMarkers = Array.from(doc.querySelectorAll('.yv-v[v]'));
23-
verseMarkers.forEach(processVerseMarker);
24-
}
25-
26-
function processVerseMarker(marker: Element, index: number, markers: Element[]): void {
27-
const verseNum = marker.getAttribute('v');
28-
if (!verseNum) return;
22+
/**
23+
* Wraps all content in a paragraph with a verse span.
24+
*/
25+
function wrapParagraphContent(doc: Document, paragraph: Element, verseNum: string): void {
26+
const children = Array.from(paragraph.childNodes);
27+
if (children.length === 0) return;
28+
29+
const wrapper = doc.createElement('span');
30+
wrapper.className = 'yv-v';
31+
wrapper.setAttribute('v', verseNum);
32+
33+
const firstChild = children[0];
34+
if (firstChild) {
35+
paragraph.insertBefore(wrapper, firstChild);
36+
}
37+
children.forEach((child) => {
38+
wrapper.appendChild(child);
39+
});
40+
}
2941

30-
const nodesToWrap = collectNodesBetweenMarkers(marker, markers[index + 1]);
31-
if (nodesToWrap.length === 0) return;
42+
/**
43+
* Wraps paragraphs between startParagraph and an optional endParagraph boundary.
44+
* If no endParagraph is provided, wraps until a verse marker is found or siblings are exhausted.
45+
*/
46+
function wrapParagraphsUntilBoundary(
47+
doc: Document,
48+
verseNum: string,
49+
startParagraph: Element | null,
50+
endParagraph?: Element | null,
51+
): void {
52+
if (!startParagraph) return;
53+
54+
let currentP: Element | null = startParagraph.nextElementSibling;
55+
56+
while (currentP && currentP !== endParagraph) {
57+
// Skip heading elements - these are structural, not verse content
58+
// See iOS implementation: https://github.com/youversion/platform-sdk-swift/blob/main/Sources/YouVersionPlatformUI/Views/Rendering/BibleVersionRendering.swift
59+
const isHeading =
60+
currentP.classList.contains('yv-h') ||
61+
currentP.matches('.s1, .s2, .s3, .s4, .ms, .ms1, .ms2, .ms3, .ms4, .mr, .sp, .sr, .qa, .r');
62+
if (isHeading) {
63+
currentP = currentP.nextElementSibling;
64+
continue;
65+
}
3266

33-
wrapNodesInVerse(marker, verseNum, nodesToWrap);
34-
handleParagraphWrapping(marker, markers[index + 1], verseNum);
35-
}
67+
if (currentP.querySelector('.yv-v[v]')) break;
3668

37-
function collectNodesBetweenMarkers(startMarker: Element, endMarker: Element | undefined): Node[] {
38-
const nodes: Node[] = [];
39-
let current: Node | null = startMarker.nextSibling;
69+
if (
70+
currentP.classList.contains('p') ||
71+
currentP.tagName === 'P'
72+
) {
73+
wrapParagraphContent(doc, currentP, verseNum);
74+
}
4075

41-
while (current && !shouldStopCollecting(current, endMarker)) {
42-
if (shouldSkipNode(current)) {
43-
current = current.nextSibling;
44-
continue;
76+
currentP = currentP.nextElementSibling;
4577
}
46-
nodes.push(current);
47-
current = current.nextSibling;
4878
}
4979

50-
return nodes;
51-
}
80+
function handleParagraphWrapping(
81+
marker: Element,
82+
nextMarker: Element | undefined,
83+
verseNum: string | null,
84+
): void {
85+
const doc = marker.ownerDocument;
86+
const currentParagraph = marker.closest('.p, p, div.p');
87+
if (!currentParagraph || !verseNum) return;
88+
89+
if (!nextMarker) {
90+
wrapParagraphsUntilBoundary(doc, verseNum, currentParagraph);
91+
return;
92+
}
5293

53-
function shouldStopCollecting(node: Node, endMarker: Element | undefined): boolean {
54-
if (node === endMarker) return true;
55-
if (endMarker && node instanceof Element && node.contains(endMarker)) return true;
56-
return false;
57-
}
94+
const nextParagraph = nextMarker.closest('.p, p, div.p');
95+
if (nextParagraph && currentParagraph !== nextParagraph) {
96+
wrapParagraphsUntilBoundary(doc, verseNum, currentParagraph, nextParagraph);
97+
}
98+
}
5899

59-
function shouldSkipNode(node: Node): boolean {
60-
return node instanceof Element && node.classList.contains('yv-h');
61-
}
100+
function wrapNodesInVerse(marker: Element, verseNum: string, nodes: Node[]): void {
101+
const wrapper = marker.ownerDocument.createElement('span');
102+
wrapper.className = 'yv-v';
103+
wrapper.setAttribute('v', verseNum);
62104

63-
function wrapNodesInVerse(marker: Element, verseNum: string, nodes: Node[]): void {
64-
const wrapper = marker.ownerDocument.createElement('span');
65-
wrapper.className = 'yv-v';
66-
wrapper.setAttribute('v', verseNum);
105+
const firstNode = nodes[0];
106+
if (firstNode) {
107+
marker.parentNode?.insertBefore(wrapper, firstNode);
108+
}
67109

68-
const firstNode = nodes[0];
69-
if (firstNode) {
70-
marker.parentNode?.insertBefore(wrapper, firstNode);
110+
nodes.forEach((node) => {
111+
wrapper.appendChild(node);
112+
});
113+
marker.remove();
71114
}
72115

73-
nodes.forEach((node) => {
74-
wrapper.appendChild(node);
75-
});
76-
marker.remove();
77-
}
78-
79-
function handleParagraphWrapping(
80-
marker: Element,
81-
nextMarker: Element | undefined,
82-
verseNum: string | null,
83-
): void {
84-
const doc = marker.ownerDocument;
85-
const currentParagraph = marker.closest('.p, p, div.p');
86-
if (!currentParagraph || !verseNum) return;
87-
88-
if (!nextMarker) {
89-
wrapParagraphsUntilBoundary(doc, verseNum, currentParagraph);
90-
return;
116+
function shouldStopCollecting(node: Node, endMarker: Element | undefined): boolean {
117+
if (node === endMarker) return true;
118+
if (endMarker && node instanceof Element && node.contains(endMarker)) return true;
119+
return false;
91120
}
92121

93-
const nextParagraph = nextMarker.closest('.p, p, div.p');
94-
if (nextParagraph && currentParagraph !== nextParagraph) {
95-
wrapParagraphsUntilBoundary(doc, verseNum, currentParagraph, nextParagraph);
122+
function shouldSkipNode(node: Node): boolean {
123+
return node instanceof Element && node.classList.contains('yv-h');
96124
}
97-
}
98-
99-
/**
100-
* Wraps paragraphs between startParagraph and an optional endParagraph boundary.
101-
* If no endParagraph is provided, wraps until a verse marker is found or siblings are exhausted.
102-
*/
103-
function wrapParagraphsUntilBoundary(
104-
doc: Document,
105-
verseNum: string,
106-
startParagraph: Element | null,
107-
endParagraph?: Element | null,
108-
): void {
109-
if (!startParagraph) return;
110-
111-
let currentP: Element | null = startParagraph.nextElementSibling;
112-
113-
while (currentP && currentP !== endParagraph) {
114-
// Skip heading elements - these are structural, not verse content
115-
// See iOS implementation: https://github.com/youversion/platform-sdk-swift/blob/main/Sources/YouVersionPlatformUI/Views/Rendering/BibleVersionRendering.swift
116-
const isHeading =
117-
currentP.classList.contains('yv-h') ||
118-
currentP.matches('.s1, .s2, .s3, .s4, .ms, .ms1, .ms2, .ms3, .ms4, .mr, .sp, .sr, .qa, .r');
119-
if (isHeading) {
120-
currentP = currentP.nextElementSibling;
121-
continue;
122-
}
123125

124-
if (currentP.querySelector('.yv-v[v]')) break;
126+
function collectNodesBetweenMarkers(startMarker: Element, endMarker: Element | undefined): Node[] {
127+
const nodes: Node[] = [];
128+
let current: Node | null = startMarker.nextSibling;
125129

126-
if (
127-
currentP.classList.contains('p') ||
128-
currentP.tagName === 'P'
129-
) {
130-
wrapParagraphContent(doc, currentP, verseNum);
130+
while (current && !shouldStopCollecting(current, endMarker)) {
131+
if (shouldSkipNode(current)) {
132+
current = current.nextSibling;
133+
continue;
134+
}
135+
nodes.push(current);
136+
current = current.nextSibling;
131137
}
132138

133-
currentP = currentP.nextElementSibling;
139+
return nodes;
134140
}
135-
}
136141

137-
/**
138-
* Wraps all content in a paragraph with a verse span.
139-
*/
140-
function wrapParagraphContent(doc: Document, paragraph: Element, verseNum: string): void {
141-
const children = Array.from(paragraph.childNodes);
142-
if (children.length === 0) return;
142+
function processVerseMarker(marker: Element, index: number, markers: Element[]): void {
143+
const verseNum = marker.getAttribute('v');
144+
if (!verseNum) return;
143145

144-
const wrapper = doc.createElement('span');
145-
wrapper.className = 'yv-v';
146-
wrapper.setAttribute('v', verseNum);
146+
const nodesToWrap = collectNodesBetweenMarkers(marker, markers[index + 1]);
147+
if (nodesToWrap.length === 0) return;
147148

148-
const firstChild = children[0];
149-
if (firstChild) {
150-
paragraph.insertBefore(wrapper, firstChild);
149+
wrapNodesInVerse(marker, verseNum, nodesToWrap);
150+
handleParagraphWrapping(marker, markers[index + 1], verseNum);
151151
}
152-
children.forEach((child) => {
153-
wrapper.appendChild(child);
154-
});
152+
153+
const verseMarkers = Array.from(doc.querySelectorAll('.yv-v[v]'));
154+
verseMarkers.forEach(processVerseMarker);
155155
}
156156

157+
157158
/**
158159
* Extracts footnotes from wrapped verse HTML and prepares data for footnote popovers.
159160
*

0 commit comments

Comments
 (0)