Skip to content
Open
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
38 changes: 37 additions & 1 deletion ios/swiftbible/Models/DailyDevotional.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ struct DevotionalVerse: Codable, Equatable {
let testament: String
}

struct DailyDevotional: Codable {
struct DailyDevotional: Codable, Equatable {
let id: Int
let message: String
let for_date: String
Expand All @@ -28,3 +28,39 @@ struct DailyDevotional: Codable {
let model: String?
let track: String?
}

extension DailyDevotional {
var cleanedForDisplay: DailyDevotional {
DailyDevotional(
id: id,
message: message.removingRedLetterTags(),
for_date: for_date,
devotional_type: devotional_type,
series_name: series_name,
series_part: series_part,
holiday_name: holiday_name,
holiday_url: holiday_url,
anchor_verse: anchor_verse,
verses: verses,
model: model,
track: track
)
}
}

extension String {
/// Removes red-letter markup from Bible text before that text is reused in
/// prose contexts such as devotionals, notifications, and saved snippets.
///
/// Bible paragraphs intentionally keep `<JESUS>` markers for red-letter
/// rendering, but MarkdownUI displays those tags literally. The regex is
/// intentionally narrow: it only strips opening/closing JESUS tags, while
/// preserving the quoted words and all other devotional markdown.
func removingRedLetterTags() -> String {
replacingOccurrences(
of: #"<\s*/?\s*JESUS\s*>"#,
with: "",
options: [.regularExpression, .caseInsensitive]
)
}
}
4 changes: 2 additions & 2 deletions ios/swiftbible/Services/CacheService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class CacheService {
do {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let data = try encoder.encode(devotional)
let data = try encoder.encode(devotional.cleanedForDisplay)
try data.write(to: fileURL, options: .atomic)
} catch {
print("Error saving devotional to cache: \(error)")
Expand All @@ -81,7 +81,7 @@ class CacheService {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let devotional = try decoder.decode(DailyDevotional.self, from: data)
return devotional
return devotional.cleanedForDisplay
} catch {
print("Error loading devotional from cache: \(error)")
return nil
Expand Down
2 changes: 1 addition & 1 deletion ios/swiftbible/Services/NotificationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ final class NotificationService: NSObject, UNUserNotificationCenterDelegate {
content.sound = .default

if let devotional = CacheService.shared.loadDevotional(for: date),
let teaser = extractTeaser(from: devotional.message) {
let teaser = extractTeaser(from: devotional.cleanedForDisplay.message) {
content.title = "Daily Devotional"
content.body = teaser
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@
showToast = false
}
}
}) {

Check warning on line 173 in ios/swiftbible/Views/Daily Devotional/DailyDevotionalView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing closure syntax should not be used when passing more than one closure argument (multiple_closures_with_trailing_closure)
Label("Copy Devotional", systemImage: "doc.on.doc")
}
}
Expand Down Expand Up @@ -322,7 +322,7 @@

@MainActor
private func applyDevotional(_ devotional: DailyDevotional) {
message = devotional.message
message = devotional.cleanedForDisplay.message
devotionalType = devotional.devotional_type ?? "single"
seriesName = devotional.series_name
seriesPart = devotional.series_part
Expand Down Expand Up @@ -440,7 +440,7 @@
.foregroundStyle(.secondary)
}

private func parseVerseReference(_ ref: String) -> (book: String, chapter: Int, verse: Int)? {

Check warning on line 443 in ios/swiftbible/Views/Daily Devotional/DailyDevotionalView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Tuples should have at most 2 members (large_tuple)
let pattern = #"^(.+?)\s+(\d+):(\d+)(?:[-\d]+)?$"#
guard let regex = try? NSRegularExpression(pattern: pattern),
let match = regex.firstMatch(in: ref, range: NSRange(ref.startIndex..., in: ref)),
Expand Down Expand Up @@ -503,7 +503,7 @@
)

// Save to cache
CacheService.shared.saveDevotional(devotional, for: date)
CacheService.shared.saveDevotional(devotional.cleanedForDisplay, for: date)
updateSavedState(for: date)
} catch {
print("No devotional found for \(dateString): \(error)")
Expand Down Expand Up @@ -568,7 +568,7 @@
isFavorite = false
}
} else {
let devotional = SavedDevotional(date: selectedDate, message: message)
let devotional = SavedDevotional(date: selectedDate, message: message.removingRedLetterTags())
context.insert(devotional)
try? context.save()
AnalyticsService.shared.capture(.devotionalSaved, properties: devotionalAnalyticsProperties())
Expand Down Expand Up @@ -797,4 +797,4 @@
DailyDevotionalView(selectedTab: .constant(.dailyDevotional))
.environment(UserViewModel())
.environment(AppViewModel())
}

Check warning on line 800 in ios/swiftbible/Views/Daily Devotional/DailyDevotionalView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

File should contain 500 lines or less: currently contains 800 (file_length)
48 changes: 48 additions & 0 deletions ios/swiftbibleTests/DailyDevotionalTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// DailyDevotionalTests.swift
// swiftbibleTests
//
// Verifies devotional prose is safe to render after Bible red-letter markup
// has been embedded in source verse text.
//

import XCTest
@testable import swiftbible

final class DailyDevotionalTests: XCTestCase {
func testRemovingRedLetterTagsKeepsJesusWords() {
let source = #"> "And he saith unto them, <JESUS>Are ye so without understanding also?</JESUS>" Mark 7:18"#

XCTAssertEqual(
source.removingRedLetterTags(),
#"> "And he saith unto them, Are ye so without understanding also?" Mark 7:18"#
)
}

func testRemovingRedLetterTagsHandlesWhitespaceAndCaseVariants() {
let source = "<jesus>Peace be unto you.</ JESUS> < JESUS >Follow me.</jesus>"

XCTAssertEqual(source.removingRedLetterTags(), "Peace be unto you. Follow me.")
}

func testCleanedForDisplaySanitizesMessageOnly() {
let devotional = DailyDevotional(
id: 7,
message: "Before <JESUS>spoken words</JESUS> after",
for_date: "2026-06-22",
devotional_type: "single",
series_name: nil,
series_part: nil,
holiday_name: nil,
holiday_url: nil,
anchor_verse: "Mark 7:18",
verses: [DevotionalVerse(book: "Mark", chapter: 7, verse: 18, testament: "new")],
model: "test-model",
track: "narrative"
)

XCTAssertEqual(devotional.cleanedForDisplay.message, "Before spoken words after")
XCTAssertEqual(devotional.cleanedForDisplay.anchor_verse, "Mark 7:18")
XCTAssertEqual(devotional.cleanedForDisplay.verses, devotional.verses)
}
}
Loading