krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

remove-splash-highlighter: Hutch/Views/Inbox/InboxThreadUtilities.swift · raw

 1import Foundation
 2
 3enum InboxThreadUtilities {
 4    nonisolated static func deriveRepositoryName(from listName: String) -> String? {
 5        let separators = ["-devel", "-patches", "-dev", ".patches"]
 6        for separator in separators where listName.hasSuffix(separator) {
 7            return String(listName.dropLast(separator.count))
 8        }
 9        return nil
10    }
11
12    /// Splits an email body into its commit message and diff, so patch mail can be
13    /// rendered as prose plus a diff rather than one undifferentiated blob.
14    ///
15    /// Shared by the inbox thread view and patchset review: sr.ht's `Patch` type
16    /// carries no diff, so the diff has to be recovered from the email body.
17    nonisolated static func segmentMessageBody(_ body: String, isPatch: Bool) -> [InboxMessageContentBlock] {
18        guard isPatch else {
19            let trimmedBody = body.trimmingCharacters(in: .whitespacesAndNewlines)
20            return trimmedBody.isEmpty ? [] : [.plainText(trimmedBody)]
21        }
22
23        let normalizedBody = normalizeLineEndings(in: body)
24        let lines = normalizedBody.components(separatedBy: "\n")
25        guard let diffStartIndex = actualDiffStartIndex(in: lines) else {
26            let trimmedBody = normalizedBody.trimmingCharacters(in: .whitespacesAndNewlines)
27            return trimmedBody.isEmpty ? [] : [.plainText(trimmedBody)]
28        }
29
30        var blocks: [InboxMessageContentBlock] = []
31        let leadingPlainText = lines[..<diffStartIndex]
32            .joined(separator: "\n")
33            .trimmingCharacters(in: .whitespacesAndNewlines)
34        if !leadingPlainText.isEmpty {
35            blocks.append(.plainText(leadingPlainText))
36        }
37
38        let remainingLines = Array(lines[diffStartIndex...])
39        let signatureIndex = remainingLines.firstIndex(where: isEmailSignatureSeparator)
40
41        let diffLines: ArraySlice<String>
42        let trailingPlainText: String
43        if let signatureIndex {
44            diffLines = remainingLines[..<signatureIndex]
45            trailingPlainText = remainingLines[signatureIndex...]
46                .joined(separator: "\n")
47                .trimmingCharacters(in: .whitespacesAndNewlines)
48        } else {
49            diffLines = remainingLines[...]
50            trailingPlainText = ""
51        }
52
53        let diff = diffLines.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
54        if !diff.isEmpty {
55            blocks.append(.diff(diff))
56        }
57
58        if !trailingPlainText.isEmpty {
59            blocks.append(.plainText(trailingPlainText))
60        }
61        return blocks
62    }
63
64    nonisolated static func actualDiffStartIndex(in lines: [String]) -> Int? {
65        if let explicitDiffIndex = lines.firstIndex(where: { $0.hasPrefix("diff --git ") }) {
66            return explicitDiffIndex
67        }
68
69        for index in lines.indices {
70            let line = lines[index]
71            guard line.hasPrefix("--- ") else { continue }
72            let nextIndex = lines.index(after: index)
73            guard nextIndex < lines.endIndex else { continue }
74            let nextLine = lines[nextIndex]
75            guard nextLine.hasPrefix("+++ ") else { continue }
76
77            let oldPath = String(line.dropFirst(4))
78            let newPath = String(nextLine.dropFirst(4))
79            let looksLikeUnifiedDiff = (oldPath.hasPrefix("a/") || oldPath == "/dev/null") &&
80                (newPath.hasPrefix("b/") || newPath == "/dev/null")
81
82            if looksLikeUnifiedDiff {
83                return index
84            }
85        }
86
87        return nil
88    }
89
90    nonisolated static func isEmailSignatureSeparator(_ line: String) -> Bool {
91        line == "-- " || line == "--"
92    }
93
94    nonisolated static func normalizeLineEndings(in text: String) -> String {
95        text
96            .replacingOccurrences(of: "\r\n", with: "\n")
97            .replacingOccurrences(of: "\r", with: "\n")
98    }
99}