diff options
| author | Dustin Mierau <dustin@mierau.me> | 2024-05-05 19:17:39 -0700 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2024-05-05 19:17:39 -0700 |
| commit | 3feb709522b38f1aa5ddedc5e1aee28597375aab (patch) | |
| tree | 9df2ad849703004901261c53bcffad318b876ad4 /Hotline | |
| parent | 4da73c630183e482e4ab2accef97ad54c6741bf4 (diff) | |
Write better URL regular expressions for our needs.
Diffstat (limited to 'Hotline')
| -rw-r--r-- | Hotline/Hotline/HotlineClient.swift | 3 | ||||
| -rw-r--r-- | Hotline/Models/Hotline.swift | 4 | ||||
| -rw-r--r-- | Hotline/Shared/AsyncLinkPreview.swift | 2 | ||||
| -rw-r--r-- | Hotline/Utility/FoundationExtensions.swift | 39 | ||||
| -rw-r--r-- | Hotline/Utility/RegularExpressions.swift | 116 | ||||
| -rw-r--r-- | Hotline/macOS/ChatView.swift | 6 | ||||
| -rw-r--r-- | Hotline/macOS/MessageBoardView.swift | 4 | ||||
| -rw-r--r-- | Hotline/macOS/MessageView.swift | 2 |
8 files changed, 144 insertions, 32 deletions
diff --git a/Hotline/Hotline/HotlineClient.swift b/Hotline/Hotline/HotlineClient.swift index 15ba73f..b759936 100644 --- a/Hotline/Hotline/HotlineClient.swift +++ b/Hotline/Hotline/HotlineClient.swift @@ -498,7 +498,8 @@ class HotlineClient: NetSocketDelegate { if matches.count > 0 { for match in matches { let range = match.range - messages.append(String(text[start..<range.lowerBound])) + let messageText = String(text[start..<range.lowerBound]) + messages.append(messageText.convertingLinksToMarkdown()) start = range.upperBound } } diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift index ccc23c1..d232533 100644 --- a/Hotline/Models/Hotline.swift +++ b/Hotline/Models/Hotline.swift @@ -211,7 +211,7 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate { } @MainActor func sendInstantMessage(_ text: String, userID: UInt16) { - let message = InstantMessage(direction: .outgoing, text: text, type: .message, date: Date()) + let message = InstantMessage(direction: .outgoing, text: text.convertingLinksToMarkdown(), type: .message, date: Date()) if self.instantMessages[userID] == nil { self.instantMessages[userID] = [message] @@ -726,7 +726,7 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate { SoundEffectPlayer.shared.playSoundEffect(.chatMessage) } - let instantMessage = InstantMessage(direction: .incoming, text: message, type: .message, date: Date()) + let instantMessage = InstantMessage(direction: .incoming, text: message.convertingLinksToMarkdown(), type: .message, date: Date()) if self.instantMessages[userID] == nil { self.instantMessages[userID] = [instantMessage] } diff --git a/Hotline/Shared/AsyncLinkPreview.swift b/Hotline/Shared/AsyncLinkPreview.swift index 9f0c4c2..89b186f 100644 --- a/Hotline/Shared/AsyncLinkPreview.swift +++ b/Hotline/Shared/AsyncLinkPreview.swift @@ -36,7 +36,7 @@ struct AsyncLinkPreview: View { LinkView(metadata: metadata) .frame(width: 200) } else { - Text(LocalizedStringKey(url!.absoluteString.convertLinksToMarkdown())) + Text(LocalizedStringKey(url!.absoluteString)) .multilineTextAlignment(.leading) .tint(Color("Link Color")) } diff --git a/Hotline/Utility/FoundationExtensions.swift b/Hotline/Utility/FoundationExtensions.swift index c154dbd..6270ac7 100644 --- a/Hotline/Utility/FoundationExtensions.swift +++ b/Hotline/Utility/FoundationExtensions.swift @@ -1,4 +1,5 @@ import Foundation +import SwiftUI enum Endianness { case big @@ -6,23 +7,13 @@ enum Endianness { } extension String { - func convertToAttributedStringWithLinks(relaxed: Bool = false) -> AttributedString { + func convertToAttributedStringWithLinks() -> AttributedString { let attributedString: NSMutableAttributedString = NSMutableAttributedString(string: self) - let urlPattern = relaxed ? - #"(hotline|http|https)?(://)?[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?"# : - #"(hotline|http|https)://[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?"# - - guard let regex = try? NSRegularExpression(pattern: urlPattern, options: .caseInsensitive) else { - return AttributedString(attributedString) - } - - regex.enumerateMatches(in: self, range: NSMakeRange(0, self.count)) { (result: NSTextCheckingResult!, _, _) in - if let newRange = Range(result.range, in: self) { - let str = String(self[newRange]) - attributedString.addAttribute(.link, value: str, range: result.range) - } + let matches = self.ranges(of: RegularExpressions.relaxedLink) + for match in matches { + attributedString.addAttribute(.link, value: self[match], range: NSRange(match, in: self)) +// attributedString.addAttribute(.underlineStyle, value: 1, range: NSRange(match, in: self)) } - return AttributedString(attributedString) } @@ -51,14 +42,18 @@ extension String { } } - func convertLinksToMarkdown() -> String { - let urlPattern = #"(hotline|http|https)?(://)?[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?"# - - guard let regex = try? NSRegularExpression(pattern: urlPattern, options: .caseInsensitive) else { - return self + func convertingLinksToMarkdown() -> String { + var cp = String(self) + cp.replace(RegularExpressions.relaxedLink) { match -> String in + let linkText = self[match.range] + var injectedScheme = "https://" + if let _ = try? RegularExpressions.supportedLinkScheme.prefixMatch(in: linkText) { + injectedScheme = "" + } + + return "[\(linkText)](\(injectedScheme)\(linkText))" } - - return regex.stringByReplacingMatches(in: self, range: NSRange(location: 0, length: self.count), withTemplate: "[$0]($0)") + return cp } } diff --git a/Hotline/Utility/RegularExpressions.swift b/Hotline/Utility/RegularExpressions.swift index 0314dd7..0a0dda9 100644 --- a/Hotline/Utility/RegularExpressions.swift +++ b/Hotline/Utility/RegularExpressions.swift @@ -20,4 +20,120 @@ struct RegularExpressions { } } } + + static let supportedLinkScheme = Regex { + Anchor.startOfLine + ChoiceOf { + "hotline" + "http" + "https" + } + "://" + }.ignoresCase().anchorsMatchLineEndings() + + static let relaxedLink = Regex { + ChoiceOf { + Anchor.startOfLine + Anchor.wordBoundary + } + Capture { + // scheme (optional) + Optionally { + ChoiceOf { + "hotline://" + "http://" + "https://" + } + } + // domain name + OneOrMore { + CharacterClass( + .anyOf(".-"), + ("a"..."z"), + ("0"..."9") + ) + } + // top-level domain name + "." + ChoiceOf { + "com" + "net" + "org" + "edu" + "gov" + "mil" + "aero" + "asia" + "biz" + "cat" + "coop" + "info" + "int" + "jobs" + "mobi" + "museum" + "name" + "pizza" + "post" + "pro" + "red" + "tel" + "today" + "travel" + "ai" + "be" + "by" + "ca" + "co" + "de" + "er" + "es" + "fr" + "gs" + "ie" + "im" + "in" + "io" + "is" + "it" + "jp" + "la" + "ly" + "ma" + "md" + "me" + "my" + "nl" + "ps" + "pt" + "ja" + "st" + "to" + "tv" + "uk" + "ws" + } + // Port + Optionally { + ":" + OneOrMore { + CharacterClass(.digit) + } + } + // path + ZeroOrMore { + CharacterClass( + .anyOf("#_-/.?=&%\\()[]"), + ("a"..."z"), + ("0"..."9") + ) + } + } + ChoiceOf { + Anchor.endOfLine + Anchor.wordBoundary + } + } + .anchorsMatchLineEndings() + .ignoresCase() } diff --git a/Hotline/macOS/ChatView.swift b/Hotline/macOS/ChatView.swift index 5bd626f..98fb852 100644 --- a/Hotline/macOS/ChatView.swift +++ b/Hotline/macOS/ChatView.swift @@ -47,7 +47,7 @@ struct ChatView: View { ScrollView(.vertical) { HStack { Spacer() - Text(msg.text.convertToAttributedStringWithLinks(relaxed: false)) + Text(msg.text.convertToAttributedStringWithLinks()) .font(.system(size: 12)) .fontDesign(.monospaced) .textSelection(.enabled) @@ -122,7 +122,7 @@ struct ChatView: View { // } // } // else { - Text(LocalizedStringKey("**\(username):** \(msg.text)".convertLinksToMarkdown())) + Text(LocalizedStringKey("**\(username):** \(msg.text)".convertingLinksToMarkdown())) .lineSpacing(4) .multilineTextAlignment(.leading) .textSelection(.enabled) @@ -130,7 +130,7 @@ struct ChatView: View { // } } else { - Text(LocalizedStringKey(msg.text.convertLinksToMarkdown())) + Text(msg.text) .lineSpacing(4) .multilineTextAlignment(.leading) .textSelection(.enabled) diff --git a/Hotline/macOS/MessageBoardView.swift b/Hotline/macOS/MessageBoardView.swift index 660ca05..2bd3713 100644 --- a/Hotline/macOS/MessageBoardView.swift +++ b/Hotline/macOS/MessageBoardView.swift @@ -12,8 +12,8 @@ struct MessageBoardView: View { if model.access?.contains(.canReadMessageBoard) != false { ScrollView { LazyVStack(alignment: .leading) { - ForEach(model.messageBoard, id: \.self) { - Text(LocalizedStringKey($0.convertLinksToMarkdown())) + ForEach(model.messageBoard, id: \.self) { msg in + Text(LocalizedStringKey(msg)) .tint(Color("Link Color")) .lineLimit(100) .lineSpacing(4) diff --git a/Hotline/macOS/MessageView.swift b/Hotline/macOS/MessageView.swift index 94d3d2e..61e640e 100644 --- a/Hotline/macOS/MessageView.swift +++ b/Hotline/macOS/MessageView.swift @@ -26,7 +26,7 @@ struct MessageView: View { Spacer() } - Text(LocalizedStringKey(msg.text.convertLinksToMarkdown())) + Text(LocalizedStringKey(msg.text)) .lineSpacing(4) .multilineTextAlignment(.leading) .textSelection(.enabled) |