108 lines
4.5 KiB
Swift
108 lines
4.5 KiB
Swift
import Foundation
|
||
|
||
/// Builds and removes the plain-text quote used when a peer does not render
|
||
/// XEP-0461 replies. XEP-0428 offsets are Unicode scalar offsets, not UTF-16.
|
||
enum MessageReplyFallback {
|
||
struct Parsed: Equatable, Sendable {
|
||
let body: String
|
||
let preview: String?
|
||
}
|
||
|
||
static func make(author: String, preview: String) -> (prefix: String, scalarCount: Int) {
|
||
let safeAuthor = author
|
||
.replacingOccurrences(of: "\n", with: " ")
|
||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
let quoteLines = preview
|
||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
.split(separator: "\n", omittingEmptySubsequences: false)
|
||
.prefix(8)
|
||
.map { "> \($0)" }
|
||
let prefix = (["> \(safeAuthor) написал(а):"] + quoteLines).joined(separator: "\n") + "\n\n"
|
||
return (prefix, prefix.unicodeScalars.count)
|
||
}
|
||
|
||
static func parse(
|
||
body: String,
|
||
fallbackStart: Int?,
|
||
fallbackEnd: Int?
|
||
) -> Parsed {
|
||
if let fallbackStart,
|
||
let fallbackEnd,
|
||
let fallback = scalarSubstring(body, start: fallbackStart, end: fallbackEnd),
|
||
let stripped = removingScalarRange(body, start: fallbackStart, end: fallbackEnd) {
|
||
return Parsed(
|
||
body: stripped.trimmingCharacters(in: .whitespacesAndNewlines),
|
||
preview: quotePreview(from: fallback)
|
||
)
|
||
}
|
||
return parseLegacyQuote(body) ?? Parsed(body: body, preview: nil)
|
||
}
|
||
|
||
static func parseLegacyQuote(_ body: String) -> Parsed? {
|
||
let lines = body.components(separatedBy: .newlines)
|
||
guard let first = lines.first,
|
||
first.trimmingCharacters(in: .whitespaces).hasPrefix(">") else { return nil }
|
||
|
||
var quoteLines: [String] = []
|
||
var bodyStart = 0
|
||
for (index, line) in lines.enumerated() {
|
||
let trimmed = line.trimmingCharacters(in: .whitespaces)
|
||
if trimmed.hasPrefix(">") {
|
||
quoteLines.append(String(trimmed.dropFirst()).trimmingCharacters(in: .whitespaces))
|
||
bodyStart = index + 1
|
||
continue
|
||
}
|
||
if trimmed.isEmpty, !quoteLines.isEmpty {
|
||
bodyStart = index + 1
|
||
continue
|
||
}
|
||
break
|
||
}
|
||
|
||
guard !quoteLines.isEmpty, bodyStart < lines.count else { return nil }
|
||
let remaining = lines[bodyStart...]
|
||
.joined(separator: "\n")
|
||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !remaining.isEmpty else { return nil }
|
||
return Parsed(body: remaining, preview: normalizedPreview(quoteLines))
|
||
}
|
||
|
||
private static func quotePreview(from fallback: String) -> String? {
|
||
let lines = fallback.components(separatedBy: .newlines).compactMap { line -> String? in
|
||
let trimmed = line.trimmingCharacters(in: .whitespaces)
|
||
guard trimmed.hasPrefix(">") else { return nil }
|
||
return String(trimmed.dropFirst()).trimmingCharacters(in: .whitespaces)
|
||
}
|
||
return normalizedPreview(lines)
|
||
}
|
||
|
||
private static func normalizedPreview(_ lines: [String]) -> String? {
|
||
var values = lines.filter { !$0.isEmpty }
|
||
if values.count > 1,
|
||
let first = values.first?.lowercased(),
|
||
first.hasSuffix(":") || first.contains(" wrote") || first.contains("написал") {
|
||
values.removeFirst()
|
||
}
|
||
let value = values.joined(separator: "\n")
|
||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !value.isEmpty else { return nil }
|
||
return value.count > 280 ? String(value.prefix(277)) + "…" : value
|
||
}
|
||
|
||
private static func scalarSubstring(_ value: String, start: Int, end: Int) -> String? {
|
||
let scalars = value.unicodeScalars
|
||
guard start >= 0, end >= start, end <= scalars.count else { return nil }
|
||
let lower = scalars.index(scalars.startIndex, offsetBy: start)
|
||
let upper = scalars.index(scalars.startIndex, offsetBy: end)
|
||
return String(value[lower..<upper])
|
||
}
|
||
|
||
private static func removingScalarRange(_ value: String, start: Int, end: Int) -> String? {
|
||
let scalars = value.unicodeScalars
|
||
guard start >= 0, end >= start, end <= scalars.count else { return nil }
|
||
let lower = scalars.index(scalars.startIndex, offsetBy: start)
|
||
let upper = scalars.index(scalars.startIndex, offsetBy: end)
|
||
return String(value[..<lower]) + String(value[upper...])
|
||
}
|
||
}
|