Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 2.1.3

### Documentation
- Added full dartdoc coverage to `AhoCorasick` and `TrieNode` — includes algorithm overview, usage examples, and per-member descriptions.
- Added dartdoc to the `Language` enum and `LanguageExtension` — documents `fromString`, `fileCode`, and all usage patterns.
- Added inline migration table and per-method docs to the deprecated `SafeText` class.

## 2.1.2

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Or manually add it to your `pubspec.yaml`:

```yaml
dependencies:
safe_text: ^2.1.2
safe_text: ^2.1.3
```

Then run:
Expand Down
22 changes: 22 additions & 0 deletions lib/safe_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,25 @@ import 'src/models/mask_strategy.dart';
import 'src/safe_text_filter.dart';
import 'src/phone_number_checker.dart';

/// Legacy facade that delegates to [SafeTextFilter] and [PhoneNumberChecker].
///
/// > **Deprecated.** Use [SafeTextFilter] and [PhoneNumberChecker] directly.
/// > They offer better modularity, higher performance via the Aho-Corasick
/// > trie, and explicit initialization via [SafeTextFilter.init].
///
/// Migration guide:
///
/// | Old call | New call |
/// |---|---|
/// | `SafeText.filterText(...)` | `SafeTextFilter.filterText(...)` |
/// | `SafeText.containsBadWord(...)` | `SafeTextFilter.containsBadWord(...)` |
/// | `SafeText.containsPhoneNumber(...)` | `PhoneNumberChecker.containsPhoneNumber(...)` |
@Deprecated(
'Use SafeTextFilter and PhoneNumberChecker directly for better modularity and high performance.')
class SafeText {
/// Filters profanity from [text].
///
/// > **Deprecated.** Use [SafeTextFilter.filterText] instead.
@Deprecated('Use SafeTextFilter.filterText instead.')
static String filterText({
required String text,
Expand All @@ -35,6 +51,9 @@ class SafeText {
);
}

/// Returns `true` if [text] contains a bad word.
///
/// > **Deprecated.** Use [SafeTextFilter.containsBadWord] instead.
@Deprecated('Use SafeTextFilter.containsBadWord instead.')
static Future<bool> containsBadWord({
required String text,
Expand All @@ -50,6 +69,9 @@ class SafeText {
);
}

/// Returns `true` if [text] contains a phone number.
///
/// > **Deprecated.** Use [PhoneNumberChecker.containsPhoneNumber] instead.
@Deprecated('Use PhoneNumberChecker.containsPhoneNumber instead.')
static Future<bool> containsPhoneNumber({
required String text,
Expand Down
79 changes: 76 additions & 3 deletions lib/src/aho_corasick.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,56 @@
/// A single node in the Aho-Corasick trie.
///
/// Each node represents a prefix of one or more patterns that have been
/// inserted via [AhoCorasick.addWord].
class TrieNode {
/// Maps a Unicode code point to the child [TrieNode] for that character.
final Map<int, TrieNode> children = {};

/// Failure (fallback) link — points to the node representing the longest
/// proper suffix of the current path that is also a valid prefix in the trie.
///
/// Set on all non-root nodes by [AhoCorasick.buildFailureLinks].
TrieNode? fail;

/// Patterns that terminate at this node.
///
/// After [AhoCorasick.buildFailureLinks] is called, this list also includes
/// patterns inherited from nodes reachable via [fail] links (the "dictionary
/// suffix links" of the classic algorithm).
final List<String> outputs = [];
}

/// An implementation of the Aho-Corasick multi-pattern string search algorithm.
///
/// Aho-Corasick finds all occurrences of a set of patterns in a text in a
/// single linear pass — O(n + m + z), where n is the text length, m is the
/// total length of all patterns, and z is the number of matches. This makes
/// it well-suited for profanity filtering with large word lists.
///
/// ## Usage
///
/// ```dart
/// final ac = AhoCorasick();
/// ac.addWord('bad');
/// ac.addWord('worse');
/// ac.buildFailureLinks(); // must be called before search
///
/// final matches = ac.search('this is bad and worse');
/// // {10: ['bad'], 20: ['worse']}
/// ```
///
/// **Important:** always call [buildFailureLinks] after adding all words and
/// before calling [search]. Omitting this step produces incorrect results.
class AhoCorasick {
final TrieNode _root = TrieNode();

/// Inserts [word] into the trie.
///
/// The word is lowercased before insertion so that [search] can operate on
/// pre-lowercased input. Empty strings are silently ignored.
///
/// Call this for every pattern you want to detect, then call
/// [buildFailureLinks] once before any calls to [search].
void addWord(String word) {
if (word.isEmpty) return;
TrieNode current = _root;
Expand All @@ -17,6 +61,15 @@ class AhoCorasick {
current.outputs.add(word.toLowerCase());
}

/// Constructs failure links for all nodes in the trie using a BFS traversal.
///
/// This is the preprocessing phase of the Aho-Corasick algorithm. It must
/// be called **once**, after all words have been added via [addWord] and
/// before any calls to [search].
///
/// Failure links allow the search to fall back to the longest matching
/// suffix instead of restarting from the root on a mismatch, which keeps
/// the search complexity linear in the length of the input.
void buildFailureLinks() {
final queue = <TrieNode>[];

Expand Down Expand Up @@ -49,9 +102,29 @@ class AhoCorasick {
}
}

/// Finds all matches in the text.
/// Returns a map where the key is the string index where the match ENDS
/// and the value is a list of matching words.
/// Searches [text] for all patterns previously added via [addWord].
///
/// Returns a [Map] where each key is the **zero-based end index** (inclusive)
/// of a match within [text], and the corresponding value is the list of
/// pattern strings that end at that position.
///
/// The search is case-insensitive — [text] is lowercased internally before
/// matching.
///
/// [buildFailureLinks] must have been called before invoking this method.
///
/// ```dart
/// final ac = AhoCorasick()
/// ..addWord('he')
/// ..addWord('she')
/// ..addWord('hers')
/// ..buildFailureLinks();
///
/// final result = ac.search('ushers');
/// // Keys represent end indices; values are matched words at that position.
/// ```
///
/// Returns an empty map if no patterns match.
Map<int, List<String>> search(String text) {
final matches = <int, List<String>>{};
TrieNode? current = _root;
Expand Down
45 changes: 45 additions & 0 deletions lib/src/models/language.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
/// Supported languages for the profanity word dataset.
///
/// Pass a value from this enum to [SafeTextFilter.init] to load the
/// corresponding bundled word list. Use [Language.all] to load every
/// available language at once (increases memory usage and initialization
/// time).
///
/// Language codes follow ISO 639 where available.
///
/// ```dart
/// // Single language
/// await SafeTextFilter.init(language: Language.english);
///
/// // Multiple languages
/// await SafeTextFilter.init(
/// languages: [Language.english, Language.spanish, Language.hindi],
/// );
///
/// // Every supported language
/// await SafeTextFilter.init(language: Language.all);
/// ```
enum Language {
afrikaans,
amharic,
Expand Down Expand Up @@ -83,7 +104,20 @@ enum Language {
all,
}

/// Utility extension on [Language] for string conversion and asset path resolution.
extension LanguageExtension on Language {
/// Returns the [Language] value that corresponds to [languageCode].
///
/// Accepts both full language names (e.g. `'english'`) and ISO 639 codes
/// (e.g. `'en'`). Matching is case-insensitive.
///
/// Falls back to [Language.english] for any unrecognized code.
///
/// ```dart
/// LanguageExtension.fromString('en'); // Language.english
/// LanguageExtension.fromString('Spanish'); // Language.spanish
/// LanguageExtension.fromString('xyz'); // Language.english (default)
/// ```
static Language fromString(String languageCode) {
switch (languageCode.toLowerCase()) {
case 'af':
Expand Down Expand Up @@ -337,6 +371,17 @@ extension LanguageExtension on Language {
}
}

/// The ISO 639 file code used to locate the bundled asset for this language.
///
/// Asset files are stored at `assets/data/<fileCode>.txt` inside the
/// package. Returns an empty string for [Language.all], which is a sentinel
/// value and does not correspond to a single file.
///
/// ```dart
/// Language.english.fileCode; // 'en'
/// Language.spanish.fileCode; // 'es'
/// Language.all.fileCode; // ''
/// ```
String get fileCode {
switch (this) {
case Language.afrikaans:
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: safe_text
description: A Flutter package for filtering out bad words from text inputs and detecting phone numbers in various formats including multiplier words.
version: 2.1.2
version: 2.1.3
homepage: https://github.com/master-wayne7/safe_text
repository: https://github.com/master-wayne7/safe_text
issue_tracker: https://github.com/master-wayne7/safe_text/issues
Expand Down
Loading