diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Helper/MastodonRegex.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Helper/MastodonRegex.swift index 76df541249..70dab7d5f1 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Helper/MastodonRegex.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Helper/MastodonRegex.swift @@ -11,7 +11,7 @@ public enum MastodonRegex { /// mention, hashtag. /// @... /// #... - public static let highlightPattern = "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))" + public static let highlightPattern = "(?:@([\\p{L}\\p{M}\\p{N}_]+)(@[\\p{L}\\p{M}\\p{N}_.-]+)?|#([^\\s.]+))" /// emoji /// :shortcode: /// accept ^\B: or \s: but not accept \B: to force user input a space to make emoji take effect @@ -21,7 +21,7 @@ public enum MastodonRegex { /// @… /// #… /// :… - public static let autoCompletePattern = "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))|(^\\B:|\\s:)([a-zA-Z0-9_]+)" + public static let autoCompletePattern = "(?:@([\\p{L}\\p{M}\\p{N}_]+)(@[\\p{L}\\p{M}\\p{N}_.-]+)?|#([^\\s.]+))|(^\\B:|\\s:)([a-zA-Z0-9_]+)" public enum Search { public static let username = "^@?[a-z0-9_-]+(@[\\S]+)?$" diff --git a/MastodonTests/MastodonRegexTests.swift b/MastodonTests/MastodonRegexTests.swift new file mode 100644 index 0000000000..cfceac4de0 --- /dev/null +++ b/MastodonTests/MastodonRegexTests.swift @@ -0,0 +1,180 @@ +// +// MastodonRegexTests.swift +// MastodonTests +// +// Created by Assistant on 2025-11-02. +// + +import XCTest +@testable import Mastodon +import MastodonUI + +final class MastodonRegexTests: XCTestCase { + + func testExample() throws { + XCTAssertTrue(true) + } + + func testUnicodeRegexPatterns() throws { + // Test that our regex patterns correctly match Unicode characters with combining marks + let highlightPattern = MastodonRegex.highlightPattern + let autoCompletePattern = MastodonRegex.autoCompletePattern + + let testCases = [ + "@café@examplë.com", + "@unikonstanz@bawü.social", + "@user@täst.de", + "@björn@exämple.com" + ] + + // Test highlight pattern + let highlightRegex = try NSRegularExpression(pattern: highlightPattern, options: []) + for testCase in testCases { + let nsString = testCase as NSString + let range = NSRange(location: 0, length: nsString.length) + let matches = highlightRegex.matches(in: testCase, options: [], range: range) + + XCTAssertEqual(matches.count, 1, "Highlight pattern should match \(testCase)") + } + + // Test autocomplete pattern + let autoCompleteRegex = try NSRegularExpression(pattern: autoCompletePattern, options: []) + for testCase in testCases { + let nsString = testCase as NSString + let range = NSRange(location: 0, length: nsString.length) + let matches = autoCompleteRegex.matches(in: testCase, options: [], range: range) + + XCTAssertEqual(matches.count, 1, "Autocomplete pattern should match \(testCase)") + } + } + + func testHighlightPatternWithUnicodeCharacters() throws { + let highlightPattern = MastodonRegex.highlightPattern + let regex = try NSRegularExpression(pattern: highlightPattern, options: []) + + // Test cases with unicode characters + let testCases = [ + "@unikonstanz@bawü.social", + "@user@täst.de", + "@björn@exämple.com", + "@用户@example.com", + "@example@例え.テスト", + "@café@examplë.com" // Test with combining marks + ] + + for testCase in testCases { + let nsString = testCase as NSString + let range = NSRange(location: 0, length: nsString.length) + let matches = regex.matches(in: testCase, options: [], range: range) + + // Should find exactly one match + XCTAssertEqual(matches.count, 1, "Failed to match \(testCase)") + + if let match = matches.first { + // The full match should cover the entire string + XCTAssertEqual(match.range.location, 0) + XCTAssertEqual(match.range.length, nsString.length) + + // The first capture group should be the username part + let usernameRange = match.range(at: 1) + if usernameRange.location != NSNotFound { + let username = nsString.substring(with: usernameRange) + XCTAssertTrue(username.count > 0, "Username capture group is empty for \(testCase)") + } + + // The second capture group should be the domain part (if present) + let domainRange = match.range(at: 2) + if domainRange.location != NSNotFound { + let domain = nsString.substring(with: domainRange) + XCTAssertTrue(domain.count > 0, "Domain capture group is empty for \(testCase)") + } + } + } + } + + func testAutoCompletePatternWithUnicodeCharacters() throws { + let autoCompletePattern = MastodonRegex.autoCompletePattern + let regex = try NSRegularExpression(pattern: autoCompletePattern, options: []) + + // Test cases with unicode characters + let testCases = [ + "@unikonstanz@bawü.social", + "@user@täst.de", + "@björn@exämple.com", + "@用户@example.com", + "@example@例え.テスト", + "@café@examplë.com" // Test with combining marks + ] + + for testCase in testCases { + let nsString = testCase as NSString + let range = NSRange(location: 0, length: nsString.length) + let matches = regex.matches(in: testCase, options: [], range: range) + + // Should find exactly one match + XCTAssertEqual(matches.count, 1, "Failed to match \(testCase)") + + if let match = matches.first { + // The full match should cover the entire string + XCTAssertEqual(match.range.location, 0) + XCTAssertEqual(match.range.length, nsString.length) + } + } + } + + func testHighlightPatternWithAsciiCharacters() throws { + let highlightPattern = MastodonRegex.highlightPattern + let regex = try NSRegularExpression(pattern: highlightPattern, options: []) + + // Test cases with ASCII characters (should still work) + let testCases = [ + "@user@example.com", + "@test@domain.org", + "@simple@site.net", + "@café@example.com" // Test with combining marks + ] + + for testCase in testCases { + let nsString = testCase as NSString + let range = NSRange(location: 0, length: nsString.length) + let matches = regex.matches(in: testCase, options: [], range: range) + + // Should find exactly one match + XCTAssertEqual(matches.count, 1, "Failed to match \(testCase)") + + if let match = matches.first { + // The full match should cover the entire string + XCTAssertEqual(match.range.location, 0) + XCTAssertEqual(match.range.length, nsString.length) + } + } + } + + func testAutoCompletePatternWithAsciiCharacters() throws { + let autoCompletePattern = MastodonRegex.autoCompletePattern + let regex = try NSRegularExpression(pattern: autoCompletePattern, options: []) + + // Test cases with ASCII characters (should still work) + let testCases = [ + "@user@example.com", + "@test@domain.org", + "@simple@site.net", + "@café@example.com" // Test with combining marks + ] + + for testCase in testCases { + let nsString = testCase as NSString + let range = NSRange(location: 0, length: nsString.length) + let matches = regex.matches(in: testCase, options: [], range: range) + + // Should find exactly one match + XCTAssertEqual(matches.count, 1, "Failed to match \(testCase)") + + if let match = matches.first { + // The full match should cover the entire string + XCTAssertEqual(match.range.location, 0) + XCTAssertEqual(match.range.length, nsString.length) + } + } + } +} \ No newline at end of file