@@ -19,141 +19,142 @@ export type VerseNotes = {
1919 * This enables simple CSS selectors like `.yv-v[v="1"] { background: yellow; }`
2020 */
2121export 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