Repository: Blaspsoft/blasp Branch: main Commit: e0a2ea52cbe7 Files: 84 Total size: 367.1 KB Directory structure: gitextract_qpmp_x7w/ ├── .github/ │ └── workflows/ │ └── main.yml ├── .gitignore ├── .styleci.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── config/ │ ├── blasp.php │ └── languages/ │ ├── english.php │ ├── french.php │ ├── german.php │ └── spanish.php ├── phpunit.xml ├── src/ │ ├── BlaspManager.php │ ├── BlaspServiceProvider.php │ ├── Blaspable.php │ ├── Console/ │ │ ├── ClearCommand.php │ │ ├── LanguagesCommand.php │ │ └── TestCommand.php │ ├── Core/ │ │ ├── Analyzer.php │ │ ├── Contracts/ │ │ │ ├── DriverInterface.php │ │ │ └── MaskStrategyInterface.php │ │ ├── Dictionary.php │ │ ├── Masking/ │ │ │ ├── CallbackMask.php │ │ │ ├── CharacterMask.php │ │ │ └── GrawlixMask.php │ │ ├── MatchedWord.php │ │ ├── Matchers/ │ │ │ ├── CompoundWordDetector.php │ │ │ ├── FalsePositiveFilter.php │ │ │ ├── PhoneticMatcher.php │ │ │ └── RegexMatcher.php │ │ ├── Normalizers/ │ │ │ ├── EnglishNormalizer.php │ │ │ ├── FrenchNormalizer.php │ │ │ ├── GermanNormalizer.php │ │ │ ├── NullNormalizer.php │ │ │ ├── SpanishNormalizer.php │ │ │ └── StringNormalizer.php │ │ ├── Result.php │ │ └── Score.php │ ├── Drivers/ │ │ ├── PatternDriver.php │ │ ├── PhoneticDriver.php │ │ ├── PipelineDriver.php │ │ └── RegexDriver.php │ ├── Enums/ │ │ └── Severity.php │ ├── Events/ │ │ ├── ContentBlocked.php │ │ ├── ModelProfanityDetected.php │ │ └── ProfanityDetected.php │ ├── Exceptions/ │ │ └── ProfanityRejectedException.php │ ├── Facades/ │ │ └── Blasp.php │ ├── Middleware/ │ │ └── CheckProfanity.php │ ├── PendingCheck.php │ ├── Rules/ │ │ └── Profanity.php │ └── Testing/ │ └── BlaspFake.php └── tests/ ├── AllLanguagesApiTest.php ├── AllLanguagesDetectionTest.php ├── BladeDirectiveTest.php ├── BlaspCheckTest.php ├── BlaspCheckValidationTest.php ├── BlaspableTest.php ├── BypassVulnerabilityTest.php ├── CacheDriverConfigurationTest.php ├── ConfigurationLoaderLanguageTest.php ├── ConfigurationLoaderTest.php ├── CustomMaskCharacterTest.php ├── DetectionStrategyRegistryTest.php ├── EdgeCaseTest.php ├── EmptyInputTest.php ├── FrenchStringNormalizerTest.php ├── GermanStringNormalizerTest.php ├── Issue24Test.php ├── Issue32FalsePositiveTest.php ├── MiddlewareAliasTest.php ├── MultiLanguageDetectionConfigTest.php ├── MultiLanguageProfanityTest.php ├── PhoneticDriverTest.php ├── PipelineDriverTest.php ├── ProfanityExpressionGeneratorTest.php ├── ResultCachingTest.php ├── SeverityMapTest.php ├── SpanishStringNormalizerTest.php ├── StrMacroTest.php ├── TestCase.php └── UuidFalsePositiveTest.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/main.yml ================================================ name: Run Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: # Step 1: Check out the repository - name: Checkout code uses: actions/checkout@v4 # Step 2: Set up PHP - name: Set up PHP uses: shivammathur/setup-php@v2 with: php-version: "8.2" # Set your desired PHP version extensions: mbstring, dom, zip # Step 3: Install Composer dependencies - name: Install dependencies run: composer install --no-interaction --prefer-dist # Step 4: Run PHPUnit tests - name: Run tests run: php ./vendor/bin/phpunit ================================================ FILE: .gitignore ================================================ /vendor composer.lock .phpunit.result.cache /.idea ================================================ FILE: .styleci.yml ================================================ preset: laravel disabled: - single_class_element_per_statement ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to `blasp` will be documented in this file ## 3.0.0 - 2025-01-05 ### Added - Custom mask character support with `maskWith()` method - Simplified API with Laravel facade pattern and method chaining - Comprehensive multi-language support (Spanish, German, French) - Expanded test coverage across all languages - Comprehensive extensibility system with full test coverage - Basic registry pattern for language normalizers - Language files publishing to ServiceProvider - Comprehensive documentation for maskWith() and all chainable methods ### Changed - Implemented dependency injection and simplified service dependencies - Extracted expression generation logic to dedicated generator - Improved substitution detection across all languages - Updated README with simplified chainable API documentation - Updated README with comprehensive multi-language support documentation - Updated README with language files publishing options - Updated README for v3.0 features ### Fixed - Resolved language switching not loading correct profanities - Prevented cross-word-boundary profanity matches ### Removed - Strategy factory, plugin manager, and default detection strategy - Domain-specific detection strategies (email, URL, phone) - Unused strict() and lenient() detection modes - README duplications and outdated references ## 1.0.0 - 201X-XX-XX - initial release ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Contributions are **welcome** and will be fully **credited**. Please read and understand the contribution guide before creating an issue or pull request. ## Etiquette This project is open source, and as such, the maintainers give their free time to build and maintain the source code held within. They make the code freely available in the hope that it will be of use to other developers. It would be extremely unfair for them to suffer abuse or anger for their hard work. Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the world that developers are civilized and selfless people. It's the duty of the maintainer to ensure that all submissions to the project are of sufficient quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. ## Viability When requesting or submitting new features, first consider whether it might be useful to others. Open source projects are used by many developers, who may have entirely different needs to your own. Think about whether or not your feature is likely to be used by other users of the project. ## Procedure Before filing an issue: - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. - Check to make sure your feature suggestion isn't already present within the project. - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. - Check the pull requests tab to ensure that the feature isn't already in progress. Before submitting a pull request: - Check the codebase to ensure that your feature doesn't already exist. - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. ## Requirements If the project maintainer has any additional requirements, you will find them listed here. - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). - **Add tests!** - Your patch won't be accepted if it doesn't have tests. - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. **Happy coding**! ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) Michael Deeming Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

Blasp Icon

> **Official API Available!** This package powers [blasp.app](https://blasp.app/) - a universal profanity filtering REST API that works with any language. Free tier with 1,000 requests/month, multi-language support, and custom word lists.

GitHub Workflow Status (main) Total Downloads Latest Version License

# Blasp - Advanced Profanity Filter for Laravel Blasp is a powerful, extensible profanity filter for Laravel. Version 4 is a ground-up rewrite with a driver-based architecture, severity scoring, masking strategies, Eloquent model integration, and a clean fluent API. ## Features - **Driver Architecture** — `regex` (detects obfuscation, substitutions, separators), `pattern` (fast exact matching), `phonetic` (catches sound-alike evasions), or `pipeline` (chains multiple drivers together). Extend with custom drivers. - **Multi-Language** — English, Spanish, German, French with language-specific normalizers. Check one, many, or all at once. - **Severity Scoring** — Words categorised as mild/moderate/high/extreme. Filter by minimum severity and get a 0-100 score. - **Masking Strategies** — Character mask (`*`, `#`), grawlix (`!@#$%`), or a custom callback. - **Eloquent Integration** — `Blaspable` trait auto-sanitizes or rejects profanity on model save. - **Middleware** — Reject or sanitize profane request fields with configurable severity. - **Validation Rules** — Fluent validation rule with language, severity, and score threshold support. - **Testing Utilities** — `Blasp::fake()` for test doubles with assertions. - **Events** — `ProfanityDetected`, `ContentBlocked`, and `ModelProfanityDetected`. ## Requirements - PHP 8.2+ - Laravel 8.0+ ## Installation ```bash composer require blaspsoft/blasp ``` Publish configuration: ```bash # Everything (config + language files) php artisan vendor:publish --tag="blasp" # Config only php artisan vendor:publish --tag="blasp-config" # Language files only php artisan vendor:publish --tag="blasp-languages" ``` ## Quick Start ```php use Blaspsoft\Blasp\Facades\Blasp; $result = Blasp::check('This is a fucking sentence'); $result->isOffensive(); // true $result->clean(); // "This is a ******* sentence" $result->original(); // "This is a fucking sentence" $result->score(); // 30 $result->count(); // 1 $result->uniqueWords(); // ['fucking'] $result->severity(); // Severity::High ``` ## Fluent API All builder methods return a `PendingCheck` and can be chained: ```php // Language selection Blasp::in('spanish')->check($text); Blasp::in('english', 'french')->check($text); Blasp::inAllLanguages()->check($text); // Language shortcuts Blasp::english()->check($text); Blasp::spanish()->check($text); Blasp::german()->check($text); Blasp::french()->check($text); // Driver selection Blasp::driver('regex')->check($text); // Full obfuscation detection (default) Blasp::driver('pattern')->check($text); // Fast exact matching Blasp::driver('phonetic')->check($text); // Sound-alike detection (e.g. "phuck", "sheit") Blasp::driver('pipeline')->check($text); // Chain multiple drivers (config-based) // Ad-hoc pipeline — chain any drivers without config Blasp::pipeline('regex', 'phonetic')->check($text); Blasp::pipeline('pattern', 'phonetic')->in('english')->mask('#')->check($text); // Shorthand modes Blasp::strict()->check($text); // Forces regex driver Blasp::lenient()->check($text); // Forces pattern driver // Masking Blasp::mask('*')->check($text); // Character mask (default) Blasp::mask('#')->check($text); // Custom character Blasp::mask('grawlix')->check($text); // !@#$% cycling Blasp::mask(fn($word, $len) => '[CENSORED]')->check($text); // Callback // Severity filtering use Blaspsoft\Blasp\Enums\Severity; Blasp::withSeverity(Severity::High)->check($text); // Ignores mild/moderate // Allow/block lists (merged with config) Blasp::allow('damn', 'hell')->check($text); Blasp::block('customword')->check($text); // Chain everything Blasp::spanish() ->mask('#') ->withSeverity(Severity::Moderate) ->check($text); // Batch checking $results = Blasp::checkMany(['text one', 'text two']); ``` ## Result Object The `Result` object is returned by every `check()` call: | Method | Returns | Description | |--------|---------|-------------| | `isOffensive()` | `bool` | Text contains profanity | | `isClean()` | `bool` | Text is clean | | `clean()` | `string` | Text with profanities masked | | `original()` | `string` | Original unmodified text | | `score()` | `int` | Severity score (0-100) | | `count()` | `int` | Total profanity matches | | `uniqueWords()` | `array` | Unique base words detected | | `severity()` | `?Severity` | Highest severity in matches | | `words()` | `Collection` | `MatchedWord` objects with position, length, severity | | `toArray()` | `array` | Full result as array | | `toJson()` | `string` | Full result as JSON | `Result` implements `JsonSerializable`, `Stringable` (returns clean text), and `Countable`. ## Detection Types The regex driver detects obfuscated profanity: | Type | Example | Detected As | |------|---------|-------------| | Straight match | `fucking` | `fucking` | | Substitution | `fÛck!ng`, `f4ck` | `fucking`, `fuck` | | Separators | `f-u-c-k-i-n-g`, `f@ck` | `fucking`, `fuck` | | Doubled | `ffuucckkiinngg` | `fucking` | | Combination | `f-uuck!ng` | `fucking` | > **Separator limit:** The regex driver allows up to 3 separator characters between each letter (e.g., `f--u--c--k`). This covers all realistic obfuscation patterns while keeping regex complexity low enough for PHP-FPM environments. The pattern driver only detects straight word-boundary matches. The phonetic driver uses `metaphone()` + Levenshtein distance to catch words that *sound like* profanity but are spelled differently: | Type | Example | Detected As | |------|---------|-------------| | Phonetic spelling | `phuck` | `fuck` | | Shortened form | `fuk` | `fuck` | | Sound-alike | `sheit` | `shit` | Configure sensitivity in `config/blasp.php` under `drivers.phonetic`. A curated false-positive list prevents common words like "fork", "duck", and "beach" from being flagged. ### Pipeline Driver The pipeline driver chains multiple drivers together so a single `check()` call runs all of them. It uses **union merge** semantics — text is flagged if **any** driver finds a match. ```php // Config-based: set 'default' => 'pipeline' or use driver('pipeline') Blasp::driver('pipeline')->check('phuck this sh1t'); // Ad-hoc: pick drivers on the fly (no config needed) Blasp::pipeline('regex', 'phonetic')->check('phuck this sh1t'); Blasp::pipeline('regex', 'pattern', 'phonetic')->check($text); ``` When multiple drivers detect the same word at the same position, duplicates are removed — only the longest match is kept. Masks are applied from the merged result, and the score is recalculated across all matches. Configure the default sub-drivers in `config/blasp.php`: ```php 'drivers' => [ 'pipeline' => [ 'drivers' => ['regex', 'phonetic'], // Drivers to chain ], ], ``` ## Eloquent Integration The `Blaspable` trait automatically checks model attributes during save: ```php use Blaspsoft\Blasp\Blaspable; class Comment extends Model { use Blaspable; protected array $blaspable = ['body', 'title']; } ``` ```php // Sanitize mode (default) — profanity is masked, model saves $comment = Comment::create(['body' => 'This is fucking great']); $comment->body; // "This is ******* great" // Check what happened $comment->hadProfanity(); // true $comment->blaspResults(); // ['body' => Result, 'title' => Result] $comment->blaspResult('body'); // Result instance ``` ### Per-Model Overrides ```php class Comment extends Model { use Blaspable; protected array $blaspable = ['body', 'title']; protected string $blaspMode = 'reject'; // 'sanitize' (default) | 'reject' protected string $blaspLanguage = 'spanish'; // null = config default protected string $blaspMask = '#'; // null = config default } ``` ### Reject Mode In reject mode, saving a model with profanity throws `ProfanityRejectedException` and the model is not persisted: ```php use Blaspsoft\Blasp\Exceptions\ProfanityRejectedException; try { $comment = Comment::create(['body' => 'profane text']); } catch (ProfanityRejectedException $e) { $e->attribute; // 'body' $e->result; // Result instance $e->model; // The unsaved model } ``` ### Disabling Checking ```php Comment::withoutBlaspChecking(function () { Comment::create(['body' => 'unchecked content']); }); ``` ### Events A `ModelProfanityDetected` event fires whenever profanity is detected on a model attribute (both sanitize and reject modes): ```php use Blaspsoft\Blasp\Events\ModelProfanityDetected; Event::listen(ModelProfanityDetected::class, function ($event) { $event->model; // The model instance $event->attribute; // Which attribute had profanity $event->result; // Result instance }); ``` ## Middleware Use `CheckProfanity` to filter incoming request fields. A `blasp` middleware alias is registered automatically: ```php // Using the short alias (recommended) Route::post('/comment', CommentController::class) ->middleware('blasp'); // With parameters: action, severity Route::post('/comment', CommentController::class) ->middleware('blasp:sanitize,mild'); // Or using the class directly use Blaspsoft\Blasp\Middleware\CheckProfanity; Route::post('/comment', CommentController::class) ->middleware(CheckProfanity::class); ``` | Action | Behaviour | |--------|-----------| | `reject` (default) | Returns 422 JSON with field errors | | `sanitize` | Replaces profane fields in the request and continues | Configure which fields to check in `config/blasp.php`: ```php 'middleware' => [ 'action' => 'reject', 'fields' => ['*'], // '*' = all fields 'except' => ['password', 'email', '_token'], // Always skipped 'severity' => 'mild', ], ``` ## Validation Rules ### String Rule ```php $request->validate([ 'comment' => ['required', 'blasp_check'], 'bio' => ['required', 'blasp_check:spanish'], ]); ``` ### Fluent Rule Object ```php use Blaspsoft\Blasp\Rules\Profanity; use Blaspsoft\Blasp\Enums\Severity; $request->validate([ 'comment' => ['required', Profanity::in('english')], 'bio' => ['required', Profanity::severity(Severity::High)], 'tagline' => ['required', Profanity::maxScore(50)], ]); ``` ## Blade Directive The `@clean` directive sanitizes and escapes text for safe display in views: ```blade

@clean($comment->body)

{{-- Equivalent to: {{ app('blasp')->check($comment->body)->clean() }} --}} ``` Output is HTML-escaped via `e()` for XSS safety. ## Str / Stringable Macros Blasp registers macros on Laravel's `Str` and `Stringable` classes: ```php use Illuminate\Support\Str; // Static methods Str::isProfane('fuck this'); // true Str::isProfane('hello'); // false Str::cleanProfanity('fuck this'); // '**** this' Str::cleanProfanity('hello'); // 'hello' // Fluent Stringable methods Str::of('fuck this')->isProfane(); // true Str::of('fuck this')->cleanProfanity(); // Stringable('**** this') Str::of('hello')->cleanProfanity()->upper(); // 'HELLO' (chaining works) ``` ## Configuration Full `config/blasp.php` reference: ```php return [ 'default' => env('BLASP_DRIVER', 'regex'), // 'regex' | 'pattern' | 'phonetic' | 'pipeline' 'language' => env('BLASP_LANGUAGE', 'english'), // Default language 'mask' => '*', // Default mask character 'severity' => 'mild', // Minimum severity 'events' => false, // Fire ProfanityDetected events 'cache' => [ 'enabled' => true, 'driver' => env('BLASP_CACHE_DRIVER'), 'ttl' => 86400, 'results' => true, // Cache check() results by content hash ], 'middleware' => [ 'action' => 'reject', 'fields' => ['*'], 'except' => ['password', 'email', '_token'], 'severity' => 'mild', ], 'model' => [ 'mode' => env('BLASP_MODEL_MODE', 'sanitize'), // 'sanitize' | 'reject' ], 'drivers' => [ 'pipeline' => [ 'drivers' => ['regex', 'phonetic'], // Sub-drivers to chain ], 'phonetic' => [ 'phonemes' => 4, // metaphone code length (2-8) 'min_word_length' => 3, // skip short words 'max_distance_ratio' => 0.6, // levenshtein threshold (0.3-0.8) 'supported_languages' => ['english'], // metaphone is English-oriented 'false_positives' => ['fork', '...'], // never flag these words ], ], 'allow' => [], // Global allow-list 'block' => [], // Global block-list 'separators' => [...], // Characters treated as separators 'substitutions' => [...], // Character leet-speak mappings 'false_positives' => [...], // Words that should never be flagged ]; ``` ## Custom Drivers Implement `DriverInterface` and register with the manager: ```php use Blaspsoft\Blasp\Core\Contracts\DriverInterface; use Blaspsoft\Blasp\Core\Result; use Blaspsoft\Blasp\Core\Dictionary; use Blaspsoft\Blasp\Core\Contracts\MaskStrategyInterface; class MyDriver implements DriverInterface { public function detect(string $text, Dictionary $dictionary, MaskStrategyInterface $mask, array $options = []): Result { // Your detection logic } } // Register in a service provider Blasp::extend('my-driver', fn($app) => new MyDriver()); // Use it Blasp::driver('my-driver')->check($text); ``` ## Caching Blasp caches `check()` results by default. When the same text is checked with the same configuration (language, driver, severity, allow/block lists), the cached result is returned instantly. ```php // First call — runs full analysis, caches result $result = Blasp::check('some text'); // Second call — returns cached result $result = Blasp::check('some text'); ``` Configure caching in `config/blasp.php`: ```php 'cache' => [ 'enabled' => true, // Master switch for all caching 'driver' => env('BLASP_CACHE_DRIVER'), // null = default cache driver 'ttl' => 86400, // Cache lifetime in seconds 'results' => true, // Cache check() results (disable independently) ], ``` Result caching is automatically bypassed when using a `CallbackMask` (closures can't be serialized). Clear both dictionary and result caches with: ```bash php artisan blasp:clear ``` Or programmatically: ```php Dictionary::clearCache(); ``` ## Artisan Commands ```bash # Clear the profanity cache php artisan blasp:clear # Test text from the command line php artisan blasp:test "some text to check" --lang=english --detail # List available languages with word counts php artisan blasp:languages ``` ## Testing ### Faking ```php use Blaspsoft\Blasp\Facades\Blasp; use Blaspsoft\Blasp\Core\Result; // Replace with a fake — all checks return clean by default Blasp::fake(); // Pre-configure specific responses Blasp::fake([ 'bad text' => Result::withMatches(['fuck']), 'clean text' => Result::none('clean text'), ]); $result = Blasp::check('bad text'); $result->isOffensive(); // true // Assertions Blasp::assertChecked(); Blasp::assertCheckedTimes(1); Blasp::assertCheckedWith('bad text'); ``` ### Disabling Filtering ```php Blasp::withoutFiltering(function () { // All checks return clean results }); ``` ## Events Enable global events with `'events' => true` in config: | Event | Fired When | Properties | |-------|------------|------------| | `ProfanityDetected` | `check()` finds profanity | `result`, `originalText` | | `ContentBlocked` | Middleware detects profanity | `result`, `request`, `field`, `action` | | `ModelProfanityDetected` | Blaspable trait detects profanity | `model`, `attribute`, `result` | `ModelProfanityDetected` always fires (not gated by the `events` config). ## Migrating from v3 ### Namespace Changes | v3 | v4 | |----|-----| | `Blaspsoft\Blasp\Facades\Blasp` | `Blaspsoft\Blasp\Facades\Blasp` (unchanged) | | `Blaspsoft\Blasp\ServiceProvider` | `Blaspsoft\Blasp\BlaspServiceProvider` | The Laravel auto-discovery handles provider/alias registration automatically. The facade namespace is the same as v3, so no import changes are needed for the facade. ### Config Changes | v3 Key | v4 Key | Notes | |--------|--------|-------| | `default_language` | `language` | `default_language` still works as alias | | `mask_character` | `mask` | `mask_character` still works as alias | | `cache_driver` | `cache.driver` | `cache_driver` still works as alias | | — | `default` | New: driver selection (`regex`/`pattern`) | | — | `severity` | New: minimum severity level | | — | `events` | New: enable global events | | — | `allow` / `block` | New: global allow/block lists | | — | `middleware` | New: middleware configuration section | | — | `model` | New: Blaspable trait configuration | ### Result API Changes | v3 Method | v4 Method | |-----------|-----------| | `hasProfanity()` | `isOffensive()` | | `getCleanString()` | `clean()` | | `getSourceString()` | `original()` | | `getProfanitiesCount()` | `count()` | | `getUniqueProfanitiesFound()` | `uniqueWords()` | All v3 methods still work as deprecated aliases. ### Builder API Changes | v3 Method | v4 Method | |-----------|-----------| | `maskWith($char)` | `mask($char)` | | `allLanguages()` | `inAllLanguages()` | | `language($lang)` | `in($lang)` | | `configure($profanities, $falsePositives)` | `block(...$words)` / `allow(...$words)` | All v3 methods still work as deprecated aliases. ### New in v4 - **Driver architecture** — `regex` and `pattern` drivers, custom driver support - **Severity system** — Mild/Moderate/High/Extreme levels with scoring - **Masking strategies** — Grawlix and callback masking - **Blaspable trait** — Automatic Eloquent model profanity checking - **Middleware** — Request-level profanity filtering - **Fluent validation rule** — `Profanity::in('spanish')->severity(Severity::High)` - **Testing utilities** — `Blasp::fake()`, assertions, `withoutFiltering()` - **Events** — `ProfanityDetected`, `ContentBlocked`, `ModelProfanityDetected` - **Artisan commands** — `blasp:clear`, `blasp:test`, `blasp:languages` - **Batch checking** — `Blasp::checkMany([...])` - **Multi-language in one call** — `Blasp::in('english', 'spanish')->check($text)` ## Contributing We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. ## Changelog See [CHANGELOG.md](CHANGELOG.md) for detailed version history. ## License Blasp is open-sourced software licensed under the [MIT license](LICENSE). ================================================ FILE: composer.json ================================================ { "name": "blaspsoft/blasp", "description": "Blasp is a powerful and customisable profanity filter package for Laravel applications", "keywords": [ "blaspsoft", "blasp" ], "homepage": "https://github.com/blaspsoft/blasp", "license": "MIT", "type": "library", "authors": [ { "name": "Michael Deeming", "email": "michael.deeming90@gmail.com", "role": "Developer" } ], "require": { "php": "^8.2", "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0" }, "require-dev": { "orchestra/testbench": "^10.0|^11.0", "phpunit/phpunit": "^11.0|^12.5.12" }, "autoload": { "psr-4": { "Blaspsoft\\Blasp\\": "src/" } }, "autoload-dev": { "psr-4": { "Blaspsoft\\Blasp\\Tests\\": "tests" } }, "minimum-stability": "stable", "prefer-stable": true, "scripts": { "test": "vendor/bin/phpunit", "test-coverage": "vendor/bin/phpunit --coverage-html coverage" }, "config": { "sort-packages": true }, "extra": { "laravel": { "providers": [ "Blaspsoft\\Blasp\\BlaspServiceProvider" ], "aliases": { "Blasp": "Blaspsoft\\Blasp\\Facades\\Blasp" } } } } ================================================ FILE: config/blasp.php ================================================ env('BLASP_DRIVER', 'regex'), /* |-------------------------------------------------------------------------- | Default Language |-------------------------------------------------------------------------- | | The default language to use for profanity detection. | */ 'language' => env('BLASP_LANGUAGE', 'english'), // Backward compat alias 'default_language' => env('BLASP_LANGUAGE', 'english'), /* |-------------------------------------------------------------------------- | Mask Character |-------------------------------------------------------------------------- | | The character used to mask detected profanities. | */ 'mask' => '*', // Backward compat alias 'mask_character' => '*', /* |-------------------------------------------------------------------------- | Minimum Severity |-------------------------------------------------------------------------- | | The minimum severity level to detect. Words below this severity | will be ignored. Options: mild, moderate, high, extreme | */ 'severity' => 'mild', /* |-------------------------------------------------------------------------- | Events |-------------------------------------------------------------------------- | | When enabled, ProfanityDetected events will be fired automatically | when profanity is found during a check. | */ 'events' => false, /* |-------------------------------------------------------------------------- | Cache Configuration |-------------------------------------------------------------------------- */ 'cache' => [ 'enabled' => true, 'driver' => env('BLASP_CACHE_DRIVER'), 'ttl' => 86400, 'results' => true, ], // Backward compat alias 'cache_driver' => env('BLASP_CACHE_DRIVER'), /* |-------------------------------------------------------------------------- | Middleware Configuration |-------------------------------------------------------------------------- */ 'middleware' => [ 'action' => 'reject', 'fields' => ['*'], 'except' => ['password', 'email', '_token'], 'severity' => 'mild', ], /* |-------------------------------------------------------------------------- | Model Configuration |-------------------------------------------------------------------------- | | Controls how the Blaspable trait behaves on Eloquent models. | 'sanitize' replaces profanity with the mask character. | 'reject' throws a ProfanityRejectedException instead of saving. | */ 'model' => [ 'mode' => env('BLASP_MODEL_MODE', 'sanitize'), ], /* |-------------------------------------------------------------------------- | Driver-Specific Configuration |-------------------------------------------------------------------------- */ 'drivers' => [ 'pipeline' => [ 'drivers' => ['regex', 'phonetic'], ], 'phonetic' => [ 'phonemes' => 4, // metaphone code length (2-8, lower=more aggressive) 'min_word_length' => 3, // skip words shorter than this 'max_distance_ratio' => 0.6, // levenshtein threshold (0.3-0.8, lower=stricter) 'supported_languages' => ['english'], 'false_positives' => [ 'fork', 'forked', 'forking', 'beach', 'beaches', 'witch', 'witches', 'sheet', 'sheets', 'deck', 'decks', 'count', 'counts', 'counter', 'county', 'ship', 'shipped', 'shipping', 'duck', 'ducked', 'ducking', 'fudge', 'fudging', 'buck', 'bucks', 'puck', 'pucks', 'bass', 'mass', 'pass', 'passed', 'heck', 'shoot', 'shot', 'what', 'white', 'while', 'whole', ], ], ], /* |-------------------------------------------------------------------------- | Character Separators |-------------------------------------------------------------------------- */ 'separators' => [ '@', '#', '%', '&', '_', ';', "'", '"', ',', '~', '`', '|', '!', '$', '^', '*', '(', ')', '-', '+', '=', '{', '}', '[', ']', ':', '<', '>', '?', '.', '/', ], /* |-------------------------------------------------------------------------- | Character Substitutions |-------------------------------------------------------------------------- */ 'substitutions' => [ '/a/' => ['a', '4', '@', '*', 'Á', 'á', 'À', 'Â', 'à', 'Â', 'â', 'Ä', 'ä', 'Ã', 'ã', 'Å', 'å', 'æ', 'Æ', 'α', 'Δ', 'Λ', 'λ'], '/b/' => ['b', '8', '\\', '3', '*', 'ß', 'Β', 'β'], '/c/' => ['c', '*', 'Ç', 'ç', 'ć', 'Ć', 'č', 'Č', '¢', '€', '<', '(', '{', '©'], '/d/' => ['d', '*', '\\', ')', 'Þ', 'þ', 'Ð', 'ð'], '/e/' => ['e', '3', '*', '€', 'È', 'è', 'É', 'é', 'Ê', 'ê', 'ë', 'Ë', 'ē', 'Ē', 'ė', 'Ė', 'ę', 'Ę', '∑'], '/f/' => ['f', '*', 'ƒ'], '/g/' => ['g', '6', '9', '*'], '/h/' => ['h', '*', 'Η'], '/i/' => ['i', '!', '|', ']', '[', '1', '*', '∫', 'Ì', 'Í', 'Î', 'Ï', 'ì', 'í', 'î', 'ï', 'ī', 'Ī', 'į', 'Į'], '/j/' => ['j', '*'], '/k/' => ['k', '*', 'Κ', 'κ'], '/l/' => ['l', '!', '|', ']', '[', '*', '£', '∫', 'Ì', 'Í', 'Î', 'Ï', 'ł', 'Ł'], '/m/' => ['m', '*'], '/n/' => ['n', '*', 'η', 'Ν', 'Π', 'ñ', 'Ñ', 'ń', 'Ń'], '/o/' => ['o', '0', '*', 'Ο', 'ο', 'Φ', '¤', '°', 'ø', 'ô', 'Ô', 'ö', 'Ö', 'ò', 'Ò', 'ó', 'Ó', 'œ', 'Œ', 'ø', 'Ø', 'ō', 'Ō', 'õ', 'Õ'], '/p/' => ['p', '*', 'ρ', 'Ρ', '¶', 'þ'], '/q/' => ['q', '*'], '/r/' => ['r', '*', '®'], '/s/' => ['s', '5', '*', '\$', '§', 'ß', 'Ś', 'ś', 'Š', 'š'], '/t/' => ['t', '*', 'Τ', 'τ'], '/u/' => ['u', 'υ', 'µ', 'û', 'ü', 'ù', 'ú', 'ū', 'Û', 'Ü', 'Ù', 'Ú', 'Ū', '@', '*'], '/v/' => ['v', '*', 'υ', 'ν'], '/w/' => ['w', '*', 'ω', 'ψ', 'Ψ'], '/x/' => ['x', '*', 'Χ', 'χ'], '/y/' => ['y', '*', '¥', 'γ', 'ÿ', 'ý', 'Ÿ', 'Ý'], '/z/' => ['z', '*', 'Ζ', 'ž', 'Ž', 'ź', 'Ź', 'ż', 'Ż'], ], /* |-------------------------------------------------------------------------- | False Positives |-------------------------------------------------------------------------- */ 'false_positives' => [ 'hello', 'scunthorpe', 'cockburn', 'penistone', 'lightwater', 'assume', 'bass', 'class', 'compass', 'pass', 'dickinson', 'middlesex', 'cockerel', 'butterscotch', 'blackcock', 'countryside', 'arsenal', 'flick', 'flicker', 'analyst', 'cocktail', 'musicals hit', 'is hit', 'blackcocktail', 'its not', ], /* |-------------------------------------------------------------------------- | Global Allow List |-------------------------------------------------------------------------- | | Words in this list will never be flagged as profanity. | */ 'allow' => [], /* |-------------------------------------------------------------------------- | Global Block List |-------------------------------------------------------------------------- | | Additional words to always flag as profanity. | */ 'block' => [], /* |-------------------------------------------------------------------------- | Backward Compatibility: Profanities |-------------------------------------------------------------------------- | | Basic profanity list for backward compatibility. | Full lists are in config/languages/*.php | */ 'profanities' => [ 'fuck', 'shit', 'damn', 'bitch', 'ass', 'hell', ], ]; ================================================ FILE: config/languages/english.php ================================================ [ 'mild' => [ 'damn', 'hell', 'crap', 'arse', 'sucks', 'piss', 'bloody', 'bollocks', 'bugger', 'crikey', 'darn', 'heck', 'turd', 'puke', 'puuke', 'puuker', 'shat', 'trots', 'vomit', 'waysted', 'wuss', 'wuzzie', ], 'moderate' => [ 'ass', 'bitch', 'bastard', 'slut', 'whore', 'douche', 'douchebag', 'skank', 'slag', 'tramp', 'tosser', 'wanker', 'wanking', 'prick', 'dick', 'knob', 'bellend', 'minger', 'git', 'twit', 'dipshit', 'jackass', 'smartass', 'dumbass', 'asshole', 'arsehole', 'shag', 'shagger', 'shagging', 'hooker', 'hussy', 'floozy', 'tart', 'sissy', 'pansy', ], 'high' => [ 'fuck', 'shit', 'cock', 'pussy', 'cunt', 'twat', 'tit', 'tits', 'fucking', 'fucker', 'motherfucker', 'bullshit', 'horseshit', 'shithead', 'shithole', 'shitface', 'fuckface', 'fuckhead', 'cocksucker', 'asswipe', 'clusterfuck', 'mindfuck', 'dumbfuck', 'fuckwit', 'shitbag', 'shitcunt', 'thundercunt', 'cum', 'jizz', 'dildo', 'blowjob', 'handjob', 'rimjob', 'fellatio', 'cunnilingus', ], 'extreme' => [ 'nigger', 'nigga', 'niggers', 'niggas', 'coon', 'darkie', 'kike', 'spic', 'spick', 'wetback', 'chink', 'gook', 'paki', 'raghead', 'towelhead', 'sandnigger', 'beaner', 'gringo', 'wop', 'dago', 'polack', 'retard', 'retarded', 'faggot', 'fag', 'dyke', 'tranny', ], ], 'profanities' => [ 'abbo', 'abortionist', 'abuser', 'ahole', 'alabama hotpocket', 'alligatorbait', 'anal', 'analannie', 'analsex', 'areola', 'arse', 'arsebagger', 'arsebandit', 'arseblaster', 'arsecowboy', 'arsefuck', 'arsefucker', 'arsehat', 'arsehole', 'arseholes', 'arsehore', 'arsejockey', 'arsekiss', 'arsekisser', 'arselick', 'arselicker', 'arselover', 'arseman', 'arsemonkey', 'arsemunch', 'arsemuncher', 'arsepacker', 'arsepirate', 'arsepuppies', 'arseranger', 'arses', 'arsewhore', 'arsewipe', 'ass', 'assbag', 'assbagger', 'assbandit', 'assbanger', 'assbite', 'assblaster', 'assclown', 'asscock', 'asscowboy', 'asscracker', 'asses', 'assface', 'assfuck', 'assfucker', 'assgoblin', 'ass-hat', 'asshat', 'asshead', 'asshole', 'assholes', 'assholz', 'asshopper', 'asshore', 'ass-jabber', 'assjacker', 'assjockey', 'asskiss', 'asskisser', 'assklown', 'asslick', 'asslicker', 'asslover', 'assman', 'assmonkey', 'ass monkey', 'assmunch', 'assmuncher', 'assnigger', 'asspacker', 'ass-pirate', 'asspirate', 'asspuppies', 'assranger', 'assshit', 'assshole', 'asssucker', 'asswad', 'asswhore', 'asswipe', 'axwound', 'azzhole', 'backdoorman', 'badfuck', 'baldy', 'ball licker', 'balllicker', 'ballsack', 'bampot', 'banging', 'barelylegal', 'barface', 'barfface', 'bassterds', 'bastard', 'bastards', 'bastardz', 'basterds', 'basterdz', 'bazongas', 'bazooms', 'beaner', 'beastality', 'beastial', 'beastiality', 'beat-off', 'beatoff', 'beatyourmeat', 'bestial', 'bestiality', 'biatch', 'bicurious', 'bigass', 'bigbastard', 'bigbutt', 'bitch', 'bitchass', 'bitcher', 'bitches', 'bitchez', 'bitchin', 'bitching', 'bitchslap', 'bitchtits', 'bitchy', 'biteme', 'blow job', 'blowjob', 'boffing', 'bohunk', 'bollick', 'bollock', 'bollocks', 'bollox', 'bondage', 'boner', 'boob', 'boobies', 'boobs', 'booby', 'bootycall', 'bountybar', 'breastjob', 'breastlover', 'breastman', 'brothel', 'brotherfucker', 'bugger', 'buggered', 'buggery', 'bukake', 'bullcrap', 'bulldike', 'bulldyke', 'bullshit', 'bumblefuck', 'bumfuck', 'bungabunga', 'bunghole', 'butchbabes', 'butchdike', 'butchdyke', 'butt-bang', 'buttbang', 'buttcheeks', 'buttface', 'butt-fuck', 'buttfuck', 'buttfucka', 'butt-fucker', 'buttfucker', 'butt-fuckers', 'buttfuckers', 'butthead', 'butthole', 'buttman', 'buttmunch', 'buttmuncher', 'butt-pirate', 'buttpirate', 'butt plug', 'buttplug', 'buttstain', 'buttwipe', 'byatch', 'cacker', 'cameljockey', 'camel toe', 'cameltoe', 'carpet muncher', 'carpetmuncher', 'cawk', 'cawks', 'chav', 'cherrypopper', 'chesticle', 'chickslick', 'chinc', 'chink', 'choad', 'chode', 'clamdigger', 'clamdiver', 'clit', 'clitface', 'clitfuck', 'clitoris', 'clogwog', 'clunge', 'clusterfuck', 'cnts', 'cntz', 'cock', 'cockass', 'cockbite', 'cockblock', 'cockblocker', 'cockburger', 'cockcowboy', 'cockface', 'cockfight', 'cockfucker', 'cock-head', 'cockhead', 'cockjockey', 'cockknob', 'cockknoker', 'cocklicker', 'cocklover', 'cockmaster', 'cockmongler', 'cockmongruel', 'cockmonkey', 'cockmuncher', 'cocknob', 'cocknose', 'cocknugget', 'cockqueen', 'cockrider', 'cocks', 'cockshit', 'cocksman', 'cocksmith', 'cocksmoke', 'cocksmoker', 'cocksniffer', 'cocksucer', 'cocksuck', 'cocksucked', 'cock-sucker', 'cocksucker', 'cocksucking', 'cocktease', 'cockwaffle', 'cocky', 'coitus', 'cok', 'commie', 'coochie', 'coochy', 'coon', 'coondog', 'cooter', 'copulate', 'cracker', 'crackpipe', 'crack-whore', 'crackwhore', 'crap', 'crappy', 'crotchjockey', 'crotchmonkey', 'crotchrot', 'cuck', 'cum', 'cumbubble', 'cumdumpster', 'cumfest', 'cumguzzler', 'cumjockey', 'cumm', 'cumquat', 'cumqueen', 'cumshot', 'cumslut', 'cumtart', 'cunilingus', 'cunillingus', 'cunnie', 'cunnilingus', 'cunntt', 'cunt', 'cuntass', 'cunteyed', 'cuntface', 'cuntfucker', 'cunthole', 'cuntlick', 'cuntlicker', 'cuntlicker', 'cuntlicking', 'cuntrag', 'cunts', 'cuntslut', 'cuntsucker', 'cuntz', 'cybersex', 'cyberslimer', 'dago', 'dammit', 'damn', 'damnation', 'damnit', 'darkie', 'darky', 'datnigga', 'deapthroat', 'deepthroat', 'deggo', 'dego', 'devilworshipper', 'dick', 'dickbag', 'dickbeaters', 'dickbrain', 'dickface', 'dickforbrains', 'dickfuck', 'dickfucker', 'dickhead', 'dickhole', 'dickjuice', 'dickless', 'dicklick', 'dicklicker', 'dickmilk', 'dickmonger', 'dicks', 'dickslap', 'dick-sneeze', 'dicksucker', 'dicksucking', 'dicktickler', 'dickwad', 'dickweasel', 'dickweed', 'dickwod', 'dike', 'dildo', 'dildos', 'dilldo', 'dilldos', 'dipshit', 'dipstick', 'dixiedike', 'dixiedyke', 'doggiestyle', 'doggystyle', 'dominatricks', 'dominatrics', 'dominatrix', 'doochbag', 'dookie', 'douch', 'douchbag', 'douche', 'douchebag', 'douche-fag', 'douchewaffle', 'drag queen', 'dragqueen', 'dragqween', 'dripdick', 'dumass', 'dumb ass', 'dumbass', 'dumbbitch', 'dumbfuck', 'dumbshit', 'dumshit', 'dyke', 'easyslut', 'eatballs', 'eatme', 'eatpussy', 'ejaculate', 'ejaculated', 'ejaculating', 'ejaculation', 'enema', 'excrement', 'facefucker', 'facist', 'faeces', 'fag', 'fagbag', 'faget', 'fagfucker', 'fagging', 'faggit', 'faggot', 'faggotcock', 'faggots', 'fagit', 'fagot', 'fags', 'fagtard', 'fagz', 'faig', 'faigs', 'fannyfucker', 'fark', 'farted', 'farting', 'farty', 'fastfuck', 'fatass', 'fatfuck', 'fatfucker', 'fatso', 'feces', 'felatio', 'felch', 'felcher', 'felching', 'fellatio', 'feltch', 'feltcher', 'feltching', 'fingerfuck', 'fingerfucked', 'fingerfucker', 'fingerfuckers', 'fingerfucking', 'fister', 'fistfuck', 'fistfucked', 'fistfucker', 'fistfucking', 'fisting', 'flamer', 'flasher', 'flid', 'flipping the bird', 'flyd', 'flydie', 'flydye', 'fondle', 'footaction', 'footfuck', 'footfucker', 'footlicker', 'fornicate', 'freakfuck', 'freakyfucker', 'freefuck', 'fubar', 'fucck', 'fuck', 'fucka', 'fuckable', 'fuckass', 'fuckbag', 'fuckboy', 'fuckbrain', 'fuckbuddy', 'fuckbutt', 'fuckbutter', 'fucked', 'fucker', 'fuckers', 'fuckersucker', 'fuckface', 'fuckfest', 'fuckfreak', 'fuckfriend', 'fuckhead', 'fuckhole', 'fuckin', 'fuckina', 'fucking', 'fuckingbitch', 'fuckinnuts', 'fuckinright', 'fuckit', 'fuckknob', 'fuckmonkey', 'fucknut', 'fucknutt', 'fuckoff', 'fuckpig', 'fuckstick', 'fucktard', 'fucktart', 'fuckup', 'fuckwad', 'fuckwhore', 'fuckwit', 'fuckwitt', 'fuckyou', 'fudge packer', 'fudgepacker', 'Fudge Packer', 'fugly', 'fuk', 'Fukah', 'Fuken', 'fuker', 'Fukin', 'Fukk', 'Fukkah', 'Fukken', 'Fukker', 'Fukkin', 'fuks', 'funfuck', 'fuuck', 'gang bang', 'gangbang', 'gangbanged', 'gangbanger', 'gatorbait', 'gayass', 'gaybob', 'gayboy', 'gaydo', 'gayfuck', 'gayfuckist', 'gaygirl', 'gaylord', 'gaymuthafuckinwhore', 'gays', 'gaysex', 'gaytard', 'gaywad', 'gayz', 'getiton', 'givehead', 'glazeddonut', 'godammit', 'goddamit', 'goddammit', 'goddamn', 'goddamned', 'god-damned', 'goddamnes', 'goddamnit', 'goddamnmuthafucker', 'goldenshower', 'gonorrehea', 'gonzagas', 'gooch', 'gook', 'gotohell', 'greaseball', 'gringo', 'grostulation', 'guido', 'gypo', 'gypp', 'gyppie', 'gyppo', 'gyppy', 'handjob', 'hard on', 'hardon', 'headfuck', 'heeb', 'hell', 'herpes', 'hijacker', 'hijacking', 'hillbillies', 'hindoo', 'hitler', 'hitlerism', 'hitlerist', 'hoar', 'hobo', 'hoe', 'hoes', 'holestuffer', 'homo', 'homobangers', 'homodumbshit', 'honger', 'honkers', 'honkey', 'honky', 'hookers', 'hoor', 'hoore', 'hore', 'horney', 'horniest', 'horny', 'horseshit', 'hosejob', 'hotdamn', 'hotpussy', 'hottotrot', 'humping', 'hymen', 'iblowu', 'idiot', 'incest', 'insest', 'internet wife', 'inthebuff', 'jackass', 'jackoff', 'jackshit', 'jagoff', 'jap', 'japcrap', 'japs', 'jerkass', 'jerk off', 'jerk-off', 'jerkoff', 'jesuschrist', 'jigaboo', 'jiggabo', 'jihad', 'jijjiboo', 'jisim', 'jism', 'jiss', 'jizim', 'jizjuice', 'jizm', 'jizm', 'jizz', 'jizzim', 'jizzum', 'jubblies', 'juggalo', 'jungle bunny', 'junglebunny', 'kiddy fiddler', 'kike', 'kinky', 'kissass', 'knobz', 'kondum', 'kooch', 'kootch', 'krap', 'krappy', 'kraut', 'kumbubble', 'kumbullbe', 'kummer', 'kumming', 'kums', 'kunilingus', 'kunnilingus', 'kunt', 'kunts', 'kuntz', 'kyke', 'labia', 'lactate', 'lady boy', 'ladyboy', 'lameass', 'lapdance', 'lardass', 'lesbain', 'lesbayn', 'lesbian', 'lesbin', 'lesbo', 'lezbe', 'lezbefriends', 'lezbo', 'lezz', 'lezzer', 'lezzie', 'lezzo', 'libido', 'lickme', 'limpdick', 'lipshits', 'lipshitz', 'livesex', 'lmfao', 'loadedgun', 'lovebone', 'lovegoo', 'lovegun', 'lovejuice', 'lovemuscle', 'lovepistol', 'loverocket', 'low life', 'lowlife', 'lubejob', 'luckycameltoe', 'manhater', 'manpaste', 'masochist', 'masokist', 'massterbait', 'masstrbait', 'masstrbate', 'mastabate', 'mastabater', 'masterbaiter', 'masterbate', 'master bates', 'masterbates', 'mastrabator', 'masturbate', 'masturbating', 'mattressprincess', 'mcfagget', 'meatbeater', 'meatrack', 'mgger', 'mggor', 'milf', 'minge', 'mofo', 'molest', 'molestation', 'molester', 'molestor', 'moneyshot', 'mooncricket', 'moron', 'mothafuck', 'mothafucka', 'mothafuckaz', 'mothafucked', 'mothafucker', 'motha fucker', 'mothafuckin', 'mothafucking', 'mothafuckings', 'motha fuker', 'motha fukkah', 'motha fukker', 'motherfuck', 'motherfucked', 'mother-fucker', 'motherfucker', 'mother fucker', 'motherfuckin', 'motherfucking', 'motherfuckings', 'mother fukah', 'mother fuker', 'mother fukkah', 'mother fukker', 'motherlovebone', 'muff', 'muffdive', 'muffdiver', 'muffindiver', 'mufflikcer', 'muncher', 'munging', 'muthafucker', 'mutha fucker', 'mutha fukah', 'mutha fuker', 'mutha fukkah', 'mutha fukker', 'nastt', 'nastybitch', 'nastyho', 'nastyslut', 'nastywhore', 'nazi', 'necro', 'negro', 'negroes', 'negroid', 'nigaboo', 'nigga', 'niggah', 'niggaracci', 'niggard', 'niggarded', 'niggarding', 'niggardliness', "niggardliness's", 'niggardly', "niggard's", 'niggards', 'niggaz', 'nigger', 'niggerhead', 'niggerhole', "nigger's", 'niggers', 'niggle', 'niggled', 'niggles', 'niggling', 'nigglings', 'niggor', 'niggur', 'niglet', 'nignog', 'nigr', 'nigra', 'nigre', 'nigur', 'niiger', 'niigr', 'nipple', 'nipplering', 'nittit', 'nlgger', 'nlggor', 'nofuckingway', 'nonce', 'nookey', 'nookie', 'nudger', 'nut case', 'nutcase', 'nutfucker', 'nut sack', 'nutsack', 'ontherag', 'orafis', 'orgasim', 'orgasim', 'orgasm', 'orgasum', 'orgies', 'orgy', 'oriface', 'orifice', 'orifiss', 'osama bin laden', 'packi', 'packie', 'packy', 'paedo', 'paedofile', 'paedophile', 'paki', 'pakie', 'paky', 'palesimian', 'panooch', 'panti', 'pearlnecklace', 'pecker', 'peckerhead', 'peckerwood', 'peedo', 'peeenus', 'peeenusss', 'peehole', 'peenus', 'peinus', 'penas', 'penile', 'penisbanger', 'penis-breath', 'penises', 'penisfucker', 'penispuffer', 'penus', 'penuus', 'perv', 'perversion', 'pervert', 'phonesex', 'phuc', 'phuck', 'phuk', 'phuked', 'phuker', 'phuking', 'phukked', 'phukker', 'phukking', 'phungky', 'phuq', 'pi55', 'picaninny', 'piccaninny', 'pickaninny', 'pikey', 'piky', 'pimper', 'pimpjuic', 'pimpjuice', 'pimpsimp', 'pindick', 'piss', 'pissed', 'pissed off', 'pisser', 'pisses', 'pissflaps', 'pisshead', 'pissin', 'pissing', 'pissoff', 'play boy', 'playboy', 'play bunny', 'playbunny', 'play girl', 'playgirl', 'plumper', 'pocketpool', 'polac', 'polack', 'polak', 'polesmoker', 'pollock', 'poon', 'poonani', 'poonany', 'poontang', 'pooperscooper', 'pooping', 'poorwhitetrash', 'poostabber', 'popimp', 'porch monkey', 'porchmonkey', 'porn', 'pornflick', 'pornking', 'porno', 'pornprincess', 'pric', 'prick', 'prik', 'prickhead', 'prostitute', 'pu55i', 'pu55y', 'pube', 'pubiclice', 'puke', 'punanny', 'punta', 'puntang', 'purinaprincess', 'pusse', 'pussee', 'pussie', 'pussies', 'pussy', 'pussyeater', 'pussyfucker', 'pussylicker', 'pussylicking', 'pussylips', 'pussylover', 'pussypounder', 'pusy', 'puto', 'puuke', 'puuker', 'queef', 'queer', 'queerbait', 'queerhole', 'queers', 'queerz', 'quim', 'qweers', 'qweerz', 'qweir', 'rag head', 'raghead', 'raped', 'rapist', 'rearend', 'rearentry', 'recktum', 'rectum', 'redneck', 'renob', 'rentafuck', 'rimjob', 'rimming', 'ruski', 'russki', 'russkie', 'sadist', 'sadom', 'saeema butt', 'sandm', 'sand nigger', 'sandnigger', 'scag', 'scank', 'scat', 'schlong', 'screwing', 'screwyou', 'scrote', 'scrotum', 'scum', 'scumbag', 'seaman staines', 'semen', 'sexed', 'sexfarm', 'sexhound', 'sexhouse', 'sexing', 'sexkitten', 'sexpot', 'sexslave', 'sextogo', 'sextoy', 'sextoys', 'sexwhore', 'sexymoma', 'sexy-slim', 'seymour butts', 'shag', 'shagger', 'shaggin', 'shagging', 'shat', 'shhit', 'shit', 'shitass', 'shitbag', 'shitbagger', 'shitbrains', 'shitbreath', 'shitcan', 'shitcanned', 'shitcunt', 'shitdick', 'shite', 'shiteater', 'shited', 'shiter', 'shitface', 'shitfaced', 'shitfit', 'shitforbrains', 'shitfuck', 'shitfucker', 'shitfull', 'shithapens', 'shithappens', 'shithead', 'shithole', 'shithouse', 'shiting', 'shitlist', 'shitola', 'shitoutofluck', 'shits', 'shitspitter', 'shitstain', 'shitted', 'shitter', 'shittiest', 'shitting', 'shitty', 'shity', 'shitz', 'shiz', 'shiznit', 'shortfuck', 'shyt', 'shyte', 'shytty', 'shyty', 'sissy', 'sixsixsix', 'sixtynine', 'sixtyniner', 'skanck', 'skank', 'skankbitch', 'skankee', 'skankey', 'skankfuck', 'skanks', 'skankwhore', 'skanky', 'skankybitch', 'skankywhore', 'skeet', 'skinflute', 'skullfuck', 'skum', 'skumbag', 'slanteye', 'slantyeye', 'slapper', 'slavedriver', 'sleezebag', 'sleezeball', 'slideitin', 'slimeball', 'slimebucket', 'slopehead', 'slopey', 'slopy', 'slut', 'slutbag', 'sluts', 'slutt', 'slutting', 'slutty', 'slutwear', 'slutwhore', 'slutz', 'smackthemonkey', 'smeg', 'smelly', 'smut', 'snatch', 'snatchpatch', 'snot', 'snowback', 'snownigger', 'sodom', 'sodomise', 'sodomite', 'sodomize', 'sodomy', 'son-of-a-bitch', 'sonofabitch', 'sonofbitch', 'spac', 'spacca', 'spaghettibender', 'spaghettinigger', 'spankthemonkey', 'spazza', 'sperm', 'spermacide', 'spermbag', 'spermhearder', 'spermherder', 'spic', 'spick', 'spig', 'spigotty', 'spik', 'spitter', 'splittail', 'splooge', 'spooge', 'spook', 'spreadeagle', 'squaw', 'stabber', 'stiffy', 'strapon', 'stripclub', 'stroking', 'stupidfuck', 'stupidfucker', 'suckass', 'suckdick', 'sucker', 'suckme', 'suckmyass', 'suckmydick', 'suckmytit', 'suckoff', 'swastika', 'tampon', 'tarbaby', 'tard', 'teat', 'teste', 'testicle', 'testicles', 'thicklips', 'thicko', 'thirdeye', 'thirdleg', 'threesome', 'thundercunt', 'timbernigger', 'tit', 'titbitnipply', 'titfuck', 'titfucker', 'titfuckin', 'titjob', 'titlicker', 'titlover', 'tits', 'tittie', 'titties', 'titty', 'tittyfuck', 'tonguethrust', 'tonguethruster', 'tonguetramp', 'torture', 'tosser', 'tosspot', 'towel head', 'towelhead', 'trailertrash', 'tramp', 'trannie', 'tranny', 'trots', 'trouser snake', 'tuckahoe', 'tunneloflove', 'turd', 'twat', 'twatlips', 'twats', 'twatwaffle', 'twink', 'twinkie', 'twobitwhore', 'unclefucker', 'unfuckable', 'upskirt', 'uptheass', 'upthebutt', 'urinate', 'urine', 'usama bin laden', 'uterus', 'vag', 'vagina', 'vaginal', 'vajayjay', 'vajina', 'va-j-j', 'valjina', 'vibrater', 'vibrator', 'vietcong', 'violate', 'violation', 'virginbreaker', 'vjayjay', 'vomit', 'vullva', 'vulva', 'wank', 'wanker', 'wanking', 'wankjob', 'waysted', 'welcher', 'wetback', 'wetspot', 'whacker', 'whigger', 'whiskeydick', 'whiskydick', 'whitenigger', 'whitetrash', 'whitey', 'whoor', 'whop', 'whore', 'whorebag', 'whoreface', 'whorefucker', 'whorehouse', 'wife beater', 'williewanker', 'wog', 'wop', 'wuss', 'wuzzie', 'x-rated', 'xrated', 'yellowman', 'zigabo', 'zipperhea', 'zipper head', 'sucks', 'bloody', 'crikey', 'darn', 'heck', 'slag', 'knob', 'bellend', 'minger', 'git', 'twit', 'smartass', 'hooker', 'hussy', 'floozy', 'tart', 'pansy', 'mindfuck', 'niggas', 'retard', 'retarded', ], 'false_positives' => [ 'hello', 'scunthorpe', 'cockburn', 'penistone', 'lightwater', 'assume', 'bass', 'class', 'compass', 'pass', 'dickinson', 'middlesex', 'cockerel', 'butterscotch', 'blackcock', 'countryside', 'arsenal', 'flick', 'flicker', 'analyst', 'cocktail', 'musicals hit', 'is hit', 'blackcocktail', 'its not', // Common words containing "ass" 'assignment', 'assign', 'assigned', 'assigns', 'assigning', 'assist', 'assistant', 'assisted', 'assists', 'assistance', 'associate', 'associated', 'associates', 'association', 'associations', 'assemble', 'assembled', 'assembles', 'assembly', 'assert', 'asserted', 'assertion', 'assertions', 'asserts', 'assess', 'assessed', 'assesses', 'assessing', 'assessment', 'assessments', 'asset', 'assets', 'assure', 'assured', 'assures', 'assurance', 'assorted', 'assortment', 'assassin', 'assassins', 'assassination', 'assassinated', 'assault', 'assaulted', 'assaults', 'passion', 'passionate', 'passions', 'passive', 'passively', 'passenger', 'passengers', 'passage', 'passages', 'passing', 'passed', 'passes', 'passport', 'passports', 'password', 'passwords', 'bypass', 'bypassed', 'bypasses', 'bypassing', 'classroom', 'classrooms', 'classic', 'classical', 'classics', 'classification', 'classifications', 'classified', 'classify', 'classmate', 'classmates', 'classed', 'classes', 'classy', 'mass', 'masses', 'massive', 'massively', 'massage', 'massages', 'massacre', 'massacres', 'embassy', 'embassies', 'ambassador', 'ambassadors', 'embarrass', 'embarrassed', 'embarrassing', 'embarrassment', 'harass', 'harassed', 'harassing', 'harassment', 'brass', 'brassy', 'crass', 'glass', 'glasses', 'glassy', 'grass', 'grasses', 'grassy', 'lass', 'lassie', 'molasses', 'morass', 'sass', 'sassy', 'trespass', 'trespassed', 'trespassing', 'surpass', 'surpassed', 'surpasses', 'compassion', 'compassionate', 'encompass', 'encompassed', 'encompasses', 'encompassing', // Common words containing "tit" 'title', 'titles', 'titled', 'subtitle', 'subtitles', 'entity', 'entities', 'identity', 'identities', 'quantity', 'quantities', 'constitution', 'constitutional', 'constitutions', 'constitute', 'constitutes', 'institution', 'institutional', 'institutions', 'petition', 'petitions', 'petitioner', 'competition', 'competitions', 'competitive', 'competitor', 'competitors', 'repetition', 'repetitions', 'repetitive', 'appetite', 'appetites', 'gratitude', 'attitude', 'attitudes', 'altitude', 'altitudes', 'aptitude', 'multitude', 'fortitude', 'latitude', 'latitudes', 'partition', 'partitions', 'practitioner', 'practitioners', 'restitution', 'prostitution', 'superstition', 'superstitions', 'superstitious', 'titillate', 'titan', 'titans', 'titanium', // Common words containing "cum" 'document', 'documents', 'documentary', 'documentation', 'documented', 'circumstance', 'circumstances', 'circumference', 'accumulate', 'accumulated', 'accumulation', 'cucumber', 'cucumbers', 'incumbent', 'incumbents', // Common words containing "ho" / "hoe" 'shoe', 'shoes', 'shoelace', 'horseshoe', // Common words containing "nig" 'night', 'nights', 'nightclub', 'nightfall', 'nightlife', 'nightmare', 'nightmares', 'nightstand', 'nighttime', 'tonight', 'overnight', 'knight', 'knights', // Common words containing "rap" 'grape', 'grapes', 'drape', 'drapes', 'scrape', 'scraped', 'scraper', 'therapy', 'therapies', 'therapist', 'therapists', // Common words containing "nob" 'noble', 'nobles', 'nobleman', 'nobility', 'snob', 'snobs', 'snobbish', 'snobby', ], 'substitutions' => [ '/a/' => ['a', '4', '@', 'Á', 'á', 'À', 'Â', 'à', 'Â', 'â', 'Ä', 'ä', 'Ã', 'ã', 'Å', 'å', 'æ', 'Æ', 'α', 'Δ', 'Λ', 'λ'], '/e/' => ['e', '3', '€', 'È', 'è', 'É', 'é', 'Ê', 'ê', 'ë', 'Ë', 'ē', 'Ē', 'ė', 'Ė', 'ę', 'Ę', '∑'], '/i/' => ['i', '!', '|', ']', '[', '1', '∫', 'Ì', 'Í', 'Î', 'Ï', 'ì', 'í', 'î', 'ï', 'ī', 'Ī', 'į', 'Į'], '/o/' => ['o', '0', 'Ο', 'ο', 'Φ', '¤', '°', 'ø', 'ô', 'Ô', 'ö', 'Ö', 'ò', 'Ò', 'ó', 'Ó', 'œ', 'Œ', 'ø', 'Ø', 'ō', 'Ō', 'õ', 'Õ'], '/u/' => ['u', 'υ', 'µ', 'û', 'ü', 'ù', 'ú', 'ū', 'Û', 'Ü', 'Ù', 'Ú', 'Ū', '@', '*'], ] ]; ================================================ FILE: config/languages/french.php ================================================ [ 'mild' => [ 'crotte', 'crottes', 'caca', 'cacas', 'zut', 'punaise', 'idiot', 'idiots', 'idiote', 'idiotes', 'bête', 'bete', 'bêtes', 'betes', 'sot', 'sots', 'sotte', 'sottes', 'niais', 'niaise', 'niaises', 'ballot', 'ballots', 'andouille', 'andouilles', ], 'moderate' => [ 'connard', 'connarde', 'con', 'conne', 'salaud', 'salope', 'garce', 'garces', 'pétasse', 'petasse', 'pétasses', 'petasses', 'bâtard', 'batard', 'bâtards', 'batards', 'bâtarde', 'batarde', 'bâtardes', 'batardes', 'abruti', 'abrutis', 'abrutie', 'abruties', 'crétin', 'cretin', 'crétins', 'cretins', 'crétine', 'cretine', 'crétines', 'cretines', 'débile', 'debile', 'débiles', 'debiles', 'imbécile', 'imbecile', 'imbéciles', 'imbeciles', 'cul', 'culs', 'trou du cul', 'trou de balle', 'cochon', 'cochons', 'cochonne', 'cochonnes', ], 'high' => [ 'merde', 'putain', 'enculé', 'encule', 'niquer', 'nique', 'baiser', 'baise', 'foutre', 'foutu', 'foutue', 'chier', 'bite', 'pute', 'fils de pute', ], 'extreme' => [ 'pédé', 'pede', 'pédés', 'pedes', 'pédéraste', 'pederaste', 'pédérastes', 'pederastes', 'tapette', 'tapettes', 'tantouze', 'tantouzes', 'fiotte', 'fiottes', 'tarlouze', 'tarlouzes', 'gouine', 'gouines', 'attardé', 'attarde', 'attardés', 'attardes', 'attardée', 'attardee', 'attardées', 'attardees', ], ], 'profanities' => [ // Common French profanities and vulgar expressions 'merde', 'putain', 'connard', 'connarde', 'con', 'conne', 'salaud', 'salope', 'enculé', 'encule', 'enculée', 'enculee', 'fils de pute', 'fils de putain', 'bordel', 'chier', 'chiasse', 'chieur', 'chieuse', 'emmerde', 'emmerder', 'emmerdeur', 'emmerdeuse', 'baiser', 'baise', 'baisé', 'baise', 'baisée', 'baisee', 'foutre', 'foutue', 'foutu', 'niquer', 'nique', 'niqué', 'nique', 'niquée', 'niquee', 'bite', 'bites', 'pine', 'pines', 'queue', 'queues', 'vit', 'verge', 'zob', 'zobs', 'biroute', 'biroutes', 'braquemart', 'braquemarts', 'dard', 'dards', 'gourdin', 'gourdins', 'gland', 'glands', 'prépuce', 'prepuce', 'prépuces', 'prepuces', 'couilles', 'couille', 'couillon', 'couillonne', 'couillons', 'couillonnes', 'roubignoles', 'roubignole', 'burnes', 'burne', 'roustons', 'rouston', 'testicules', 'testicule', 'génitoires', 'genitoires', 'génitoire', 'genitoire', 'chatte', 'chattes', 'minou', 'minous', 'con', 'cons', 'moule', 'moules', 'fente', 'fentes', 'cramouille', 'cramouilles', 'crevasse', 'crevasses', 'cyprine', 'cyprines', 'foufoune', 'foufounes', 'motte', 'mottes', 'touffe', 'touffes', 'abricot', 'abricots', 'nichons', 'nichon', 'tétons', 'teton', 'téton', 'tetons', 'roberts', 'robert', 'doudounes', 'doudoune', 'lolos', 'lolo', 'miches', 'miche', 'mamelles', 'mamelle', 'seins', 'sein', 'nénés', 'nene', 'nénée', 'nenee', 'roploplos', 'roploplo', 'flotteurs', 'flotteur', 'amortisseurs', 'amortisseur', 'airbags', 'airbag', 'cul', 'culs', 'fesses', 'fesse', 'pétard', 'petard', 'pétards', 'petards', 'postérieur', 'posterieur', 'postérieurs', 'posterieurs', 'derrière', 'derriere', 'derrières', 'derrieres', 'fion', 'fions', 'trou du cul', 'trou de balle', 'anus', 'orifice', 'orifices', 'rosette', 'rosettes', 'rondelle', 'rondelles', 'bague', 'bagues', 'anneau', 'anneaux', 'pédé', 'pede', 'pédés', 'pedes', 'pédéraste', 'pederaste', 'pédérastes', 'pederastes', 'tapette', 'tapettes', 'tante', 'tantes', 'tantouze', 'tantouzes', 'fiotte', 'fiottes', 'tarlouze', 'tarlouzes', 'tafiole', 'tafioles', 'gouine', 'gouines', 'lesbienne', 'lesbiennes', 'tribade', 'tribades', 'saphique', 'saphiques', 'lesbos', 'lesbo', 'garce', 'garces', 'pétasse', 'petasse', 'pétasses', 'petasses', 'traînée', 'trainee', 'traînées', 'trainees', 'pute', 'putes', 'putain', 'putains', 'catin', 'catins', 'caillera', 'cailleras', 'racaille', 'racailles', 'voyou', 'voyous', 'truand', 'truands', 'bandit', 'bandits', 'malfrat', 'malfrats', 'gangster', 'gangsters', 'criminel', 'criminels', 'criminelle', 'criminelles', 'assassin', 'assassins', 'tueur', 'tueurs', 'tueuse', 'tueuses', 'meurtrier', 'meurtriers', 'meurtrière', 'meurtrieres', 'bourrin', 'bourrins', 'bourrine', 'bourrines', 'rustre', 'rustres', 'plouc', 'ploucs', 'péquenaud', 'pequenaud', 'péquenauds', 'pequenauds', 'cul-terreux', 'cul terreux', 'bouseux', 'boueuse', 'bouseux', 'bouseuses', 'bâtard', 'batard', 'bâtards', 'batards', 'bâtarde', 'batarde', 'bâtardes', 'batardes', 'salopard', 'salopards', 'saloparde', 'salopardes', 'fumier', 'fumiers', 'ordure', 'ordures', 'pourriture', 'pourritures', 'charogne', 'charognes', 'raclure', 'raclures', 'déchet', 'dechet', 'déchets', 'dechets', 'rebut', 'rebuts', 'lie', 'lies', 'écume', 'ecume', 'écumes', 'ecumes', 'fange', 'fanges', 'boue', 'boues', 'vase', 'vases', 'crotte', 'crottes', 'étron', 'etron', 'étrons', 'etrons', 'caca', 'cacas', 'bouse', 'bouses', 'fiente', 'fientes', 'colombin', 'colombins', 'boudin', 'boudins', 'saucisse', 'saucisses', 'andouille', 'andouilles', 'crétin', 'cretin', 'crétins', 'cretins', 'crétine', 'cretine', 'crétines', 'cretines', 'débile', 'debile', 'débiles', 'debiles', 'attardé', 'attarde', 'attardés', 'attardes', 'attardée', 'attardee', 'attardées', 'attardees', 'demeuré', 'demeure', 'demeurés', 'demeures', 'demeurée', 'demeuree', 'demeurées', 'demeurees', 'simple', 'simples', 'idiot', 'idiots', 'idiote', 'idiotes', 'imbécile', 'imbecile', 'imbéciles', 'imbeciles', 'stupide', 'stupides', 'bête', 'bete', 'bêtes', 'betes', 'sot', 'sots', 'sotte', 'sottes', 'niais', 'niaise', 'niaises', 'nigaud', 'nigauds', 'nigaude', 'nigaudes', 'benêt', 'benet', 'benêts', 'benets', 'benête', 'benete', 'benêtes', 'benetes', 'ballot', 'ballots', 'ballotte', 'ballottes', 'balourd', 'balourds', 'balourde', 'balourdes', 'lourdaud', 'lourdauds', 'lourdaude', 'lourdaudes', 'abruti', 'abrutis', 'abrutie', 'abruties', 'bourrique', 'bourriques', 'âne', 'ane', 'ânes', 'anes', 'ânesse', 'anesse', 'ânesses', 'anesses', 'baudet', 'baudets', 'bourricot', 'bourricots', 'gourde', 'gourdes', 'cornichon', 'cornichons', 'navet', 'navets', 'nouille', 'nouilles', 'patate', 'patates', 'buse', 'buses', 'dinde', 'dindes', 'dindon', 'dindons', 'oie', 'oies', 'jars', 'bécasse', 'becasse', 'bécasses', 'becasses', 'bécassine', 'becassine', 'bécassines', 'becassines', 'poule', 'poules', 'poulet', 'poulets', 'coquette', 'coquettes', 'coq', 'coqs', 'chapon', 'chapons', 'poularde', 'poulardes', 'poussin', 'poussins', 'poussinière', 'poussiniere', 'poussinières', 'poussinieres', 'cochon', 'cochons', 'cochonne', 'cochonnes', 'porc', 'porcs', 'truie', 'truies', 'pourceau', 'pourceaux', 'goret', 'gorets', 'cochonnet', 'cochonnets', 'cochonnaille', 'cochonnailles', 'verrat', 'verrats', 'bauge', 'bauges', 'porcherie', 'porcheries', 'étable', 'etable', 'étables', 'etables', 'écurie', 'ecurie', 'écuries', 'ecuries', 'box', 'boxs', 'stalle', 'stalles', 'enclos', 'clos', 'parc', 'parcs', 'paddock', 'paddocks', 'pâturage', 'paturage', 'pâturages', 'paturages', 'prairie', 'prairies', 'pré', 'pre', 'prés', 'pres', 'herbage', 'herbages', 'pacage', 'pacages', 'pâture', 'pature', 'pâtures', 'patures', 'fourrage', 'fourrages', 'foin', 'foins', 'paille', 'pailles', 'litière', 'litiere', 'litières', 'litieres', 'fumier', 'fumiers', 'purin', 'purins', 'lisier', 'lisiers', 'compost', 'composts', 'engrais', 'fertilisant', 'fertilisants', 'amendement', 'amendements', 'terreau', 'terreaux', 'humus', 'tourbe', 'tourbes', 'mousse', 'mousses', 'lichen', 'lichens', 'algue', 'algues', 'varech', 'varechs', 'goémon', 'goemon', 'goémons', 'goemons', 'sargasse', 'sargasses', 'zostère', 'zostere', 'zostères', 'zosteres', 'laminaire', 'laminaires', 'fucus', 'ulve', 'ulves', 'spiruline', 'spirulines', 'chlorelle', 'chlorelles', 'microalgue', 'microalgues', 'phytoplancton', 'phytoplanctons', 'zooplancton', 'zooplanctons', 'plancton', 'planctons', 'krill', 'krills', 'copépode', 'copepode', 'copépodes', 'copepodes', 'rotifère', 'rotifere', 'rotifères', 'rotiferes', 'protozoaire', 'protozoaires', 'paramècie', 'paramecie', 'paramécies', 'paramecies', 'amibe', 'amibes', 'euglène', 'euglene', 'euglènes', 'euglenes', 'volvox', 'hydre', 'hydres', 'méduse', 'meduse', 'méduses', 'meduses', 'polype', 'polypes', 'corail', 'coraux', 'anémone', 'anemone', 'anémones', 'anemones', 'actinie', 'actinies', 'éponge', 'eponge', 'éponges', 'eponges', 'spongieux', 'spongieuse', 'spongieuses', 'poreux', 'poreuse', 'poreuses', 'alvéolé', 'alveole', 'alvéolés', 'alveoles', 'alvéolée', 'alveolee', 'alvéolées', 'alveolees', 'cellulaire', 'cellulaires', 'cellule', 'cellules', 'cytoplasme', 'cytoplasmes', 'noyau', 'noyaux', 'nucléole', 'nucleole', 'nucléoles', 'nucleoles', 'chromosome', 'chromosomes', 'chromatine', 'chromatines', 'gène', 'gene', 'gènes', 'genes', 'génome', 'genome', 'génomes', 'genomes', 'génétique', 'genetique', 'génétiques', 'genetiques', 'héréditaire', 'hereditaire', 'héréditaires', 'hereditaires', 'hérédité', 'heredite', 'hérédités', 'heredites', 'descendance', 'descendances', 'progéniture', 'progeniture', 'progénitures', 'progenitures', 'postérité', 'posterite', 'postérités', 'posterites', 'lignée', 'lignee', 'lignées', 'lignees', 'dynastie', 'dynasties', 'famille', 'familles', 'clan', 'clans', 'tribu', 'tribus', 'peuplade', 'peuplades', 'ethnie', 'ethnies', 'race', 'races', 'espèce', 'espece', 'espèces', 'especes', 'genre', 'genres', 'variété', 'variete', 'variétés', 'varietes', 'sous-espèce', 'sous-espece', 'sous-espèces', 'sous-especes', 'subspecies', 'sous-variété', 'sous-variete', 'sous-variétés', 'sous-varietes', 'cultivar', 'cultivars', 'hybride', 'hybrides', 'croisement', 'croisements', 'métissage', 'metissage', 'métissages', 'metissages', 'brassage', 'brassages', 'mélange', 'melange', 'mélanges', 'melanges', 'mixture', 'mixtures', 'composition', 'compositions', 'formule', 'formules', 'recette', 'recettes', 'procédé', 'procede', 'procédés', 'procedes', 'méthode', 'methode', 'méthodes', 'methodes', 'technique', 'techniques', 'procédure', 'procedure', 'procédures', 'procedures', 'protocole', 'protocoles', 'marche', 'marches', 'démarche', 'demarche', 'démarches', 'demarches', 'approche', 'approches', 'façon', 'facon', 'façons', 'facons', 'manière', 'maniere', 'manières', 'manieres', 'mode', 'modes', 'modalité', 'modalite', 'modalités', 'modalites', 'moyen', 'moyens', 'outil', 'outils', 'instrument', 'instruments', 'ustensile', 'ustensiles', 'appareil', 'appareils', 'dispositif', 'dispositifs', 'mécanisme', 'mecanisme', 'mécanismes', 'mecanismes', 'machine', 'machines', 'engin', 'engins', 'équipement', 'equipement', 'équipements', 'equipements', 'matériel', 'materiel', 'matériels', 'materiels', 'outillage', 'outillages', 'machinerie', 'machineries', 'mécanique', 'mecanique', 'mécaniques', 'mecaniques', 'automatique', 'automatiques', 'électrique', 'electrique', 'électriques', 'electriques', 'électronique', 'electronique', 'électroniques', 'electroniques', 'numérique', 'numerique', 'numériques', 'numeriques', 'digital', 'digitaux', 'digitale', 'digitales', 'informatique', 'informatiques', 'ordinateur', 'ordinateurs', 'computer', 'computers', 'pc', 'pcs', 'micro', 'micros', 'portable', 'portables', 'laptop', 'laptops', 'tablette', 'tablettes', 'smartphone', 'smartphones', 'téléphone', 'telephone', 'téléphones', 'telephones', 'mobile', 'mobiles', 'cellulaire', 'cellulaires', 'sans-fil', 'sans fil', 'wifi', 'bluetooth', 'internet', 'web', 'site', 'sites', 'page', 'pages', 'lien', 'liens', 'url', 'urls', 'adresse', 'adresses', 'email', 'emails', 'courriel', 'courriels', 'message', 'messages', 'texto', 'textos', 'sms', 'mms', 'chat', 'chats', 'forum', 'forums', 'blog', 'blogs', 'réseau', 'reseau', 'réseaux', 'reseaux', 'social', 'sociaux', 'sociale', 'sociales', 'facebook', 'twitter', 'instagram', 'linkedin', 'youtube', 'google', 'yahoo', 'bing', 'moteur', 'moteurs', 'recherche', 'recherches', 'requête', 'requete', 'requêtes', 'requetes', 'base', 'bases', 'donnée', 'donnee', 'données', 'donnees', 'information', 'informations', 'renseignement', 'renseignements', 'détail', 'detail', 'détails', 'details', 'précision', 'precision', 'précisions', 'precisions', 'exactitude', 'exactitudes', 'justesse', 'justesses', 'vérité', 'verite', 'vérités', 'verites', 'réalité', 'realite', 'réalités', 'realites', 'fait', 'faits', 'élément', 'element', 'éléments', 'elements', 'composant', 'composants', 'composante', 'composantes', 'partie', 'parties', 'portion', 'portions', 'section', 'sections', 'segment', 'segments', 'fragment', 'fragments', 'morceau', 'morceaux', 'bout', 'bouts', 'extrémité', 'extremite', 'extrémités', 'extremites', 'pointe', 'pointes', 'sommet', 'sommets', 'pic', 'pics', 'cime', 'cimes', 'faîte', 'faite', 'faîtes', 'faites', 'crête', 'crete', 'crêtes', 'cretes', 'arête', 'arete', 'arêtes', 'aretes', 'angle', 'angles', 'coin', 'coins', 'recoin', 'recoins', 'recess', 'alcôve', 'alcove', 'alcôves', 'alcoves', 'niche', 'niches', 'anfractuosité', 'anfractuosite', 'anfractuosités', 'anfractuosites', 'cavité', 'cavite', 'cavités', 'cavites', 'trou', 'trous', 'creux', 'orifice', 'orifices', 'ouverture', 'ouvertures', 'fente', 'fentes', 'fissure', 'fissures', 'crevasse', 'crevasses', 'lézarde', 'lezarde', 'lézardes', 'lezardes', 'gerçure', 'gercure', 'gerçures', 'gercures', 'cassure', 'cassures', 'fracture', 'fractures', 'rupture', 'ruptures', 'brisure', 'brisures', 'félure', 'felure', 'félures', 'felures', 'brèche', 'breche', 'brèches', 'breches', 'trouée', 'trouee', 'trouées', 'trouees', 'percée', 'percee', 'percées', 'percees', 'passage', 'passages', 'couloir', 'couloirs', 'corridor', 'corridors', 'galerie', 'galeries', 'tunnel', 'tunnels', 'souterrain', 'souterrains', 'grotte', 'grottes', 'caverne', 'cavernes', 'antre', 'antres', 'tanière', 'taniere', 'tanières', 'tanieres', 'gîte', 'gite', 'gîtes', 'gites', 'refuge', 'refuges', 'abri', 'abris', 'cachette', 'cachettes', 'planque', 'planques', 'repaire', 'repaires', 'retraite', 'retraites', 'ermitage', 'ermitages', 'solitude', 'solitudes', 'isolement', 'isolements', 'séparation', 'separation', 'séparations', 'separations', 'division', 'divisions', 'cloison', 'cloisons', 'paroi', 'parois', 'mur', 'murs', 'muraille', 'murailles', 'rempart', 'remparts', 'fortification', 'fortifications', 'défense', 'defense', 'défenses', 'defenses', 'protection', 'protections', 'blindage', 'blindages', 'cuirasse', 'cuirasses', 'armure', 'armures', 'bouclier', 'boucliers', 'écu', 'ecu', 'écus', 'ecus', 'pavois', 'rondache', 'rondaches', 'targe', 'targes', 'carapace', 'carapaces', 'coquille', 'coquilles', 'écaille', 'ecaille', 'écailles', 'ecailles', 'plaque', 'plaques', 'lame', 'lames', 'feuille', 'feuilles', 'pellicule', 'pellicules', 'membrane', 'membranes', 'tissu', 'tissus', 'étoffe', 'etoffe', 'étoffes', 'etoffes', 'textile', 'textiles', 'fibre', 'fibres', 'fil', 'fils', 'filament', 'filaments', 'brin', 'brins', 'corde', 'cordes', 'ficelle', 'ficelles', 'câble', 'cable', 'câbles', 'cables', 'chaîne', 'chaine', 'chaînes', 'chaines', 'maillon', 'maillons', 'anneau', 'anneaux', 'bague', 'bagues', 'alliance', 'alliances', 'jonc', 'joncs', 'chevalière', 'chevaliere', 'chevalières', 'chevalieres', 'solitaire', 'solitaires', 'diamant', 'diamants', 'pierre', 'pierres', 'gemme', 'gemmes', 'bijou', 'bijoux', 'joyau', 'joyaux', 'parure', 'parures', 'ornement', 'ornements', 'décoration', 'decoration', 'décorations', 'decorations', 'enjolivement', 'enjolivements', 'embellissement', 'embellissements', 'agrément', 'agrement', 'agréments', 'agrements', 'atour', 'atours', 'apparence', 'apparences', 'aspect', 'aspects', 'allure', 'allures', 'prestance', 'prestances', 'élégance', 'elegance', 'élégances', 'elegances', 'raffinement', 'raffinements', 'distinction', 'distinctions', 'classe', 'classes', 'style', 'styles', 'genre', 'genres', 'mode', 'modes', 'tendance', 'tendances', 'fashion', 'fashions', 'couture', 'coutures', 'prêt-à-porter', 'pret-a-porter', 'haute-couture', 'haute couture', 'confection', 'confections', 'vêtement', 'vetement', 'vêtements', 'vetements', 'habit', 'habits', 'tenue', 'tenues', 'costume', 'costumes', 'toilette', 'toilettes', 'mise', 'mises', 'accoutrement', 'accoutrements', 'harnachement', 'harnachements', 'équipement', 'equipement', 'équipements', 'equipements', 'attirail', 'attirails', 'matériel', 'materiel', 'matériels', 'materiels', 'outillage', 'outillages', 'arsenal', 'arsenaux', 'armement', 'armements', 'panoplie', 'panoplies', 'collection', 'collections', 'assortiment', 'assortiments', 'gamme', 'gammes', 'palette', 'palettes', 'éventail', 'eventail', 'éventails', 'eventails', 'choix', 'sélection', 'selection', 'sélections', 'selections', 'tri', 'tris', 'triage', 'triages', 'criblage', 'criblages', 'filtrage', 'filtrages', 'épuration', 'epuration', 'épurations', 'epurations', 'purification', 'purifications', 'assainissement', 'assainissements', 'nettoyage', 'nettoyages', 'lavage', 'lavages', 'rinçage', 'rincage', 'rinçages', 'rincages', 'lessivage', 'lessivages', 'blanchiment', 'blanchiments', 'dégraissage', 'degraissage', 'dégraissages', 'degraissages', 'détachage', 'detachage', 'détachages', 'detachages', 'décrassage', 'decrassage', 'décrassages', 'decrassages', 'récurage', 'recurage', 'récurages', 'recurages', 'frottage', 'frottages', 'brossage', 'brossages', 'polissage', 'polissages', 'lustrage', 'lustrages', 'cirage', 'cirages', 'encaustique', 'encaustiques', 'cire', 'cires', 'pommade', 'pommades', 'baume', 'baumes', 'crème', 'creme', 'crèmes', 'cremes', 'onguent', 'onguents', 'liniment', 'liniments', 'embrocation', 'embrocations', 'friction', 'frictions', 'massage', 'massages', 'pétrissage', 'petrissage', 'pétrissages', 'petrissages', 'malaxage', 'malaxages', 'manipulation', 'manipulations', 'maniement', 'maniements', 'manutention', 'manutentions', 'transport', 'transports', 'acheminement', 'acheminements', 'convoyage', 'convoyages', 'livraison', 'livraisons', 'distribution', 'distributions', 'répartition', 'repartition', 'répartitions', 'repartitions', 'partage', 'partages', 'division', 'divisions', 'séparation', 'separation', 'séparations', 'separations', 'scission', 'scissions', 'coupure', 'coupures', 'découpage', 'decoupage', 'découpages', 'decoupages', 'sectionnement', 'sectionnements', 'segmentation', 'segmentations', 'morcellement', 'morcellements', 'fragmentation', 'fragmentations', 'émiettement', 'emiettement', 'émiettements', 'emiettements', 'pulvérisation', 'pulverisation', 'pulvérisations', 'pulverisations', 'atomisation', 'atomisations', 'vaporisation', 'vaporisations', 'évaporation', 'evaporation', 'évaporations', 'evaporations', 'sublimation', 'sublimations', 'distillation', 'distillations', 'condensation', 'condensations', 'liquéfaction', 'liquefaction', 'liquéfactions', 'liquefactions', 'solidification', 'solidifications', 'cristallisation', 'cristallisations', 'congélation', 'congelation', 'congélations', 'congelations', 'gel', 'gels', 'glaciation', 'glaciations', 'refroidissement', 'refroidissements', 'réfrigération', 'refrigeration', 'réfrigérations', 'refrigerations', 'zut', 'punaise', ], 'false_positives' => [ // Common French words that might be detected as false positives 'analyse', 'analyses', 'classe', 'classes', 'passer', 'passage', 'passages', 'expression', 'expressions', 'assassin', 'assassins', 'assassiner', 'assassinat', 'assassinats', 'entreprise', 'entreprises', 'entrepreneur', 'entrepreneurs', 'affaire', 'affaires', 'travail', 'travaux', 'travailler', 'travailleur', 'travailleurs', 'travailleuse', 'travailleuses', 'emploi', 'emplois', 'employé', 'employe', 'employés', 'employes', 'employée', 'employee', 'employées', 'employees', 'employeur', 'employeurs', 'bureau', 'bureaux', 'ordinateur', 'ordinateurs', 'machine', 'machines', 'appareil', 'appareils', 'dispositif', 'dispositifs', 'instrument', 'instruments', 'outil', 'outils', 'utilité', 'utilites', 'fonction', 'fonctions', 'fonctionner', 'fonctionnement', 'fonctionnements', 'caractéristique', 'caracteristique', 'caractéristiques', 'caracteristiques', 'spécialité', 'specialite', 'spécialités', 'specialites', 'spécialiste', 'specialiste', 'spécialistes', 'specialistes', 'spécialiser', 'specialiser', 'spécialisé', 'specialise', 'spécialisée', 'specialisee', 'spécialisés', 'specialises', 'spécialisées', 'specialisees', 'spécialisation', 'specialisation', 'spécialisations', 'specialisations', 'professionnel', 'professionnels', 'professionnelle', 'professionnelles', 'profession', 'professions', 'professeur', 'professeurs', 'enseigner', 'enseignement', 'enseignements', 'éducation', 'education', 'éducatif', 'educatif', 'éducative', 'educative', 'éducatifs', 'educatifs', 'éducatives', 'educatives', 'éduquer', 'eduquer', 'éduqué', 'eduque', 'éduquée', 'eduquee', 'éduqués', 'eduques', 'éduquées', 'eduquees', 'éducateur', 'educateur', 'éducateurs', 'educateurs', 'éducatrice', 'educatrice', 'éducatrices', 'educatrices', 'étudiant', 'etudiant', 'étudiants', 'etudiants', 'étudiante', 'etudiante', 'étudiantes', 'etudiantes', 'étudier', 'etudier', 'étude', 'etude', 'études', 'etudes', 'étudié', 'etudie', 'étudiée', 'etudiee', 'étudiés', 'etudies', 'étudiées', 'etudiees', 'recherche', 'recherches', 'rechercher', 'chercheur', 'chercheurs', 'chercheuse', 'chercheuses', 'scientifique', 'scientifiques', 'science', 'sciences', 'connaissance', 'connaissances', 'connaître', 'connaitre', 'connu', 'connue', 'connus', 'connues', 'savoir', 'savoirs', 'su', 'sue', 'sus', 'sues', 'sagesse', 'sagesses', 'sage', 'sages', 'intelligence', 'intelligences', 'intelligent', 'intelligents', 'intelligente', 'intelligentes', 'talent', 'talents', 'talentueux', 'talentueuse', 'talentueuses', 'habileté', 'habilete', 'habiletés', 'habiletes', 'habile', 'habiles', 'adresse', 'adresses', 'adroit', 'adroits', 'adroite', 'adroites', 'maître', 'maitre', 'maîtres', 'maitres', 'maîtresse', 'maitresse', 'maîtresses', 'maitresses', 'maîtrise', 'maitrise', 'maîtrises', 'maitrises', 'maîtriser', 'maitriser', 'maîtrisé', 'maitrise', 'maîtrisée', 'maitrisee', 'maîtrisés', 'maitrises', 'maîtrisées', 'maitrisees', 'domaine', 'domaines', 'dominer', 'dominé', 'domine', 'dominée', 'dominee', 'dominés', 'domines', 'dominées', 'dominees', 'contrôle', 'controle', 'contrôles', 'controles', 'contrôler', 'controler', 'contrôlé', 'controle', 'contrôlée', 'controlee', 'contrôlés', 'controles', 'contrôlées', 'controlees', 'administration', 'administrations', 'administrer', 'administrateur', 'administrateurs', 'administratrice', 'administratrices', 'gestion', 'gestions', 'gérer', 'gerer', 'géré', 'gere', 'gérée', 'geree', 'gérés', 'geres', 'gérées', 'gerees', 'gestionnaire', 'gestionnaires', 'organisation', 'organisations', 'organiser', 'organisé', 'organise', 'organisée', 'organisee', 'organisés', 'organises', 'organisées', 'organisees', 'organisateur', 'organisateurs', 'organisatrice', 'organisatrices', 'système', 'systeme', 'systèmes', 'systemes', 'systématique', 'systematique', 'systématiques', 'systematiques', 'méthode', 'methode', 'méthodes', 'methodes', 'méthodologie', 'methodologie', 'méthodologies', 'methodologies', 'processus', 'traiter', 'traité', 'traite', 'traitée', 'traitee', 'traités', 'traites', 'traitées', 'traitees', 'procédure', 'procedure', 'procédures', 'procedures', 'procéder', 'proceder', 'protocole', 'protocoles', 'norme', 'normes', 'normal', 'normaux', 'normale', 'normales', 'normalité', 'normalite', 'normalités', 'normalites', 'normaliser', 'normalisé', 'normalise', 'normalisée', 'normalisee', 'normalisés', 'normalises', 'normalisées', 'normalisees', 'standard', 'standards', 'standardiser', 'standardisé', 'standardise', 'standardisée', 'standardisee', 'standardisés', 'standardises', 'standardisées', 'standardisees', 'règle', 'regle', 'règles', 'regles', 'règlement', 'reglement', 'règlements', 'reglements', 'réglementer', 'reglementer', 'réglementaire', 'reglementaire', 'réglementaires', 'reglementaires', 'réguler', 'reguler', 'régulier', 'regulier', 'réguliers', 'reguliers', 'régulière', 'reguliere', 'régulières', 'regulieres', 'régularité', 'regularite', 'régularités', 'regularites', 'régulariser', 'regulariser', 'régularisé', 'regularise', 'régularisée', 'regularisee', 'régularisés', 'regularises', 'régularisées', 'regularisees', 'loi', 'lois', 'légal', 'legal', 'légaux', 'legaux', 'légale', 'legale', 'légales', 'legales', 'légalité', 'legalite', 'légalités', 'legalites', 'légaliser', 'legaliser', 'légalisé', 'legalise', 'légalisée', 'legalisee', 'légalisés', 'legalises', 'légalisées', 'legalisees', 'droit', 'droits', 'juridique', 'juridiques', 'justice', 'justices', 'juste', 'justes', 'injuste', 'injustes', 'injustice', 'injustices', 'tribunal', 'tribunaux', 'juge', 'juges', 'jugement', 'jugements', 'juger', 'jugé', 'juge', 'jugée', 'jugee', 'jugés', 'juges', 'jugées', 'jugees', 'sentence', 'sentences', 'condamnation', 'condamnations', 'condamner', 'condamné', 'condamne', 'condamnée', 'condamnee', 'condamnés', 'condamnes', 'condamnées', 'condamnees', 'punition', 'punitions', 'punir', 'puni', 'punie', 'punis', 'punies', 'peine', 'peines', 'prison', 'prisons', 'emprisonner', 'emprisonné', 'emprisonne', 'emprisonnée', 'emprisonnee', 'emprisonnés', 'emprisonnes', 'emprisonnées', 'emprisonnees', 'prisonnier', 'prisonniers', 'prisonnière', 'prisonniere', 'prisonnières', 'prisonnieres', 'détenu', 'detenu', 'détenus', 'detenus', 'détenue', 'detenue', 'détenues', 'detenues', 'pénitencier', 'penitencier', 'pénitenciers', 'penitenciers', 'maison', 'maisons', 'arrêt', 'arret', 'arrêts', 'arrets', ], 'substitutions' => [ '/à/' => ['à', 'a', '@', '4'], '/â/' => ['â', 'a', '@', '4'], '/ä/' => ['ä', 'a', '@', '4'], '/á/' => ['á', 'a', '@', '4'], '/ã/' => ['ã', 'a', '@', '4'], '/å/' => ['å', 'a', '@', '4'], '/æ/' => ['æ', 'ae', 'a'], '/è/' => ['è', 'e', '3', '€'], '/é/' => ['é', 'e', '3', '€'], '/ê/' => ['ê', 'e', '3', '€'], '/ë/' => ['ë', 'e', '3', '€'], '/ì/' => ['ì', 'i', '1', '!', '|'], '/í/' => ['í', 'i', '1', '!', '|'], '/î/' => ['î', 'i', '1', '!', '|'], '/ï/' => ['ï', 'i', '1', '!', '|'], '/ò/' => ['ò', 'o', '0', 'ø'], '/ó/' => ['ó', 'o', '0', 'ø'], '/ô/' => ['ô', 'o', '0', 'ø'], '/ö/' => ['ö', 'o', '0', 'ø'], '/õ/' => ['õ', 'o', '0', 'ø'], '/ø/' => ['ø', 'o', '0'], '/œ/' => ['œ', 'oe', 'o'], '/ù/' => ['ù', 'u', 'ü'], '/ú/' => ['ú', 'u', 'ü'], '/û/' => ['û', 'u', 'ü'], '/ü/' => ['ü', 'u', 'ù'], '/u/' => ['u', 'ù', 'ú', 'û', 'ü', '@', '*'], '/ÿ/' => ['ÿ', 'y', 'i'], '/ç/' => ['ç', 'c', 's'], '/ñ/' => ['ñ', 'n', '~n'], '/c/' => ['c', 'k', 'ç', 's'], '/k/' => ['k', 'c', 'q'], '/ph/' => ['ph', 'f'], '/qu/' => ['qu', 'k', 'q'], '/x/' => ['x', 'ks', 'gs'], '/z/' => ['z', 's'], '/j/' => ['j', 'g'], '/g/' => ['g', 'j'], ] ]; ================================================ FILE: config/languages/german.php ================================================ [ 'mild' => [ 'mist', 'kacke', 'verdammt', 'verdammte', 'verdammter', 'verdammtes', 'blöd', 'bloed', 'blöde', 'bloede', 'blöder', 'bloeder', 'blödes', 'bloedes', 'doof', 'doofe', 'doofer', 'doofes', 'dumm', 'dumme', 'dummer', 'dummes', 'albern', 'alberne', 'alberner', 'albernes', 'peinlich', 'peinliche', 'peinlicher', 'peinliches', ], 'moderate' => [ 'arsch', 'arschloch', 'arschlöcher', 'arschlocher', 'schlampe', 'nutte', 'hure', 'wichser', 'depp', 'trottel', 'idiot', 'vollidiot', 'bescheuert', 'bescheuerte', 'bescheuerter', 'bescheuertes', 'bekloppt', 'bekloppte', 'bekloppter', 'beklopptes', 'schwanz', 'pimmel', 'hintern', 'po', 'popo', 'schwul', 'schwuler', 'schwule', 'schwules', ], 'high' => [ 'scheiße', 'scheisse', 'ficken', 'fick', 'gefickt', 'verfickt', 'fotze', 'muschi', 'möse', 'moese', 'hurensohn', 'hurenkind', 'arschficker', 'vögeln', 'voegeln', 'bumsen', ], 'extreme' => [ 'tunte', 'tuntig', 'kampflesbe', 'kampflesben', 'kanake', 'kanaken', 'neger', 'negerin', 'zigeuner', 'zigeunerin', 'retardiert', 'retardierte', 'retardierter', ], ], 'profanities' => [ // Common German profanities and vulgar expressions 'scheiße', 'scheisse', 'scheiß', 'scheiss', 'kacke', 'mist', 'arsch', 'arschloch', 'arschlöcher', 'arschlocher', 'ficken', 'fick', 'gefickt', 'verfickt', 'verfickte', 'verfickter', 'verficktes', 'verdammt', 'verdammte', 'verdammter', 'verdammtes', 'hurensohn', 'hurenkind', 'hure', 'nutte', 'schlampe', 'fotze', 'muschi', 'möse', 'moese', 'schwanz', 'pimmel', 'dödel', 'doedel', 'lümmel', 'luemmel', 'rute', 'zipfel', 'glied', 'eier', 'hoden', 'klöten', 'kloeten', 'sack', 'hodensack', 'nüsse', 'nuesse', 'kugeln', 'beutel', 'titten', 'brüste', 'brueste', 'busen', 'möpse', 'moepse', 'hupen', 'vorbau', 'körbchen', 'koerbchen', 'milchdrüsen', 'milchdruesen', 'warzen', 'nippel', 'brustwarzen', 'zitzen', 'hintern', 'po', 'popo', 'gesäß', 'gesaess', 'kehrseite', 'vier buchstaben', 'allerwertester', 'rückseite', 'rueckseite', 'backen', 'pobacken', 'arschbacken', 'speck', 'hinterteil', 'schwul', 'schwuler', 'schwule', 'schwules', 'homo', 'homos', 'homosexuell', 'homosexuelle', 'homosexueller', 'homosexuelles', 'tuntig', 'tunte', 'warm', 'warmer', 'warme', 'warmes', 'lesbe', 'lesben', 'lesbisch', 'lesbische', 'lesbischer', 'lesbisches', 'kampflesbe', 'kampflesben', 'butze', 'butzen', 'wichser', 'wichsen', 'wichst', 'gewichst', 'onanieren', 'onaniert', 'masturbieren', 'masturbiert', 'selbstbefriedigung', 'handjob', 'blasen', 'bläst', 'blaest', 'blowjob', 'oral', 'lecken', 'leckt', 'geleckt', 'cunnilingus', 'fellatio', 'lutschen', 'lutscht', 'gelutscht', 'saugen', 'saugt', 'gesaugt', 'pusten', 'pustet', 'gepustet', 'vögeln', 'voegeln', 'vögelt', 'voegelt', 'gevögelt', 'gevoegelt', 'bumsen', 'bumst', 'gebumst', 'poppen', 'poppt', 'gepoppt', 'knallen', 'knallt', 'geknallt', 'nageln', 'nagelt', 'genagelt', 'rammeln', 'rammelt', 'gerammelt', 'durchnageln', 'durchnagelt', 'durchgenagelt', 'durchficken', 'durchfickt', 'durchgefickt', 'rannehmen', 'rannimmt', 'rangenommen', 'besteigen', 'besteigt', 'bestiegen', 'bespringen', 'bespringt', 'besprungen', 'penis', 'vagina', 'vulva', 'klitoris', 'kitzler', 'schamlippen', 'venushügel', 'venushuegel', 'scham', 'geschlecht', 'geschlechtsteil', 'geschlechtsteile', 'genitalien', 'intimbereich', 'unterkörper', 'unterkoerper', 'lenden', 'lendengegend', 'schritt', 'schrittbereich', 'unterleib', 'becken', 'beckenboden', 'damm', 'perineum', 'anus', 'after', 'poloch', 'arschloch', 'rosette', 'poperze', 'hintertür', 'hintertuer', 'ausgang', 'darmausgang', 'enddarm', 'rektum', 'mastdarm', 'analbereich', 'afterbereich', 'hinterlader', 'arschficker', 'arschficken', 'arschgefickt', 'analverkehr', 'analsex', 'sodomie', 'sodomist', 'sodomistin', 'pervers', 'perverse', 'perverser', 'perverses', 'perversling', 'pervertiert', 'pervertierte', 'pervertierter', 'pervertiertes', 'versaut', 'versaute', 'versauter', 'versautes', 'schmutzig', 'schmutzige', 'schmutziger', 'schmutziges', 'dreckig', 'dreckige', 'dreckiger', 'dreckiges', 'dreck', 'unrat', 'abschaum', 'pack', 'gesindel', 'pöbel', 'poebel', 'mob', 'kanaille', 'lumpen', 'lump', 'schuft', 'schurke', 'halunke', 'gauner', 'ganove', 'gangster', 'verbrecher', 'kriminell', 'kriminelle', 'krimineller', 'kriminelles', 'asozial', 'asoziale', 'asozialer', 'asoziales', 'asi', 'prollig', 'prollige', 'prolliger', 'prolliges', 'proll', 'prolet', 'unterschicht', 'prekariat', 'hartz', 'hartzer', 'arbeitslos', 'arbeitslose', 'arbeitsloser', 'arbeitsloses', 'sozialhilfe', 'sozialschmarotzer', 'schmarotzer', 'parasit', 'parasiten', 'ungeziefer', 'schädling', 'schaedling', 'schädlinge', 'schaedlinge', 'plage', 'pest', 'seuche', 'krankheit', 'leiden', 'übel', 'uebel', 'böse', 'boese', 'schlecht', 'schlimm', 'schrecklich', 'furchtbar', 'entsetzlich', 'grauenhaft', 'grausam', 'brutal', 'roh', 'primitiv', 'primitive', 'primitiver', 'primitives', 'barbarisch', 'barbarische', 'barbarischer', 'barbarisches', 'wild', 'wilde', 'wilder', 'wildes', 'ungezähmt', 'ungebildet', 'ungebildete', 'ungebildeter', 'ungebildetes', 'dumm', 'dumme', 'dummer', 'dummes', 'doof', 'doofe', 'doofer', 'doofes', 'blöd', 'bloed', 'blöde', 'bloede', 'blöder', 'bloeder', 'blödes', 'bloedes', 'bescheuert', 'bescheuerte', 'bescheuerter', 'bescheuertes', 'bekloppt', 'bekloppte', 'bekloppter', 'beklopptes', 'verrückt', 'verrueckt', 'verrückte', 'verrueckte', 'verrückter', 'verrueckter', 'verrücktes', 'verruecktes', 'irre', 'irrer', 'irres', 'wahnsinnig', 'wahnsinnige', 'wahnsinniger', 'wahnsinniges', 'gestört', 'gestoert', 'gestörte', 'gestoerte', 'gestörter', 'gestoerter', 'gestörtes', 'gestoertes', 'krank', 'kranke', 'kranker', 'krankes', 'pathologisch', 'pathologische', 'pathologischer', 'pathologisches', 'abnormal', 'abnormale', 'abnormaler', 'abnormales', 'unnormal', 'unnormale', 'unnormaler', 'unnormales', 'abartig', 'abartige', 'abartiger', 'abartiges', 'widerlich', 'widerliche', 'widerlicher', 'widerliches', 'ekelhaft', 'ekelhafte', 'ekelhafter', 'ekelhaftes', 'eklig', 'eklige', 'ekliger', 'ekliges', 'widerwertig', 'widerwertige', 'widerwertiger', 'widerwertiges', 'abstoßend', 'abstossend', 'abstoßende', 'abstossende', 'abstoßender', 'abstossender', 'abstoßendes', 'abstossendes', 'absurd', 'absurde', 'absurder', 'absurdes', 'lächerlich', 'laecherlich', 'lächerliche', 'laecherliche', 'lächerlicher', 'laecherlicher', 'lächerliches', 'laecherliches', 'albern', 'alberne', 'alberner', 'albernes', 'affig', 'affige', 'affiger', 'affiges', 'närrisch', 'naerrisch', 'närrische', 'naerrische', 'närrischer', 'naerrischer', 'närrisches', 'naerrisches', 'töricht', 'toericht', 'törichte', 'toerichte', 'törichter', 'toerichter', 'törichtes', 'toerichtes', 'blamabel', 'blamable', 'blamabler', 'blamables', 'peinlich', 'peinliche', 'peinlicher', 'peinliches', 'beschämend', 'beschaemend', 'beschämende', 'beschaemende', 'beschämender', 'beschaemender', 'beschämendes', 'beschaemendes', 'schmählich', 'schmaehlich', 'schmähliche', 'schmaehliche', 'schmählicher', 'schmaehlicher', 'schmähliches', 'schmaehliches', 'schändlich', 'schaendlich', 'schändliche', 'schaendliche', 'schändlicher', 'schaendlicher', 'schändliches', 'schaendliches', 'gemein', 'gemeine', 'gemeiner', 'gemeines', 'niederträchtig', 'niedertraechtig', 'niederträchtige', 'niedertraechtige', 'niederträchtiger', 'niedertraechtiger', 'niederträchtiges', 'niedertraechtiges', 'hinterhältig', 'hinterhaeltig', 'hinterhältige', 'hinterhaeltige', 'hinterhältiger', 'hinterhaeltiger', 'hinterhältiges', 'hinterhaeltiges', 'heimtückisch', 'heimtueckisch', 'heimtückische', 'heimtueckische', 'heimtückischer', 'heimtueckischer', 'heimtückisches', 'heimtueckisches', 'falsch', 'falsche', 'falscher', 'falsches', 'verlogen', 'verlogene', 'verlogener', 'verlogenes', 'heuchlerisch', 'heuchlerische', 'heuchlerischer', 'heuchlerisches', 'scheinheilig', 'scheinheilige', 'scheinheiliger', 'scheinheiliges', 'doppelzüngig', 'doppelzuengig', 'doppelzüngige', 'doppelzuengige', 'doppelzüngiger', 'doppelzuengiger', 'doppelzüngiges', 'doppelzuengiges', 'verlogen', 'verlogene', 'verlogener', 'verlogenes', 'unaufrichtig', 'unaufrichtige', 'unaufrichtiger', 'unaufrichtiges', 'unehrlich', 'unehrliche', 'unehrlicher', 'unehrliches', 'betrügerisch', 'betruegerisch', 'betrügerische', 'betruegerische', 'betrügerischer', 'betruegerischer', 'betrügerisches', 'betruegerisches', 'schwindelhaft', 'schwindlerisch', 'schwindlerische', 'schwindlerischer', 'schwindlerisches', 'unredlich', 'unredliche', 'unredlicher', 'unredliches', 'unlauter', 'unlautere', 'unlauterer', 'unlauteres', 'unseriös', 'unserioes', 'unseriöse', 'unserioes', 'unseriöser', 'unserioeser', 'unseriöses', 'unserioes', 'dubios', 'dubiose', 'dubioser', 'dubioses', 'fragwürdig', 'fragwuerdig', 'fragwürdige', 'fragwuerdige', 'fragwürdiger', 'fragwuerdiger', 'fragwürdiges', 'fragwuerdiges', 'zweifelhaft', 'zweifelhafte', 'zweifelhafter', 'zweifelhaftes', 'suspekt', 'suspekte', 'suspekter', 'suspektes', 'verdächtig', 'verdaechtig', 'verdächtige', 'verdaechtige', 'verdächtiger', 'verdaechtiger', 'verdächtiges', 'verdaechtiges', 'obskur', 'obskure', 'obskurer', 'obskures', 'dunkel', 'dunkle', 'dunkler', 'dunkles', 'finster', 'finstere', 'finsterer', 'finsteres', 'schwarz', 'schwarze', 'schwarzer', 'schwarzes', 'düster', 'duester', 'düstere', 'duestere', 'düsterer', 'duesterer', 'düsteres', 'duesteres', 'trüb', 'trueb', 'trübe', 'truebe', 'trüber', 'trueber', 'trübes', 'truebes', 'matt', 'matte', 'matter', 'mattes', 'fahl', 'fahle', 'fahler', 'fahles', 'blass', 'blasse', 'blasser', 'blasses', 'bleich', 'bleiche', 'bleicher', 'bleiches', 'käsig', 'kaesig', 'käsige', 'kaesige', 'käsiger', 'kaesiger', 'käsiges', 'kaesiges', 'kränklich', 'kraenklich', 'kränkliche', 'kraenkliche', 'kränklicher', 'kraenklicher', 'kränkliches', 'kraenkliches', 'schwächlich', 'schwaechlich', 'schwächliche', 'schwaechliche', 'schwächlicher', 'schwaechlicher', 'schwächliches', 'schwaechliches', 'schwach', 'schwache', 'schwacher', 'schwaches', 'kraftlos', 'kraftlose', 'kraftloser', 'kraftloses', 'energielos', 'energielose', 'energieloser', 'energieloses', 'müde', 'muede', 'müder', 'mueder', 'müdes', 'muedes', 'erschöpft', 'erschoepft', 'erschöpfte', 'erschoepfte', 'erschöpfter', 'erschoepfter', 'erschöpftes', 'erschoepftes', 'ausgepowert', 'ausgepowerte', 'ausgepower', 'ausgepowertes', 'kaputt', 'kaputte', 'kaputter', 'kaputtes', 'defekt', 'defekte', 'defekter', 'defektes', 'hinüber', 'hinueber', 'im arsch', 'futsch', 'dahin', 'ruiniert', 'ruinierte', 'ruinierter', 'ruiniertes', 'zerstört', 'zerstoert', 'zerstörte', 'zerstoerte', 'zerstörter', 'zerstoerter', 'zerstörtes', 'zerstoertes', 'zerbrochen', 'zerbrochene', 'zerbrochener', 'zerbrochenes', 'zerschmettert', 'zerschmetterte', 'zerschmetterter', 'zerschmettertes', 'demoliert', 'demolierte', 'demolierter', 'demoliertes', 'vernichtet', 'vernichtete', 'vernichteter', 'vernichtetes', 'ausgelöscht', 'ausgeloescht', 'ausgelöschte', 'ausgeloeschte', 'ausgelöschter', 'ausgeloeschter', 'ausgelöschtes', 'ausgeloeschtes', 'eliminiert', 'eliminierte', 'eliminierter', 'eliminiertes', 'getötet', 'getoetet', 'getötete', 'getoetete', 'getöteter', 'getoeteter', 'getötetes', 'getoetetes', 'umgebracht', 'umgebrachte', 'umgebrachter', 'umgebrachtes', 'ermordet', 'ermordete', 'ermordeter', 'ermordetes', 'hingerichtet', 'hingerichtete', 'hingerichteter', 'hingerichtetes', 'exekutiert', 'exekutierte', 'exekutierter', 'exekutiertes', 'liquidiert', 'liquidierte', 'liquidierter', 'liquidiertes', 'abgemurkst', 'abgemurks', 'abgemurkstes', 'kaltgemacht', 'kaltgemachte', 'kaltgemachter', 'kaltgemachtes', 'plattgemacht', 'plattgemachte', 'plattgemachter', 'plattgemachtes', 'fertiggemacht', 'fertiggemachte', 'fertiggemachter', 'fertiggemachtes', 'kaputtgemacht', 'kaputtgemachte', 'kaputtgemachter', 'kaputtgemachtes', 'totgemacht', 'totgemachte', 'totgemachter', 'totgemachtes', 'totgeschlagen', 'totgeschlagene', 'totgeschlagener', 'totgeschlagenes', 'totgeprügelt', 'totgeprügelte', 'totgeprügelter', 'totgeprügeltes', 'totgetrampelt', 'totgetrampelte', 'totgetrampelter', 'totgetrampe', 'totgefahren', 'totgefahrene', 'totgefahrener', 'totgefahrenes', 'überfahren', 'ueberfahren', 'überfahrene', 'ueberfahrene', 'überfahrener', 'ueberfahrener', 'überfahrenes', 'ueberfahrenes', 'totgefahren', 'totgefahrene', 'totgefahrener', 'totgefahrenes', 'erstickt', 'erstickte', 'erstickter', 'ersticktes', 'erwürgt', 'erwuergt', 'erwürgte', 'erwuergte', 'erwürgter', 'erwuergter', 'erwürgtes', 'erwuergtes', 'erdrosselt', 'erdrosselte', 'erdrosselter', 'erdrosseltes', 'stranguliert', 'strangulierte', 'strangulierter', 'stranguliertes', 'gehängt', 'gehaengt', 'gehängte', 'gehaengte', 'gehängter', 'gehaengter', 'gehängtes', 'gehaengtes', 'aufgehängt', 'aufgehaengt', 'aufgehängte', 'aufgehaengte', 'aufgehängter', 'aufgehaengter', 'aufgehängtes', 'aufgehaengtes', 'erhängt', 'erhaengt', 'erhängte', 'erhaengte', 'erhängter', 'erhaengter', 'erhängtes', 'erhaengtes', 'verbrannt', 'verbrannte', 'verbrannter', 'verbranntes', 'angezündet', 'angezuendet', 'angezündete', 'angezuendete', 'angezündeter', 'angezuendeter', 'angezündetes', 'angezuendetes', 'abgefackelt', 'abgefackelte', 'abgefackelter', 'abgefackeltes', 'niedergebrannt', 'niedergebrannte', 'niedergebrannter', 'niedergebranntes', 'eingeäschert', 'eingeaeschert', 'eingeäscherte', 'eingeaescherte', 'eingeäscherter', 'eingeaescherter', 'eingeäschertes', 'eingeaeschertes', 'verbrannt', 'verbrannte', 'verbrannter', 'verbranntes', 'verkohlt', 'verkohlte', 'verkohlter', 'verkohltes', 'verkocht', 'verkochte', 'verkochter', 'verkochtes', 'versotten', 'versottene', 'versottener', 'versottenes', 'versotten', 'versoffene', 'versoffener', 'versoffenes', 'besoffen', 'besoffene', 'besoffener', 'besoffenes', 'betrunken', 'betrunkene', 'betrunkener', 'betrunkenes', 'angetrunken', 'angetrunkene', 'angetrunkener', 'angetrunkenes', 'alkoholisiert', 'alkoholisierte', 'alkoholisierter', 'alkoholisiertes', 'breit', 'breite', 'breiter', 'breites', 'zu', 'zugedröhnt', 'zugedroehnt', 'zugedröhnte', 'zugedroehnte', 'zugedröhnter', 'zugedroeh', 'zugedröhntes', 'zugedroeh', 'dicht', 'dichte', 'dichter', 'dichtes', 'voll', 'volle', 'voller', 'volles', 'hinüber', 'hinueber', 'weggetreten', 'weggetretene', 'weggetretener', 'weggetretenes', 'weg', 'wege', 'weger', 'weges', 'drauf', 'high', 'highe', 'higher', 'highes', 'stoned', 'stoner', 'stones', 'bekifft', 'bekiffte', 'bekiffter', 'bekifftes', 'zugekifft', 'zugekiffte', 'zugekiffter', 'zugekifftes', 'zugeraucht', 'zugerauchte', 'zugerauchter', 'zugerauchtes', 'stramm', 'stramme', 'strammer', 'strammes', 'dicht', 'dichte', 'dichter', 'dichtes', 'platt', 'platte', 'platter', 'plattes', 'depp', 'trottel', 'idiot', 'vollidiot', 'kanake', 'kanaken', 'neger', 'negerin', 'zigeuner', 'zigeunerin', 'retardiert', 'retardierte', 'retardierter', ], 'false_positives' => [ // Common German words that might be detected as false positives 'analyse', 'analysen', 'analysieren', 'analysiert', 'analysierte', 'analysierter', 'analysiertes', 'klasse', 'klassen', 'klassisch', 'klassische', 'klassischer', 'klassisches', 'passen', 'passt', 'gepasst', 'passage', 'passagen', 'ausdruck', 'ausdrücke', 'ausdruecke', 'ausdrücklich', 'ausdruecklich', 'ausdrückliche', 'ausdrueckliche', 'ausdrücklicher', 'ausdruecklicher', 'ausdrückliches', 'ausdrueckliches', 'mörder', 'moerder', 'morden', 'mordet', 'gemordet', 'mord', 'morde', 'mordtat', 'mordtaten', 'unternehmen', 'unternehmens', 'unternehmung', 'unternehmungen', 'unternehmer', 'unternehmerin', 'geschäft', 'geschaeft', 'geschäfte', 'geschaefte', 'geschäftlich', 'geschaeftlich', 'geschäftliche', 'geschaeftliche', 'geschäftlicher', 'geschaeftlicher', 'geschäftliches', 'geschaeftliches', 'arbeit', 'arbeiten', 'arbeiter', 'arbeiterin', 'arbeiterinnen', 'arbeitsplatz', 'arbeitsplätze', 'arbeitsplaetze', 'anstellung', 'anstellungen', 'angestellt', 'angestellte', 'angestellter', 'angestelltes', 'arbeitgeber', 'arbeitgeberin', 'arbeitnehmer', 'arbeitnehmerin', 'büro', 'buero', 'büros', 'bueros', 'computer', 'computers', 'rechner', 'maschine', 'maschinen', 'maschinell', 'maschinelle', 'maschineller', 'maschinelles', 'gerät', 'geraet', 'geräte', 'geraete', 'apparat', 'apparate', 'vorrichtung', 'vorrichtungen', 'instrument', 'instrumente', 'werkzeug', 'werkzeuge', 'hilfsmittel', 'nutzen', 'nützen', 'nuetzen', 'nützlich', 'nuetzlich', 'nützliche', 'nuetzliche', 'nützlicher', 'nuetzlicher', 'nützliches', 'nuetzliches', 'funktion', 'funktionen', 'funktionieren', 'funktioniert', 'funktionierte', 'funktioniertes', 'eigenschaft', 'eigenschaften', 'charakteristikum', 'charakteristika', 'charakteristisch', 'charakteristische', 'charakteristischer', 'charakteristisches', 'spezialität', 'spezialitaet', 'spezialitäten', 'spezialitaeten', 'spezialist', 'spezialisten', 'spezialistin', 'spezialistinnen', 'spezialisieren', 'spezialisiert', 'spezialisierte', 'spezialisierter', 'spezialisiertes', 'spezialisierung', 'spezialisierungen', 'beruflich', 'berufliche', 'beruflicher', 'berufliches', 'beruf', 'berufe', 'lehrer', 'lehrerin', 'lehrerinnen', 'unterrichten', 'unterrichtet', 'unterrichtete', 'unterrichtetes', 'unterricht', 'lehre', 'lehren', 'lehrte', 'gelehrt', 'gelehrte', 'gelehrter', 'gelehrtes', 'bildung', 'bildungen', 'bildungswesen', 'ausbildung', 'ausbildungen', 'erziehung', 'erziehen', 'erzieht', 'erzogen', 'erzogene', 'erzogener', 'erzogenes', 'erzieher', 'erzieherin', 'erzieherinnen', 'student', 'studenten', 'studentin', 'studentinnen', 'studieren', 'studiert', 'studierte', 'studierter', 'studiertes', 'studium', 'studien', 'studie', 'forschung', 'forschungen', 'forschen', 'forscht', 'forschte', 'geforscht', 'forscher', 'forscherin', 'forscherinnen', 'wissenschaft', 'wissenschaften', 'wissenschaftlich', 'wissenschaftliche', 'wissenschaftlicher', 'wissenschaftliches', 'wissenschaftler', 'wissenschaftlerin', 'wissenschaftlerinnen', 'wissen', 'weiss', 'weiß', 'gewusst', 'wusste', 'wissend', 'wissende', 'wissender', 'wissendes', 'kenntnis', 'kenntnisse', 'kennen', 'kennt', 'kannte', 'gekannt', 'bekannt', 'bekannte', 'bekannter', 'bekanntes', 'erkennen', 'erkennt', 'erkannte', 'erkannt', 'erkannte', 'erkannter', 'erkanntes', 'erkenntnis', 'erkenntnisse', 'weisheit', 'weise', 'weisen', 'wies', 'gewiesen', 'intelligent', 'intelligente', 'intelligenter', 'intelligentes', 'intelligenz', 'talent', 'talente', 'talentiert', 'talentierte', 'talentierter', 'talentiertes', 'fähigkeit', 'faehigkeit', 'fähigkeiten', 'faehigkeiten', 'fähig', 'faehig', 'fähige', 'faehige', 'fähiger', 'faehiger', 'fähiges', 'faehiges', 'geschick', 'geschickt', 'geschickte', 'geschickter', 'geschicktes', 'geschicklichkeit', 'geschicklichkeiten', 'fertigkeit', 'fertigkeiten', 'können', 'koennen', 'kann', 'konnte', 'gekonnt', 'meister', 'meisterin', 'meisterinnen', 'meisterschaft', 'meisterschaften', 'meistern', 'meistert', 'meisterte', 'gemeistert', 'bereich', 'bereiche', 'gebiet', 'gebiete', 'domain', 'domäne', 'domaene', 'domänen', 'domaenen', 'beherrschen', 'beherrscht', 'beherrschte', 'beherrschtes', 'beherrschung', 'kontrolle', 'kontrollieren', 'kontrolliert', 'kontrollierte', 'kontrolliertes', 'verwaltung', 'verwaltungen', 'verwalten', 'verwaltet', 'verwaltete', 'verwaltetes', 'verwalter', 'verwalterin', 'verwalterinnen', 'management', 'managements', 'managen', 'gemanagt', 'manager', 'managerin', 'managerinnen', 'führung', 'fuehrung', 'führungen', 'fuehrungen', 'führen', 'fuehren', 'führt', 'fuehrt', 'führte', 'fuehrte', 'geführt', 'gefuehrt', 'führer', 'fuehrer', 'führerin', 'fuehrerin', 'führerinnen', 'fuehrerinnen', 'leitung', 'leitungen', 'leiten', 'leitet', 'leitete', 'geleitet', 'leiter', 'leiterin', 'leiterinnen', 'organisation', 'organisationen', 'organisieren', 'organisiert', 'organisierte', 'organisierter', 'organisiertes', 'system', 'systeme', 'systematisch', 'systematische', 'systematischer', 'systematisches', 'methode', 'methoden', 'methodisch', 'methodische', 'methodischer', 'methodisches', 'verfahren', 'prozess', 'prozesse', 'prozessieren', 'prozessiert', 'prozessierte', 'prozessiertes', 'ablauf', 'abläufe', 'ablaeufe', 'vorgang', 'vorgänge', 'vorgaenge', 'procedere', 'protokoll', 'protokolle', 'norm', 'normen', 'normieren', 'normiert', 'normierte', 'normierter', 'normiertes', 'normal', 'normale', 'normaler', 'normales', 'normalität', 'normalitaet', 'standard', 'standards', 'standardisieren', 'standardisiert', 'standardisierte', 'standardisierter', 'standardisiertes', 'regel', 'regeln', 'reglement', 'reglements', 'reglementieren', 'reglementiert', 'reglementierte', 'reglementiertes', 'regulieren', 'reguliert', 'regulierte', 'reguliertes', 'regular', 'reguläre', 'regulaere', 'regulärer', 'regulaerer', 'reguläres', 'regulaeres', 'regelmäßig', 'regelmaessig', 'regelmäßige', 'regelmaessige', 'regelmäßiger', 'regelmaessiger', 'regelmäßiges', 'regelmaessiges', 'gesetz', 'gesetze', 'gesetzlich', 'gesetzliche', 'gesetzlicher', 'gesetzliches', 'legal', 'legale', 'legaler', 'legales', 'legalität', 'legalitaet', 'legitimität', 'legitimiatet', 'legitim', 'legitime', 'legitimer', 'legitimes', 'legitimieren', 'legitimiert', 'legitimierte', 'legitimiertes', 'recht', 'rechte', 'rechtlich', 'rechtliche', 'rechtlicher', 'rechtliches', 'rechtmäßig', 'rechtmaessig', 'rechtmäßige', 'rechtmaessige', 'rechtmäßiger', 'rechtmaessiger', 'rechtmäßiges', 'rechtmaessiges', 'gerechtigkeit', 'gerecht', 'gerechte', 'gerechter', 'gerechtes', 'ungerecht', 'ungerechte', 'ungerechter', 'ungerechtes', 'ungerechtigkeit', 'ungerechtigkeiten', 'gericht', 'gerichte', 'richter', 'richterin', 'richterinnen', 'richten', 'richtet', 'richtete', 'gerichtet', 'urteil', 'urteile', 'urteilen', 'beurteilen', 'beurteilt', 'beurteilte', 'beurteiltes', 'beurteilung', 'beurteilungen', 'verurteilung', 'verurteilungen', 'verurteilen', 'verurteilt', 'verurteilte', 'verurteiltes', 'schuld', 'schuldig', 'schuldige', 'schuldiger', 'schuldiges', 'strafe', 'strafen', 'bestrafen', 'bestraft', 'bestrafte', 'bestrafter', 'bestrafftes', 'bestrafung', 'bestrafungen', 'gefängnis', 'gefaengnis', 'gefängnisse', 'gefaengnisse', 'knast', 'einsperren', 'eingesperrt', 'eingesperrte', 'eingesperrter', 'eingesperrtes', 'häftling', 'haeftling', 'häftlinge', 'haeftlinge', 'sträfling', 'straefling', 'sträflinge', 'straeflinge', 'inhaftiert', 'inhaftierte', 'inhaftierter', 'inhaftiertes', 'inhaftierung', 'inhaftierungen', 'festnahme', 'festnahmen', 'festnehmen', 'festgenommen', 'verhaftung', 'verhaftungen', 'verhaften', 'verhaftet', 'verhaftete', 'verhafteter', 'verhaftetes', 'arrest', 'arrestieren', 'arrestiert', 'arrestierte', 'arrestiertes', ], 'substitutions' => [ '/ä/' => ['ä', 'a', 'ae', '@', '4'], '/ö/' => ['ö', 'o', 'oe', '0', 'ø'], '/ü/' => ['ü', 'u', 'ue'], '/ß/' => ['ß', 'ss', 's'], '/á/' => ['á', 'a', '@', '4'], '/à/' => ['à', 'a', '@', '4'], '/â/' => ['â', 'a', '@', '4'], '/ã/' => ['ã', 'a', '@', '4'], '/å/' => ['å', 'a', '@', '4'], '/æ/' => ['æ', 'ae', 'a'], '/é/' => ['é', 'e', '3', '€'], '/è/' => ['è', 'e', '3', '€'], '/ê/' => ['ê', 'e', '3', '€'], '/ë/' => ['ë', 'e', '3', '€'], '/í/' => ['í', 'i', '1', '!', '|'], '/ì/' => ['ì', 'i', '1', '!', '|'], '/î/' => ['î', 'i', '1', '!', '|'], '/ï/' => ['ï', 'i', '1', '!', '|'], '/ó/' => ['ó', 'o', '0', 'ø'], '/ò/' => ['ò', 'o', '0', 'ø'], '/ô/' => ['ô', 'o', '0', 'ø'], '/õ/' => ['õ', 'o', '0', 'ø'], '/ø/' => ['ø', 'o', '0'], '/ú/' => ['ú', 'u', 'ü'], '/ù/' => ['ù', 'u', 'ü'], '/û/' => ['û', 'u', 'ü'], '/u/' => ['u', 'ü', 'ù', 'ú', 'û', '@', '*'], '/c/' => ['c', 'k', 's', 'z'], '/k/' => ['k', 'c', 'ck'], '/ck/' => ['ck', 'k', 'c'], '/z/' => ['z', 's', 'tz'], '/tz/' => ['tz', 'z', 's'], '/pf/' => ['pf', 'f', 'p'], '/ph/' => ['ph', 'f'], '/sch/' => ['sch', 'sh', 'ch'], '/ch/' => ['ch', 'sh', 'x'], '/ie/' => ['ie', 'i', 'y'], '/ei/' => ['ei', 'ai', 'ey'], '/ai/' => ['ai', 'ei', 'ay'], '/au/' => ['au', 'aw', 'ou'], '/eu/' => ['eu', 'oi', 'oy'], '/äu/' => ['äu', 'aeu', 'oy'], '/dt/' => ['dt', 't', 'd'], '/st/' => ['st', 's', 't'], ] ]; ================================================ FILE: config/languages/spanish.php ================================================ [ 'mild' => [ 'maldito', 'maldita', 'maldición', 'maldicion', 'carajo', 'hostia', 'hostias', 'jolines', 'joline', 'jobar', 'joroba', 'caca', 'mear', 'meada', 'peo', 'pedorro', 'pedorra', 'pedos', 'tonto', 'tonta', 'bobo', 'boba', 'baboso', 'babosa', 'cursi', 'pesado', 'pesada', 'latoso', 'latosa', ], 'moderate' => [ 'cabrón', 'cabron', 'cabrona', 'cabrones', 'cabronazo', 'perra', 'zorra', 'gilipollas', 'gilipolla', 'imbécil', 'imbecil', 'idiota', 'estúpido', 'estupido', 'estúpida', 'estupida', 'pendejo', 'pendeja', 'mamón', 'mamon', 'boludo', 'boluda', 'pelotudo', 'pelotuda', 'culo', 'ojete', 'putilla', 'putita', 'capullo', 'coñazo', 'conazo', 'putada', ], 'high' => [ 'mierda', 'joder', 'coño', 'puta', 'puto', 'chingar', 'chingado', 'chingada', 'pinche', 'verga', 'follar', 'follada', 'follando', 'hijo de puta', 'hijoputa', 'concha', 'cojones', ], 'extreme' => [ 'maricón', 'maricon', 'marica', 'maricona', 'mariconazo', 'tortillera', 'bollera', 'retrasado', 'retrasada', 'retardado', 'retardada', 'mongoloide', 'subnormal', ], ], 'profanities' => [ // Common Spanish profanities and vulgar expressions 'mierda', 'joder', 'coño', 'cabrón', 'cabron', 'puta', 'puto', 'jodido', 'jodida', 'hijo de puta', 'hijoputa', 'gilipollas', 'gilipolla', 'imbécil', 'imbecil', 'idiota', 'estúpido', 'estupido', 'pendejo', 'pendeja', 'mamón', 'mamon', 'mamada', 'chingar', 'chingas', 'chingado', 'chingada', 'pinche', 'verga', 'carajo', 'cojones', 'huevos', 'huevón', 'huevon', 'maricón', 'maricon', 'marica', 'homosexual', 'tortillera', 'bollera', 'follar', 'folla', 'follada', 'follando', 'culiar', 'culear', 'culo', 'ojete', 'concha', 'chocha', 'chochito', 'chucha', 'almeja', 'zorra', 'zorro', 'putilla', 'putita', 'perra', 'perro', 'cabrona', 'cabrones', 'puton', 'putón', 'putona', 'putañero', 'putanero', 'polla', 'picha', 'rabo', 'nabo', 'cipote', 'chorizo', 'salchicha', 'salchichón', 'salchichon', 'miembro', 'pene', 'pijo', 'capullo', 'caput', 'gusano', 'rata', 'caca', 'mear', 'meada', 'orín', 'orin', 'orina', 'orinarse', 'cagar', 'cagada', 'cagarse', 'cagón', 'cagon', 'cagona', 'culiacan', 'culiao', 'culiado', 'culero', 'culera', 'nalgas', 'trasero', 'pompis', 'pompas', 'tetona', 'tetuda', 'tetas', 'pechos', 'chichonas', 'chichona', 'zángano', 'zangano', 'cabronazo', 'hijoelagranputa', 'hijoeputa', 'malparido', 'malparida', 'desgraciado', 'desgraciada', 'sinvergüenza', 'sinverguenza', 'cochino', 'cochina', 'guarro', 'guarra', 'sucio', 'sucia', 'asqueroso', 'asquerosa', 'repugnante', 'vomitivo', 'vomitiva', 'nauseabundo', 'nauseabunda', 'escoria', 'basura', 'porquería', 'porqueria', 'maldito', 'maldita', 'condenado', 'condenada', 'jodón', 'jodon', 'jodona', 'molesto', 'molesta', 'fastidioso', 'fastidiosa', 'cabronazo', 'maricona', 'mariconazo', 'bolludo', 'bolluda', 'boludo', 'boluda', 'pelotudo', 'pelotuda', 'tarado', 'tarada', 'retrasado', 'retrasada', 'retardado', 'retardada', 'mongoloide', 'subnormal', 'anormal', 'deficiente', 'tonto', 'tonta', 'bobo', 'boba', 'baboso', 'babosa', 'babas', 'ñoño', 'ñona', 'cursi', 'ridículo', 'ridiculo', 'ridícula', 'ridicula', 'estúpida', 'estupida', 'gorda', 'gordo', 'gordinflas', 'gordinfla', 'ballena', 'vaca', 'cerda', 'cerdo', 'chancho', 'chancha', 'marrano', 'marrana', 'cochino', 'cochina', 'puerco', 'puerca', 'animal', 'bestia', 'salvaje', 'bárbaro', 'barbaro', 'bárbara', 'barbara', 'bruto', 'bruta', 'burro', 'burra', 'asno', 'asna', 'mula', 'mulo', 'bestia', 'fiera', 'demonio', 'diablo', 'diabla', 'satanás', 'satanas', 'lucifer', 'maldición', 'maldicion', 'carajo', 'hostia', 'hostias', 'jolines', 'joline', 'jobar', 'joroba', 'cojonudo', 'cojonuda', 'cojudo', 'cojuda', 'acojonante', 'descojonarse', 'descojonar', 'tocapelotas', 'tocacojones', 'rompepelotas', 'rompecojones', 'pelmazos', 'pelmazo', 'pelma', 'plasta', 'pesado', 'pesada', 'pesao', 'pesá', 'latoso', 'latosa', 'coñazo', 'conazo', 'putada', 'jodienda', 'follón', 'follon', 'lío', 'lio', 'marrón', 'marron', 'peo', 'pedorro', 'pedorra', 'pedos', 'ventosidad', 'flatulencia', 'gases', 'tirarse pedos', 'echar pedos', 'soltar pedos', 'heder', 'apestar', 'oler mal', 'tufo', 'peste', 'pestilencia', 'putrefacción', 'putrefaccion', 'putrefacto', 'putrefacta', 'podrido', 'podrida', 'rancio', 'rancia', 'agrio', 'agria', 'amargo', 'amarga', 'salado', 'salada', 'soso', 'sosa', 'insípido', 'insipido', 'insípida', 'insipida', 'desabrido', 'desabrida', 'malo', 'mala', 'malísimo', 'malisimo', 'malísima', 'malisima', 'pésimo', 'pesimo', 'pésima', 'pesima', 'horrible', 'horroroso', 'horrorosa', 'terrorífico', 'terrorifico', 'terrorífica', 'terrorifica', 'espantoso', 'espantosa', 'horripilante', 'espeluznante', 'escalofriante', 'siniestro', 'siniestra', 'tenebroso', 'tenebrosa', 'lúgubre', 'lugubre', 'sombrío', 'sombrio', 'sombría', 'sombria', 'triste', 'melancólico', 'melancolico', 'melancólica', 'melancolica', 'deprimido', 'deprimida', 'depresivo', 'depresiva', 'suicida', 'morir', 'muerte', 'muerto', 'muerta', 'cadáver', 'cadaver', 'difunto', 'difunta', 'finado', 'finada', 'fallecido', 'fallecida', 'occiso', 'occisa', 'fiambre', 'estirar la pata', 'diñar', 'dinar', 'palmar', 'pelar', 'espichar', 'fenecer', 'expirar', 'perecer', 'sucumbir', 'fallecer', 'óbito', 'obito', 'defunción', 'defuncion', 'deceso', 'tránsito', 'transito', 'partida', 'despedida', 'adiós', 'adios', 'hasta la vista', 'hasta luego', 'hasta pronto', 'hasta mañana', 'hasta manana', 'chau', 'chao', 'bye', 'goodbye', ], 'false_positives' => [ // Common Spanish words that might be detected as false positives 'análisis', 'analisis', 'clase', 'clases', 'paso', 'pasos', 'expresión', 'expresion', 'expresiones', 'asesino', 'asesina', 'asesinar', 'asesinato', 'empresa', 'empresas', 'empresario', 'empresaria', 'negocio', 'negocios', 'trabajo', 'trabajos', 'trabajar', 'trabajador', 'trabajadora', 'empleo', 'empleos', 'empleado', 'empleada', 'empleador', 'empleadora', 'oficina', 'oficinas', 'oficinista', 'escritorio', 'escritorios', 'computadora', 'computadoras', 'computador', 'computadores', 'ordenador', 'ordenadores', 'máquina', 'maquina', 'máquinas', 'maquinas', 'aparato', 'aparatos', 'dispositivo', 'dispositivos', 'instrumento', 'instrumentos', 'herramienta', 'herramientas', 'útil', 'util', 'útiles', 'utiles', 'utilidad', 'utilidades', 'función', 'funcion', 'funciones', 'funcional', 'funcionalidad', 'funcionalidades', 'característico', 'caracteristico', 'característica', 'caracteristica', 'características', 'caracteristicas', 'especialidad', 'especialidades', 'especialista', 'especialistas', 'especializar', 'especializado', 'especializada', 'especialización', 'especializacion', 'profesional', 'profesionales', 'profesión', 'profesion', 'profesiones', 'profesor', 'profesora', 'profesores', 'profesoras', 'enseñar', 'ensenar', 'enseñanza', 'ensenanza', 'enseñanzas', 'ensenanzas', 'educación', 'educacion', 'educativo', 'educativa', 'educativos', 'educativas', 'educar', 'educado', 'educada', 'educador', 'educadora', 'educadores', 'educadoras', 'estudiante', 'estudiantes', 'estudiar', 'estudio', 'estudios', 'estudiado', 'estudiada', 'investigación', 'investigacion', 'investigaciones', 'investigar', 'investigador', 'investigadora', 'investigadores', 'investigadoras', 'científico', 'cientifico', 'científica', 'cientifica', 'científicos', 'cientificos', 'científicas', 'cientificas', 'ciencia', 'ciencias', 'conocimiento', 'conocimientos', 'conocer', 'conocido', 'conocida', 'conocidos', 'conocidas', 'saber', 'sabido', 'sabida', 'sabidos', 'sabidas', 'sabiduría', 'sabiduria', 'sabio', 'sabia', 'sabios', 'sabias', 'inteligente', 'inteligentes', 'inteligencia', 'inteligencias', 'talento', 'talentos', 'talentoso', 'talentosa', 'talentosos', 'talentosas', 'habilidad', 'habilidades', 'hábil', 'habil', 'hábiles', 'habiles', 'destreza', 'destrezas', 'destro', 'destra', 'diestro', 'diestra', 'diestros', 'diestras', 'maestro', 'maestra', 'maestros', 'maestras', 'maestría', 'maestria', 'maestrías', 'maestrias', 'dominio', 'dominios', 'dominar', 'dominado', 'dominada', 'dominados', 'dominadas', 'control', 'controles', 'controlar', 'controlado', 'controlada', 'controlados', 'controladas', 'administración', 'administracion', 'administrar', 'administrador', 'administradora', 'administradores', 'administradoras', 'gestión', 'gestion', 'gestiones', 'gestionar', 'gestor', 'gestora', 'gestores', 'gestoras', 'organización', 'organizacion', 'organizaciones', 'organizar', 'organizador', 'organizadora', 'organizadores', 'organizadoras', 'sistema', 'sistemas', 'sistemático', 'sistematico', 'sistemática', 'sistematica', 'sistemáticos', 'sistematicos', 'sistemáticas', 'sistematicas', 'método', 'metodo', 'métodos', 'metodos', 'metodología', 'metodologia', 'metodologías', 'metodologias', 'proceso', 'procesos', 'procesar', 'procesado', 'procesada', 'procesados', 'procesadas', 'procedimiento', 'procedimientos', 'proceder', 'protocolo', 'protocolos', 'norma', 'normas', 'normal', 'normales', 'normalidad', 'normalidades', 'normalizar', 'normalizado', 'normalizada', 'normalizados', 'normalizadas', 'estándar', 'estandar', 'estándares', 'estandares', 'estandarizar', 'estandarizado', 'estandarizada', 'estandarizados', 'estandarizadas', 'regla', 'reglas', 'reglamento', 'reglamentos', 'reglamentar', 'reglamentario', 'reglamentaria', 'reglamentarios', 'reglamentarias', 'regular', 'regulares', 'regularidad', 'regularidades', 'regularizar', 'regularizado', 'regularizada', 'regularizados', 'regularizadas', 'ley', 'leyes', 'legal', 'legales', 'legalidad', 'legalidades', 'legalizar', 'legalizado', 'legalizada', 'legalizados', 'legalizadas', 'derecho', 'derechos', 'jurídico', 'juridico', 'jurídica', 'juridica', 'jurídicos', 'juridicos', 'jurídicas', 'juridicas', 'justicia', 'justicias', 'justo', 'justa', 'justos', 'justas', 'injusto', 'injusta', 'injustos', 'injustas', 'injusticia', 'injusticias', 'tribunal', 'tribunales', 'juez', 'jueces', 'jueza', 'juezas', 'juzgar', 'juzgado', 'juzgada', 'juzgados', 'juzgadas', 'sentencia', 'sentencias', 'sentenciar', 'sentenciado', 'sentenciada', 'sentenciados', 'sentenciadas', 'condena', 'condenas', 'condenar', 'condenado', 'condenada', 'condenados', 'condenadas', 'castigo', 'castigos', 'castigar', 'castigado', 'castigada', 'castigados', 'castigadas', 'pena', 'penas', 'penar', 'penado', 'penada', 'penados', 'penadas', 'prisión', 'prision', 'prisiones', 'cárcel', 'carcel', 'cárceles', 'carceles', 'encarcelar', 'encarcelado', 'encarcelada', 'encarcelados', 'encarceladas', 'preso', 'presa', 'presos', 'presas', 'presidio', 'presidios', 'penitenciaría', 'penitenciaria', 'penitenciarías', 'penitenciarias', 'reformatorio', 'reformatorios', ], 'substitutions' => [ '/ñ/' => ['ñ', 'n', '~n', 'ni'], '/á/' => ['á', 'a', '@', '4'], '/é/' => ['é', 'e', '3', '€'], '/í/' => ['í', 'i', '1', '!', '|'], '/ó/' => ['ó', 'o', '0', 'ø'], '/ú/' => ['ú', 'u', 'ü'], '/ü/' => ['ü', 'u', 'ú'], '/u/' => ['u', 'ú', 'ü', '@', '*'], '/c/' => ['c', 'k', 'ç'], '/ll/' => ['ll', 'y', 'i'], '/rr/' => ['rr', 'r'], '/ch/' => ['ch', 'x'], '/z/' => ['z', 's', 'c'], '/j/' => ['j', 'x', 'h'], '/g/' => ['g', 'j', 'h'], '/b/' => ['b', 'v', 'w'], '/v/' => ['v', 'b', 'w'], ] ]; ================================================ FILE: phpunit.xml ================================================ ./tests ================================================ FILE: src/BlaspManager.php ================================================ app = $app; } public function driver(?string $driver = null): PendingCheck { return $this->newPendingCheck()->driver($driver ?? $this->getDefaultDriver()); } public function resolveDriver(string $name): DriverInterface { if (!isset($this->drivers[$name])) { $this->drivers[$name] = $this->createDriver($name); } return $this->drivers[$name]; } protected function createDriver(string $name): DriverInterface { if (isset($this->customCreators[$name])) { return ($this->customCreators[$name])($this->app); } $method = 'create' . ucfirst($name) . 'Driver'; if (method_exists($this, $method)) { return $this->$method(); } throw new InvalidArgumentException("Driver [{$name}] not supported."); } public function createRegexDriver(): DriverInterface { return new RegexDriver(); } public function createPatternDriver(): DriverInterface { return new PatternDriver(); } public function createPhoneticDriver(): DriverInterface { $config = $this->app['config']->get('blasp.drivers.phonetic', []); return new PhoneticDriver( phonemes: $config['phonemes'] ?? 4, minWordLength: $config['min_word_length'] ?? 3, maxDistanceRatio: $config['max_distance_ratio'] ?? 0.6, phoneticFalsePositives: $config['false_positives'] ?? [], supportedLanguages: $config['supported_languages'] ?? ['english'], ); } public function createPipelineDriver(): DriverInterface { $config = $this->app['config']->get('blasp.drivers.pipeline', []); $driverNames = $config['drivers'] ?? ['regex', 'phonetic']; if (!is_array($driverNames)) { throw new InvalidArgumentException('blasp.drivers.pipeline.drivers must be an array of driver names.'); } foreach ($driverNames as $name) { if (!is_string($name) || trim($name) === '') { throw new InvalidArgumentException('Each pipeline driver name must be a non-empty string.'); } if (strtolower(trim($name)) === 'pipeline') { throw new InvalidArgumentException('Pipeline driver cannot contain itself. Remove "pipeline" from blasp.drivers.pipeline.drivers.'); } } $resolvedDrivers = array_map( fn (string $name) => $this->resolveDriver($name), $driverNames, ); return new PipelineDriver($resolvedDrivers); } public function extend(string $driver, Closure $callback): self { $this->customCreators[$driver] = $callback; return $this; } public function getDefaultDriver(): string { return $this->app['config']->get('blasp.default', 'regex'); } public function newPendingCheck(): PendingCheck { return new PendingCheck($this); } public function pipeline(string ...$drivers): PendingCheck { return $this->newPendingCheck()->pipeline(...$drivers); } // --- Shortcut methods that create PendingCheck --- public function check(?string $text): \Blaspsoft\Blasp\Core\Result { return $this->newPendingCheck()->check($text); } public function checkMany(array $texts): array { return $this->newPendingCheck()->checkMany($texts); } public function __call(string $method, array $parameters): mixed { return $this->newPendingCheck()->$method(...$parameters); } public function getApp(): Application { return $this->app; } } ================================================ FILE: src/BlaspServiceProvider.php ================================================ app->runningInConsole()) { $this->publishes([ __DIR__ . '/../config/blasp.php' => config_path('blasp.php'), ], 'blasp-config'); $this->publishes([ __DIR__ . '/../config/languages' => config_path('languages'), ], 'blasp-languages'); $this->publishes([ __DIR__ . '/../config/blasp.php' => config_path('blasp.php'), __DIR__ . '/../config/languages' => config_path('languages'), ], 'blasp'); $this->commands([ Console\ClearCommand::class, Console\TestCommand::class, Console\LanguagesCommand::class, ]); } $this->registerValidationRule(); $this->registerMiddlewareAlias(); $this->registerBladeDirectives(); $this->registerStringMacros(); } public function register(): void { $this->mergeConfigFrom(__DIR__ . '/../config/blasp.php', 'blasp'); $this->app->singleton('blasp', function ($app) { return new BlaspManager($app); }); $this->app->alias('blasp', BlaspManager::class); } protected function registerValidationRule(): void { $this->app['validator']->extend('blasp_check', function ($attribute, $value, $parameters) { if (!is_string($value) || $value === '') { return true; } $language = $parameters[0] ?? config('blasp.language', config('blasp.default_language', 'english')); $manager = $this->app->make('blasp'); $result = $manager->in($language)->check($value); return !$result->isOffensive(); }, 'The :attribute contains profanity.'); } protected function registerMiddlewareAlias(): void { $this->app['router']->aliasMiddleware('blasp', Middleware\CheckProfanity::class); } protected function registerBladeDirectives(): void { Blade::directive('clean', function (string $expression) { return "check({$expression})->clean()); ?>"; }); } protected function registerStringMacros(): void { Str::macro('isProfane', function (string $text): bool { return app('blasp')->check($text)->isOffensive(); }); Str::macro('cleanProfanity', function (string $text): string { return app('blasp')->check($text)->clean(); }); Stringable::macro('isProfane', function (): bool { return app('blasp')->check((string) $this)->isOffensive(); }); Stringable::macro('cleanProfanity', function (): Stringable { return new Stringable(app('blasp')->check((string) $this)->clean()); }); } } ================================================ FILE: src/Blaspable.php ================================================ */ protected array $blaspResultsCache = []; public static function bootBlaspable(): void { static::saving(function (Model $model) { if (static::$blaspCheckingDisabled) { return; } $model->blaspResultsCache = []; $attributes = $model->blaspable ?? []; $dirty = $model->getDirty(); $mode = $model->blaspMode ?? config('blasp.model.mode', 'sanitize'); foreach ($attributes as $attr) { if (!isset($dirty[$attr]) || !is_string($dirty[$attr])) { continue; } /** @var PendingCheck $check */ $check = app('blasp')->newPendingCheck(); if ($lang = ($model->blaspLanguage ?? null)) { $check = $check->in($lang); } if ($mask = ($model->blaspMask ?? null)) { $check = $check->mask($mask); } $result = $check->check($dirty[$attr]); $model->blaspResultsCache[$attr] = $result; if ($result->isOffensive()) { event(new ModelProfanityDetected($model, $attr, $result)); if ($mode === 'reject') { throw ProfanityRejectedException::forModel($model, $attr, $result); } $model->setAttribute($attr, $result->clean()); } } }); } public function hadProfanity(): bool { foreach ($this->blaspResultsCache as $result) { if ($result->isOffensive()) { return true; } } return false; } /** @return array */ public function blaspResults(): array { return $this->blaspResultsCache; } public function blaspResult(string $attribute): ?Result { return $this->blaspResultsCache[$attribute] ?? null; } public static function withoutBlaspChecking(Closure $callback): mixed { $previousState = static::$blaspCheckingDisabled; static::$blaspCheckingDisabled = true; try { return $callback(); } finally { static::$blaspCheckingDisabled = $previousState; } } } ================================================ FILE: src/Console/ClearCommand.php ================================================ info('Blasp cache cleared successfully!'); } } ================================================ FILE: src/Console/LanguagesCommand.php ================================================ table(['Language', 'Profanities', 'False Positives', 'Severity Map'], $rows); } } ================================================ FILE: src/Console/TestCommand.php ================================================ argument('text'); $language = $this->option('lang') ?? config('blasp.language', config('blasp.default_language', 'english')); $manager = app('blasp'); $result = $manager->in($language)->check($text); $this->info("Input: {$text}"); $this->info("Language: {$language}"); $this->newLine(); if ($result->isOffensive()) { $this->error('Profanity detected!'); $this->table( ['Property', 'Value'], [ ['Clean text', $result->clean()], ['Score', $result->score()], ['Count', $result->count()], ['Severity', $result->severity()?->value ?? 'n/a'], ['Unique words', implode(', ', $result->uniqueWords())], ] ); if ($this->option('detail')) { $this->newLine(); $this->info('Matched words:'); $rows = []; foreach ($result->words() as $word) { $rows[] = [ $word->text, $word->base, $word->severity->value, $word->position, $word->length, ]; } $this->table(['Text', 'Base', 'Severity', 'Position', 'Length'], $rows); } } else { $this->info('No profanity detected. Text is clean.'); } } } ================================================ FILE: src/Core/Analyzer.php ================================================ detect($text, $dictionary, $mask, $options); } } ================================================ FILE: src/Core/Contracts/DriverInterface.php ================================================ profanities = $profanities; $this->falsePositives = $falsePositives; $this->separators = $separators; $this->substitutions = $substitutions; $this->severityMap = $severityMap; $this->normalizer = $normalizer; $this->allowList = array_map('strtolower', $allowList); $this->blockList = array_map('strtolower', $blockList); $this->language = $language; // Apply block list — add extra words to profanities foreach ($this->blockList as $word) { if (!in_array($word, $this->profanities)) { $this->profanities[] = $word; $this->severityMap[$word] = Severity::High; } } // Remove allow-listed words if (!empty($this->allowList)) { $this->profanities = array_values(array_filter( $this->profanities, fn($p) => !in_array(strtolower($p), $this->allowList) )); } if ($profanityExpressions !== null) { $this->profanityExpressions = $profanityExpressions; } else { $this->profanityExpressions = (new RegexMatcher())->generateExpressions( $this->profanities, $this->separators, $this->substitutions ); } } public static function forLanguage(string $language, array $options = []): self { if (!preg_match('/^[a-zA-Z0-9_-]+$/', $language)) { return new self( profanities: [], falsePositives: [], separators: [], substitutions: [], severityMap: [], normalizer: new EnglishNormalizer(), language: $language, ); } $config = self::loadLanguageConfig($language); $globalConfig = self::loadGlobalConfig(); $profanities = $config['profanities'] ?? []; $falsePositives = $config['false_positives'] ?? []; $severityMap = self::buildSeverityMap($config); $substitutions = $globalConfig['substitutions'] ?? []; if (isset($config['substitutions']) && is_array($config['substitutions'])) { foreach ($config['substitutions'] as $pattern => $values) { if (is_array($values)) { $substitutions[$pattern] = array_values(array_unique(array_merge( $substitutions[$pattern] ?? [], $values ))); } } } return new self( profanities: $profanities, falsePositives: $falsePositives, separators: $globalConfig['separators'] ?? [], substitutions: $substitutions, severityMap: $severityMap, normalizer: self::getNormalizerForLanguage($language), allowList: $options['allow'] ?? [], blockList: $options['block'] ?? [], language: $language, ); } public static function forLanguages(array $languages, array $options = []): self { $allProfanities = []; $allFalsePositives = []; $allSeverityMap = []; $globalConfig = self::loadGlobalConfig(); $substitutions = $globalConfig['substitutions'] ?? []; foreach ($languages as $language) { if (!preg_match('/^[a-zA-Z0-9_-]+$/', $language)) { continue; } $config = self::loadLanguageConfig($language); $allProfanities = array_merge($allProfanities, $config['profanities'] ?? []); $allFalsePositives = array_merge($allFalsePositives, $config['false_positives'] ?? []); $allSeverityMap = array_merge($allSeverityMap, self::buildSeverityMap($config)); // Merge accent/diacritic substitutions only if (isset($config['substitutions']) && is_array($config['substitutions'])) { foreach ($config['substitutions'] as $pattern => $values) { if (is_array($values)) { $plainKey = trim($pattern, '/'); if (mb_strlen($plainKey, 'UTF-8') > 1 || preg_match('/^[a-zA-Z]$/', $plainKey)) { continue; } $substitutions[$pattern] = array_values(array_unique(array_merge( $substitutions[$pattern] ?? [], $values ))); } } } } return new self( profanities: array_values(array_unique($allProfanities)), falsePositives: array_values(array_unique($allFalsePositives)), separators: $globalConfig['separators'] ?? [], substitutions: $substitutions, severityMap: $allSeverityMap, normalizer: self::getNormalizerForLanguage('english'), allowList: $options['allow'] ?? [], blockList: $options['block'] ?? [], language: implode(',', $languages), ); } public static function forAllLanguages(array $options = []): self { $languages = self::getAvailableLanguages(); return self::forLanguages($languages, $options); } public function getProfanities(): array { return $this->profanities; } public function getFalsePositives(): array { return $this->falsePositives; } public function getProfanityExpressions(): array { return $this->profanityExpressions; } public function getSeverity(string $word): Severity { $lower = strtolower($word); return $this->severityMap[$lower] ?? Severity::High; } public function getNormalizer(): StringNormalizer { return $this->normalizer; } public function getLanguage(): string { return $this->language; } public function getSeparators(): array { return $this->separators; } public function getSubstitutions(): array { return $this->substitutions; } // --- Static helpers --- public static function getAvailableLanguages(): array { $possiblePaths = [ config_path('languages'), __DIR__ . '/../../config/languages', realpath(__DIR__ . '/../../config/languages'), ]; $languagesPath = null; foreach ($possiblePaths as $path) { if ($path && is_dir($path)) { $languagesPath = $path; break; } } if (!$languagesPath) { return ['english']; } $languageFiles = glob($languagesPath . '/*.php'); $languages = []; foreach ($languageFiles as $languageFile) { $languages[] = basename($languageFile, '.php'); } return empty($languages) ? ['english'] : $languages; } public static function loadLanguageConfig(string $language): array { if (!preg_match('/^[a-zA-Z0-9_-]+$/', $language)) { return ['profanities' => [], 'false_positives' => []]; } $possiblePaths = [ config_path("languages/{$language}.php"), __DIR__ . "/../../config/languages/{$language}.php", realpath(__DIR__ . "/../../config/languages/{$language}.php"), ]; $languageFile = null; foreach ($possiblePaths as $path) { if ($path && file_exists($path)) { $languageFile = $path; break; } } if (!$languageFile) { return ['profanities' => [], 'false_positives' => []]; } $config = require $languageFile; if (!is_array($config) || !isset($config['profanities'])) { return ['profanities' => [], 'false_positives' => []]; } return $config; } private static function loadGlobalConfig(): array { return [ 'separators' => config('blasp.separators', config('blasp.drivers.regex.separators', [])), 'substitutions' => config('blasp.substitutions', config('blasp.drivers.regex.substitutions', [])), 'false_positives' => config('blasp.false_positives', []), ]; } private static function buildSeverityMap(array $config): array { $map = []; if (isset($config['severity']) && is_array($config['severity'])) { foreach ($config['severity'] as $level => $words) { $severity = Severity::tryFrom($level) ?? Severity::High; foreach ($words as $word) { $map[strtolower($word)] = $severity; } } } // Words only in profanities (not in severity map) default to High if (isset($config['profanities'])) { foreach ($config['profanities'] as $word) { $lower = strtolower($word); if (!isset($map[$lower])) { $map[$lower] = Severity::High; } } } return $map; } public static function getNormalizerForLanguage(string $language): StringNormalizer { if (!isset(self::$normalizers[$language])) { self::$normalizers[$language] = match (strtolower($language)) { 'english' => new EnglishNormalizer(), 'spanish' => new SpanishNormalizer(), 'german' => new GermanNormalizer(), 'french' => new FrenchNormalizer(), default => new EnglishNormalizer(), }; } return self::$normalizers[$language]; } // --- Caching --- public static function clearCache(): void { $cache = self::getCache(); $keys = $cache->get('blasp_cache_keys', []); foreach ($keys as $key) { $cache->forget($key); } $cache->forget('blasp_cache_keys'); // Also clear result cache keys $resultKeys = $cache->get('blasp_result_cache_keys', []); foreach ($resultKeys as $key) { $cache->forget($key); } $cache->forget('blasp_result_cache_keys'); } private static function getCache(): \Illuminate\Contracts\Cache\Repository { $driver = config('blasp.cache.driver', config('blasp.cache_driver')); return $driver !== null ? Cache::store($driver) : Cache::store(); } } ================================================ FILE: src/Core/Masking/CallbackMask.php ================================================ callback)($word, $length); } } ================================================ FILE: src/Core/Masking/CharacterMask.php ================================================ character = mb_substr($character, 0, 1); } public function mask(string $word, int $length): string { return str_repeat($this->character, $length); } } ================================================ FILE: src/Core/Masking/GrawlixMask.php ================================================ $this->text, 'base' => $this->base, 'severity' => $this->severity->value, 'position' => $this->position, 'length' => $this->length, 'language' => $this->language, ]; } public function jsonSerialize(): mixed { return $this->toArray(); } } ================================================ FILE: src/Core/Matchers/CompoundWordDetector.php ================================================ strlen($profanityKey)) { return false; } $matchLower = strtolower($matchedText); $wordLower = strtolower($fullWord); foreach (self::SUFFIXES as $suffix) { if ($wordLower === $matchLower . $suffix) { return false; } } $pos = strpos($wordLower, $matchLower); if ($pos !== false) { $remainder = substr($wordLower, 0, $pos) . substr($wordLower, $pos + strlen($matchLower)); foreach ($profanityExpressions as $profanity => $_) { if (strlen($profanity) >= 3 && stripos($remainder, $profanity) !== false) { return false; } } } return true; } } ================================================ FILE: src/Core/Matchers/FalsePositiveFilter.php ================================================ falsePositivesMap = array_flip(array_map('strtolower', $falsePositives)); } public function isFalsePositive(string $word): bool { return isset($this->falsePositivesMap[strtolower($word)]); } public function isInsideHexToken(string $string, int $start, int $length): bool { $end = $start + $length; $strLen = strlen($string); $tokenStart = $start; while ($tokenStart > 0 && preg_match('/[0-9a-fA-F\-]/', $string[$tokenStart - 1])) { $tokenStart--; } $tokenEnd = $end; while ($tokenEnd < $strLen && preg_match('/[0-9a-fA-F\-]/', $string[$tokenEnd])) { $tokenEnd++; } $token = substr($string, $tokenStart, $tokenEnd - $tokenStart); $token = trim($token, '-'); if (preg_match('/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/', $token)) { return true; } $stripped = str_replace('-', '', $token); if (strlen($stripped) >= 8 && preg_match('/^[0-9a-fA-F]+$/', $stripped) && preg_match('/[0-9]/', $stripped)) { return true; } return false; } public function isSpanningWordBoundary(string $matchedText, string $fullString, int $matchStart): bool { if (!preg_match('/\s+/', $matchedText)) { return false; } $parts = preg_split('/\s+/', $matchedText); if (count($parts) <= 1) { return false; } $singleCharCount = 0; foreach ($parts as $part) { if (mb_strlen($part, 'UTF-8') === 1 && preg_match('/[a-z]/iu', $part)) { $singleCharCount++; } } if ($singleCharCount === count($parts)) { return false; } $matchStartChar = mb_strlen(substr($fullString, 0, $matchStart), 'UTF-8'); $matchEndChar = $matchStartChar + mb_strlen($matchedText, 'UTF-8'); $embeddedAtStart = false; $embeddedAtEnd = false; if ($matchStartChar > 0) { $charBefore = mb_substr($fullString, $matchStartChar - 1, 1, 'UTF-8'); if (preg_match('/\w/u', $charBefore)) { $embeddedAtStart = true; } } if ($matchEndChar < mb_strlen($fullString, 'UTF-8')) { $charAfter = mb_substr($fullString, $matchEndChar, 1, 'UTF-8'); if (preg_match('/\w/u', $charAfter)) { $embeddedAtEnd = true; } } if ($embeddedAtStart && $embeddedAtEnd) { return true; } if ($embeddedAtStart && !$embeddedAtEnd) { $standaloneParts = array_slice($parts, 1); $standalonePortion = implode(' ', $standaloneParts); $hasLetter = preg_match('/[a-z]/iu', $standalonePortion); $hasNonLetter = preg_match('/[^a-z\s]/iu', $standalonePortion); if ($hasLetter && $hasNonLetter) { return false; } return true; } if (!$embeddedAtStart && $embeddedAtEnd) { $standaloneParts = array_slice($parts, 0, -1); $standalonePortion = implode(' ', $standaloneParts); $hasLetter = preg_match('/[a-z]/iu', $standalonePortion); $hasNonLetter = preg_match('/[^a-z\s]/iu', $standalonePortion); if ($hasLetter && $hasNonLetter) { return false; } return true; } return false; } public function getFullWordContext(string $string, int $start, int $length): string { $left = $start; $right = $start + $length; while ($left > 0 && preg_match('/\w/', $string[$left - 1])) { $left--; } while ($right < strlen($string) && preg_match('/\w/', $string[$right])) { $right++; } return substr($string, $left, $right - $left); } } ================================================ FILE: src/Core/Matchers/PhoneticMatcher.php ================================================ > metaphone code → list of profanity words */ private array $index = []; public function __construct( array $profanities, private int $phonemes = 4, private int $minWordLength = 3, private float $maxDistanceRatio = 0.6, private array $phoneticFalsePositives = [], ) { $this->phoneticFalsePositives = array_map(fn($fp) => mb_strtolower($fp, 'UTF-8'), $this->phoneticFalsePositives); $this->buildIndex($profanities); } private function buildIndex(array $profanities): void { foreach ($profanities as $word) { $lower = mb_strtolower($word, 'UTF-8'); if (mb_strlen($lower, 'UTF-8') < $this->minWordLength) { continue; } $code = metaphone($lower, $this->phonemes); if ($code === '') { continue; } $this->index[$code][] = $lower; } // Deduplicate foreach ($this->index as $code => $words) { $this->index[$code] = array_values(array_unique($words)); } } public function match(string $word): ?string { $lower = strtolower($word); if (mb_strlen($lower, 'UTF-8') < $this->minWordLength) { return null; } if (in_array($lower, $this->phoneticFalsePositives, true)) { return null; } $code = metaphone($lower, $this->phonemes); if ($code === '' || !isset($this->index[$code])) { return null; } $bestMatch = null; $bestDistance = PHP_INT_MAX; foreach ($this->index[$code] as $profanity) { $distance = levenshtein($lower, $profanity); $maxLen = max(mb_strlen($lower, 'UTF-8'), mb_strlen($profanity, 'UTF-8')); $threshold = (int) ceil($this->maxDistanceRatio * $maxLen); if ($distance <= $threshold && $distance < $bestDistance) { $bestDistance = $distance; $bestMatch = $profanity; } } return $bestMatch; } } ================================================ FILE: src/Core/Matchers/RegexMatcher.php ================================================ generateSeparatorExpression($separators); $substitutionExpressions = $this->generateSubstitutionExpressions($substitutions); $profanityExpressions = []; foreach ($profanities as $profanity) { $profanityExpressions[$profanity] = $this->generateProfanityExpression( $profanity, $substitutionExpressions, $separatorExpression ); } return $profanityExpressions; } public function generateSeparatorExpression(array $separators): string { $normalSeparators = array_filter($separators, fn($sep) => $sep !== '.'); $pattern = $this->generateEscapedExpression($normalSeparators, self::ESCAPED_SEPARATOR_CHARACTERS, ''); return '(?:' . $pattern . '|\.(?=\w)){0,3}?'; } public function generateSubstitutionExpressions(array $substitutions): array { $characterExpressions = []; foreach ($substitutions as $character => $substitutionOptions) { $hasMultiChar = false; foreach ($substitutionOptions as $option) { if (mb_strlen($option, 'UTF-8') > 1 && !preg_match('/^\\\\.$/u', $option)) { $hasMultiChar = true; break; } } if ($hasMultiChar) { $escaped = array_map(function ($opt) { if (preg_match('/^\\\\.$/u', $opt)) { return $opt; } return preg_quote($opt, '/'); }, $substitutionOptions); $characterExpressions[$character] = '(?:' . implode('|', $escaped) . ')+' . self::SEPARATOR_PLACEHOLDER; } else { $characterExpressions[$character] = $this->generateEscapedExpression($substitutionOptions, [], '+') . self::SEPARATOR_PLACEHOLDER; } } return $characterExpressions; } public function generateProfanityExpression(string $profanity, array $substitutionExpressions, string $separatorExpression): string { $plainSubstitutions = []; foreach ($substitutionExpressions as $pattern => $replacement) { $plainKey = trim($pattern, '/'); $plainSubstitutions[$plainKey] = $replacement; } uksort($plainSubstitutions, fn($a, $b) => mb_strlen($b, 'UTF-8') - mb_strlen($a, 'UTF-8')); $expression = ''; $i = 0; $len = mb_strlen($profanity, 'UTF-8'); while ($i < $len) { $matched = false; foreach ($plainSubstitutions as $key => $replacement) { $keyLen = mb_strlen($key, 'UTF-8'); if ($i + $keyLen <= $len && mb_substr($profanity, $i, $keyLen, 'UTF-8') === $key) { $expression .= $replacement; $i += $keyLen; $matched = true; break; } } if (!$matched) { $expression .= preg_quote(mb_substr($profanity, $i, 1, 'UTF-8'), '/'); $i++; } } $expression = str_replace(self::SEPARATOR_PLACEHOLDER, $separatorExpression, $expression); $expression = '/' . $expression . '/iu'; return $expression; } private function generateEscapedExpression(array $characters = [], array $escapedCharacters = [], string $quantifier = '*?'): string { $regex = $escapedCharacters; foreach ($characters as $character) { $regex[] = preg_quote($character, '/'); } return '[' . implode('', $regex) . ']' . $quantifier; } } ================================================ FILE: src/Core/Normalizers/EnglishNormalizer.php ================================================ 'a', 'â' => 'a', 'ä' => 'a', 'á' => 'a', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'ö' => 'o', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ý' => 'y', 'ÿ' => 'y', 'À' => 'A', 'Â' => 'A', 'Ä' => 'A', 'Á' => 'A', 'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Ö' => 'O', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'Ý' => 'Y', 'Ÿ' => 'Y', 'ç' => 'c', 'Ç' => 'C', 'œ' => 'oe', 'Œ' => 'OE', 'æ' => 'ae', 'Æ' => 'AE', ]; return strtr($string, $frenchAccents); } } ================================================ FILE: src/Core/Normalizers/GermanNormalizer.php ================================================ 'ae', 'Ä' => 'AE', 'ö' => 'oe', 'Ö' => 'OE', 'ü' => 'ue', 'Ü' => 'UE', 'ß' => 'ss', ]; $normalizedString = strtr($string, $germanMappings); $normalizedString = preg_replace_callback('/sch/i', function ($matches) { $match = $matches[0]; if ($match === 'SCH') return 'SH'; if ($match === 'Sch') return 'Sh'; return 'sh'; }, $normalizedString); return $normalizedString; } } ================================================ FILE: src/Core/Normalizers/NullNormalizer.php ================================================ 'a', 'Á' => 'A', 'é' => 'e', 'É' => 'E', 'í' => 'i', 'Í' => 'I', 'ó' => 'o', 'Ó' => 'O', 'ú' => 'u', 'Ú' => 'U', 'ü' => 'u', 'Ü' => 'U', 'ñ' => 'n', 'Ñ' => 'N', ]; $normalizedString = strtr($string, $spanishMappings); $normalizedString = preg_replace_callback('/\bll(?=[aeiouáéíóúü])/i', function ($matches) { $match = $matches[0]; if ($match === 'LL') return 'Y'; if ($match === 'Ll') return 'Y'; return 'y'; }, $normalizedString); $normalizedString = preg_replace_callback('/rr/i', function ($matches) { $match = $matches[0]; if ($match === 'RR') return 'R'; if ($match === 'Rr') return 'R'; return 'r'; }, $normalizedString); return $normalizedString; } } ================================================ FILE: src/Core/Normalizers/StringNormalizer.php ================================================ matchedWords = new Collection($matchedWords); } // --- New v4 API --- public function isClean(): bool { return $this->matchedWords->isEmpty(); } public function isOffensive(): bool { return $this->matchedWords->isNotEmpty(); } public function clean(): string { return $this->cleanText; } public function original(): string { return $this->originalText; } public function score(): int { return $this->scoreValue; } public function count(): int { return $this->matchedWords->count(); } public function uniqueWords(): array { return $this->matchedWords->pluck('base')->unique()->values()->all(); } public function severity(): ?Severity { if ($this->matchedWords->isEmpty()) { return null; } return $this->matchedWords ->sortByDesc(fn (MatchedWord $w) => $w->severity->weight()) ->first() ->severity; } public function words(): Collection { return $this->matchedWords; } // --- Deprecated v3 backward-compat methods --- /** @deprecated Use isOffensive() instead */ public function hasProfanity(): bool { return $this->isOffensive(); } /** @deprecated Use clean() instead */ public function getCleanString(): string { return $this->clean(); } /** @deprecated Use original() instead */ public function getSourceString(): string { return $this->original(); } /** @deprecated Use count() instead */ public function getProfanitiesCount(): int { return $this->count(); } /** @deprecated Use uniqueWords() instead */ public function getUniqueProfanitiesFound(): array { return $this->uniqueWords(); } // --- Static constructors --- public static function none(string $text): self { return new self($text, $text, [], 0); } public static function fromArray(array $data): self { $matchedWords = []; foreach ($data['words'] ?? [] as $wordData) { $matchedWords[] = new MatchedWord( text: $wordData['text'], base: $wordData['base'], severity: Severity::tryFrom($wordData['severity']) ?? Severity::High, position: $wordData['position'], length: $wordData['length'], language: $wordData['language'] ?? 'english', ); } return new self( $data['original'] ?? '', $data['clean'] ?? '', $matchedWords, $data['score'] ?? 0, ); } public static function withMatches(array $words, string $originalText = '', string $cleanText = ''): self { $matchedWords = []; foreach ($words as $word) { if ($word instanceof MatchedWord) { $matchedWords[] = $word; } else { $matchedWords[] = new MatchedWord( text: $word, base: $word, severity: Severity::High, position: 0, length: mb_strlen($word), ); } } $totalWords = max(1, count(preg_split('/\s+/u', trim($originalText ?: implode(' ', $words)), -1, PREG_SPLIT_NO_EMPTY))); $score = Score::calculate($matchedWords, $totalWords); return new self($originalText, $cleanText ?: $originalText, $matchedWords, $score); } // --- Serialization --- public function toArray(): array { return [ 'original' => $this->originalText, 'clean' => $this->cleanText, 'is_offensive' => $this->isOffensive(), 'score' => $this->scoreValue, 'count' => $this->count(), 'unique_words' => $this->uniqueWords(), 'severity' => $this->severity()?->value, 'words' => $this->matchedWords->map->toArray()->all(), ]; } public function toJson(int $options = 0): string { return json_encode($this->toArray(), $options); } public function jsonSerialize(): mixed { return $this->toArray(); } public function __toString(): string { return $this->cleanText; } } ================================================ FILE: src/Core/Score.php ================================================ severity->weight(); } $density = count($matchedWords) / max(1, $totalWordCount); $normalized = (int) ($rawScore * (1 + $density)); return min(100, $normalized); } } ================================================ FILE: src/Drivers/PatternDriver.php ================================================ getProfanities(); $falsePositives = array_map(fn($fp) => mb_strtolower($fp, 'UTF-8'), $dictionary->getFalsePositives()); // Sort profanities by length descending for longest-match-first usort($profanities, fn($a, $b) => mb_strlen($b) - mb_strlen($a)); foreach ($profanities as $profanity) { $lowerProfanity = mb_strtolower($profanity, 'UTF-8'); $pattern = '/\b' . preg_quote($lowerProfanity, '/') . '\b/iu'; if (preg_match_all($pattern, $lowerText, $matches, PREG_OFFSET_CAPTURE)) { foreach ($matches[0] as $match) { $start = mb_strlen(substr($lowerText, 0, $match[1]), 'UTF-8'); $length = mb_strlen($match[0], 'UTF-8'); $originalMatch = mb_substr($text, $start, $length); // Skip false positives if (in_array($lowerProfanity, $falsePositives)) { continue; } $matchedWords[] = new MatchedWord( text: $originalMatch, base: $profanity, severity: $dictionary->getSeverity($profanity), position: $start, length: $length, language: $dictionary->getLanguage(), ); } } } // Apply severity filter before dedup so shorter high-severity matches aren't swallowed $minimumSeverity = $options['severity'] ?? null; if ($minimumSeverity instanceof Severity) { $matchedWords = array_values(array_filter( $matchedWords, fn(MatchedWord $w) => $w->severity->isAtLeast($minimumSeverity) )); } // Deduplicate overlapping matches (longest-first already recorded) usort($matchedWords, fn($a, $b) => $a->position - $b->position ?: $b->length - $a->length); $deduplicated = []; $coveredEnd = -1; foreach ($matchedWords as $mw) { if ($mw->position >= $coveredEnd) { $deduplicated[] = $mw; $coveredEnd = $mw->position + $mw->length; } } $matchedWords = $deduplicated; // Rebuild cleanText from surviving matches (right-to-left) $cleanText = $text; $sorted = $matchedWords; usort($sorted, fn($a, $b) => $b->position - $a->position); foreach ($sorted as $word) { $replacement = $mask->mask($word->text, $word->length); $cleanText = mb_substr($cleanText, 0, $word->position) . $replacement . mb_substr($cleanText, $word->position + $word->length); } $totalWords = max(1, count(preg_split('/\s+/u', trim($text), -1, PREG_SPLIT_NO_EMPTY))); $scoreValue = Score::calculate($matchedWords, $totalWords); return new Result($text, $cleanText, $matchedWords, $scoreValue); } } ================================================ FILE: src/Drivers/PhoneticDriver.php ================================================ getLanguage(); $languages = array_map('strtolower', explode(',', $language)); $supported = array_map('strtolower', $this->supportedLanguages); $isSupported = false; foreach ($languages as $lang) { if (in_array(trim($lang), $supported, true)) { $isSupported = true; break; } } if (!$isSupported) { return new Result($text, $text, [], 0); } $filter = new FalsePositiveFilter($dictionary->getFalsePositives()); $matcher = new PhoneticMatcher( profanities: $dictionary->getProfanities(), phonemes: $this->phonemes, minWordLength: $this->minWordLength, maxDistanceRatio: $this->maxDistanceRatio, phoneticFalsePositives: $this->phoneticFalsePositives, ); $normalizer = $dictionary->getNormalizer(); $normalized = $normalizer->normalize($text); // Tokenize preg_match_all('/\b[\w\']+\b/u', $normalized, $matches, PREG_OFFSET_CAPTURE); $tokens = $matches[0] ?? []; $matchedWords = []; foreach ($tokens as $token) { $word = $token[0]; $byteStart = $token[1]; $byteLength = strlen($word); $start = mb_strlen(substr($normalized, 0, $byteStart), 'UTF-8'); $length = mb_strlen($word, 'UTF-8'); // Skip dictionary false positives if ($filter->isFalsePositive($word)) { continue; } // Skip hex/UUID tokens (filter uses byte-level operations) if ($filter->isInsideHexToken($normalized, $byteStart, $byteLength)) { continue; } $baseWord = $matcher->match($word); if ($baseWord === null) { continue; } $originalWord = mb_substr($text, $start, $length); $matchedWords[] = new MatchedWord( text: $originalWord, base: $baseWord, severity: $dictionary->getSeverity($baseWord), position: $start, length: $length, language: $dictionary->getLanguage(), ); } // Apply severity filter $minimumSeverity = $options['severity'] ?? null; if ($minimumSeverity instanceof Severity) { $matchedWords = array_values(array_filter( $matchedWords, fn(MatchedWord $w) => $w->severity->isAtLeast($minimumSeverity) )); } // Rebuild cleanText from surviving matches (right-to-left) $cleanText = $text; $sorted = $matchedWords; usort($sorted, fn($a, $b) => $b->position - $a->position); foreach ($sorted as $word) { $replacement = $mask->mask($word->text, $word->length); $cleanText = mb_substr($cleanText, 0, $word->position) . $replacement . mb_substr($cleanText, $word->position + $word->length); } $totalWords = max(1, count(preg_split('/\s+/u', trim($text), -1, PREG_SPLIT_NO_EMPTY))); $scoreValue = Score::calculate($matchedWords, $totalWords); return new Result($text, $cleanText, $matchedWords, $scoreValue); } } ================================================ FILE: src/Drivers/PipelineDriver.php ================================================ drivers as $driver) { $result = $driver->detect($text, $dictionary, $mask, $options); foreach ($result->words() as $match) { $allMatches[] = $match; } } if (empty($allMatches)) { return new Result($text, $text, [], 0); } // 2. Sort by position ascending, then length descending usort($allMatches, function (MatchedWord $a, MatchedWord $b) { if ($a->position !== $b->position) { return $a->position <=> $b->position; } return $b->length <=> $a->length; }); // 3. Deduplicate overlapping position ranges (greedy, longest-first at each position) $kept = []; foreach ($allMatches as $match) { $overlaps = false; foreach ($kept as $existing) { $existingEnd = $existing->position + $existing->length; $matchEnd = $match->position + $match->length; if ($match->position < $existingEnd && $matchEnd > $existing->position) { $overlaps = true; break; } } if (!$overlaps) { $kept[] = $match; } } // 4. Build clean text by applying masks right-to-left (preserves positions) $cleanText = $text; $reversed = array_reverse($kept); foreach ($reversed as $match) { $replacement = $mask->mask($match->text, $match->length); $cleanText = mb_substr($cleanText, 0, $match->position, 'UTF-8') . $replacement . mb_substr($cleanText, $match->position + $match->length, null, 'UTF-8'); } // 5. Recalculate score from merged matches $totalWords = max(1, count(preg_split('/\s+/u', trim($text), -1, PREG_SPLIT_NO_EMPTY))); $scoreValue = Score::calculate($kept, $totalWords); return new Result($text, $cleanText, $kept, $scoreValue); } } ================================================ FILE: src/Drivers/RegexDriver.php ================================================ filter = new FalsePositiveFilter($dictionary->getFalsePositives()); $this->compoundDetector = new CompoundWordDetector(); $profanityExpressions = $dictionary->getProfanityExpressions(); // Sort by key length descending (longest profanity first) uksort($profanityExpressions, fn($a, $b) => strlen($b) - strlen($a)); $normalizer = $dictionary->getNormalizer(); $normalizedString = $normalizer->normalize($text); $originalNormalized = preg_replace('/\s+/', ' ', $normalizedString); // Immutable copy for position lookups — never mutated $immutableNormalized = $originalNormalized; $matchedWords = []; $uniqueMap = []; $profanitiesCount = 0; $continue = true; // Track masked character ranges so we don't re-match them $maskedRanges = []; while ($continue) { $continue = false; $normalizedString = preg_replace('/\s+/', ' ', $normalizedString); foreach ($profanityExpressions as $profanity => $expression) { preg_match_all($expression, $normalizedString, $matches, PREG_OFFSET_CAPTURE); if (!empty($matches[0])) { foreach ($matches[0] as $match) { $byteStart = $match[1]; $byteLength = strlen($match[0]); $start = mb_strlen(substr($normalizedString, 0, $byteStart), 'UTF-8'); $length = mb_strlen($match[0], 'UTF-8'); $matchedText = $match[0]; // Skip if this range overlaps with an already-masked range $matchEnd = $start + $length; $alreadyMasked = false; foreach ($maskedRanges as [$mStart, $mEnd]) { if ($start < $mEnd && $matchEnd > $mStart) { $alreadyMasked = true; break; } } if ($alreadyMasked) { continue; } // Check word boundary spanning (filter uses byte-level operations) if ($this->filter->isSpanningWordBoundary($matchedText, $normalizedString, $byteStart)) { continue; } // Check hex/UUID token (filter uses byte-level operations) if ($this->filter->isInsideHexToken($normalizedString, $byteStart, $byteLength)) { continue; } // Full word context for false positive check (filter uses byte-level operations) $fullWord = $this->filter->getFullWordContext($normalizedString, $byteStart, $byteLength); // Check pure alpha substring against original (unmasked) normalized $originalFullWord = $this->filter->getFullWordContext($immutableNormalized, $byteStart, $byteLength); if ($this->compoundDetector->isPureAlphaSubstring($matchedText, $originalFullWord, $profanity, $profanityExpressions)) { continue; } // False positive check if ($this->filter->isFalsePositive($fullWord)) { continue; } $continue = true; // Mask in normalizedString only (needed for loop termination) // Use SOH control char internally to avoid re-matching when '*' is // a valid substitution character in profanity patterns $normalizedString = mb_substr($normalizedString, 0, $start) . str_repeat("\x01", $length) . mb_substr($normalizedString, $start + $length); // Record masked range using character positions from immutable string $maskedRanges[] = [$start, $matchEnd]; // Track match — use position derived from immutable normalized string $profanitiesCount++; // Get the original text at this position from the original input $originalMatchText = mb_substr($text, $start, $length); $matchedWords[] = new MatchedWord( text: $originalMatchText, base: $profanity, severity: $dictionary->getSeverity($profanity), position: $start, length: $length, language: $dictionary->getLanguage(), ); if (!isset($uniqueMap[$profanity])) { $uniqueMap[$profanity] = true; } } } } } // Apply severity filter before masking so low-severity matches don't suppress overlapping ones $minimumSeverity = $options['severity'] ?? null; if ($minimumSeverity instanceof Severity) { $matchedWords = array_values(array_filter( $matchedWords, fn(MatchedWord $w) => $w->severity->isAtLeast($minimumSeverity) )); } // Rebuild cleanText from surviving matches (right-to-left) $workingCleanString = $text; $sorted = $matchedWords; usort($sorted, fn($a, $b) => $b->position - $a->position); foreach ($sorted as $word) { $replacement = $mask->mask($word->text, $word->length); $workingCleanString = mb_substr($workingCleanString, 0, $word->position) . $replacement . mb_substr($workingCleanString, $word->position + $word->length); } $totalWords = max(1, count(preg_split('/\s+/u', trim($text), -1, PREG_SPLIT_NO_EMPTY))); $scoreValue = Score::calculate($matchedWords, $totalWords); return new Result($text, $workingCleanString, $matchedWords, $scoreValue); } } ================================================ FILE: src/Enums/Severity.php ================================================ 5, self::Moderate => 15, self::High => 30, self::Extreme => 50, }; } public function isAtLeast(self $minimum): bool { return $this->weight() >= $minimum->weight(); } } ================================================ FILE: src/Events/ContentBlocked.php ================================================ uniqueWords())); } public static function forModel(Model $model, string $attribute, Result $result): static { return new static($model, $attribute, $result); } } ================================================ FILE: src/Facades/Blasp.php ================================================ assertChecked(); } public static function assertCheckedTimes(int $times): void { $instance = static::getFacadeRoot(); if (!$instance instanceof BlaspFake) { throw new \RuntimeException('Blasp::assertCheckedTimes() requires Blasp::fake() to be called first.'); } $instance->assertCheckedTimes($times); } } ================================================ FILE: src/Middleware/CheckProfanity.php ================================================ only($fields))->except($except)->all(); } else { $input = $request->except($except); } $textFields = $this->extractTextFields($input); foreach ($textFields as $field => $value) { $pendingCheck = $this->manager->newPendingCheck(); if ($minimumSeverity) { $pendingCheck = $pendingCheck->withSeverity($minimumSeverity); } $result = $pendingCheck->check($value); if ($result->isOffensive()) { if (config('blasp.events', false)) { event(new ContentBlocked($result, $request, $field, $action)); } if ($action === 'reject') { return response()->json([ 'message' => 'The request contains inappropriate content.', 'errors' => [$field => ['The ' . $field . ' field contains profanity.']], ], 422); } if ($action === 'sanitize') { $request->merge([$field => $result->clean()]); } } } return $next($request); } protected function extractTextFields(array $input): array { $fields = []; foreach ($input as $key => $value) { if (is_string($value) && !empty(trim($value))) { $fields[$key] = $value; } } return $fields; } } ================================================ FILE: src/PendingCheck.php ================================================ manager = $manager; } // --- Fluent builder methods --- public function driver(string $driver): self { $this->driverName = $driver; return $this; } public function in(string ...$languages): self { $this->languages = $languages; return $this; } public function inAllLanguages(): self { $this->allLanguages = true; return $this; } public function mask(string|Closure $mask): self { if ($mask instanceof Closure) { $this->maskStrategy = new CallbackMask($mask); } elseif ($mask === 'grawlix') { $this->maskStrategy = new GrawlixMask(); } else { $this->maskStrategy = new CharacterMask($mask); } return $this; } public function allow(string ...$words): self { $this->allowList = array_merge($this->allowList, $words); return $this; } public function block(string ...$words): self { $this->blockList = array_merge($this->blockList, $words); return $this; } public function withSeverity(Severity $severity): self { $this->minimumSeverity = $severity; return $this; } public function strict(): self { $this->strictMode = true; $this->lenientMode = false; return $this; } public function lenient(): self { $this->lenientMode = true; $this->strictMode = false; return $this; } public function pipeline(string ...$drivers): self { $this->pipelineDrivers = $drivers; return $this; } // --- Deprecated backward-compat builder methods --- /** @deprecated Use mask() instead */ public function maskWith(string $character): self { return $this->mask($character); } /** @deprecated Use inAllLanguages() instead */ public function allLanguages(): self { return $this->inAllLanguages(); } /** @deprecated Use in() instead */ public function language(string $language): self { return $this->in($language); } // --- Language shortcuts --- public function english(): self { return $this->in('english'); } public function spanish(): self { return $this->in('spanish'); } public function german(): self { return $this->in('german'); } public function french(): self { return $this->in('french'); } // --- Configure (backward-compat) --- public function configure(?array $profanities = null, ?array $falsePositives = null): self { if ($profanities !== null) { $this->blockList = array_merge($this->blockList, $profanities); } return $this; } // --- Execute --- public function check(?string $text): Result { $text = $text ?? ''; if ($this->shouldCache()) { $cacheKey = $this->buildCacheKey($text); $cache = $this->getCache(); $ttl = config('blasp.cache.ttl', 86400); $cached = $cache->get($cacheKey); if ($cached !== null) { return Result::fromArray($cached); } $result = $this->performCheck($text); $cache->put($cacheKey, $result->toArray(), $ttl); $this->trackCacheKey($cacheKey); return $result; } return $this->performCheck($text); } protected function performCheck(string $text): Result { $dictionary = $this->buildDictionary(); $driver = $this->resolveDriver(); $mask = $this->resolveMask(); $options = []; if ($this->minimumSeverity !== null) { $options['severity'] = $this->minimumSeverity; } $analyzer = new Analyzer(); $result = $analyzer->analyze($text, $driver, $dictionary, $mask, $options); // Fire event if configured if ($result->isOffensive() && config('blasp.events', false)) { event(new ProfanityDetected($result, $text)); } return $result; } public function checkMany(array $texts): array { $results = []; foreach ($texts as $key => $text) { $results[$key] = $this->check($text); } return $results; } // --- Internal --- protected function buildDictionary(): Dictionary { $options = [ 'allow' => array_merge(config('blasp.allow', []), $this->allowList), 'block' => array_merge(config('blasp.block', []), $this->blockList), ]; if ($this->allLanguages) { return Dictionary::forAllLanguages($options); } if (!empty($this->languages)) { if (count($this->languages) === 1) { return Dictionary::forLanguage($this->languages[0], $options); } return Dictionary::forLanguages($this->languages, $options); } $defaultLanguage = config('blasp.language', config('blasp.default_language', 'english')); return Dictionary::forLanguage($defaultLanguage, $options); } protected function resolveDriver(): \Blaspsoft\Blasp\Core\Contracts\DriverInterface { if ($this->pipelineDrivers !== null) { $resolved = array_map( fn (string $name) => $this->manager->resolveDriver($name), $this->pipelineDrivers, ); return new PipelineDriver($resolved); } $driverName = $this->driverName ?? $this->manager->getDefaultDriver(); if ($this->lenientMode) { $driverName = 'pattern'; } return $this->manager->resolveDriver($driverName); } protected function resolveMask(): MaskStrategyInterface { if ($this->maskStrategy !== null) { return $this->maskStrategy; } $maskConfig = config('blasp.mask', config('blasp.mask_character', '*')); return new CharacterMask($maskConfig); } // --- Caching --- protected function shouldCache(): bool { if (!config('blasp.cache.enabled', true)) { return false; } if (!config('blasp.cache.results', true)) { return false; } if ($this->maskStrategy instanceof CallbackMask) { return false; } return true; } protected function buildCacheKey(string $text): string { $parts = [ 'text' => $text, 'driver' => $this->driverName ?? config('blasp.default', 'regex'), 'pipeline' => $this->pipelineDrivers, 'languages' => $this->languages, 'all_languages' => $this->allLanguages, 'allow' => $this->allowList, 'block' => $this->blockList, 'severity' => $this->minimumSeverity?->value, 'strict' => $this->strictMode, 'lenient' => $this->lenientMode, 'mask' => $this->maskStrategy ? serialize($this->maskStrategy) : null, ]; return 'blasp_result_' . md5(serialize($parts)); } protected function getCache(): \Illuminate\Contracts\Cache\Repository { $driver = config('blasp.cache.driver', config('blasp.cache_driver')); return $driver !== null ? Cache::store($driver) : Cache::store(); } protected function trackCacheKey(string $key): void { $cache = $this->getCache(); $keys = $cache->get('blasp_result_cache_keys', []); $keys[] = $key; $keys = array_unique($keys); // Evict oldest keys when exceeding the configured limit $maxKeys = config('blasp.cache.max_tracked_keys', 1000); if (count($keys) > $maxKeys) { $keys = array_slice($keys, -$maxKeys); } $cache->forever('blasp_result_cache_keys', $keys); } } ================================================ FILE: src/Rules/Profanity.php ================================================ language = $language; return $this; } public function maxScore(int $score): self { $this->maxScore = $score; return $this; } public function severity(Severity $severity): self { $this->minimumSeverity = $severity; return $this; } public static function __callStatic(string $name, array $arguments): self { return (new self())->$name(...$arguments); } public function validate(string $attribute, mixed $value, Closure $fail): void { if (!is_string($value)) { return; } $manager = app('blasp'); $pendingCheck = $manager->newPendingCheck(); if ($this->language) { $pendingCheck = $pendingCheck->in($this->language); } if ($this->minimumSeverity) { $pendingCheck = $pendingCheck->withSeverity($this->minimumSeverity); } $result = $pendingCheck->check($value); if ($this->maxScore !== null) { if ($result->score() > $this->maxScore) { $fail('The :attribute contains profanity.'); } return; } if ($result->isOffensive()) { $fail('The :attribute contains profanity.'); } } } ================================================ FILE: src/Testing/BlaspFake.php ================================================ fakeResults = $fakeResults; } public function check(?string $text): Result { $text = $text ?? ''; $this->checksPerformed[] = $text; if (isset($this->fakeResults[$text])) { return $this->fakeResults[$text]; } return Result::none($text); } public function checkMany(array $texts): array { $results = []; foreach ($texts as $key => $text) { $results[$key] = $this->check($text); } return $results; } public function assertChecked(): void { Assert::assertNotEmpty($this->checksPerformed, 'Expected at least one check to be performed.'); } public function assertCheckedTimes(int $times): void { Assert::assertCount( $times, $this->checksPerformed, "Expected {$times} checks but " . count($this->checksPerformed) . ' were performed.' ); } public function assertCheckedWith(string $text): void { Assert::assertContains($text, $this->checksPerformed, "Expected check with text: {$text}"); } // Builder methods return self (no-op in fake mode, just pass through to check) public function __call(string $method, array $parameters): self { return $this; } public function in(string ...$languages): self { return $this; } public function inAllLanguages(): self { return $this; } public function allLanguages(): self { return $this; } public function english(): self { return $this; } public function spanish(): self { return $this; } public function german(): self { return $this; } public function french(): self { return $this; } public function mask(string $mask): self { return $this; } public function maskWith(string $character): self { return $this; } public function language(string $language): self { return $this; } public function driver(string $driver): self { return $this; } public function configure(?array $profanities = null, ?array $falsePositives = null): self { return $this; } } ================================================ FILE: tests/AllLanguagesApiTest.php ================================================ check('This is fucking amazing'); $this->assertTrue($result->hasProfanity()); $this->assertEquals('This is ******* amazing', $result->getCleanString()); $result = Blasp::allLanguages()->check('esto es una mierda'); $this->assertTrue($result->hasProfanity()); $this->assertEquals('esto es una ******', $result->getCleanString()); $result = Blasp::allLanguages()->check('das ist scheiße'); $this->assertTrue($result->hasProfanity()); $this->assertEquals('das ist *******', $result->getCleanString()); $result = Blasp::allLanguages()->check('c\'est de la merde'); $this->assertTrue($result->hasProfanity()); $this->assertEquals('c\'est de la *****', $result->getCleanString()); } public function test_mixed_language_content() { $result = Blasp::allLanguages()->check('This shit is mierda and scheiße'); $this->assertTrue($result->hasProfanity()); $this->assertEquals('This **** is ****** and *******', $result->getCleanString()); $this->assertEquals(3, $result->getProfanitiesCount()); } public function test_chainable_all_languages() { $result = Blasp::allLanguages()->check('damn merde'); $this->assertTrue($result->hasProfanity()); } public function test_language_shortcuts_vs_all() { $text = 'fucking merde scheiße mierda'; $englishResult = Blasp::english()->check($text); $this->assertEquals(1, $englishResult->getProfanitiesCount()); $allResult = Blasp::allLanguages()->check($text); $this->assertEquals(4, $allResult->getProfanitiesCount()); $this->assertStringNotContainsString('fucking', $allResult->getCleanString()); $this->assertStringNotContainsString('merde', $allResult->getCleanString()); $this->assertStringNotContainsString('scheiße', $allResult->getCleanString()); $this->assertStringContainsString('*******', $allResult->getCleanString()); } public function test_direct_manager_all_languages() { $manager = app('blasp'); $result = $manager->inAllLanguages()->check('This fuck is merde'); $this->assertTrue($result->hasProfanity()); $this->assertEquals(2, $result->getProfanitiesCount()); } public function test_configure_with_all_languages() { $result = Blasp::allLanguages() ->block('customword') ->check('customword and fuck'); $this->assertTrue($result->hasProfanity()); $this->assertStringContainsString('*', $result->getCleanString()); } } ================================================ FILE: tests/AllLanguagesDetectionTest.php ================================================ [ 'text' => 'You are a fucking cunt', 'expected_profanities' => ['fucking', 'cunt'], 'min_count' => 2 ], 'german' => [ 'text' => 'Du bist eine verdammte Fotze', 'expected_profanities' => ['verdammte', 'fotze'], 'min_count' => 2 ], 'french' => [ 'text' => 'Tu es un putain de connard', 'expected_profanities' => ['putain', 'connard'], 'min_count' => 2 ], 'spanish' => [ 'text' => 'Eres un maldito hijo de puta', 'expected_profanities' => ['maldito', 'hijo de puta', 'puta'], 'min_count' => 2 ] ]; foreach ($testCases as $language => $testCase) { $result = Blasp::in($language)->check($testCase['text']); $this->assertTrue( $result->isOffensive(), "[$language] Failed to detect profanities in: {$testCase['text']}" ); $this->assertGreaterThanOrEqual( $testCase['min_count'], $result->count(), "[$language] Expected at least {$testCase['min_count']} profanities, got {$result->count()}" ); foreach ($testCase['expected_profanities'] as $profanity) { $this->assertStringNotContainsString( $profanity, strtolower($result->clean()), "[$language] '$profanity' was not censored" ); } $this->assertStringContainsString( '*', $result->clean(), "[$language] No asterisks found in censored string" ); } } public function test_language_variations() { $variations = [ 'german' => [ 'verdammte' => ['VERDAMMTE', 'Verdammte', 'verdammte', 'VeRdAmMtE'], 'scheisse' => ['SCHEISSE', 'Scheisse', 'scheisse', 'ScHeIsSe', 'scheisse'] ], 'french' => [ 'merde' => ['MERDE', 'Merde', 'merde', 'MeRdE'], 'putain' => ['PUTAIN', 'Putain', 'putain', 'PuTaIn'] ], 'spanish' => [ 'mierda' => ['MIERDA', 'Mierda', 'mierda', 'MiErDa'], 'joder' => ['JODER', 'Joder', 'joder', 'JoDeR'] ], 'english' => [ 'fuck' => ['FUCK', 'Fuck', 'fuck', 'FuCk', 'f@ck', 'f*ck'], 'shit' => ['SHIT', 'Shit', 'shit', 'ShIt', 'sh1t', 'sh!t'] ] ]; foreach ($variations as $language => $words) { foreach ($words as $base => $variants) { foreach ($variants as $variant) { $testText = "This contains $variant here"; $result = Blasp::in($language)->check($testText); $this->assertTrue( $result->isOffensive(), "[$language] Failed to detect variant '$variant' of '$base'" ); } } } } public function test_language_normalizers() { // German-specific: umlauts and eszett $germanTests = ['scheisse', 'Scheisse', 'SCHEISSE']; foreach ($germanTests as $input) { $result = Blasp::german()->check("Das ist $input test"); $this->assertTrue( $result->isOffensive(), "German normalizer failed for '$input'" ); } // French-specific: accents $frenchTests = ['connard', 'CONNARD', 'Connard']; foreach ($frenchTests as $input) { $result = Blasp::french()->check("C'est un $input ici"); $this->assertTrue( $result->isOffensive(), "French normalizer failed for '$input'" ); } } } ================================================ FILE: tests/BladeDirectiveTest.php ================================================ ' . $compiled); return ob_get_clean(); } public function test_clean_directive_masks_profane_text() { $output = $this->renderBlade('@clean($text)', ['text' => 'This is a fucking sentence']); $this->assertStringNotContainsString('fucking', $output); $this->assertStringContainsString('*', $output); } public function test_clean_directive_passes_clean_text_unchanged() { $output = $this->renderBlade('@clean($text)', ['text' => 'This is a clean sentence']); $this->assertSame('This is a clean sentence', $output); } public function test_clean_directive_escapes_html_for_xss_safety() { $output = $this->renderBlade('@clean($text)', ['text' => '']); $this->assertStringNotContainsString('