diff --git a/lib/src/usecase/profanity_validator.dart b/lib/src/usecase/profanity_validator.dart index 7e656551..a6121b8f 100644 --- a/lib/src/usecase/profanity_validator.dart +++ b/lib/src/usecase/profanity_validator.dart @@ -21,7 +21,8 @@ class ProfanityValidator extends BaseValidator { this.useAllLocales = false, super.errorText, super.checkNullOrEmpty, - }) : profanityList = profanityList ?? + }) : customErrorText = errorText, + profanityList = profanityList ?? (useAllLocales ? Profanity.profanityListAll() : Profanity.of(FormBuilderLocalizations.current.localeName) @@ -33,14 +34,20 @@ class ProfanityValidator extends BaseValidator { /// Whether to use all locales to check for profanity. final bool useAllLocales; + /// The custom error message returned if the value is invalid. + final String? customErrorText; + @override String get translatedErrorText => FormBuilderLocalizations.current .profanityErrorText(profanityList.join(', ')); @override String? validateValue(String valueCandidate) { - if (profanityList.contains(valueCandidate)) { - return errorText; + final List profanityFound = profanityAllFound(valueCandidate); + if (profanityFound.isNotEmpty) { + return customErrorText ?? + FormBuilderLocalizations.current + .profanityErrorText(profanityFound.join(', ')); } return null; @@ -48,13 +55,16 @@ class ProfanityValidator extends BaseValidator { /// Returns a list of profanity words found in the value. List profanityAllFound(String valueCandidate) { - List inputSplit = valueCandidate.split(' '); + final RegExp regExp = RegExp(r'\b\w+\b'); + final List inputSplit = regExp + .allMatches(valueCandidate) + .map((RegExpMatch match) => match.group(0)!.toLowerCase()) + .toList(); + List profanityFound = []; for (String word in profanityList) { - for (int i = 0; i < inputSplit.length; i++) { - if (inputSplit[i].toLowerCase() == word) { - profanityFound.add(word); - } + if (inputSplit.contains(word.toLowerCase())) { + profanityFound.add(word); } } return profanityFound; @@ -62,22 +72,24 @@ class ProfanityValidator extends BaseValidator { /// Returns the value with profanity words censored. String profanityCensored(String valueCandidate, {String? replaceWith}) { - List inputSplit = valueCandidate.split(' '); - for (String word in profanityList) { - if (replaceWith == null) { - for (int i = 0; i < inputSplit.length; i++) { - if (inputSplit[i].toLowerCase() == word) { - inputSplit[i] = '*' * word.length; - } - } + final RegExp regExp = RegExp(r'\b\w+\b'); + final Iterable matches = regExp.allMatches(valueCandidate); + + StringBuffer buffer = StringBuffer(); + int lastMatchEnd = 0; + + for (final RegExpMatch match in matches) { + buffer.write(valueCandidate.substring(lastMatchEnd, match.start)); + String word = match.group(0)!; + if (profanityList.contains(word.toLowerCase())) { + buffer.write(replaceWith ?? '*' * word.length); } else { - for (int i = 0; i < inputSplit.length; i++) { - if (inputSplit[i].toLowerCase() == word) { - inputSplit[i] = replaceWith; - } - } + buffer.write(word); } + lastMatchEnd = match.end; } - return inputSplit.join(' '); + + buffer.write(valueCandidate.substring(lastMatchEnd)); + return buffer.toString(); } } diff --git a/test/src/usecase/profanity_validator_test.dart b/test/src/usecase/profanity_validator_test.dart new file mode 100644 index 00000000..d6b8cfc0 --- /dev/null +++ b/test/src/usecase/profanity_validator_test.dart @@ -0,0 +1,161 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; + +void main() { + final List profanityList = [ + 'badword1', + 'badword2', + 'badword3', + ]; + const String customErrorMessage = + 'This text contains inappropriate language.'; + + group('ProfanityValidator -', () { + test('should return null for strings without profanity', () { + // Arrange + final ProfanityValidator validator = + ProfanityValidator(profanityList: profanityList); + + // Act & Assert + expect(validator.validate('This is a clean sentence.'), isNull); + expect(validator.validate('Nothing wrong here!'), isNull); + }); + + test('should return the default error message for strings with profanity', + () { + // Arrange + final ProfanityValidator validator = + ProfanityValidator(profanityList: profanityList); + const String badSentence = 'This is badword1.'; + const String badSentence2 = 'Another badword2 example.'; + + // Act & Assert + expect( + validator.validate(badSentence), + equals( + FormBuilderLocalizations.current.profanityErrorText( + validator.profanityAllFound(badSentence).join(', ')), + ), + ); + expect( + validator.validate(badSentence2), + equals( + FormBuilderLocalizations.current.profanityErrorText( + validator.profanityAllFound(badSentence2).join(', ')), + ), + ); + }); + + test('should return the custom error message for strings with profanity', + () { + // Arrange + final ProfanityValidator validator = ProfanityValidator( + profanityList: profanityList, + errorText: customErrorMessage, + ); + + // Act & Assert + expect( + validator.validate('This is badword1.'), equals(customErrorMessage)); + expect(validator.validate('Another badword2 example.'), + equals(customErrorMessage)); + }); + + test( + 'should return null when the value is an empty string and null check is disabled', + () { + // Arrange + final ProfanityValidator validator = ProfanityValidator( + profanityList: profanityList, + checkNullOrEmpty: false, + ); + const String value = ''; + + // Act & Assert + final String? result = validator.validate(value); + expect(result, isNull); + }); + + test('should return null when the value is null and null check is disabled', + () { + // Arrange + final ProfanityValidator validator = ProfanityValidator( + profanityList: profanityList, + checkNullOrEmpty: false, + ); + const String? value = null; + + // Act & Assert + final String? result = validator.validate(value); + expect(result, isNull); + }); + + test('should return a list of profanity words found in the string', () { + // Arrange + final ProfanityValidator validator = ProfanityValidator( + profanityList: profanityList, + ); + + // Act & Assert + expect(validator.profanityAllFound('This is badword1 and badword3.'), + equals(['badword1', 'badword3'])); + expect(validator.profanityAllFound('Clean sentence.'), equals([])); + }); + + test('should return the string with profanity words censored', () { + // Arrange + final ProfanityValidator validator = ProfanityValidator( + profanityList: profanityList, + ); + + // Act & Assert + expect(validator.profanityCensored('This is badword1 and badword3.'), + equals('This is ******** and ********.')); + expect(validator.profanityCensored('Another badword2 example.'), + equals('Another ******** example.')); + }); + + test( + 'should return the string with profanity words replaced with a custom string', + () { + // Arrange + final ProfanityValidator validator = ProfanityValidator( + profanityList: profanityList, + ); + + // Act & Assert + expect( + validator.profanityCensored('This is badword1 and badword3.', + replaceWith: '[censored]'), + equals('This is [censored] and [censored].')); + expect( + validator.profanityCensored('Another badword2 example.', + replaceWith: '[censored]'), + equals('Another [censored] example.')); + }); + + test( + 'should use all locales for profanity check when useAllLocales is true', + () { + // Arrange + final ProfanityValidator validator = ProfanityValidator( + useAllLocales: true, + ); + + // Act & Assert + expect(validator.validate('This is shit.'), isNotNull); + expect(validator.validate('Another apeshit example.'), isNotNull); + }); + + test( + 'should initialize with empty constructor and use default profanity list', + () { + // Arrange + final ProfanityValidator validator = ProfanityValidator(); + + // Act & Assert + expect(validator.validate('This is a clean sentence.'), isNull); + expect(validator.validate('shit'), isNotNull); + }); + }); +}