Skip to content

Commit 53f3158

Browse files
authored
fix: don't render headlines when they're at the end of a passage
1 parent d0f6234 commit 53f3158

File tree

3 files changed

+283
-2
lines changed

3 files changed

+283
-2
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,12 @@ Building AI applications with Bible content? Access YouVersion's LLM-optimized e
260260

261261
We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md) for details on development setup, code style, and the pull request process.
262262

263+
## Bible Rendering Tests
264+
265+
Run focused tests: `swift test --filter BibleVersionRenderingTests`
266+
267+
Note: this suite currently seeds `ChapterDiskCache` as a test harness. Once `BibleVersionRendering.textBlocks` accepts raw HTML directly, migrate tests to inject HTML without disk cache writes.
268+
263269
## Support
264270

265271
- **Issues**: [GitHub Issues](https://github.com/youversion/platform-sdk-swift/issues)

Sources/YouVersionPlatformUI/Views/Rendering/BibleVersionRendering.swift

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,19 +365,44 @@ public enum BibleVersionRendering {
365365
marginTop: &marginTop
366366
)
367367

368-
for child in node.children {
368+
for (index, child) in node.children.enumerated() {
369369
if child.type == .block || child.type == .table {
370-
if !stateUp.isTextEmpty {
370+
let hadPendingText = !stateUp.isTextEmpty
371+
if hadPendingText {
371372
if stateUp.rendering {
372373
ret.append(createBlock(stateDown: stateDown, stateUp: &stateUp, marginTop: marginTop))
373374
}
374375
stateUp.clearText()
375376
}
377+
let isHeader = child.classes.contains("yv-h") || child.classes.contains("yvh")
378+
let savedRendering = stateUp.rendering
379+
380+
if isHeader && stateIn.renderHeadlines {
381+
let followingChildren = node.children.dropFirst(index + 1)
382+
let nextVerse = followingChildren
383+
.compactMap { firstVerseInNode($0) }
384+
.first
385+
let immediateNextVerse = followingChildren.first.flatMap { firstVerseInNode($0) }
386+
let isNextVerseInRange = nextVerse != nil
387+
&& nextVerse! >= stateIn.fromVerse
388+
&& nextVerse! <= stateIn.toVerse
389+
390+
if !stateUp.rendering && isNextVerseInRange {
391+
stateUp.rendering = true
392+
} else if stateUp.rendering && nextVerse != nil && !isNextVerseInRange && !hadPendingText && immediateNextVerse != nil {
393+
stateUp.rendering = false
394+
}
395+
}
396+
376397
if child.type == .block {
377398
handleNodeBlock(node: child, stateIn: stateIn, stateDown: stateDown, stateUp: &stateUp, ret: &ret)
378399
} else if child.type == .table {
379400
handleNodeTable(node: child, stateIn: stateIn, stateDown: stateDown, stateUp: &stateUp, ret: &ret)
380401
}
402+
403+
if isHeader {
404+
stateUp.rendering = savedRendering
405+
}
381406
} else {
382407
if child.type == .span && child.classes.contains("qs") { // Selah. Force a line break and right-alignment.
383408
if !stateUp.isTextEmpty {
@@ -591,6 +616,21 @@ public enum BibleVersionRendering {
591616
return localCopy
592617
}
593618

619+
/// Finds the first verse number in a node's subtree by searching for verse-labeled spans.
620+
private static func firstVerseInNode(_ node: BibleTextNode) -> Int? {
621+
if node.classes.contains("yv-v") || node.classes.contains("verse") {
622+
if let v = node.attributes["v"], let vi = Int(v) {
623+
return vi
624+
}
625+
}
626+
for child in node.children {
627+
if let found = firstVerseInNode(child) {
628+
return found
629+
}
630+
}
631+
return nil
632+
}
633+
594634
private static func assertionFailed(
595635
_ message: String,
596636
string: String? = nil,
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import SwiftUI
2+
import Testing
3+
@testable import YouVersionPlatformCore
4+
@testable import YouVersionPlatformUI
5+
6+
@MainActor
7+
@Suite struct BibleVersionRenderingTests {
8+
private let defaultVersionId = 1
9+
private let fonts = BibleTextFonts(familyName: "Times New Roman", baseSize: 16)
10+
11+
private func renderBlocks(
12+
html: String,
13+
reference: BibleReference,
14+
renderHeadlines: Bool = true
15+
) async throws -> [BibleTextBlock] {
16+
let node = try #require(try BibleTextNode.parse(html))
17+
18+
let blocks = try await BibleVersionRendering.textBlocks(
19+
from: node,
20+
reference: reference,
21+
renderHeadlines: renderHeadlines,
22+
renderVerseNumbers: true,
23+
footnotesMode: .none,
24+
textColor: .black,
25+
wocColor: .red,
26+
fonts: fonts
27+
)
28+
29+
return try #require(blocks)
30+
}
31+
32+
private func hasHeaderContaining(_ blocks: [BibleTextBlock], text: String) -> Bool {
33+
blocks.contains { block in
34+
let runs = block.text.asAttributedString.runs[\.bibleTextCategory]
35+
let hasHeader = runs.contains { $0.0 == .header }
36+
return hasHeader && block.text.characters.contains(text)
37+
}
38+
}
39+
40+
private func hasScriptureContaining(_ blocks: [BibleTextBlock], text: String) -> Bool {
41+
blocks.contains { block in
42+
block.text.characters.contains(text)
43+
}
44+
}
45+
46+
@Test func testHeaderBeforeFirstVerseInRangeIsRendered() async throws {
47+
let html = """
48+
<div>
49+
<div class="yv-h s1"><span>The List</span></div>
50+
<div class="p">
51+
<span class="yv-v" v="5"></span>
52+
<span class="yv-vlbl">5</span>
53+
Fifth verse text.
54+
</div>
55+
<div class="p">
56+
<span class="yv-v" v="6"></span>
57+
<span class="yv-vlbl">6</span>
58+
Sixth verse text.
59+
</div>
60+
</div>
61+
"""
62+
63+
let reference = BibleReference(versionId: defaultVersionId, bookUSFM: "GEN", chapter: 1, verseStart: 5, verseEnd: 10)
64+
65+
let blocks = try await renderBlocks(html: html, reference: reference)
66+
#expect(hasHeaderContaining(blocks, text: "The List"))
67+
#expect(hasScriptureContaining(blocks, text: "Fifth verse text."))
68+
}
69+
70+
@Test func testHeaderInMiddleOfLastVerseInRangeIsRendered() async throws {
71+
let html = """
72+
<div>
73+
<div class="p">
74+
<span class="yv-v" v="5"></span>
75+
<span class="yv-vlbl">5</span>
76+
Part one of verse five.
77+
</div>
78+
<div class="yv-h s1"><span>Mid-Verse Header</span></div>
79+
<div class="p">
80+
Part two of verse five.
81+
</div>
82+
</div>
83+
"""
84+
85+
let reference = BibleReference(versionId: defaultVersionId, bookUSFM: "GEN", chapter: 1, verseStart: 1, verseEnd: 5)
86+
87+
let blocks = try await renderBlocks(html: html, reference: reference)
88+
#expect(hasHeaderContaining(blocks, text: "Mid-Verse Header"))
89+
#expect(hasScriptureContaining(blocks, text: "Part one of verse five."))
90+
#expect(hasScriptureContaining(blocks, text: "Part two of verse five."))
91+
}
92+
93+
@Test func testHeaderEmbeddedWithinVerseAfterInlineTextIsRendered() async throws {
94+
let html = """
95+
<div>
96+
<div class="p">
97+
<span class="yv-v" v="2"></span>
98+
<span class="yv-vlbl">2</span>
99+
He said
100+
<div class="yv-h s1"><span>The Beatitudes</span></div>
101+
blessed are the poor in spirit.
102+
</div>
103+
</div>
104+
"""
105+
106+
let reference = BibleReference(versionId: defaultVersionId, bookUSFM: "MAT", chapter: 5, verseStart: 2, verseEnd: 2)
107+
108+
let blocks = try await renderBlocks(html: html, reference: reference)
109+
#expect(hasHeaderContaining(blocks, text: "The Beatitudes"))
110+
}
111+
112+
@Test func testHeaderFollowingVerseTwoIsRenderedForRangeOneToTwo() async throws {
113+
let html = """
114+
<div>
115+
<div class="s1 yv-h">Introduction to the Sermon on the Mount</div>
116+
<div class="p">
117+
<span class="yv-v" v="1"></span><span class="yv-vlbl">1</span>
118+
When Jesus saw the crowds, He went up on the mountain and sat down.
119+
<span class="yv-v" v="2"></span><span class="yv-vlbl">2</span>
120+
and he began to teach them.
121+
</div>
122+
<div class="s1 yv-h">The Beatitudes</div>
123+
<div class="m">He said:</div>
124+
<div class="q1">
125+
<span class="yv-v" v="3"></span><span class="yv-vlbl">3</span>
126+
Blessed are the poor in spirit.
127+
</div>
128+
</div>
129+
"""
130+
131+
let reference = BibleReference(versionId: defaultVersionId, bookUSFM: "MAT", chapter: 5, verseStart: 1, verseEnd: 2)
132+
133+
let blocks = try await renderBlocks(html: html, reference: reference)
134+
#expect(hasHeaderContaining(blocks, text: "Introduction to the Sermon on the Mount"))
135+
#expect(hasHeaderContaining(blocks, text: "The Beatitudes"))
136+
#expect(hasScriptureContaining(blocks, text: "He said:"))
137+
#expect(!hasScriptureContaining(blocks, text: "Blessed are the poor in spirit."))
138+
}
139+
140+
@Test func testHeaderAfterLastVerseInRangeIsNotRendered() async throws {
141+
let html = """
142+
<div>
143+
<div class="p">
144+
<span class="yv-v" v="5"></span>
145+
<span class="yv-vlbl">5</span>
146+
Fifth verse text.
147+
</div>
148+
<div class="yv-h s1"><span>Next Section</span></div>
149+
<div class="p">
150+
<span class="yv-v" v="6"></span>
151+
<span class="yv-vlbl">6</span>
152+
Sixth verse text.
153+
</div>
154+
</div>
155+
"""
156+
157+
let reference = BibleReference(versionId: defaultVersionId, bookUSFM: "GEN", chapter: 1, verseStart: 1, verseEnd: 5)
158+
159+
let blocks = try await renderBlocks(html: html, reference: reference)
160+
#expect(hasScriptureContaining(blocks, text: "Fifth verse text."))
161+
#expect(!hasHeaderContaining(blocks, text: "Next Section"))
162+
#expect(!hasScriptureContaining(blocks, text: "Sixth verse text."))
163+
}
164+
165+
@Test func testGenesisTwoOneToThreeDoesNotIncludeEndHeader() async throws {
166+
let html = """
167+
<div>
168+
<div class="p">
169+
<span class="yv-v" v="1"></span><span class="yv-vlbl">1</span>
170+
Thus the heavens and the earth were completed in all their vast array.
171+
<span class="yv-v" v="2"></span><span class="yv-vlbl">2</span>
172+
By the seventh day God had finished the work He had been doing.
173+
<span class="yv-v" v="3"></span><span class="yv-vlbl">3</span>
174+
Then God blessed the seventh day and sanctified it.
175+
</div>
176+
<div class="s1 yv-h">Man and Woman in the Garden</div>
177+
<div class="p">
178+
<span class="yv-v" v="4"></span><span class="yv-vlbl">4</span>
179+
This is the account of the heavens and the earth when they were created.
180+
</div>
181+
</div>
182+
"""
183+
184+
let reference = BibleReference(versionId: defaultVersionId, bookUSFM: "GEN", chapter: 2, verseStart: 1, verseEnd: 3)
185+
186+
let blocks = try await renderBlocks(html: html, reference: reference)
187+
#expect(hasScriptureContaining(blocks, text: "Thus the heavens and the earth were completed"))
188+
#expect(!hasHeaderContaining(blocks, text: "Man and Woman in the Garden"))
189+
#expect(!hasScriptureContaining(blocks, text: "This is the account of the heavens and the earth"))
190+
}
191+
192+
@Test func testHeaderBeforeOutOfRangeVerseIsNotRendered() async throws {
193+
let html = """
194+
<div>
195+
<div class="yv-h s1"><span>Early Section</span></div>
196+
<div class="p">
197+
<span class="yv-v" v="3"></span>
198+
<span class="yv-vlbl">3</span>
199+
Third verse text.
200+
</div>
201+
<div class="p">
202+
<span class="yv-v" v="5"></span>
203+
<span class="yv-vlbl">5</span>
204+
Fifth verse text.
205+
</div>
206+
</div>
207+
"""
208+
209+
let reference = BibleReference(versionId: defaultVersionId, bookUSFM: "GEN", chapter: 1, verseStart: 5, verseEnd: 10)
210+
211+
let blocks = try await renderBlocks(html: html, reference: reference)
212+
#expect(!hasHeaderContaining(blocks, text: "Early Section"))
213+
#expect(!hasScriptureContaining(blocks, text: "Third verse text."))
214+
#expect(hasScriptureContaining(blocks, text: "Fifth verse text."))
215+
}
216+
217+
@Test func testHeaderIsNotRenderedWhenRenderHeadlinesIsFalse() async throws {
218+
let html = """
219+
<div>
220+
<div class="yv-h s1"><span>Visible Header</span></div>
221+
<div class="p">
222+
<span class="yv-v" v="1"></span>
223+
<span class="yv-vlbl">1</span>
224+
First verse text.
225+
</div>
226+
</div>
227+
"""
228+
229+
let reference = BibleReference(versionId: defaultVersionId, bookUSFM: "GEN", chapter: 1, verseStart: 1, verseEnd: 5)
230+
231+
let blocks = try await renderBlocks(html: html, reference: reference, renderHeadlines: false)
232+
#expect(!hasHeaderContaining(blocks, text: "Visible Header"))
233+
#expect(hasScriptureContaining(blocks, text: "First verse text."))
234+
}
235+
}

0 commit comments

Comments
 (0)