aboutsummaryrefslogtreecommitdiff
path: root/Hotline
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2024-05-05 19:17:39 -0700
committerDustin Mierau <dustin@mierau.me>2024-05-05 19:17:39 -0700
commit3feb709522b38f1aa5ddedc5e1aee28597375aab (patch)
tree9df2ad849703004901261c53bcffad318b876ad4 /Hotline
parent4da73c630183e482e4ab2accef97ad54c6741bf4 (diff)
Write better URL regular expressions for our needs.
Diffstat (limited to 'Hotline')
-rw-r--r--Hotline/Hotline/HotlineClient.swift3
-rw-r--r--Hotline/Models/Hotline.swift4
-rw-r--r--Hotline/Shared/AsyncLinkPreview.swift2
-rw-r--r--Hotline/Utility/FoundationExtensions.swift39
-rw-r--r--Hotline/Utility/RegularExpressions.swift116
-rw-r--r--Hotline/macOS/ChatView.swift6
-rw-r--r--Hotline/macOS/MessageBoardView.swift4
-rw-r--r--Hotline/macOS/MessageView.swift2
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)