import Foundation /// Cloze-Helpers. 1:1-Port der Funktionen aus /// `cards/packages/cards-domain/src/cloze.ts`. /// /// **Web-Parität-Hinweis:** Web rendert Cloze client-side. Native macht /// dasselbe, weil Server keinen Render-Endpoint dafür hat. Pure-String- /// Manipulation, kein FSRS — Mocking gegen die TS-Implementierung via /// Fixture-Tests. /// /// Markup: `{{cN::answer}}` oder `{{cN::answer::hint}}`. N ist /// 1-basierte Cluster-ID. Mehrere Cluster pro Karte → mehrere /// Sub-Index-Reviews. enum Cloze { // swiftlint:disable large_tuple /// Pattern für `{{cN::answer(::hint)?}}`. Pro Call konstruiert, /// weil `Regex` unter Strict-Concurrency nicht Sendable ist. /// Tuple-Output (whole-match, id, answer, hint?) ist Regex-Builder- /// bedingt — Lint-Regel `large_tuple` greift hier nicht. private static var clusterPattern: Regex<(Substring, Substring, Substring, Substring?)> { #/\{\{c(\d+)::([^}]*?)(?:::([^}]*?))?\}\}/# } // swiftlint:enable large_tuple /// Distinct Cluster-IDs, sortiert. static func extractClusterIds(_ text: String) -> [Int] { var ids = Set() for match in text.matches(of: clusterPattern) { if let n = Int(match.output.1), n >= 1 { ids.insert(n) } } return ids.sorted() } /// Hint für einen Cluster (erstes Vorkommen gewinnt). static func hint(for text: String, clusterId: Int) -> String? { for match in text.matches(of: clusterPattern) { if let n = Int(match.output.1), n == clusterId, let hint = match.output.3 { return String(hint) } } return nil } /// Anzahl distinct Cluster — entspricht Sub-Index-Count. static func subIndexCount(_ text: String) -> Int { extractClusterIds(text).count } /// Mapping Sub-Index → Cluster-ID. static func clusterId(for text: String, subIndex: Int) -> Int? { let ids = extractClusterIds(text) guard ids.indices.contains(subIndex) else { return nil } return ids[subIndex] } /// Prompt-Render: aktiver Cluster wird zu `[…]` (oder `[hint]`), /// alle anderen werden auf ihre Antwort expandiert. static func renderPrompt(_ text: String, activeClusterId: Int) -> String { text.replacing(clusterPattern) { match in guard let n = Int(match.output.1) else { return String(match.output.0) } if n == activeClusterId { if let hint = match.output.3 { return "[\(hint)]" } return "[…]" } return String(match.output.2) } } /// Antwort-Render: alle Cluster expandiert. Aktiver Cluster wird /// als Markdown-Bold markiert. static func renderAnswer(_ text: String, activeClusterId: Int) -> String { text.replacing(clusterPattern) { match in guard let n = Int(match.output.1) else { return String(match.output.0) } let answer = String(match.output.2) return n == activeClusterId ? "**\(answer)**" : answer } } }