Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,12 @@ Building AI applications with Bible content? Access YouVersion's LLM-optimized e

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

## Bible Rendering Tests

Run focused tests: `swift test --filter BibleVersionRenderingTests`

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.

## Support

- **Issues**: [GitHub Issues](https://github.com/youversion/platform-sdk-swift/issues)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,19 +365,44 @@ public enum BibleVersionRendering {
marginTop: &marginTop
)

for child in node.children {
for (index, child) in node.children.enumerated() {
if child.type == .block || child.type == .table {
if !stateUp.isTextEmpty {
let hadPendingText = !stateUp.isTextEmpty
if hadPendingText {
if stateUp.rendering {
ret.append(createBlock(stateDown: stateDown, stateUp: &stateUp, marginTop: marginTop))
}
stateUp.clearText()
}
let isHeader = child.classes.contains("yv-h") || child.classes.contains("yvh")
let savedRendering = stateUp.rendering

if isHeader && stateIn.renderHeadlines {
let followingChildren = node.children.dropFirst(index + 1)
let nextVerse = followingChildren
.compactMap { firstVerseInNode($0) }
.first
let immediateNextVerse = followingChildren.first.flatMap { firstVerseInNode($0) }
let isNextVerseInRange = nextVerse != nil
&& nextVerse! >= stateIn.fromVerse
&& nextVerse! <= stateIn.toVerse

if !stateUp.rendering && isNextVerseInRange {
stateUp.rendering = true
} else if stateUp.rendering && nextVerse != nil && !isNextVerseInRange && !hadPendingText && immediateNextVerse != nil {
stateUp.rendering = false
}
}

if child.type == .block {
handleNodeBlock(node: child, stateIn: stateIn, stateDown: stateDown, stateUp: &stateUp, ret: &ret)
} else if child.type == .table {
handleNodeTable(node: child, stateIn: stateIn, stateDown: stateDown, stateUp: &stateUp, ret: &ret)
}

if isHeader {
stateUp.rendering = savedRendering
}
} else {
if child.type == .span && child.classes.contains("qs") { // Selah. Force a line break and right-alignment.
if !stateUp.isTextEmpty {
Expand Down Expand Up @@ -591,6 +616,21 @@ public enum BibleVersionRendering {
return localCopy
}

/// Finds the first verse number in a node's subtree by searching for verse-labeled spans.
private static func firstVerseInNode(_ node: BibleTextNode) -> Int? {
if node.classes.contains("yv-v") || node.classes.contains("verse") {
if let v = node.attributes["v"], let vi = Int(v) {
return vi
}
}
for child in node.children {
if let found = firstVerseInNode(child) {
return found
}
}
return nil
}

private static func assertionFailed(
_ message: String,
string: String? = nil,
Expand Down
235 changes: 235 additions & 0 deletions Tests/YouVersionPlatformUITests/BibleVersionRenderingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import SwiftUI
import Testing
@testable import YouVersionPlatformCore
@testable import YouVersionPlatformUI

@MainActor
@Suite struct BibleVersionRenderingTests {
private let defaultVersionId = 1
private let fonts = BibleTextFonts(familyName: "Times New Roman", baseSize: 16)

private func renderBlocks(
html: String,
reference: BibleReference,
renderHeadlines: Bool = true
) async throws -> [BibleTextBlock] {
let node = try #require(try BibleTextNode.parse(html))

let blocks = try await BibleVersionRendering.textBlocks(
from: node,
reference: reference,
renderHeadlines: renderHeadlines,
renderVerseNumbers: true,
footnotesMode: .none,
textColor: .black,
wocColor: .red,
fonts: fonts
)

return try #require(blocks)
}

private func hasHeaderContaining(_ blocks: [BibleTextBlock], text: String) -> Bool {
blocks.contains { block in
let runs = block.text.asAttributedString.runs[\.bibleTextCategory]
let hasHeader = runs.contains { $0.0 == .header }
return hasHeader && block.text.characters.contains(text)
}
}

private func hasScriptureContaining(_ blocks: [BibleTextBlock], text: String) -> Bool {
blocks.contains { block in
block.text.characters.contains(text)
}
}

@Test func testHeaderBeforeFirstVerseInRangeIsRendered() async throws {
let html = """
<div>
<div class="yv-h s1"><span>The List</span></div>
<div class="p">
<span class="yv-v" v="5"></span>
<span class="yv-vlbl">5</span>
Fifth verse text.
</div>
<div class="p">
<span class="yv-v" v="6"></span>
<span class="yv-vlbl">6</span>
Sixth verse text.
</div>
</div>
"""

let reference = BibleReference(versionId: defaultVersionId, bookUSFM: "GEN", chapter: 1, verseStart: 5, verseEnd: 10)

let blocks = try await renderBlocks(html: html, reference: reference)
#expect(hasHeaderContaining(blocks, text: "The List"))
#expect(hasScriptureContaining(blocks, text: "Fifth verse text."))
}

@Test func testHeaderInMiddleOfLastVerseInRangeIsRendered() async throws {
let html = """
<div>
<div class="p">
<span class="yv-v" v="5"></span>
<span class="yv-vlbl">5</span>
Part one of verse five.
</div>
<div class="yv-h s1"><span>Mid-Verse Header</span></div>
<div class="p">
Part two of verse five.
</div>
</div>
"""

let reference = BibleReference(versionId: defaultVersionId, bookUSFM: "GEN", chapter: 1, verseStart: 1, verseEnd: 5)

let blocks = try await renderBlocks(html: html, reference: reference)
#expect(hasHeaderContaining(blocks, text: "Mid-Verse Header"))
#expect(hasScriptureContaining(blocks, text: "Part one of verse five."))
#expect(hasScriptureContaining(blocks, text: "Part two of verse five."))
}

@Test func testHeaderEmbeddedWithinVerseAfterInlineTextIsRendered() async throws {
let html = """
<div>
<div class="p">
<span class="yv-v" v="2"></span>
<span class="yv-vlbl">2</span>
He said
<div class="yv-h s1"><span>The Beatitudes</span></div>
blessed are the poor in spirit.
</div>
</div>
"""

let reference = BibleReference(versionId: defaultVersionId, bookUSFM: "MAT", chapter: 5, verseStart: 2, verseEnd: 2)

let blocks = try await renderBlocks(html: html, reference: reference)
#expect(hasHeaderContaining(blocks, text: "The Beatitudes"))
}

@Test func testHeaderFollowingVerseTwoIsRenderedForRangeOneToTwo() async throws {
let html = """
<div>
<div class="s1 yv-h">Introduction to the Sermon on the Mount</div>
<div class="p">
<span class="yv-v" v="1"></span><span class="yv-vlbl">1</span>
When Jesus saw the crowds, He went up on the mountain and sat down.
<span class="yv-v" v="2"></span><span class="yv-vlbl">2</span>
and he began to teach them.
</div>
<div class="s1 yv-h">The Beatitudes</div>
<div class="m">He said:</div>
<div class="q1">
<span class="yv-v" v="3"></span><span class="yv-vlbl">3</span>
Blessed are the poor in spirit.
</div>
</div>
"""

let reference = BibleReference(versionId: defaultVersionId, bookUSFM: "MAT", chapter: 5, verseStart: 1, verseEnd: 2)

let blocks = try await renderBlocks(html: html, reference: reference)
#expect(hasHeaderContaining(blocks, text: "Introduction to the Sermon on the Mount"))
#expect(hasHeaderContaining(blocks, text: "The Beatitudes"))
#expect(hasScriptureContaining(blocks, text: "He said:"))
#expect(!hasScriptureContaining(blocks, text: "Blessed are the poor in spirit."))
}

@Test func testHeaderAfterLastVerseInRangeIsNotRendered() async throws {
let html = """
<div>
<div class="p">
<span class="yv-v" v="5"></span>
<span class="yv-vlbl">5</span>
Fifth verse text.
</div>
<div class="yv-h s1"><span>Next Section</span></div>
<div class="p">
<span class="yv-v" v="6"></span>
<span class="yv-vlbl">6</span>
Sixth verse text.
</div>
</div>
"""

let reference = BibleReference(versionId: defaultVersionId, bookUSFM: "GEN", chapter: 1, verseStart: 1, verseEnd: 5)

let blocks = try await renderBlocks(html: html, reference: reference)
#expect(hasScriptureContaining(blocks, text: "Fifth verse text."))
#expect(!hasHeaderContaining(blocks, text: "Next Section"))
#expect(!hasScriptureContaining(blocks, text: "Sixth verse text."))
}

@Test func testGenesisTwoOneToThreeDoesNotIncludeEndHeader() async throws {
let html = """
<div>
<div class="p">
<span class="yv-v" v="1"></span><span class="yv-vlbl">1</span>
Thus the heavens and the earth were completed in all their vast array.
<span class="yv-v" v="2"></span><span class="yv-vlbl">2</span>
By the seventh day God had finished the work He had been doing.
<span class="yv-v" v="3"></span><span class="yv-vlbl">3</span>
Then God blessed the seventh day and sanctified it.
</div>
<div class="s1 yv-h">Man and Woman in the Garden</div>
<div class="p">
<span class="yv-v" v="4"></span><span class="yv-vlbl">4</span>
This is the account of the heavens and the earth when they were created.
</div>
</div>
"""

let reference = BibleReference(versionId: defaultVersionId, bookUSFM: "GEN", chapter: 2, verseStart: 1, verseEnd: 3)

let blocks = try await renderBlocks(html: html, reference: reference)
#expect(hasScriptureContaining(blocks, text: "Thus the heavens and the earth were completed"))
#expect(!hasHeaderContaining(blocks, text: "Man and Woman in the Garden"))
#expect(!hasScriptureContaining(blocks, text: "This is the account of the heavens and the earth"))
}

@Test func testHeaderBeforeOutOfRangeVerseIsNotRendered() async throws {
let html = """
<div>
<div class="yv-h s1"><span>Early Section</span></div>
<div class="p">
<span class="yv-v" v="3"></span>
<span class="yv-vlbl">3</span>
Third verse text.
</div>
<div class="p">
<span class="yv-v" v="5"></span>
<span class="yv-vlbl">5</span>
Fifth verse text.
</div>
</div>
"""

let reference = BibleReference(versionId: defaultVersionId, bookUSFM: "GEN", chapter: 1, verseStart: 5, verseEnd: 10)

let blocks = try await renderBlocks(html: html, reference: reference)
#expect(!hasHeaderContaining(blocks, text: "Early Section"))
#expect(!hasScriptureContaining(blocks, text: "Third verse text."))
#expect(hasScriptureContaining(blocks, text: "Fifth verse text."))
}

@Test func testHeaderIsNotRenderedWhenRenderHeadlinesIsFalse() async throws {
let html = """
<div>
<div class="yv-h s1"><span>Visible Header</span></div>
<div class="p">
<span class="yv-v" v="1"></span>
<span class="yv-vlbl">1</span>
First verse text.
</div>
</div>
"""

let reference = BibleReference(versionId: defaultVersionId, bookUSFM: "GEN", chapter: 1, verseStart: 1, verseEnd: 5)

let blocks = try await renderBlocks(html: html, reference: reference, renderHeadlines: false)
#expect(!hasHeaderContaining(blocks, text: "Visible Header"))
#expect(hasScriptureContaining(blocks, text: "First verse text."))
}
}