diff --git a/Sources/Fluid/Services/ASRService.swift b/Sources/Fluid/Services/ASRService.swift index f8a244fa..db06f8ee 100644 --- a/Sources/Fluid/Services/ASRService.swift +++ b/Sources/Fluid/Services/ASRService.swift @@ -2982,8 +2982,13 @@ final class ASRService: ObservableObject { guard !trigger.isEmpty else { continue } let escapedTrigger = NSRegularExpression.escapedPattern(for: trigger) + // Only apply `\b` word boundaries where the trigger edge is a word + // character. Punctuation-only triggers (e.g. "¿") have no adjacent + // word boundary, so `\b…\b` would never match them. + let leadingBoundary = (trigger.first?.isWordCharacter ?? false) ? "\\b" : "" + let trailingBoundary = (trigger.last?.isWordCharacter ?? false) ? "\\b" : "" guard let regex = try? NSRegularExpression( - pattern: "\\b" + escapedTrigger + "\\b", + pattern: leadingBoundary + escapedTrigger + trailingBoundary, options: .caseInsensitive ) else { continue } @@ -3131,6 +3136,12 @@ private extension Character { return false } } + + /// Matches the regex `\w` class (letters, digits, or underscore) so we only + /// wrap dictionary triggers in `\b` boundaries where they actually apply. + var isWordCharacter: Bool { + self == "_" || self.isLetter || self.isNumber + } } // swiftlint:enable type_body_length diff --git a/Sources/Fluid/UI/CustomDictionaryView.swift b/Sources/Fluid/UI/CustomDictionaryView.swift index c1c37a2e..8de52864 100644 --- a/Sources/Fluid/UI/CustomDictionaryView.swift +++ b/Sources/Fluid/UI/CustomDictionaryView.swift @@ -192,8 +192,8 @@ struct CustomDictionaryView: View { } private var canAddManualReplacement: Bool { + // An empty replacement is allowed: it deletes the trigger from the transcript. !self.manualTriggers.isEmpty && - !self.manualReplacement.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && self.manualDuplicateTriggers.isEmpty } @@ -2221,8 +2221,8 @@ struct EditDictionaryEntrySheet: View { } private var canSave: Bool { + // An empty replacement is allowed: it deletes the trigger from the transcript. !self.parseTriggers().isEmpty && - !self.replacement.trimmingCharacters(in: .whitespaces).isEmpty && self.duplicateTriggers.isEmpty }