|
|
@@ -0,0 +1,95 @@
|
|
|
+import SwiftUI
|
|
|
+
|
|
|
+extension String {
|
|
|
+ func attributedStringHighlightingCorrections(
|
|
|
+ in correctedText: String,
|
|
|
+ corrections: [TextCorrection],
|
|
|
+ fontSize: CGFloat = 14
|
|
|
+ ) -> AttributedString {
|
|
|
+ var attributed = AttributedString(correctedText)
|
|
|
+ attributed.font = .system(size: fontSize)
|
|
|
+ attributed.foregroundColor = AppTheme.textPrimary
|
|
|
+
|
|
|
+ guard !corrections.isEmpty else { return attributed }
|
|
|
+
|
|
|
+ let highlightRanges = Self.highlightRanges(
|
|
|
+ originalText: self,
|
|
|
+ correctedText: correctedText,
|
|
|
+ corrections: corrections
|
|
|
+ )
|
|
|
+
|
|
|
+ for range in highlightRanges {
|
|
|
+ guard let attributeRange = Range(range, in: attributed) else { continue }
|
|
|
+ attributed[attributeRange].foregroundColor = AppTheme.teal
|
|
|
+ attributed[attributeRange].font = .system(size: fontSize, weight: .semibold)
|
|
|
+ }
|
|
|
+
|
|
|
+ return attributed
|
|
|
+ }
|
|
|
+
|
|
|
+ private static func highlightRanges(
|
|
|
+ originalText: String,
|
|
|
+ correctedText: String,
|
|
|
+ corrections: [TextCorrection]
|
|
|
+ ) -> [Range<String.Index>] {
|
|
|
+ var result = originalText
|
|
|
+ var ranges: [Range<String.Index>] = []
|
|
|
+
|
|
|
+ let located = corrections.compactMap { correction -> (Range<String.Index>, String)? in
|
|
|
+ guard let range = result.range(of: correction.original) else { return nil }
|
|
|
+ return (range, correction.suggestion)
|
|
|
+ }
|
|
|
+ .sorted { $0.0.lowerBound > $1.0.lowerBound }
|
|
|
+
|
|
|
+ for (range, suggestion) in located {
|
|
|
+ let highlightStart = range.lowerBound
|
|
|
+ guard let highlightEnd = result.index(
|
|
|
+ highlightStart,
|
|
|
+ offsetBy: suggestion.count,
|
|
|
+ limitedBy: result.endIndex
|
|
|
+ ) else { continue }
|
|
|
+
|
|
|
+ result.replaceSubrange(range, with: suggestion)
|
|
|
+ ranges.append(highlightStart..<highlightEnd)
|
|
|
+ }
|
|
|
+
|
|
|
+ if result == correctedText {
|
|
|
+ return ranges
|
|
|
+ }
|
|
|
+
|
|
|
+ return findSuggestionRanges(in: correctedText, corrections: corrections)
|
|
|
+ }
|
|
|
+
|
|
|
+ private static func findSuggestionRanges(
|
|
|
+ in text: String,
|
|
|
+ corrections: [TextCorrection]
|
|
|
+ ) -> [Range<String.Index>] {
|
|
|
+ var usedRanges: [Range<String.Index>] = []
|
|
|
+ var result: [Range<String.Index>] = []
|
|
|
+
|
|
|
+ for correction in corrections where !correction.suggestion.isEmpty {
|
|
|
+ var searchStart = text.startIndex
|
|
|
+
|
|
|
+ while searchStart < text.endIndex {
|
|
|
+ guard let range = text.range(
|
|
|
+ of: correction.suggestion,
|
|
|
+ range: searchStart..<text.endIndex
|
|
|
+ ) else { break }
|
|
|
+
|
|
|
+ let overlaps = usedRanges.contains { used in
|
|
|
+ range.lowerBound < used.upperBound && used.lowerBound < range.upperBound
|
|
|
+ }
|
|
|
+
|
|
|
+ if !overlaps {
|
|
|
+ usedRanges.append(range)
|
|
|
+ result.append(range)
|
|
|
+ break
|
|
|
+ }
|
|
|
+
|
|
|
+ searchStart = text.index(after: range.lowerBound)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return result
|
|
|
+ }
|
|
|
+}
|