diff --git a/README.md b/README.md index ef50c74..8aa3dad 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/Sources/YouVersionPlatformUI/Views/Rendering/BibleVersionRendering.swift b/Sources/YouVersionPlatformUI/Views/Rendering/BibleVersionRendering.swift index b19d729..06c7b59 100644 --- a/Sources/YouVersionPlatformUI/Views/Rendering/BibleVersionRendering.swift +++ b/Sources/YouVersionPlatformUI/Views/Rendering/BibleVersionRendering.swift @@ -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 { @@ -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, diff --git a/Tests/YouVersionPlatformUITests/BibleVersionRenderingTests.swift b/Tests/YouVersionPlatformUITests/BibleVersionRenderingTests.swift new file mode 100644 index 0000000..9d13e3f --- /dev/null +++ b/Tests/YouVersionPlatformUITests/BibleVersionRenderingTests.swift @@ -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 = """ +