[
  {
    "path": ".github/workflows/main.yml",
    "content": "name: Run Tests\n\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      # Step 1: Check out the repository\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      # Step 2: Set up PHP\n      - name: Set up PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: \"8.2\" # Set your desired PHP version\n          extensions: mbstring, dom, zip\n\n      # Step 3: Install Composer dependencies\n      - name: Install dependencies\n        run: composer install --no-interaction --prefer-dist\n\n      # Step 4: Run PHPUnit tests\n      - name: Run tests\n        run: php ./vendor/bin/phpunit\n"
  },
  {
    "path": ".gitignore",
    "content": "/vendor\ncomposer.lock\n.phpunit.result.cache\n/.idea"
  },
  {
    "path": ".styleci.yml",
    "content": "preset: laravel\n\ndisabled:\n  - single_class_element_per_statement\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to `blasp` will be documented in this file\n\n## 3.0.0 - 2025-01-05\n\n### Added\n- Custom mask character support with `maskWith()` method\n- Simplified API with Laravel facade pattern and method chaining\n- Comprehensive multi-language support (Spanish, German, French)\n- Expanded test coverage across all languages\n- Comprehensive extensibility system with full test coverage\n- Basic registry pattern for language normalizers\n- Language files publishing to ServiceProvider\n- Comprehensive documentation for maskWith() and all chainable methods\n\n### Changed\n- Implemented dependency injection and simplified service dependencies\n- Extracted expression generation logic to dedicated generator\n- Improved substitution detection across all languages\n- Updated README with simplified chainable API documentation\n- Updated README with comprehensive multi-language support documentation\n- Updated README with language files publishing options\n- Updated README for v3.0 features\n\n### Fixed\n- Resolved language switching not loading correct profanities\n- Prevented cross-word-boundary profanity matches\n\n### Removed\n- Strategy factory, plugin manager, and default detection strategy\n- Domain-specific detection strategies (email, URL, phone)\n- Unused strict() and lenient() detection modes\n- README duplications and outdated references\n\n## 1.0.0 - 201X-XX-XX\n\n- initial release\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nContributions are **welcome** and will be fully **credited**.\n\nPlease read and understand the contribution guide before creating an issue or pull request.\n\n## Etiquette\n\nThis project is open source, and as such, the maintainers give their free time to build and maintain the source code\nheld within. They make the code freely available in the hope that it will be of use to other developers. It would be\nextremely unfair for them to suffer abuse or anger for their hard work.\n\nPlease be considerate towards maintainers when raising issues or presenting pull requests. Let's show the\nworld that developers are civilized and selfless people.\n\nIt's the duty of the maintainer to ensure that all submissions to the project are of sufficient\nquality 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.\n\n## Viability\n\nWhen requesting or submitting new features, first consider whether it might be useful to others. Open\nsource projects are used by many developers, who may have entirely different needs to your own. Think about\nwhether or not your feature is likely to be used by other users of the project.\n\n## Procedure\n\nBefore filing an issue:\n\n- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident.\n- Check to make sure your feature suggestion isn't already present within the project.\n- Check the pull requests tab to ensure that the bug doesn't have a fix in progress.\n- Check the pull requests tab to ensure that the feature isn't already in progress.\n\nBefore submitting a pull request:\n\n- Check the codebase to ensure that your feature doesn't already exist.\n- Check the pull requests to ensure that another person hasn't already submitted the feature or fix.\n\n## Requirements\n\nIf the project maintainer has any additional requirements, you will find them listed here.\n\n- **[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).\n\n- **Add tests!** - Your patch won't be accepted if it doesn't have tests.\n\n- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.\n\n- **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option.\n\n- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.\n\n- **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.\n\n**Happy coding**!\n"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\n\nCopyright (c) Michael Deeming\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <img src=\"./assets/icon.png\" alt=\"Blasp Icon\" width=\"150\" height=\"150\"/>\n</p>\n\n> **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.\n\n<p align=\"center\">\n    <a href=\"https://github.com/Blaspsoft/blasp/actions/workflows/main.yml\"><img alt=\"GitHub Workflow Status (main)\" src=\"https://github.com/Blaspsoft/blasp/actions/workflows/main.yml/badge.svg\"></a>\n    <a href=\"https://packagist.org/packages/blaspsoft/blasp\"><img alt=\"Total Downloads\" src=\"https://img.shields.io/packagist/dt/blaspsoft/blasp\"></a>\n    <a href=\"https://packagist.org/packages/blaspsoft/blasp\"><img alt=\"Latest Version\" src=\"https://img.shields.io/packagist/v/blaspsoft/blasp\"></a>\n    <a href=\"https://packagist.org/packages/blaspsoft/blasp\"><img alt=\"License\" src=\"https://img.shields.io/packagist/l/blaspsoft/blasp\"></a>\n</p>\n\n# Blasp - Advanced Profanity Filter for Laravel\n\nBlasp 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.\n\n## Features\n\n- **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.\n- **Multi-Language** — English, Spanish, German, French with language-specific normalizers. Check one, many, or all at once.\n- **Severity Scoring** — Words categorised as mild/moderate/high/extreme. Filter by minimum severity and get a 0-100 score.\n- **Masking Strategies** — Character mask (`*`, `#`), grawlix (`!@#$%`), or a custom callback.\n- **Eloquent Integration** — `Blaspable` trait auto-sanitizes or rejects profanity on model save.\n- **Middleware** — Reject or sanitize profane request fields with configurable severity.\n- **Validation Rules** — Fluent validation rule with language, severity, and score threshold support.\n- **Testing Utilities** — `Blasp::fake()` for test doubles with assertions.\n- **Events** — `ProfanityDetected`, `ContentBlocked`, and `ModelProfanityDetected`.\n\n## Requirements\n\n- PHP 8.2+\n- Laravel 8.0+\n\n## Installation\n\n```bash\ncomposer require blaspsoft/blasp\n```\n\nPublish configuration:\n\n```bash\n# Everything (config + language files)\nphp artisan vendor:publish --tag=\"blasp\"\n\n# Config only\nphp artisan vendor:publish --tag=\"blasp-config\"\n\n# Language files only\nphp artisan vendor:publish --tag=\"blasp-languages\"\n```\n\n## Quick Start\n\n```php\nuse Blaspsoft\\Blasp\\Facades\\Blasp;\n\n$result = Blasp::check('This is a fucking sentence');\n\n$result->isOffensive();  // true\n$result->clean();         // \"This is a ******* sentence\"\n$result->original();      // \"This is a fucking sentence\"\n$result->score();         // 30\n$result->count();         // 1\n$result->uniqueWords();   // ['fucking']\n$result->severity();      // Severity::High\n```\n\n## Fluent API\n\nAll builder methods return a `PendingCheck` and can be chained:\n\n```php\n// Language selection\nBlasp::in('spanish')->check($text);\nBlasp::in('english', 'french')->check($text);\nBlasp::inAllLanguages()->check($text);\n\n// Language shortcuts\nBlasp::english()->check($text);\nBlasp::spanish()->check($text);\nBlasp::german()->check($text);\nBlasp::french()->check($text);\n\n// Driver selection\nBlasp::driver('regex')->check($text);     // Full obfuscation detection (default)\nBlasp::driver('pattern')->check($text);   // Fast exact matching\nBlasp::driver('phonetic')->check($text);  // Sound-alike detection (e.g. \"phuck\", \"sheit\")\nBlasp::driver('pipeline')->check($text);  // Chain multiple drivers (config-based)\n\n// Ad-hoc pipeline — chain any drivers without config\nBlasp::pipeline('regex', 'phonetic')->check($text);\nBlasp::pipeline('pattern', 'phonetic')->in('english')->mask('#')->check($text);\n\n// Shorthand modes\nBlasp::strict()->check($text);   // Forces regex driver\nBlasp::lenient()->check($text);  // Forces pattern driver\n\n// Masking\nBlasp::mask('*')->check($text);        // Character mask (default)\nBlasp::mask('#')->check($text);        // Custom character\nBlasp::mask('grawlix')->check($text);  // !@#$% cycling\nBlasp::mask(fn($word, $len) => '[CENSORED]')->check($text);  // Callback\n\n// Severity filtering\nuse Blaspsoft\\Blasp\\Enums\\Severity;\nBlasp::withSeverity(Severity::High)->check($text);  // Ignores mild/moderate\n\n// Allow/block lists (merged with config)\nBlasp::allow('damn', 'hell')->check($text);\nBlasp::block('customword')->check($text);\n\n// Chain everything\nBlasp::spanish()\n    ->mask('#')\n    ->withSeverity(Severity::Moderate)\n    ->check($text);\n\n// Batch checking\n$results = Blasp::checkMany(['text one', 'text two']);\n```\n\n## Result Object\n\nThe `Result` object is returned by every `check()` call:\n\n| Method | Returns | Description |\n|--------|---------|-------------|\n| `isOffensive()` | `bool` | Text contains profanity |\n| `isClean()` | `bool` | Text is clean |\n| `clean()` | `string` | Text with profanities masked |\n| `original()` | `string` | Original unmodified text |\n| `score()` | `int` | Severity score (0-100) |\n| `count()` | `int` | Total profanity matches |\n| `uniqueWords()` | `array` | Unique base words detected |\n| `severity()` | `?Severity` | Highest severity in matches |\n| `words()` | `Collection` | `MatchedWord` objects with position, length, severity |\n| `toArray()` | `array` | Full result as array |\n| `toJson()` | `string` | Full result as JSON |\n\n`Result` implements `JsonSerializable`, `Stringable` (returns clean text), and `Countable`.\n\n## Detection Types\n\nThe regex driver detects obfuscated profanity:\n\n| Type | Example | Detected As |\n|------|---------|-------------|\n| Straight match | `fucking` | `fucking` |\n| Substitution | `fÛck!ng`, `f4ck` | `fucking`, `fuck` |\n| Separators | `f-u-c-k-i-n-g`, `f@ck` | `fucking`, `fuck` |\n| Doubled | `ffuucckkiinngg` | `fucking` |\n| Combination | `f-uuck!ng` | `fucking` |\n\n> **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.\n\nThe pattern driver only detects straight word-boundary matches.\n\nThe phonetic driver uses `metaphone()` + Levenshtein distance to catch words that *sound like* profanity but are spelled differently:\n\n| Type | Example | Detected As |\n|------|---------|-------------|\n| Phonetic spelling | `phuck` | `fuck` |\n| Shortened form | `fuk` | `fuck` |\n| Sound-alike | `sheit` | `shit` |\n\nConfigure sensitivity in `config/blasp.php` under `drivers.phonetic`. A curated false-positive list prevents common words like \"fork\", \"duck\", and \"beach\" from being flagged.\n\n### Pipeline Driver\n\nThe 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.\n\n```php\n// Config-based: set 'default' => 'pipeline' or use driver('pipeline')\nBlasp::driver('pipeline')->check('phuck this sh1t');\n\n// Ad-hoc: pick drivers on the fly (no config needed)\nBlasp::pipeline('regex', 'phonetic')->check('phuck this sh1t');\nBlasp::pipeline('regex', 'pattern', 'phonetic')->check($text);\n```\n\nWhen 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.\n\nConfigure the default sub-drivers in `config/blasp.php`:\n\n```php\n'drivers' => [\n    'pipeline' => [\n        'drivers' => ['regex', 'phonetic'],  // Drivers to chain\n    ],\n],\n```\n\n## Eloquent Integration\n\nThe `Blaspable` trait automatically checks model attributes during save:\n\n```php\nuse Blaspsoft\\Blasp\\Blaspable;\n\nclass Comment extends Model\n{\n    use Blaspable;\n\n    protected array $blaspable = ['body', 'title'];\n}\n```\n\n```php\n// Sanitize mode (default) — profanity is masked, model saves\n$comment = Comment::create(['body' => 'This is fucking great']);\n$comment->body; // \"This is ******* great\"\n\n// Check what happened\n$comment->hadProfanity();            // true\n$comment->blaspResults();            // ['body' => Result, 'title' => Result]\n$comment->blaspResult('body');       // Result instance\n```\n\n### Per-Model Overrides\n\n```php\nclass Comment extends Model\n{\n    use Blaspable;\n\n    protected array $blaspable = ['body', 'title'];\n    protected string $blaspMode = 'reject';     // 'sanitize' (default) | 'reject'\n    protected string $blaspLanguage = 'spanish'; // null = config default\n    protected string $blaspMask = '#';           // null = config default\n}\n```\n\n### Reject Mode\n\nIn reject mode, saving a model with profanity throws `ProfanityRejectedException` and the model is not persisted:\n\n```php\nuse Blaspsoft\\Blasp\\Exceptions\\ProfanityRejectedException;\n\ntry {\n    $comment = Comment::create(['body' => 'profane text']);\n} catch (ProfanityRejectedException $e) {\n    $e->attribute; // 'body'\n    $e->result;    // Result instance\n    $e->model;     // The unsaved model\n}\n```\n\n### Disabling Checking\n\n```php\nComment::withoutBlaspChecking(function () {\n    Comment::create(['body' => 'unchecked content']);\n});\n```\n\n### Events\n\nA `ModelProfanityDetected` event fires whenever profanity is detected on a model attribute (both sanitize and reject modes):\n\n```php\nuse Blaspsoft\\Blasp\\Events\\ModelProfanityDetected;\n\nEvent::listen(ModelProfanityDetected::class, function ($event) {\n    $event->model;     // The model instance\n    $event->attribute; // Which attribute had profanity\n    $event->result;    // Result instance\n});\n```\n\n## Middleware\n\nUse `CheckProfanity` to filter incoming request fields. A `blasp` middleware alias is registered automatically:\n\n```php\n// Using the short alias (recommended)\nRoute::post('/comment', CommentController::class)\n    ->middleware('blasp');\n\n// With parameters: action, severity\nRoute::post('/comment', CommentController::class)\n    ->middleware('blasp:sanitize,mild');\n\n// Or using the class directly\nuse Blaspsoft\\Blasp\\Middleware\\CheckProfanity;\n\nRoute::post('/comment', CommentController::class)\n    ->middleware(CheckProfanity::class);\n```\n\n| Action | Behaviour |\n|--------|-----------|\n| `reject` (default) | Returns 422 JSON with field errors |\n| `sanitize` | Replaces profane fields in the request and continues |\n\nConfigure which fields to check in `config/blasp.php`:\n\n```php\n'middleware' => [\n    'action' => 'reject',\n    'fields' => ['*'],                            // '*' = all fields\n    'except' => ['password', 'email', '_token'],  // Always skipped\n    'severity' => 'mild',\n],\n```\n\n## Validation Rules\n\n### String Rule\n\n```php\n$request->validate([\n    'comment' => ['required', 'blasp_check'],\n    'bio'     => ['required', 'blasp_check:spanish'],\n]);\n```\n\n### Fluent Rule Object\n\n```php\nuse Blaspsoft\\Blasp\\Rules\\Profanity;\nuse Blaspsoft\\Blasp\\Enums\\Severity;\n\n$request->validate([\n    'comment' => ['required', Profanity::in('english')],\n    'bio'     => ['required', Profanity::severity(Severity::High)],\n    'tagline' => ['required', Profanity::maxScore(50)],\n]);\n```\n\n## Blade Directive\n\nThe `@clean` directive sanitizes and escapes text for safe display in views:\n\n```blade\n<p>@clean($comment->body)</p>\n\n{{-- Equivalent to: {{ app('blasp')->check($comment->body)->clean() }} --}}\n```\n\nOutput is HTML-escaped via `e()` for XSS safety.\n\n## Str / Stringable Macros\n\nBlasp registers macros on Laravel's `Str` and `Stringable` classes:\n\n```php\nuse Illuminate\\Support\\Str;\n\n// Static methods\nStr::isProfane('fuck this');        // true\nStr::isProfane('hello');            // false\nStr::cleanProfanity('fuck this');   // '**** this'\nStr::cleanProfanity('hello');       // 'hello'\n\n// Fluent Stringable methods\nStr::of('fuck this')->isProfane();          // true\nStr::of('fuck this')->cleanProfanity();     // Stringable('**** this')\nStr::of('hello')->cleanProfanity()->upper(); // 'HELLO' (chaining works)\n```\n\n## Configuration\n\nFull `config/blasp.php` reference:\n\n```php\nreturn [\n    'default'   => env('BLASP_DRIVER', 'regex'),       // 'regex' | 'pattern' | 'phonetic' | 'pipeline'\n    'language'  => env('BLASP_LANGUAGE', 'english'),    // Default language\n    'mask'      => '*',                                 // Default mask character\n    'severity'  => 'mild',                              // Minimum severity\n    'events'    => false,                               // Fire ProfanityDetected events\n\n    'cache' => [\n        'enabled' => true,\n        'driver'  => env('BLASP_CACHE_DRIVER'),\n        'ttl'     => 86400,\n        'results' => true,          // Cache check() results by content hash\n    ],\n\n    'middleware' => [\n        'action'   => 'reject',\n        'fields'   => ['*'],\n        'except'   => ['password', 'email', '_token'],\n        'severity' => 'mild',\n    ],\n\n    'model' => [\n        'mode' => env('BLASP_MODEL_MODE', 'sanitize'),  // 'sanitize' | 'reject'\n    ],\n\n    'drivers' => [\n        'pipeline' => [\n            'drivers' => ['regex', 'phonetic'],    // Sub-drivers to chain\n        ],\n        'phonetic' => [\n            'phonemes' => 4,                       // metaphone code length (2-8)\n            'min_word_length' => 3,                // skip short words\n            'max_distance_ratio' => 0.6,           // levenshtein threshold (0.3-0.8)\n            'supported_languages' => ['english'],  // metaphone is English-oriented\n            'false_positives' => ['fork', '...'],  // never flag these words\n        ],\n    ],\n\n    'allow'  => [],    // Global allow-list\n    'block'  => [],    // Global block-list\n\n    'separators'      => [...],  // Characters treated as separators\n    'substitutions'   => [...],  // Character leet-speak mappings\n    'false_positives' => [...],  // Words that should never be flagged\n];\n```\n\n## Custom Drivers\n\nImplement `DriverInterface` and register with the manager:\n\n```php\nuse Blaspsoft\\Blasp\\Core\\Contracts\\DriverInterface;\nuse Blaspsoft\\Blasp\\Core\\Result;\nuse Blaspsoft\\Blasp\\Core\\Dictionary;\nuse Blaspsoft\\Blasp\\Core\\Contracts\\MaskStrategyInterface;\n\nclass MyDriver implements DriverInterface\n{\n    public function detect(string $text, Dictionary $dictionary, MaskStrategyInterface $mask, array $options = []): Result\n    {\n        // Your detection logic\n    }\n}\n\n// Register in a service provider\nBlasp::extend('my-driver', fn($app) => new MyDriver());\n\n// Use it\nBlasp::driver('my-driver')->check($text);\n```\n\n## Caching\n\nBlasp 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.\n\n```php\n// First call — runs full analysis, caches result\n$result = Blasp::check('some text');\n\n// Second call — returns cached result\n$result = Blasp::check('some text');\n```\n\nConfigure caching in `config/blasp.php`:\n\n```php\n'cache' => [\n    'enabled' => true,                      // Master switch for all caching\n    'driver'  => env('BLASP_CACHE_DRIVER'), // null = default cache driver\n    'ttl'     => 86400,                     // Cache lifetime in seconds\n    'results' => true,                      // Cache check() results (disable independently)\n],\n```\n\nResult caching is automatically bypassed when using a `CallbackMask` (closures can't be serialized). Clear both dictionary and result caches with:\n\n```bash\nphp artisan blasp:clear\n```\n\nOr programmatically:\n\n```php\nDictionary::clearCache();\n```\n\n## Artisan Commands\n\n```bash\n# Clear the profanity cache\nphp artisan blasp:clear\n\n# Test text from the command line\nphp artisan blasp:test \"some text to check\" --lang=english --detail\n\n# List available languages with word counts\nphp artisan blasp:languages\n```\n\n## Testing\n\n### Faking\n\n```php\nuse Blaspsoft\\Blasp\\Facades\\Blasp;\nuse Blaspsoft\\Blasp\\Core\\Result;\n\n// Replace with a fake — all checks return clean by default\nBlasp::fake();\n\n// Pre-configure specific responses\nBlasp::fake([\n    'bad text'   => Result::withMatches(['fuck']),\n    'clean text' => Result::none('clean text'),\n]);\n\n$result = Blasp::check('bad text');\n$result->isOffensive(); // true\n\n// Assertions\nBlasp::assertChecked();\nBlasp::assertCheckedTimes(1);\nBlasp::assertCheckedWith('bad text');\n```\n\n### Disabling Filtering\n\n```php\nBlasp::withoutFiltering(function () {\n    // All checks return clean results\n});\n```\n\n## Events\n\nEnable global events with `'events' => true` in config:\n\n| Event | Fired When | Properties |\n|-------|------------|------------|\n| `ProfanityDetected` | `check()` finds profanity | `result`, `originalText` |\n| `ContentBlocked` | Middleware detects profanity | `result`, `request`, `field`, `action` |\n| `ModelProfanityDetected` | Blaspable trait detects profanity | `model`, `attribute`, `result` |\n\n`ModelProfanityDetected` always fires (not gated by the `events` config).\n\n## Migrating from v3\n\n### Namespace Changes\n\n| v3 | v4 |\n|----|-----|\n| `Blaspsoft\\Blasp\\Facades\\Blasp` | `Blaspsoft\\Blasp\\Facades\\Blasp` (unchanged) |\n| `Blaspsoft\\Blasp\\ServiceProvider` | `Blaspsoft\\Blasp\\BlaspServiceProvider` |\n\nThe 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.\n\n### Config Changes\n\n| v3 Key | v4 Key | Notes |\n|--------|--------|-------|\n| `default_language` | `language` | `default_language` still works as alias |\n| `mask_character` | `mask` | `mask_character` still works as alias |\n| `cache_driver` | `cache.driver` | `cache_driver` still works as alias |\n| — | `default` | New: driver selection (`regex`/`pattern`) |\n| — | `severity` | New: minimum severity level |\n| — | `events` | New: enable global events |\n| — | `allow` / `block` | New: global allow/block lists |\n| — | `middleware` | New: middleware configuration section |\n| — | `model` | New: Blaspable trait configuration |\n\n### Result API Changes\n\n| v3 Method | v4 Method |\n|-----------|-----------|\n| `hasProfanity()` | `isOffensive()` |\n| `getCleanString()` | `clean()` |\n| `getSourceString()` | `original()` |\n| `getProfanitiesCount()` | `count()` |\n| `getUniqueProfanitiesFound()` | `uniqueWords()` |\n\nAll v3 methods still work as deprecated aliases.\n\n### Builder API Changes\n\n| v3 Method | v4 Method |\n|-----------|-----------|\n| `maskWith($char)` | `mask($char)` |\n| `allLanguages()` | `inAllLanguages()` |\n| `language($lang)` | `in($lang)` |\n| `configure($profanities, $falsePositives)` | `block(...$words)` / `allow(...$words)` |\n\nAll v3 methods still work as deprecated aliases.\n\n### New in v4\n\n- **Driver architecture** — `regex` and `pattern` drivers, custom driver support\n- **Severity system** — Mild/Moderate/High/Extreme levels with scoring\n- **Masking strategies** — Grawlix and callback masking\n- **Blaspable trait** — Automatic Eloquent model profanity checking\n- **Middleware** — Request-level profanity filtering\n- **Fluent validation rule** — `Profanity::in('spanish')->severity(Severity::High)`\n- **Testing utilities** — `Blasp::fake()`, assertions, `withoutFiltering()`\n- **Events** — `ProfanityDetected`, `ContentBlocked`, `ModelProfanityDetected`\n- **Artisan commands** — `blasp:clear`, `blasp:test`, `blasp:languages`\n- **Batch checking** — `Blasp::checkMany([...])`\n- **Multi-language in one call** — `Blasp::in('english', 'spanish')->check($text)`\n\n## Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.\n\n## Changelog\n\nSee [CHANGELOG.md](CHANGELOG.md) for detailed version history.\n\n## License\n\nBlasp is open-sourced software licensed under the [MIT license](LICENSE).\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"blaspsoft/blasp\",\n    \"description\": \"Blasp is a powerful and customisable profanity filter package for Laravel applications\",\n    \"keywords\": [\n        \"blaspsoft\",\n        \"blasp\"\n    ],\n    \"homepage\": \"https://github.com/blaspsoft/blasp\",\n    \"license\": \"MIT\",\n    \"type\": \"library\",\n    \"authors\": [\n        {\n            \"name\": \"Michael Deeming\",\n            \"email\": \"michael.deeming90@gmail.com\",\n            \"role\": \"Developer\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^8.2\",\n        \"illuminate/support\": \"^8.0|^9.0|^10.0|^11.0|^12.0|^13.0\"\n    },\n    \"require-dev\": {\n        \"orchestra/testbench\": \"^10.0|^11.0\",\n        \"phpunit/phpunit\": \"^11.0|^12.5.12\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Blaspsoft\\\\Blasp\\\\\": \"src/\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Blaspsoft\\\\Blasp\\\\Tests\\\\\": \"tests\"\n        }\n    },\n    \"minimum-stability\": \"stable\",\n    \"prefer-stable\": true,\n    \"scripts\": {\n        \"test\": \"vendor/bin/phpunit\",\n        \"test-coverage\": \"vendor/bin/phpunit --coverage-html coverage\"\n    },\n    \"config\": {\n        \"sort-packages\": true\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"Blaspsoft\\\\Blasp\\\\BlaspServiceProvider\"\n            ],\n            \"aliases\": {\n                \"Blasp\": \"Blaspsoft\\\\Blasp\\\\Facades\\\\Blasp\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "config/blasp.php",
    "content": "<?php\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Default Driver\n    |--------------------------------------------------------------------------\n    |\n    | The default detection driver. 'regex' provides full obfuscation\n    | detection. 'pattern' is faster but only matches exact words.\n    |\n    */\n    'default' => env('BLASP_DRIVER', 'regex'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Default Language\n    |--------------------------------------------------------------------------\n    |\n    | The default language to use for profanity detection.\n    |\n    */\n    'language' => env('BLASP_LANGUAGE', 'english'),\n\n    // Backward compat alias\n    'default_language' => env('BLASP_LANGUAGE', 'english'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Mask Character\n    |--------------------------------------------------------------------------\n    |\n    | The character used to mask detected profanities.\n    |\n    */\n    'mask' => '*',\n\n    // Backward compat alias\n    'mask_character' => '*',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Minimum Severity\n    |--------------------------------------------------------------------------\n    |\n    | The minimum severity level to detect. Words below this severity\n    | will be ignored. Options: mild, moderate, high, extreme\n    |\n    */\n    'severity' => 'mild',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Events\n    |--------------------------------------------------------------------------\n    |\n    | When enabled, ProfanityDetected events will be fired automatically\n    | when profanity is found during a check.\n    |\n    */\n    'events' => false,\n\n    /*\n    |--------------------------------------------------------------------------\n    | Cache Configuration\n    |--------------------------------------------------------------------------\n    */\n    'cache' => [\n        'enabled' => true,\n        'driver' => env('BLASP_CACHE_DRIVER'),\n        'ttl' => 86400,\n        'results' => true,\n    ],\n\n    // Backward compat alias\n    'cache_driver' => env('BLASP_CACHE_DRIVER'),\n\n    /*\n    |--------------------------------------------------------------------------\n    | Middleware Configuration\n    |--------------------------------------------------------------------------\n    */\n    'middleware' => [\n        'action' => 'reject',\n        'fields' => ['*'],\n        'except' => ['password', 'email', '_token'],\n        'severity' => 'mild',\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Model Configuration\n    |--------------------------------------------------------------------------\n    |\n    | Controls how the Blaspable trait behaves on Eloquent models.\n    | 'sanitize' replaces profanity with the mask character.\n    | 'reject' throws a ProfanityRejectedException instead of saving.\n    |\n    */\n    'model' => [\n        'mode' => env('BLASP_MODEL_MODE', 'sanitize'),\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Driver-Specific Configuration\n    |--------------------------------------------------------------------------\n    */\n    'drivers' => [\n        'pipeline' => [\n            'drivers' => ['regex', 'phonetic'],\n        ],\n\n        'phonetic' => [\n            'phonemes' => 4,              // metaphone code length (2-8, lower=more aggressive)\n            'min_word_length' => 3,        // skip words shorter than this\n            'max_distance_ratio' => 0.6,   // levenshtein threshold (0.3-0.8, lower=stricter)\n            'supported_languages' => ['english'],\n            'false_positives' => [\n                'fork', 'forked', 'forking',\n                'beach', 'beaches',\n                'witch', 'witches',\n                'sheet', 'sheets',\n                'deck', 'decks',\n                'count', 'counts', 'counter', 'county',\n                'ship', 'shipped', 'shipping',\n                'duck', 'ducked', 'ducking',\n                'fudge', 'fudging',\n                'buck', 'bucks',\n                'puck', 'pucks',\n                'bass',\n                'mass',\n                'pass', 'passed',\n                'heck',\n                'shoot', 'shot',\n                'what', 'white', 'while', 'whole',\n            ],\n        ],\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Character Separators\n    |--------------------------------------------------------------------------\n    */\n    'separators' => [\n        '@', '#', '%', '&', '_', ';', \"'\", '\"', ',', '~', '`', '|',\n        '!', '$', '^', '*', '(', ')', '-', '+', '=', '{', '}',\n        '[', ']', ':', '<', '>', '?', '.', '/',\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Character Substitutions\n    |--------------------------------------------------------------------------\n    */\n    'substitutions' => [\n        '/a/' => ['a', '4', '@', '*', 'Á', 'á', 'À', 'Â', 'à', 'Â', 'â', 'Ä', 'ä', 'Ã', 'ã', 'Å', 'å', 'æ', 'Æ', 'α', 'Δ', 'Λ', 'λ'],\n        '/b/' => ['b', '8', '\\\\', '3', '*', 'ß', 'Β', 'β'],\n        '/c/' => ['c', '*', 'Ç', 'ç', 'ć', 'Ć', 'č', 'Č', '¢', '€', '<', '(', '{', '©'],\n        '/d/' => ['d', '*', '\\\\', ')', 'Þ', 'þ', 'Ð', 'ð'],\n        '/e/' => ['e', '3', '*', '€', 'È', 'è', 'É', 'é', 'Ê', 'ê', 'ë', 'Ë', 'ē', 'Ē', 'ė', 'Ė', 'ę', 'Ę', '∑'],\n        '/f/' => ['f', '*', 'ƒ'],\n        '/g/' => ['g', '6', '9', '*'],\n        '/h/' => ['h', '*', 'Η'],\n        '/i/' => ['i', '!', '|', ']', '[', '1', '*', '∫', 'Ì', 'Í', 'Î', 'Ï', 'ì', 'í', 'î', 'ï', 'ī', 'Ī', 'į', 'Į'],\n        '/j/' => ['j', '*'],\n        '/k/' => ['k', '*', 'Κ', 'κ'],\n        '/l/' => ['l', '!', '|', ']', '[', '*', '£', '∫', 'Ì', 'Í', 'Î', 'Ï', 'ł', 'Ł'],\n        '/m/' => ['m', '*'],\n        '/n/' => ['n', '*', 'η', 'Ν', 'Π', 'ñ', 'Ñ', 'ń', 'Ń'],\n        '/o/' => ['o', '0', '*', 'Ο', 'ο', 'Φ', '¤', '°', 'ø', 'ô', 'Ô', 'ö', 'Ö', 'ò', 'Ò', 'ó', 'Ó', 'œ', 'Œ', 'ø', 'Ø', 'ō', 'Ō', 'õ', 'Õ'],\n        '/p/' => ['p', '*', 'ρ', 'Ρ', '¶', 'þ'],\n        '/q/' => ['q', '*'],\n        '/r/' => ['r', '*', '®'],\n        '/s/' => ['s', '5', '*', '\\$', '§', 'ß', 'Ś', 'ś', 'Š', 'š'],\n        '/t/' => ['t', '*', 'Τ', 'τ'],\n        '/u/' => ['u', 'υ', 'µ', 'û', 'ü', 'ù', 'ú', 'ū', 'Û', 'Ü', 'Ù', 'Ú', 'Ū', '@', '*'],\n        '/v/' => ['v', '*', 'υ', 'ν'],\n        '/w/' => ['w', '*', 'ω', 'ψ', 'Ψ'],\n        '/x/' => ['x', '*', 'Χ', 'χ'],\n        '/y/' => ['y', '*', '¥', 'γ', 'ÿ', 'ý', 'Ÿ', 'Ý'],\n        '/z/' => ['z', '*', 'Ζ', 'ž', 'Ž', 'ź', 'Ź', 'ż', 'Ż'],\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | False Positives\n    |--------------------------------------------------------------------------\n    */\n    'false_positives' => [\n        'hello', 'scunthorpe', 'cockburn', 'penistone', 'lightwater',\n        'assume', 'bass', 'class', 'compass', 'pass',\n        'dickinson', 'middlesex', 'cockerel', 'butterscotch', 'blackcock',\n        'countryside', 'arsenal', 'flick', 'flicker', 'analyst',\n        'cocktail', 'musicals hit', 'is hit', 'blackcocktail', 'its not',\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Global Allow List\n    |--------------------------------------------------------------------------\n    |\n    | Words in this list will never be flagged as profanity.\n    |\n    */\n    'allow' => [],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Global Block List\n    |--------------------------------------------------------------------------\n    |\n    | Additional words to always flag as profanity.\n    |\n    */\n    'block' => [],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Backward Compatibility: Profanities\n    |--------------------------------------------------------------------------\n    |\n    | Basic profanity list for backward compatibility.\n    | Full lists are in config/languages/*.php\n    |\n    */\n    'profanities' => [\n        'fuck', 'shit', 'damn', 'bitch', 'ass', 'hell',\n    ],\n\n];\n"
  },
  {
    "path": "config/languages/english.php",
    "content": "<?php\n\nreturn [\n    'severity' => [\n        'mild' => [\n            'damn', 'hell', 'crap', 'arse', 'sucks', 'piss', 'bloody',\n            'bollocks', 'bugger', 'crikey', 'darn', 'heck', 'turd',\n            'puke', 'puuke', 'puuker', 'shat', 'trots', 'vomit',\n            'waysted', 'wuss', 'wuzzie',\n        ],\n        'moderate' => [\n            'ass', 'bitch', 'bastard', 'slut', 'whore', 'douche',\n            'douchebag', 'skank', 'slag', 'tramp', 'tosser', 'wanker',\n            'wanking', 'prick', 'dick', 'knob', 'bellend', 'minger',\n            'git', 'twit', 'dipshit', 'jackass', 'smartass', 'dumbass',\n            'asshole', 'arsehole', 'shag', 'shagger', 'shagging',\n            'hooker', 'hussy', 'floozy', 'tart', 'sissy', 'pansy',\n        ],\n        'high' => [\n            'fuck', 'shit', 'cock', 'pussy', 'cunt', 'twat', 'tit', 'tits',\n            'fucking', 'fucker', 'motherfucker', 'bullshit', 'horseshit',\n            'shithead', 'shithole', 'shitface', 'fuckface', 'fuckhead',\n            'cocksucker', 'asswipe', 'clusterfuck', 'mindfuck',\n            'dumbfuck', 'fuckwit', 'shitbag', 'shitcunt',\n            'thundercunt', 'cum', 'jizz', 'dildo', 'blowjob',\n            'handjob', 'rimjob', 'fellatio', 'cunnilingus',\n        ],\n        'extreme' => [\n            'nigger', 'nigga', 'niggers', 'niggas', 'coon', 'darkie',\n            'kike', 'spic', 'spick', 'wetback', 'chink', 'gook',\n            'paki', 'raghead', 'towelhead', 'sandnigger', 'beaner',\n            'gringo', 'wop', 'dago', 'polack', 'retard', 'retarded',\n            'faggot', 'fag', 'dyke', 'tranny',\n        ],\n    ],\n\n    'profanities' => [\n        'abbo',\n        'abortionist',\n        'abuser',\n        'ahole',\n        'alabama hotpocket',\n        'alligatorbait',\n        'anal',\n        'analannie',\n        'analsex',\n        'areola',\n        'arse',\n        'arsebagger',\n        'arsebandit',\n        'arseblaster',\n        'arsecowboy',\n        'arsefuck',\n        'arsefucker',\n        'arsehat',\n        'arsehole',\n        'arseholes',\n        'arsehore',\n        'arsejockey',\n        'arsekiss',\n        'arsekisser',\n        'arselick',\n        'arselicker',\n        'arselover',\n        'arseman',\n        'arsemonkey',\n        'arsemunch',\n        'arsemuncher',\n        'arsepacker',\n        'arsepirate',\n        'arsepuppies',\n        'arseranger',\n        'arses',\n        'arsewhore',\n        'arsewipe',\n        'ass',\n        'assbag',\n        'assbagger',\n        'assbandit',\n        'assbanger',\n        'assbite',\n        'assblaster',\n        'assclown',\n        'asscock',\n        'asscowboy',\n        'asscracker',\n        'asses',\n        'assface',\n        'assfuck',\n        'assfucker',\n        'assgoblin',\n        'ass-hat',\n        'asshat',\n        'asshead',\n        'asshole',\n        'assholes',\n        'assholz',\n        'asshopper',\n        'asshore',\n        'ass-jabber',\n        'assjacker',\n        'assjockey',\n        'asskiss',\n        'asskisser',\n        'assklown',\n        'asslick',\n        'asslicker',\n        'asslover',\n        'assman',\n        'assmonkey',\n        'ass monkey',\n        'assmunch',\n        'assmuncher',\n        'assnigger',\n        'asspacker',\n        'ass-pirate',\n        'asspirate',\n        'asspuppies',\n        'assranger',\n        'assshit',\n        'assshole',\n        'asssucker',\n        'asswad',\n        'asswhore',\n        'asswipe',\n        'axwound',\n        'azzhole',\n        'backdoorman',\n        'badfuck',\n        'baldy',\n        'ball licker',\n        'balllicker',\n        'ballsack',\n        'bampot',\n        'banging',\n        'barelylegal',\n        'barface',\n        'barfface',\n        'bassterds',\n        'bastard',\n        'bastards',\n        'bastardz',\n        'basterds',\n        'basterdz',\n        'bazongas',\n        'bazooms',\n        'beaner',\n        'beastality',\n        'beastial',\n        'beastiality',\n        'beat-off',\n        'beatoff',\n        'beatyourmeat',\n        'bestial',\n        'bestiality',\n        'biatch',\n        'bicurious',\n        'bigass',\n        'bigbastard',\n        'bigbutt',\n        'bitch',\n        'bitchass',\n        'bitcher',\n        'bitches',\n        'bitchez',\n        'bitchin',\n        'bitching',\n        'bitchslap',\n        'bitchtits',\n        'bitchy',\n        'biteme',\n        'blow job',\n        'blowjob',\n        'boffing',\n        'bohunk',\n        'bollick',\n        'bollock',\n        'bollocks',\n        'bollox',\n        'bondage',\n        'boner',\n        'boob',\n        'boobies',\n        'boobs',\n        'booby',\n        'bootycall',\n        'bountybar',\n        'breastjob',\n        'breastlover',\n        'breastman',\n        'brothel',\n        'brotherfucker',\n        'bugger',\n        'buggered',\n        'buggery',\n        'bukake',\n        'bullcrap',\n        'bulldike',\n        'bulldyke',\n        'bullshit',\n        'bumblefuck',\n        'bumfuck',\n        'bungabunga',\n        'bunghole',\n        'butchbabes',\n        'butchdike',\n        'butchdyke',\n        'butt-bang',\n        'buttbang',\n        'buttcheeks',\n        'buttface',\n        'butt-fuck',\n        'buttfuck',\n        'buttfucka',\n        'butt-fucker',\n        'buttfucker',\n        'butt-fuckers',\n        'buttfuckers',\n        'butthead',\n        'butthole',\n        'buttman',\n        'buttmunch',\n        'buttmuncher',\n        'butt-pirate',\n        'buttpirate',\n        'butt plug',\n        'buttplug',\n        'buttstain',\n        'buttwipe',\n        'byatch',\n        'cacker',\n        'cameljockey',\n        'camel toe',\n        'cameltoe',\n        'carpet muncher',\n        'carpetmuncher',\n        'cawk',\n        'cawks',\n        'chav',\n        'cherrypopper',\n        'chesticle',\n        'chickslick',\n        'chinc',\n        'chink',\n        'choad',\n        'chode',\n        'clamdigger',\n        'clamdiver',\n        'clit',\n        'clitface',\n        'clitfuck',\n        'clitoris',\n        'clogwog',\n        'clunge',\n        'clusterfuck',\n        'cnts',\n        'cntz',\n        'cock',\n        'cockass',\n        'cockbite',\n        'cockblock',\n        'cockblocker',\n        'cockburger',\n        'cockcowboy',\n        'cockface',\n        'cockfight',\n        'cockfucker',\n        'cock-head',\n        'cockhead',\n        'cockjockey',\n        'cockknob',\n        'cockknoker',\n        'cocklicker',\n        'cocklover',\n        'cockmaster',\n        'cockmongler',\n        'cockmongruel',\n        'cockmonkey',\n        'cockmuncher',\n        'cocknob',\n        'cocknose',\n        'cocknugget',\n        'cockqueen',\n        'cockrider',\n        'cocks',\n        'cockshit',\n        'cocksman',\n        'cocksmith',\n        'cocksmoke',\n        'cocksmoker',\n        'cocksniffer',\n        'cocksucer',\n        'cocksuck',\n        'cocksucked',\n        'cock-sucker',\n        'cocksucker',\n        'cocksucking',\n        'cocktease',\n        'cockwaffle',\n        'cocky',\n        'coitus',\n        'cok',\n        'commie',\n        'coochie',\n        'coochy',\n        'coon',\n        'coondog',\n        'cooter',\n        'copulate',\n        'cracker',\n        'crackpipe',\n        'crack-whore',\n        'crackwhore',\n        'crap',\n        'crappy',\n        'crotchjockey',\n        'crotchmonkey',\n        'crotchrot',\n        'cuck',\n        'cum',\n        'cumbubble',\n        'cumdumpster',\n        'cumfest',\n        'cumguzzler',\n        'cumjockey',\n        'cumm',\n        'cumquat',\n        'cumqueen',\n        'cumshot',\n        'cumslut',\n        'cumtart',\n        'cunilingus',\n        'cunillingus',\n        'cunnie',\n        'cunnilingus',\n        'cunntt',\n        'cunt',\n        'cuntass',\n        'cunteyed',\n        'cuntface',\n        'cuntfucker',\n        'cunthole',\n        'cuntlick',\n        'cuntlicker',\n        'cuntlicker',\n        'cuntlicking',\n        'cuntrag',\n        'cunts',\n        'cuntslut',\n        'cuntsucker',\n        'cuntz',\n        'cybersex',\n        'cyberslimer',\n        'dago',\n        'dammit',\n        'damn',\n        'damnation',\n        'damnit',\n        'darkie',\n        'darky',\n        'datnigga',\n        'deapthroat',\n        'deepthroat',\n        'deggo',\n        'dego',\n        'devilworshipper',\n        'dick',\n        'dickbag',\n        'dickbeaters',\n        'dickbrain',\n        'dickface',\n        'dickforbrains',\n        'dickfuck',\n        'dickfucker',\n        'dickhead',\n        'dickhole',\n        'dickjuice',\n        'dickless',\n        'dicklick',\n        'dicklicker',\n        'dickmilk',\n        'dickmonger',\n        'dicks',\n        'dickslap',\n        'dick-sneeze',\n        'dicksucker',\n        'dicksucking',\n        'dicktickler',\n        'dickwad',\n        'dickweasel',\n        'dickweed',\n        'dickwod',\n        'dike',\n        'dildo',\n        'dildos',\n        'dilldo',\n        'dilldos',\n        'dipshit',\n        'dipstick',\n        'dixiedike',\n        'dixiedyke',\n        'doggiestyle',\n        'doggystyle',\n        'dominatricks',\n        'dominatrics',\n        'dominatrix',\n        'doochbag',\n        'dookie',\n        'douch',\n        'douchbag',\n        'douche',\n        'douchebag',\n        'douche-fag',\n        'douchewaffle',\n        'drag queen',\n        'dragqueen',\n        'dragqween',\n        'dripdick',\n        'dumass',\n        'dumb ass',\n        'dumbass',\n        'dumbbitch',\n        'dumbfuck',\n        'dumbshit',\n        'dumshit',\n        'dyke',\n        'easyslut',\n        'eatballs',\n        'eatme',\n        'eatpussy',\n        'ejaculate',\n        'ejaculated',\n        'ejaculating',\n        'ejaculation',\n        'enema',\n        'excrement',\n        'facefucker',\n        'facist',\n        'faeces',\n        'fag',\n        'fagbag',\n        'faget',\n        'fagfucker',\n        'fagging',\n        'faggit',\n        'faggot',\n        'faggotcock',\n        'faggots',\n        'fagit',\n        'fagot',\n        'fags',\n        'fagtard',\n        'fagz',\n        'faig',\n        'faigs',\n        'fannyfucker',\n        'fark',\n        'farted',\n        'farting',\n        'farty',\n        'fastfuck',\n        'fatass',\n        'fatfuck',\n        'fatfucker',\n        'fatso',\n        'feces',\n        'felatio',\n        'felch',\n        'felcher',\n        'felching',\n        'fellatio',\n        'feltch',\n        'feltcher',\n        'feltching',\n        'fingerfuck',\n        'fingerfucked',\n        'fingerfucker',\n        'fingerfuckers',\n        'fingerfucking',\n        'fister',\n        'fistfuck',\n        'fistfucked',\n        'fistfucker',\n        'fistfucking',\n        'fisting',\n        'flamer',\n        'flasher',\n        'flid',\n        'flipping the bird',\n        'flyd',\n        'flydie',\n        'flydye',\n        'fondle',\n        'footaction',\n        'footfuck',\n        'footfucker',\n        'footlicker',\n        'fornicate',\n        'freakfuck',\n        'freakyfucker',\n        'freefuck',\n        'fubar',\n        'fucck',\n        'fuck',\n        'fucka',\n        'fuckable',\n        'fuckass',\n        'fuckbag',\n        'fuckboy',\n        'fuckbrain',\n        'fuckbuddy',\n        'fuckbutt',\n        'fuckbutter',\n        'fucked',\n        'fucker',\n        'fuckers',\n        'fuckersucker',\n        'fuckface',\n        'fuckfest',\n        'fuckfreak',\n        'fuckfriend',\n        'fuckhead',\n        'fuckhole',\n        'fuckin',\n        'fuckina',\n        'fucking',\n        'fuckingbitch',\n        'fuckinnuts',\n        'fuckinright',\n        'fuckit',\n        'fuckknob',\n        'fuckmonkey',\n        'fucknut',\n        'fucknutt',\n        'fuckoff',\n        'fuckpig',\n        'fuckstick',\n        'fucktard',\n        'fucktart',\n        'fuckup',\n        'fuckwad',\n        'fuckwhore',\n        'fuckwit',\n        'fuckwitt',\n        'fuckyou',\n        'fudge packer',\n        'fudgepacker',\n        'Fudge Packer',\n        'fugly',\n        'fuk',\n        'Fukah',\n        'Fuken',\n        'fuker',\n        'Fukin',\n        'Fukk',\n        'Fukkah',\n        'Fukken',\n        'Fukker',\n        'Fukkin',\n        'fuks',\n        'funfuck',\n        'fuuck',\n        'gang bang',\n        'gangbang',\n        'gangbanged',\n        'gangbanger',\n        'gatorbait',\n        'gayass',\n        'gaybob',\n        'gayboy',\n        'gaydo',\n        'gayfuck',\n        'gayfuckist',\n        'gaygirl',\n        'gaylord',\n        'gaymuthafuckinwhore',\n        'gays',\n        'gaysex',\n        'gaytard',\n        'gaywad',\n        'gayz',\n        'getiton',\n        'givehead',\n        'glazeddonut',\n        'godammit',\n        'goddamit',\n        'goddammit',\n        'goddamn',\n        'goddamned',\n        'god-damned',\n        'goddamnes',\n        'goddamnit',\n        'goddamnmuthafucker',\n        'goldenshower',\n        'gonorrehea',\n        'gonzagas',\n        'gooch',\n        'gook',\n        'gotohell',\n        'greaseball',\n        'gringo',\n        'grostulation',\n        'guido',\n        'gypo',\n        'gypp',\n        'gyppie',\n        'gyppo',\n        'gyppy',\n        'handjob',\n        'hard on',\n        'hardon',\n        'headfuck',\n        'heeb',\n        'hell',\n        'herpes',\n        'hijacker',\n        'hijacking',\n        'hillbillies',\n        'hindoo',\n        'hitler',\n        'hitlerism',\n        'hitlerist',\n        'hoar',\n        'hobo',\n        'hoe',\n        'hoes',\n        'holestuffer',\n        'homo',\n        'homobangers',\n        'homodumbshit',\n        'honger',\n        'honkers',\n        'honkey',\n        'honky',\n        'hookers',\n        'hoor',\n        'hoore',\n        'hore',\n        'horney',\n        'horniest',\n        'horny',\n        'horseshit',\n        'hosejob',\n        'hotdamn',\n        'hotpussy',\n        'hottotrot',\n        'humping',\n        'hymen',\n        'iblowu',\n        'idiot',\n        'incest',\n        'insest',\n        'internet wife',\n        'inthebuff',\n        'jackass',\n        'jackoff',\n        'jackshit',\n        'jagoff',\n        'jap',\n        'japcrap',\n        'japs',\n        'jerkass',\n        'jerk off',\n        'jerk-off',\n        'jerkoff',\n        'jesuschrist',\n        'jigaboo',\n        'jiggabo',\n        'jihad',\n        'jijjiboo',\n        'jisim',\n        'jism',\n        'jiss',\n        'jizim',\n        'jizjuice',\n        'jizm',\n        'jizm',\n        'jizz',\n        'jizzim',\n        'jizzum',\n        'jubblies',\n        'juggalo',\n        'jungle bunny',\n        'junglebunny',\n        'kiddy fiddler',\n        'kike',\n        'kinky',\n        'kissass',\n        'knobz',\n        'kondum',\n        'kooch',\n        'kootch',\n        'krap',\n        'krappy',\n        'kraut',\n        'kumbubble',\n        'kumbullbe',\n        'kummer',\n        'kumming',\n        'kums',\n        'kunilingus',\n        'kunnilingus',\n        'kunt',\n        'kunts',\n        'kuntz',\n        'kyke',\n        'labia',\n        'lactate',\n        'lady boy',\n        'ladyboy',\n        'lameass',\n        'lapdance',\n        'lardass',\n        'lesbain',\n        'lesbayn',\n        'lesbian',\n        'lesbin',\n        'lesbo',\n        'lezbe',\n        'lezbefriends',\n        'lezbo',\n        'lezz',\n        'lezzer',\n        'lezzie',\n        'lezzo',\n        'libido',\n        'lickme',\n        'limpdick',\n        'lipshits',\n        'lipshitz',\n        'livesex',\n        'lmfao',\n        'loadedgun',\n        'lovebone',\n        'lovegoo',\n        'lovegun',\n        'lovejuice',\n        'lovemuscle',\n        'lovepistol',\n        'loverocket',\n        'low life',\n        'lowlife',\n        'lubejob',\n        'luckycameltoe',\n        'manhater',\n        'manpaste',\n        'masochist',\n        'masokist',\n        'massterbait',\n        'masstrbait',\n        'masstrbate',\n        'mastabate',\n        'mastabater',\n        'masterbaiter',\n        'masterbate',\n        'master bates',\n        'masterbates',\n        'mastrabator',\n        'masturbate',\n        'masturbating',\n        'mattressprincess',\n        'mcfagget',\n        'meatbeater',\n        'meatrack',\n        'mgger',\n        'mggor',\n        'milf',\n        'minge',\n        'mofo',\n        'molest',\n        'molestation',\n        'molester',\n        'molestor',\n        'moneyshot',\n        'mooncricket',\n        'moron',\n        'mothafuck',\n        'mothafucka',\n        'mothafuckaz',\n        'mothafucked',\n        'mothafucker',\n        'motha fucker',\n        'mothafuckin',\n        'mothafucking',\n        'mothafuckings',\n        'motha fuker',\n        'motha fukkah',\n        'motha fukker',\n        'motherfuck',\n        'motherfucked',\n        'mother-fucker',\n        'motherfucker',\n        'mother fucker',\n        'motherfuckin',\n        'motherfucking',\n        'motherfuckings',\n        'mother fukah',\n        'mother fuker',\n        'mother fukkah',\n        'mother fukker',\n        'motherlovebone',\n        'muff',\n        'muffdive',\n        'muffdiver',\n        'muffindiver',\n        'mufflikcer',\n        'muncher',\n        'munging',\n        'muthafucker',\n        'mutha fucker',\n        'mutha fukah',\n        'mutha fuker',\n        'mutha fukkah',\n        'mutha fukker',\n        'nastt',\n        'nastybitch',\n        'nastyho',\n        'nastyslut',\n        'nastywhore',\n        'nazi',\n        'necro',\n        'negro',\n        'negroes',\n        'negroid',\n        'nigaboo',\n        'nigga',\n        'niggah',\n        'niggaracci',\n        'niggard',\n        'niggarded',\n        'niggarding',\n        'niggardliness',\n        \"niggardliness's\",\n        'niggardly',\n        \"niggard's\",\n        'niggards',\n        'niggaz',\n        'nigger',\n        'niggerhead',\n        'niggerhole',\n        \"nigger's\",\n        'niggers',\n        'niggle',\n        'niggled',\n        'niggles',\n        'niggling',\n        'nigglings',\n        'niggor',\n        'niggur',\n        'niglet',\n        'nignog',\n        'nigr',\n        'nigra',\n        'nigre',\n        'nigur',\n        'niiger',\n        'niigr',\n        'nipple',\n        'nipplering',\n        'nittit',\n        'nlgger',\n        'nlggor',\n        'nofuckingway',\n        'nonce',\n        'nookey',\n        'nookie',\n        'nudger',\n        'nut case',\n        'nutcase',\n        'nutfucker',\n        'nut sack',\n        'nutsack',\n        'ontherag',\n        'orafis',\n        'orgasim',\n        'orgasim',\n        'orgasm',\n        'orgasum',\n        'orgies',\n        'orgy',\n        'oriface',\n        'orifice',\n        'orifiss',\n        'osama bin laden',\n        'packi',\n        'packie',\n        'packy',\n        'paedo',\n        'paedofile',\n        'paedophile',\n        'paki',\n        'pakie',\n        'paky',\n        'palesimian',\n        'panooch',\n        'panti',\n        'pearlnecklace',\n        'pecker',\n        'peckerhead',\n        'peckerwood',\n        'peedo',\n        'peeenus',\n        'peeenusss',\n        'peehole',\n        'peenus',\n        'peinus',\n        'penas',\n        'penile',\n        'penisbanger',\n        'penis-breath',\n        'penises',\n        'penisfucker',\n        'penispuffer',\n        'penus',\n        'penuus',\n        'perv',\n        'perversion',\n        'pervert',\n        'phonesex',\n        'phuc',\n        'phuck',\n        'phuk',\n        'phuked',\n        'phuker',\n        'phuking',\n        'phukked',\n        'phukker',\n        'phukking',\n        'phungky',\n        'phuq',\n        'pi55',\n        'picaninny',\n        'piccaninny',\n        'pickaninny',\n        'pikey',\n        'piky',\n        'pimper',\n        'pimpjuic',\n        'pimpjuice',\n        'pimpsimp',\n        'pindick',\n        'piss',\n        'pissed',\n        'pissed off',\n        'pisser',\n        'pisses',\n        'pissflaps',\n        'pisshead',\n        'pissin',\n        'pissing',\n        'pissoff',\n        'play boy',\n        'playboy',\n        'play bunny',\n        'playbunny',\n        'play girl',\n        'playgirl',\n        'plumper',\n        'pocketpool',\n        'polac',\n        'polack',\n        'polak',\n        'polesmoker',\n        'pollock',\n        'poon',\n        'poonani',\n        'poonany',\n        'poontang',\n        'pooperscooper',\n        'pooping',\n        'poorwhitetrash',\n        'poostabber',\n        'popimp',\n        'porch monkey',\n        'porchmonkey',\n        'porn',\n        'pornflick',\n        'pornking',\n        'porno',\n        'pornprincess',\n        'pric',\n        'prick',\n        'prik',\n        'prickhead',\n        'prostitute',\n        'pu55i',\n        'pu55y',\n        'pube',\n        'pubiclice',\n        'puke',\n        'punanny',\n        'punta',\n        'puntang',\n        'purinaprincess',\n        'pusse',\n        'pussee',\n        'pussie',\n        'pussies',\n        'pussy',\n        'pussyeater',\n        'pussyfucker',\n        'pussylicker',\n        'pussylicking',\n        'pussylips',\n        'pussylover',\n        'pussypounder',\n        'pusy',\n        'puto',\n        'puuke',\n        'puuker',\n        'queef',\n        'queer',\n        'queerbait',\n        'queerhole',\n        'queers',\n        'queerz',\n        'quim',\n        'qweers',\n        'qweerz',\n        'qweir',\n        'rag head',\n        'raghead',\n        'raped',\n        'rapist',\n        'rearend',\n        'rearentry',\n        'recktum',\n        'rectum',\n        'redneck',\n        'renob',\n        'rentafuck',\n        'rimjob',\n        'rimming',\n        'ruski',\n        'russki',\n        'russkie',\n        'sadist',\n        'sadom',\n        'saeema butt',\n        'sandm',\n        'sand nigger',\n        'sandnigger',\n        'scag',\n        'scank',\n        'scat',\n        'schlong',\n        'screwing',\n        'screwyou',\n        'scrote',\n        'scrotum',\n        'scum',\n        'scumbag',\n        'seaman staines',\n        'semen',\n        'sexed',\n        'sexfarm',\n        'sexhound',\n        'sexhouse',\n        'sexing',\n        'sexkitten',\n        'sexpot',\n        'sexslave',\n        'sextogo',\n        'sextoy',\n        'sextoys',\n        'sexwhore',\n        'sexymoma',\n        'sexy-slim',\n        'seymour butts',\n        'shag',\n        'shagger',\n        'shaggin',\n        'shagging',\n        'shat',\n        'shhit',\n        'shit',\n        'shitass',\n        'shitbag',\n        'shitbagger',\n        'shitbrains',\n        'shitbreath',\n        'shitcan',\n        'shitcanned',\n        'shitcunt',\n        'shitdick',\n        'shite',\n        'shiteater',\n        'shited',\n        'shiter',\n        'shitface',\n        'shitfaced',\n        'shitfit',\n        'shitforbrains',\n        'shitfuck',\n        'shitfucker',\n        'shitfull',\n        'shithapens',\n        'shithappens',\n        'shithead',\n        'shithole',\n        'shithouse',\n        'shiting',\n        'shitlist',\n        'shitola',\n        'shitoutofluck',\n        'shits',\n        'shitspitter',\n        'shitstain',\n        'shitted',\n        'shitter',\n        'shittiest',\n        'shitting',\n        'shitty',\n        'shity',\n        'shitz',\n        'shiz',\n        'shiznit',\n        'shortfuck',\n        'shyt',\n        'shyte',\n        'shytty',\n        'shyty',\n        'sissy',\n        'sixsixsix',\n        'sixtynine',\n        'sixtyniner',\n        'skanck',\n        'skank',\n        'skankbitch',\n        'skankee',\n        'skankey',\n        'skankfuck',\n        'skanks',\n        'skankwhore',\n        'skanky',\n        'skankybitch',\n        'skankywhore',\n        'skeet',\n        'skinflute',\n        'skullfuck',\n        'skum',\n        'skumbag',\n        'slanteye',\n        'slantyeye',\n        'slapper',\n        'slavedriver',\n        'sleezebag',\n        'sleezeball',\n        'slideitin',\n        'slimeball',\n        'slimebucket',\n        'slopehead',\n        'slopey',\n        'slopy',\n        'slut',\n        'slutbag',\n        'sluts',\n        'slutt',\n        'slutting',\n        'slutty',\n        'slutwear',\n        'slutwhore',\n        'slutz',\n        'smackthemonkey',\n        'smeg',\n        'smelly',\n        'smut',\n        'snatch',\n        'snatchpatch',\n        'snot',\n        'snowback',\n        'snownigger',\n        'sodom',\n        'sodomise',\n        'sodomite',\n        'sodomize',\n        'sodomy',\n        'son-of-a-bitch',\n        'sonofabitch',\n        'sonofbitch',\n        'spac',\n        'spacca',\n        'spaghettibender',\n        'spaghettinigger',\n        'spankthemonkey',\n        'spazza',\n        'sperm',\n        'spermacide',\n        'spermbag',\n        'spermhearder',\n        'spermherder',\n        'spic',\n        'spick',\n        'spig',\n        'spigotty',\n        'spik',\n        'spitter',\n        'splittail',\n        'splooge',\n        'spooge',\n        'spook',\n        'spreadeagle',\n        'squaw',\n        'stabber',\n        'stiffy',\n        'strapon',\n        'stripclub',\n        'stroking',\n        'stupidfuck',\n        'stupidfucker',\n        'suckass',\n        'suckdick',\n        'sucker',\n        'suckme',\n        'suckmyass',\n        'suckmydick',\n        'suckmytit',\n        'suckoff',\n        'swastika',\n        'tampon',\n        'tarbaby',\n        'tard',\n        'teat',\n        'teste',\n        'testicle',\n        'testicles',\n        'thicklips',\n        'thicko',\n        'thirdeye',\n        'thirdleg',\n        'threesome',\n        'thundercunt',\n        'timbernigger',\n        'tit',\n        'titbitnipply',\n        'titfuck',\n        'titfucker',\n        'titfuckin',\n        'titjob',\n        'titlicker',\n        'titlover',\n        'tits',\n        'tittie',\n        'titties',\n        'titty',\n        'tittyfuck',\n        'tonguethrust',\n        'tonguethruster',\n        'tonguetramp',\n        'torture',\n        'tosser',\n        'tosspot',\n        'towel head',\n        'towelhead',\n        'trailertrash',\n        'tramp',\n        'trannie',\n        'tranny',\n        'trots',\n        'trouser snake',\n        'tuckahoe',\n        'tunneloflove',\n        'turd',\n        'twat',\n        'twatlips',\n        'twats',\n        'twatwaffle',\n        'twink',\n        'twinkie',\n        'twobitwhore',\n        'unclefucker',\n        'unfuckable',\n        'upskirt',\n        'uptheass',\n        'upthebutt',\n        'urinate',\n        'urine',\n        'usama bin laden',\n        'uterus',\n        'vag',\n        'vagina',\n        'vaginal',\n        'vajayjay',\n        'vajina',\n        'va-j-j',\n        'valjina',\n        'vibrater',\n        'vibrator',\n        'vietcong',\n        'violate',\n        'violation',\n        'virginbreaker',\n        'vjayjay',\n        'vomit',\n        'vullva',\n        'vulva',\n        'wank',\n        'wanker',\n        'wanking',\n        'wankjob',\n        'waysted',\n        'welcher',\n        'wetback',\n        'wetspot',\n        'whacker',\n        'whigger',\n        'whiskeydick',\n        'whiskydick',\n        'whitenigger',\n        'whitetrash',\n        'whitey',\n        'whoor',\n        'whop',\n        'whore',\n        'whorebag',\n        'whoreface',\n        'whorefucker',\n        'whorehouse',\n        'wife beater',\n        'williewanker',\n        'wog',\n        'wop',\n        'wuss',\n        'wuzzie',\n        'x-rated',\n        'xrated',\n        'yellowman',\n        'zigabo',\n        'zipperhea',\n        'zipper head',\n        'sucks',\n        'bloody',\n        'crikey',\n        'darn',\n        'heck',\n        'slag',\n        'knob',\n        'bellend',\n        'minger',\n        'git',\n        'twit',\n        'smartass',\n        'hooker',\n        'hussy',\n        'floozy',\n        'tart',\n        'pansy',\n        'mindfuck',\n        'niggas',\n        'retard',\n        'retarded',\n    ],\n    \n    'false_positives' => [\n        'hello',\n        'scunthorpe',\n        'cockburn',\n        'penistone',\n        'lightwater',\n        'assume',\n        'bass',\n        'class',\n        'compass',\n        'pass',\n        'dickinson',\n        'middlesex',\n        'cockerel',\n        'butterscotch',\n        'blackcock',\n        'countryside',\n        'arsenal',\n        'flick',\n        'flicker',\n        'analyst',\n        'cocktail',\n        'musicals hit',\n        'is hit',\n        'blackcocktail',\n        'its not',\n        // Common words containing \"ass\"\n        'assignment',\n        'assign',\n        'assigned',\n        'assigns',\n        'assigning',\n        'assist',\n        'assistant',\n        'assisted',\n        'assists',\n        'assistance',\n        'associate',\n        'associated',\n        'associates',\n        'association',\n        'associations',\n        'assemble',\n        'assembled',\n        'assembles',\n        'assembly',\n        'assert',\n        'asserted',\n        'assertion',\n        'assertions',\n        'asserts',\n        'assess',\n        'assessed',\n        'assesses',\n        'assessing',\n        'assessment',\n        'assessments',\n        'asset',\n        'assets',\n        'assure',\n        'assured',\n        'assures',\n        'assurance',\n        'assorted',\n        'assortment',\n        'assassin',\n        'assassins',\n        'assassination',\n        'assassinated',\n        'assault',\n        'assaulted',\n        'assaults',\n        'passion',\n        'passionate',\n        'passions',\n        'passive',\n        'passively',\n        'passenger',\n        'passengers',\n        'passage',\n        'passages',\n        'passing',\n        'passed',\n        'passes',\n        'passport',\n        'passports',\n        'password',\n        'passwords',\n        'bypass',\n        'bypassed',\n        'bypasses',\n        'bypassing',\n        'classroom',\n        'classrooms',\n        'classic',\n        'classical',\n        'classics',\n        'classification',\n        'classifications',\n        'classified',\n        'classify',\n        'classmate',\n        'classmates',\n        'classed',\n        'classes',\n        'classy',\n        'mass',\n        'masses',\n        'massive',\n        'massively',\n        'massage',\n        'massages',\n        'massacre',\n        'massacres',\n        'embassy',\n        'embassies',\n        'ambassador',\n        'ambassadors',\n        'embarrass',\n        'embarrassed',\n        'embarrassing',\n        'embarrassment',\n        'harass',\n        'harassed',\n        'harassing',\n        'harassment',\n        'brass',\n        'brassy',\n        'crass',\n        'glass',\n        'glasses',\n        'glassy',\n        'grass',\n        'grasses',\n        'grassy',\n        'lass',\n        'lassie',\n        'molasses',\n        'morass',\n        'sass',\n        'sassy',\n        'trespass',\n        'trespassed',\n        'trespassing',\n        'surpass',\n        'surpassed',\n        'surpasses',\n        'compassion',\n        'compassionate',\n        'encompass',\n        'encompassed',\n        'encompasses',\n        'encompassing',\n        // Common words containing \"tit\"\n        'title',\n        'titles',\n        'titled',\n        'subtitle',\n        'subtitles',\n        'entity',\n        'entities',\n        'identity',\n        'identities',\n        'quantity',\n        'quantities',\n        'constitution',\n        'constitutional',\n        'constitutions',\n        'constitute',\n        'constitutes',\n        'institution',\n        'institutional',\n        'institutions',\n        'petition',\n        'petitions',\n        'petitioner',\n        'competition',\n        'competitions',\n        'competitive',\n        'competitor',\n        'competitors',\n        'repetition',\n        'repetitions',\n        'repetitive',\n        'appetite',\n        'appetites',\n        'gratitude',\n        'attitude',\n        'attitudes',\n        'altitude',\n        'altitudes',\n        'aptitude',\n        'multitude',\n        'fortitude',\n        'latitude',\n        'latitudes',\n        'partition',\n        'partitions',\n        'practitioner',\n        'practitioners',\n        'restitution',\n        'prostitution',\n        'superstition',\n        'superstitions',\n        'superstitious',\n        'titillate',\n        'titan',\n        'titans',\n        'titanium',\n        // Common words containing \"cum\"\n        'document',\n        'documents',\n        'documentary',\n        'documentation',\n        'documented',\n        'circumstance',\n        'circumstances',\n        'circumference',\n        'accumulate',\n        'accumulated',\n        'accumulation',\n        'cucumber',\n        'cucumbers',\n        'incumbent',\n        'incumbents',\n        // Common words containing \"ho\" / \"hoe\"\n        'shoe',\n        'shoes',\n        'shoelace',\n        'horseshoe',\n        // Common words containing \"nig\"\n        'night',\n        'nights',\n        'nightclub',\n        'nightfall',\n        'nightlife',\n        'nightmare',\n        'nightmares',\n        'nightstand',\n        'nighttime',\n        'tonight',\n        'overnight',\n        'knight',\n        'knights',\n        // Common words containing \"rap\"\n        'grape',\n        'grapes',\n        'drape',\n        'drapes',\n        'scrape',\n        'scraped',\n        'scraper',\n        'therapy',\n        'therapies',\n        'therapist',\n        'therapists',\n        // Common words containing \"nob\"\n        'noble',\n        'nobles',\n        'nobleman',\n        'nobility',\n        'snob',\n        'snobs',\n        'snobbish',\n        'snobby',\n    ],\n    \n    'substitutions' => [\n        '/a/' => ['a', '4', '@', 'Á', 'á', 'À', 'Â', 'à', 'Â', 'â', 'Ä', 'ä', 'Ã', 'ã', 'Å', 'å', 'æ', 'Æ', 'α', 'Δ', 'Λ', 'λ'],\n        '/e/' => ['e', '3', '€', 'È', 'è', 'É', 'é', 'Ê', 'ê', 'ë', 'Ë', 'ē', 'Ē', 'ė', 'Ė', 'ę', 'Ę', '∑'],\n        '/i/' => ['i', '!', '|', ']', '[', '1', '∫', 'Ì', 'Í', 'Î', 'Ï', 'ì', 'í', 'î', 'ï', 'ī', 'Ī', 'į', 'Į'],\n        '/o/' => ['o', '0', 'Ο', 'ο', 'Φ', '¤', '°', 'ø', 'ô', 'Ô', 'ö', 'Ö', 'ò', 'Ò', 'ó', 'Ó', 'œ', 'Œ', 'ø', 'Ø', 'ō', 'Ō', 'õ', 'Õ'],\n        '/u/' => ['u', 'υ', 'µ', 'û', 'ü', 'ù', 'ú', 'ū', 'Û', 'Ü', 'Ù', 'Ú', 'Ū', '@', '*'],\n    ]\n];"
  },
  {
    "path": "config/languages/french.php",
    "content": "<?php\n\nreturn [\n    'severity' => [\n        'mild' => [\n            'crotte', 'crottes', 'caca', 'cacas', 'zut',\n            'punaise',\n            'idiot', 'idiots', 'idiote', 'idiotes',\n            'bête', 'bete', 'bêtes', 'betes',\n            'sot', 'sots', 'sotte', 'sottes',\n            'niais', 'niaise', 'niaises',\n            'ballot', 'ballots', 'andouille', 'andouilles',\n        ],\n        'moderate' => [\n            'connard', 'connarde', 'con', 'conne',\n            'salaud', 'salope', 'garce', 'garces',\n            'pétasse', 'petasse', 'pétasses', 'petasses',\n            'bâtard', 'batard', 'bâtards', 'batards',\n            'bâtarde', 'batarde', 'bâtardes', 'batardes',\n            'abruti', 'abrutis', 'abrutie', 'abruties',\n            'crétin', 'cretin', 'crétins', 'cretins',\n            'crétine', 'cretine', 'crétines', 'cretines',\n            'débile', 'debile', 'débiles', 'debiles',\n            'imbécile', 'imbecile', 'imbéciles', 'imbeciles',\n            'cul', 'culs', 'trou du cul', 'trou de balle',\n            'cochon', 'cochons', 'cochonne', 'cochonnes',\n        ],\n        'high' => [\n            'merde', 'putain', 'enculé', 'encule',\n            'niquer', 'nique', 'baiser', 'baise',\n            'foutre', 'foutu', 'foutue', 'chier',\n            'bite', 'pute', 'fils de pute',\n        ],\n        'extreme' => [\n            'pédé', 'pede', 'pédés', 'pedes',\n            'pédéraste', 'pederaste', 'pédérastes', 'pederastes',\n            'tapette', 'tapettes', 'tantouze', 'tantouzes',\n            'fiotte', 'fiottes', 'tarlouze', 'tarlouzes',\n            'gouine', 'gouines',\n            'attardé', 'attarde', 'attardés', 'attardes',\n            'attardée', 'attardee', 'attardées', 'attardees',\n        ],\n    ],\n\n    'profanities' => [\n        // Common French profanities and vulgar expressions\n        'merde',\n        'putain',\n        'connard',\n        'connarde',\n        'con',\n        'conne',\n        'salaud',\n        'salope',\n        'enculé',\n        'encule',\n        'enculée',\n        'enculee',\n        'fils de pute',\n        'fils de putain',\n        'bordel',\n        'chier',\n        'chiasse',\n        'chieur',\n        'chieuse',\n        'emmerde',\n        'emmerder',\n        'emmerdeur',\n        'emmerdeuse',\n        'baiser',\n        'baise',\n        'baisé',\n        'baise',\n        'baisée',\n        'baisee',\n        'foutre',\n        'foutue',\n        'foutu',\n        'niquer',\n        'nique',\n        'niqué',\n        'nique',\n        'niquée',\n        'niquee',\n        'bite',\n        'bites',\n        'pine',\n        'pines',\n        'queue',\n        'queues',\n        'vit',\n        'verge',\n        'zob',\n        'zobs',\n        'biroute',\n        'biroutes',\n        'braquemart',\n        'braquemarts',\n        'dard',\n        'dards',\n        'gourdin',\n        'gourdins',\n        'gland',\n        'glands',\n        'prépuce',\n        'prepuce',\n        'prépuces',\n        'prepuces',\n        'couilles',\n        'couille',\n        'couillon',\n        'couillonne',\n        'couillons',\n        'couillonnes',\n        'roubignoles',\n        'roubignole',\n        'burnes',\n        'burne',\n        'roustons',\n        'rouston',\n        'testicules',\n        'testicule',\n        'génitoires',\n        'genitoires',\n        'génitoire',\n        'genitoire',\n        'chatte',\n        'chattes',\n        'minou',\n        'minous',\n        'con',\n        'cons',\n        'moule',\n        'moules',\n        'fente',\n        'fentes',\n        'cramouille',\n        'cramouilles',\n        'crevasse',\n        'crevasses',\n        'cyprine',\n        'cyprines',\n        'foufoune',\n        'foufounes',\n        'motte',\n        'mottes',\n        'touffe',\n        'touffes',\n        'abricot',\n        'abricots',\n        'nichons',\n        'nichon',\n        'tétons',\n        'teton',\n        'téton',\n        'tetons',\n        'roberts',\n        'robert',\n        'doudounes',\n        'doudoune',\n        'lolos',\n        'lolo',\n        'miches',\n        'miche',\n        'mamelles',\n        'mamelle',\n        'seins',\n        'sein',\n        'nénés',\n        'nene',\n        'nénée',\n        'nenee',\n        'roploplos',\n        'roploplo',\n        'flotteurs',\n        'flotteur',\n        'amortisseurs',\n        'amortisseur',\n        'airbags',\n        'airbag',\n        'cul',\n        'culs',\n        'fesses',\n        'fesse',\n        'pétard',\n        'petard',\n        'pétards',\n        'petards',\n        'postérieur',\n        'posterieur',\n        'postérieurs',\n        'posterieurs',\n        'derrière',\n        'derriere',\n        'derrières',\n        'derrieres',\n        'fion',\n        'fions',\n        'trou du cul',\n        'trou de balle',\n        'anus',\n        'orifice',\n        'orifices',\n        'rosette',\n        'rosettes',\n        'rondelle',\n        'rondelles',\n        'bague',\n        'bagues',\n        'anneau',\n        'anneaux',\n        'pédé',\n        'pede',\n        'pédés',\n        'pedes',\n        'pédéraste',\n        'pederaste',\n        'pédérastes',\n        'pederastes',\n        'tapette',\n        'tapettes',\n        'tante',\n        'tantes',\n        'tantouze',\n        'tantouzes',\n        'fiotte',\n        'fiottes',\n        'tarlouze',\n        'tarlouzes',\n        'tafiole',\n        'tafioles',\n        'gouine',\n        'gouines',\n        'lesbienne',\n        'lesbiennes',\n        'tribade',\n        'tribades',\n        'saphique',\n        'saphiques',\n        'lesbos',\n        'lesbo',\n        'garce',\n        'garces',\n        'pétasse',\n        'petasse',\n        'pétasses',\n        'petasses',\n        'traînée',\n        'trainee',\n        'traînées',\n        'trainees',\n        'pute',\n        'putes',\n        'putain',\n        'putains',\n        'catin',\n        'catins',\n        'caillera',\n        'cailleras',\n        'racaille',\n        'racailles',\n        'voyou',\n        'voyous',\n        'truand',\n        'truands',\n        'bandit',\n        'bandits',\n        'malfrat',\n        'malfrats',\n        'gangster',\n        'gangsters',\n        'criminel',\n        'criminels',\n        'criminelle',\n        'criminelles',\n        'assassin',\n        'assassins',\n        'tueur',\n        'tueurs',\n        'tueuse',\n        'tueuses',\n        'meurtrier',\n        'meurtriers',\n        'meurtrière',\n        'meurtrieres',\n        'bourrin',\n        'bourrins',\n        'bourrine',\n        'bourrines',\n        'rustre',\n        'rustres',\n        'plouc',\n        'ploucs',\n        'péquenaud',\n        'pequenaud',\n        'péquenauds',\n        'pequenauds',\n        'cul-terreux',\n        'cul terreux',\n        'bouseux',\n        'boueuse',\n        'bouseux',\n        'bouseuses',\n        'bâtard',\n        'batard',\n        'bâtards',\n        'batards',\n        'bâtarde',\n        'batarde',\n        'bâtardes',\n        'batardes',\n        'salopard',\n        'salopards',\n        'saloparde',\n        'salopardes',\n        'fumier',\n        'fumiers',\n        'ordure',\n        'ordures',\n        'pourriture',\n        'pourritures',\n        'charogne',\n        'charognes',\n        'raclure',\n        'raclures',\n        'déchet',\n        'dechet',\n        'déchets',\n        'dechets',\n        'rebut',\n        'rebuts',\n        'lie',\n        'lies',\n        'écume',\n        'ecume',\n        'écumes',\n        'ecumes',\n        'fange',\n        'fanges',\n        'boue',\n        'boues',\n        'vase',\n        'vases',\n        'crotte',\n        'crottes',\n        'étron',\n        'etron',\n        'étrons',\n        'etrons',\n        'caca',\n        'cacas',\n        'bouse',\n        'bouses',\n        'fiente',\n        'fientes',\n        'colombin',\n        'colombins',\n        'boudin',\n        'boudins',\n        'saucisse',\n        'saucisses',\n        'andouille',\n        'andouilles',\n        'crétin',\n        'cretin',\n        'crétins',\n        'cretins',\n        'crétine',\n        'cretine',\n        'crétines',\n        'cretines',\n        'débile',\n        'debile',\n        'débiles',\n        'debiles',\n        'attardé',\n        'attarde',\n        'attardés',\n        'attardes',\n        'attardée',\n        'attardee',\n        'attardées',\n        'attardees',\n        'demeuré',\n        'demeure',\n        'demeurés',\n        'demeures',\n        'demeurée',\n        'demeuree',\n        'demeurées',\n        'demeurees',\n        'simple',\n        'simples',\n        'idiot',\n        'idiots',\n        'idiote',\n        'idiotes',\n        'imbécile',\n        'imbecile',\n        'imbéciles',\n        'imbeciles',\n        'stupide',\n        'stupides',\n        'bête',\n        'bete',\n        'bêtes',\n        'betes',\n        'sot',\n        'sots',\n        'sotte',\n        'sottes',\n        'niais',\n        'niaise',\n        'niaises',\n        'nigaud',\n        'nigauds',\n        'nigaude',\n        'nigaudes',\n        'benêt',\n        'benet',\n        'benêts',\n        'benets',\n        'benête',\n        'benete',\n        'benêtes',\n        'benetes',\n        'ballot',\n        'ballots',\n        'ballotte',\n        'ballottes',\n        'balourd',\n        'balourds',\n        'balourde',\n        'balourdes',\n        'lourdaud',\n        'lourdauds',\n        'lourdaude',\n        'lourdaudes',\n        'abruti',\n        'abrutis',\n        'abrutie',\n        'abruties',\n        'bourrique',\n        'bourriques',\n        'âne',\n        'ane',\n        'ânes',\n        'anes',\n        'ânesse',\n        'anesse',\n        'ânesses',\n        'anesses',\n        'baudet',\n        'baudets',\n        'bourricot',\n        'bourricots',\n        'gourde',\n        'gourdes',\n        'cornichon',\n        'cornichons',\n        'navet',\n        'navets',\n        'nouille',\n        'nouilles',\n        'patate',\n        'patates',\n        'buse',\n        'buses',\n        'dinde',\n        'dindes',\n        'dindon',\n        'dindons',\n        'oie',\n        'oies',\n        'jars',\n        'bécasse',\n        'becasse',\n        'bécasses',\n        'becasses',\n        'bécassine',\n        'becassine',\n        'bécassines',\n        'becassines',\n        'poule',\n        'poules',\n        'poulet',\n        'poulets',\n        'coquette',\n        'coquettes',\n        'coq',\n        'coqs',\n        'chapon',\n        'chapons',\n        'poularde',\n        'poulardes',\n        'poussin',\n        'poussins',\n        'poussinière',\n        'poussiniere',\n        'poussinières',\n        'poussinieres',\n        'cochon',\n        'cochons',\n        'cochonne',\n        'cochonnes',\n        'porc',\n        'porcs',\n        'truie',\n        'truies',\n        'pourceau',\n        'pourceaux',\n        'goret',\n        'gorets',\n        'cochonnet',\n        'cochonnets',\n        'cochonnaille',\n        'cochonnailles',\n        'verrat',\n        'verrats',\n        'bauge',\n        'bauges',\n        'porcherie',\n        'porcheries',\n        'étable',\n        'etable',\n        'étables',\n        'etables',\n        'écurie',\n        'ecurie',\n        'écuries',\n        'ecuries',\n        'box',\n        'boxs',\n        'stalle',\n        'stalles',\n        'enclos',\n        'clos',\n        'parc',\n        'parcs',\n        'paddock',\n        'paddocks',\n        'pâturage',\n        'paturage',\n        'pâturages',\n        'paturages',\n        'prairie',\n        'prairies',\n        'pré',\n        'pre',\n        'prés',\n        'pres',\n        'herbage',\n        'herbages',\n        'pacage',\n        'pacages',\n        'pâture',\n        'pature',\n        'pâtures',\n        'patures',\n        'fourrage',\n        'fourrages',\n        'foin',\n        'foins',\n        'paille',\n        'pailles',\n        'litière',\n        'litiere',\n        'litières',\n        'litieres',\n        'fumier',\n        'fumiers',\n        'purin',\n        'purins',\n        'lisier',\n        'lisiers',\n        'compost',\n        'composts',\n        'engrais',\n        'fertilisant',\n        'fertilisants',\n        'amendement',\n        'amendements',\n        'terreau',\n        'terreaux',\n        'humus',\n        'tourbe',\n        'tourbes',\n        'mousse',\n        'mousses',\n        'lichen',\n        'lichens',\n        'algue',\n        'algues',\n        'varech',\n        'varechs',\n        'goémon',\n        'goemon',\n        'goémons',\n        'goemons',\n        'sargasse',\n        'sargasses',\n        'zostère',\n        'zostere',\n        'zostères',\n        'zosteres',\n        'laminaire',\n        'laminaires',\n        'fucus',\n        'ulve',\n        'ulves',\n        'spiruline',\n        'spirulines',\n        'chlorelle',\n        'chlorelles',\n        'microalgue',\n        'microalgues',\n        'phytoplancton',\n        'phytoplanctons',\n        'zooplancton',\n        'zooplanctons',\n        'plancton',\n        'planctons',\n        'krill',\n        'krills',\n        'copépode',\n        'copepode',\n        'copépodes',\n        'copepodes',\n        'rotifère',\n        'rotifere',\n        'rotifères',\n        'rotiferes',\n        'protozoaire',\n        'protozoaires',\n        'paramècie',\n        'paramecie',\n        'paramécies',\n        'paramecies',\n        'amibe',\n        'amibes',\n        'euglène',\n        'euglene',\n        'euglènes',\n        'euglenes',\n        'volvox',\n        'hydre',\n        'hydres',\n        'méduse',\n        'meduse',\n        'méduses',\n        'meduses',\n        'polype',\n        'polypes',\n        'corail',\n        'coraux',\n        'anémone',\n        'anemone',\n        'anémones',\n        'anemones',\n        'actinie',\n        'actinies',\n        'éponge',\n        'eponge',\n        'éponges',\n        'eponges',\n        'spongieux',\n        'spongieuse',\n        'spongieuses',\n        'poreux',\n        'poreuse',\n        'poreuses',\n        'alvéolé',\n        'alveole',\n        'alvéolés',\n        'alveoles',\n        'alvéolée',\n        'alveolee',\n        'alvéolées',\n        'alveolees',\n        'cellulaire',\n        'cellulaires',\n        'cellule',\n        'cellules',\n        'cytoplasme',\n        'cytoplasmes',\n        'noyau',\n        'noyaux',\n        'nucléole',\n        'nucleole',\n        'nucléoles',\n        'nucleoles',\n        'chromosome',\n        'chromosomes',\n        'chromatine',\n        'chromatines',\n        'gène',\n        'gene',\n        'gènes',\n        'genes',\n        'génome',\n        'genome',\n        'génomes',\n        'genomes',\n        'génétique',\n        'genetique',\n        'génétiques',\n        'genetiques',\n        'héréditaire',\n        'hereditaire',\n        'héréditaires',\n        'hereditaires',\n        'hérédité',\n        'heredite',\n        'hérédités',\n        'heredites',\n        'descendance',\n        'descendances',\n        'progéniture',\n        'progeniture',\n        'progénitures',\n        'progenitures',\n        'postérité',\n        'posterite',\n        'postérités',\n        'posterites',\n        'lignée',\n        'lignee',\n        'lignées',\n        'lignees',\n        'dynastie',\n        'dynasties',\n        'famille',\n        'familles',\n        'clan',\n        'clans',\n        'tribu',\n        'tribus',\n        'peuplade',\n        'peuplades',\n        'ethnie',\n        'ethnies',\n        'race',\n        'races',\n        'espèce',\n        'espece',\n        'espèces',\n        'especes',\n        'genre',\n        'genres',\n        'variété',\n        'variete',\n        'variétés',\n        'varietes',\n        'sous-espèce',\n        'sous-espece',\n        'sous-espèces',\n        'sous-especes',\n        'subspecies',\n        'sous-variété',\n        'sous-variete',\n        'sous-variétés',\n        'sous-varietes',\n        'cultivar',\n        'cultivars',\n        'hybride',\n        'hybrides',\n        'croisement',\n        'croisements',\n        'métissage',\n        'metissage',\n        'métissages',\n        'metissages',\n        'brassage',\n        'brassages',\n        'mélange',\n        'melange',\n        'mélanges',\n        'melanges',\n        'mixture',\n        'mixtures',\n        'composition',\n        'compositions',\n        'formule',\n        'formules',\n        'recette',\n        'recettes',\n        'procédé',\n        'procede',\n        'procédés',\n        'procedes',\n        'méthode',\n        'methode',\n        'méthodes',\n        'methodes',\n        'technique',\n        'techniques',\n        'procédure',\n        'procedure',\n        'procédures',\n        'procedures',\n        'protocole',\n        'protocoles',\n        'marche',\n        'marches',\n        'démarche',\n        'demarche',\n        'démarches',\n        'demarches',\n        'approche',\n        'approches',\n        'façon',\n        'facon',\n        'façons',\n        'facons',\n        'manière',\n        'maniere',\n        'manières',\n        'manieres',\n        'mode',\n        'modes',\n        'modalité',\n        'modalite',\n        'modalités',\n        'modalites',\n        'moyen',\n        'moyens',\n        'outil',\n        'outils',\n        'instrument',\n        'instruments',\n        'ustensile',\n        'ustensiles',\n        'appareil',\n        'appareils',\n        'dispositif',\n        'dispositifs',\n        'mécanisme',\n        'mecanisme',\n        'mécanismes',\n        'mecanismes',\n        'machine',\n        'machines',\n        'engin',\n        'engins',\n        'équipement',\n        'equipement',\n        'équipements',\n        'equipements',\n        'matériel',\n        'materiel',\n        'matériels',\n        'materiels',\n        'outillage',\n        'outillages',\n        'machinerie',\n        'machineries',\n        'mécanique',\n        'mecanique',\n        'mécaniques',\n        'mecaniques',\n        'automatique',\n        'automatiques',\n        'électrique',\n        'electrique',\n        'électriques',\n        'electriques',\n        'électronique',\n        'electronique',\n        'électroniques',\n        'electroniques',\n        'numérique',\n        'numerique',\n        'numériques',\n        'numeriques',\n        'digital',\n        'digitaux',\n        'digitale',\n        'digitales',\n        'informatique',\n        'informatiques',\n        'ordinateur',\n        'ordinateurs',\n        'computer',\n        'computers',\n        'pc',\n        'pcs',\n        'micro',\n        'micros',\n        'portable',\n        'portables',\n        'laptop',\n        'laptops',\n        'tablette',\n        'tablettes',\n        'smartphone',\n        'smartphones',\n        'téléphone',\n        'telephone',\n        'téléphones',\n        'telephones',\n        'mobile',\n        'mobiles',\n        'cellulaire',\n        'cellulaires',\n        'sans-fil',\n        'sans fil',\n        'wifi',\n        'bluetooth',\n        'internet',\n        'web',\n        'site',\n        'sites',\n        'page',\n        'pages',\n        'lien',\n        'liens',\n        'url',\n        'urls',\n        'adresse',\n        'adresses',\n        'email',\n        'emails',\n        'courriel',\n        'courriels',\n        'message',\n        'messages',\n        'texto',\n        'textos',\n        'sms',\n        'mms',\n        'chat',\n        'chats',\n        'forum',\n        'forums',\n        'blog',\n        'blogs',\n        'réseau',\n        'reseau',\n        'réseaux',\n        'reseaux',\n        'social',\n        'sociaux',\n        'sociale',\n        'sociales',\n        'facebook',\n        'twitter',\n        'instagram',\n        'linkedin',\n        'youtube',\n        'google',\n        'yahoo',\n        'bing',\n        'moteur',\n        'moteurs',\n        'recherche',\n        'recherches',\n        'requête',\n        'requete',\n        'requêtes',\n        'requetes',\n        'base',\n        'bases',\n        'donnée',\n        'donnee',\n        'données',\n        'donnees',\n        'information',\n        'informations',\n        'renseignement',\n        'renseignements',\n        'détail',\n        'detail',\n        'détails',\n        'details',\n        'précision',\n        'precision',\n        'précisions',\n        'precisions',\n        'exactitude',\n        'exactitudes',\n        'justesse',\n        'justesses',\n        'vérité',\n        'verite',\n        'vérités',\n        'verites',\n        'réalité',\n        'realite',\n        'réalités',\n        'realites',\n        'fait',\n        'faits',\n        'élément',\n        'element',\n        'éléments',\n        'elements',\n        'composant',\n        'composants',\n        'composante',\n        'composantes',\n        'partie',\n        'parties',\n        'portion',\n        'portions',\n        'section',\n        'sections',\n        'segment',\n        'segments',\n        'fragment',\n        'fragments',\n        'morceau',\n        'morceaux',\n        'bout',\n        'bouts',\n        'extrémité',\n        'extremite',\n        'extrémités',\n        'extremites',\n        'pointe',\n        'pointes',\n        'sommet',\n        'sommets',\n        'pic',\n        'pics',\n        'cime',\n        'cimes',\n        'faîte',\n        'faite',\n        'faîtes',\n        'faites',\n        'crête',\n        'crete',\n        'crêtes',\n        'cretes',\n        'arête',\n        'arete',\n        'arêtes',\n        'aretes',\n        'angle',\n        'angles',\n        'coin',\n        'coins',\n        'recoin',\n        'recoins',\n        'recess',\n        'alcôve',\n        'alcove',\n        'alcôves',\n        'alcoves',\n        'niche',\n        'niches',\n        'anfractuosité',\n        'anfractuosite',\n        'anfractuosités',\n        'anfractuosites',\n        'cavité',\n        'cavite',\n        'cavités',\n        'cavites',\n        'trou',\n        'trous',\n        'creux',\n        'orifice',\n        'orifices',\n        'ouverture',\n        'ouvertures',\n        'fente',\n        'fentes',\n        'fissure',\n        'fissures',\n        'crevasse',\n        'crevasses',\n        'lézarde',\n        'lezarde',\n        'lézardes',\n        'lezardes',\n        'gerçure',\n        'gercure',\n        'gerçures',\n        'gercures',\n        'cassure',\n        'cassures',\n        'fracture',\n        'fractures',\n        'rupture',\n        'ruptures',\n        'brisure',\n        'brisures',\n        'félure',\n        'felure',\n        'félures',\n        'felures',\n        'brèche',\n        'breche',\n        'brèches',\n        'breches',\n        'trouée',\n        'trouee',\n        'trouées',\n        'trouees',\n        'percée',\n        'percee',\n        'percées',\n        'percees',\n        'passage',\n        'passages',\n        'couloir',\n        'couloirs',\n        'corridor',\n        'corridors',\n        'galerie',\n        'galeries',\n        'tunnel',\n        'tunnels',\n        'souterrain',\n        'souterrains',\n        'grotte',\n        'grottes',\n        'caverne',\n        'cavernes',\n        'antre',\n        'antres',\n        'tanière',\n        'taniere',\n        'tanières',\n        'tanieres',\n        'gîte',\n        'gite',\n        'gîtes',\n        'gites',\n        'refuge',\n        'refuges',\n        'abri',\n        'abris',\n        'cachette',\n        'cachettes',\n        'planque',\n        'planques',\n        'repaire',\n        'repaires',\n        'retraite',\n        'retraites',\n        'ermitage',\n        'ermitages',\n        'solitude',\n        'solitudes',\n        'isolement',\n        'isolements',\n        'séparation',\n        'separation',\n        'séparations',\n        'separations',\n        'division',\n        'divisions',\n        'cloison',\n        'cloisons',\n        'paroi',\n        'parois',\n        'mur',\n        'murs',\n        'muraille',\n        'murailles',\n        'rempart',\n        'remparts',\n        'fortification',\n        'fortifications',\n        'défense',\n        'defense',\n        'défenses',\n        'defenses',\n        'protection',\n        'protections',\n        'blindage',\n        'blindages',\n        'cuirasse',\n        'cuirasses',\n        'armure',\n        'armures',\n        'bouclier',\n        'boucliers',\n        'écu',\n        'ecu',\n        'écus',\n        'ecus',\n        'pavois',\n        'rondache',\n        'rondaches',\n        'targe',\n        'targes',\n        'carapace',\n        'carapaces',\n        'coquille',\n        'coquilles',\n        'écaille',\n        'ecaille',\n        'écailles',\n        'ecailles',\n        'plaque',\n        'plaques',\n        'lame',\n        'lames',\n        'feuille',\n        'feuilles',\n        'pellicule',\n        'pellicules',\n        'membrane',\n        'membranes',\n        'tissu',\n        'tissus',\n        'étoffe',\n        'etoffe',\n        'étoffes',\n        'etoffes',\n        'textile',\n        'textiles',\n        'fibre',\n        'fibres',\n        'fil',\n        'fils',\n        'filament',\n        'filaments',\n        'brin',\n        'brins',\n        'corde',\n        'cordes',\n        'ficelle',\n        'ficelles',\n        'câble',\n        'cable',\n        'câbles',\n        'cables',\n        'chaîne',\n        'chaine',\n        'chaînes',\n        'chaines',\n        'maillon',\n        'maillons',\n        'anneau',\n        'anneaux',\n        'bague',\n        'bagues',\n        'alliance',\n        'alliances',\n        'jonc',\n        'joncs',\n        'chevalière',\n        'chevaliere',\n        'chevalières',\n        'chevalieres',\n        'solitaire',\n        'solitaires',\n        'diamant',\n        'diamants',\n        'pierre',\n        'pierres',\n        'gemme',\n        'gemmes',\n        'bijou',\n        'bijoux',\n        'joyau',\n        'joyaux',\n        'parure',\n        'parures',\n        'ornement',\n        'ornements',\n        'décoration',\n        'decoration',\n        'décorations',\n        'decorations',\n        'enjolivement',\n        'enjolivements',\n        'embellissement',\n        'embellissements',\n        'agrément',\n        'agrement',\n        'agréments',\n        'agrements',\n        'atour',\n        'atours',\n        'apparence',\n        'apparences',\n        'aspect',\n        'aspects',\n        'allure',\n        'allures',\n        'prestance',\n        'prestances',\n        'élégance',\n        'elegance',\n        'élégances',\n        'elegances',\n        'raffinement',\n        'raffinements',\n        'distinction',\n        'distinctions',\n        'classe',\n        'classes',\n        'style',\n        'styles',\n        'genre',\n        'genres',\n        'mode',\n        'modes',\n        'tendance',\n        'tendances',\n        'fashion',\n        'fashions',\n        'couture',\n        'coutures',\n        'prêt-à-porter',\n        'pret-a-porter',\n        'haute-couture',\n        'haute couture',\n        'confection',\n        'confections',\n        'vêtement',\n        'vetement',\n        'vêtements',\n        'vetements',\n        'habit',\n        'habits',\n        'tenue',\n        'tenues',\n        'costume',\n        'costumes',\n        'toilette',\n        'toilettes',\n        'mise',\n        'mises',\n        'accoutrement',\n        'accoutrements',\n        'harnachement',\n        'harnachements',\n        'équipement',\n        'equipement',\n        'équipements',\n        'equipements',\n        'attirail',\n        'attirails',\n        'matériel',\n        'materiel',\n        'matériels',\n        'materiels',\n        'outillage',\n        'outillages',\n        'arsenal',\n        'arsenaux',\n        'armement',\n        'armements',\n        'panoplie',\n        'panoplies',\n        'collection',\n        'collections',\n        'assortiment',\n        'assortiments',\n        'gamme',\n        'gammes',\n        'palette',\n        'palettes',\n        'éventail',\n        'eventail',\n        'éventails',\n        'eventails',\n        'choix',\n        'sélection',\n        'selection',\n        'sélections',\n        'selections',\n        'tri',\n        'tris',\n        'triage',\n        'triages',\n        'criblage',\n        'criblages',\n        'filtrage',\n        'filtrages',\n        'épuration',\n        'epuration',\n        'épurations',\n        'epurations',\n        'purification',\n        'purifications',\n        'assainissement',\n        'assainissements',\n        'nettoyage',\n        'nettoyages',\n        'lavage',\n        'lavages',\n        'rinçage',\n        'rincage',\n        'rinçages',\n        'rincages',\n        'lessivage',\n        'lessivages',\n        'blanchiment',\n        'blanchiments',\n        'dégraissage',\n        'degraissage',\n        'dégraissages',\n        'degraissages',\n        'détachage',\n        'detachage',\n        'détachages',\n        'detachages',\n        'décrassage',\n        'decrassage',\n        'décrassages',\n        'decrassages',\n        'récurage',\n        'recurage',\n        'récurages',\n        'recurages',\n        'frottage',\n        'frottages',\n        'brossage',\n        'brossages',\n        'polissage',\n        'polissages',\n        'lustrage',\n        'lustrages',\n        'cirage',\n        'cirages',\n        'encaustique',\n        'encaustiques',\n        'cire',\n        'cires',\n        'pommade',\n        'pommades',\n        'baume',\n        'baumes',\n        'crème',\n        'creme',\n        'crèmes',\n        'cremes',\n        'onguent',\n        'onguents',\n        'liniment',\n        'liniments',\n        'embrocation',\n        'embrocations',\n        'friction',\n        'frictions',\n        'massage',\n        'massages',\n        'pétrissage',\n        'petrissage',\n        'pétrissages',\n        'petrissages',\n        'malaxage',\n        'malaxages',\n        'manipulation',\n        'manipulations',\n        'maniement',\n        'maniements',\n        'manutention',\n        'manutentions',\n        'transport',\n        'transports',\n        'acheminement',\n        'acheminements',\n        'convoyage',\n        'convoyages',\n        'livraison',\n        'livraisons',\n        'distribution',\n        'distributions',\n        'répartition',\n        'repartition',\n        'répartitions',\n        'repartitions',\n        'partage',\n        'partages',\n        'division',\n        'divisions',\n        'séparation',\n        'separation',\n        'séparations',\n        'separations',\n        'scission',\n        'scissions',\n        'coupure',\n        'coupures',\n        'découpage',\n        'decoupage',\n        'découpages',\n        'decoupages',\n        'sectionnement',\n        'sectionnements',\n        'segmentation',\n        'segmentations',\n        'morcellement',\n        'morcellements',\n        'fragmentation',\n        'fragmentations',\n        'émiettement',\n        'emiettement',\n        'émiettements',\n        'emiettements',\n        'pulvérisation',\n        'pulverisation',\n        'pulvérisations',\n        'pulverisations',\n        'atomisation',\n        'atomisations',\n        'vaporisation',\n        'vaporisations',\n        'évaporation',\n        'evaporation',\n        'évaporations',\n        'evaporations',\n        'sublimation',\n        'sublimations',\n        'distillation',\n        'distillations',\n        'condensation',\n        'condensations',\n        'liquéfaction',\n        'liquefaction',\n        'liquéfactions',\n        'liquefactions',\n        'solidification',\n        'solidifications',\n        'cristallisation',\n        'cristallisations',\n        'congélation',\n        'congelation',\n        'congélations',\n        'congelations',\n        'gel',\n        'gels',\n        'glaciation',\n        'glaciations',\n        'refroidissement',\n        'refroidissements',\n        'réfrigération',\n        'refrigeration',\n        'réfrigérations',\n        'refrigerations',\n        'zut',\n        'punaise',\n    ],\n    \n    'false_positives' => [\n        // Common French words that might be detected as false positives\n        'analyse',\n        'analyses',\n        'classe',\n        'classes',\n        'passer',\n        'passage',\n        'passages',\n        'expression',\n        'expressions',\n        'assassin',\n        'assassins',\n        'assassiner',\n        'assassinat',\n        'assassinats',\n        'entreprise',\n        'entreprises',\n        'entrepreneur',\n        'entrepreneurs',\n        'affaire',\n        'affaires',\n        'travail',\n        'travaux',\n        'travailler',\n        'travailleur',\n        'travailleurs',\n        'travailleuse',\n        'travailleuses',\n        'emploi',\n        'emplois',\n        'employé',\n        'employe',\n        'employés',\n        'employes',\n        'employée',\n        'employee',\n        'employées',\n        'employees',\n        'employeur',\n        'employeurs',\n        'bureau',\n        'bureaux',\n        'ordinateur',\n        'ordinateurs',\n        'machine',\n        'machines',\n        'appareil',\n        'appareils',\n        'dispositif',\n        'dispositifs',\n        'instrument',\n        'instruments',\n        'outil',\n        'outils',\n        'utilité',\n        'utilites',\n        'fonction',\n        'fonctions',\n        'fonctionner',\n        'fonctionnement',\n        'fonctionnements',\n        'caractéristique',\n        'caracteristique',\n        'caractéristiques',\n        'caracteristiques',\n        'spécialité',\n        'specialite',\n        'spécialités',\n        'specialites',\n        'spécialiste',\n        'specialiste',\n        'spécialistes',\n        'specialistes',\n        'spécialiser',\n        'specialiser',\n        'spécialisé',\n        'specialise',\n        'spécialisée',\n        'specialisee',\n        'spécialisés',\n        'specialises',\n        'spécialisées',\n        'specialisees',\n        'spécialisation',\n        'specialisation',\n        'spécialisations',\n        'specialisations',\n        'professionnel',\n        'professionnels',\n        'professionnelle',\n        'professionnelles',\n        'profession',\n        'professions',\n        'professeur',\n        'professeurs',\n        'enseigner',\n        'enseignement',\n        'enseignements',\n        'éducation',\n        'education',\n        'éducatif',\n        'educatif',\n        'éducative',\n        'educative',\n        'éducatifs',\n        'educatifs',\n        'éducatives',\n        'educatives',\n        'éduquer',\n        'eduquer',\n        'éduqué',\n        'eduque',\n        'éduquée',\n        'eduquee',\n        'éduqués',\n        'eduques',\n        'éduquées',\n        'eduquees',\n        'éducateur',\n        'educateur',\n        'éducateurs',\n        'educateurs',\n        'éducatrice',\n        'educatrice',\n        'éducatrices',\n        'educatrices',\n        'étudiant',\n        'etudiant',\n        'étudiants',\n        'etudiants',\n        'étudiante',\n        'etudiante',\n        'étudiantes',\n        'etudiantes',\n        'étudier',\n        'etudier',\n        'étude',\n        'etude',\n        'études',\n        'etudes',\n        'étudié',\n        'etudie',\n        'étudiée',\n        'etudiee',\n        'étudiés',\n        'etudies',\n        'étudiées',\n        'etudiees',\n        'recherche',\n        'recherches',\n        'rechercher',\n        'chercheur',\n        'chercheurs',\n        'chercheuse',\n        'chercheuses',\n        'scientifique',\n        'scientifiques',\n        'science',\n        'sciences',\n        'connaissance',\n        'connaissances',\n        'connaître',\n        'connaitre',\n        'connu',\n        'connue',\n        'connus',\n        'connues',\n        'savoir',\n        'savoirs',\n        'su',\n        'sue',\n        'sus',\n        'sues',\n        'sagesse',\n        'sagesses',\n        'sage',\n        'sages',\n        'intelligence',\n        'intelligences',\n        'intelligent',\n        'intelligents',\n        'intelligente',\n        'intelligentes',\n        'talent',\n        'talents',\n        'talentueux',\n        'talentueuse',\n        'talentueuses',\n        'habileté',\n        'habilete',\n        'habiletés',\n        'habiletes',\n        'habile',\n        'habiles',\n        'adresse',\n        'adresses',\n        'adroit',\n        'adroits',\n        'adroite',\n        'adroites',\n        'maître',\n        'maitre',\n        'maîtres',\n        'maitres',\n        'maîtresse',\n        'maitresse',\n        'maîtresses',\n        'maitresses',\n        'maîtrise',\n        'maitrise',\n        'maîtrises',\n        'maitrises',\n        'maîtriser',\n        'maitriser',\n        'maîtrisé',\n        'maitrise',\n        'maîtrisée',\n        'maitrisee',\n        'maîtrisés',\n        'maitrises',\n        'maîtrisées',\n        'maitrisees',\n        'domaine',\n        'domaines',\n        'dominer',\n        'dominé',\n        'domine',\n        'dominée',\n        'dominee',\n        'dominés',\n        'domines',\n        'dominées',\n        'dominees',\n        'contrôle',\n        'controle',\n        'contrôles',\n        'controles',\n        'contrôler',\n        'controler',\n        'contrôlé',\n        'controle',\n        'contrôlée',\n        'controlee',\n        'contrôlés',\n        'controles',\n        'contrôlées',\n        'controlees',\n        'administration',\n        'administrations',\n        'administrer',\n        'administrateur',\n        'administrateurs',\n        'administratrice',\n        'administratrices',\n        'gestion',\n        'gestions',\n        'gérer',\n        'gerer',\n        'géré',\n        'gere',\n        'gérée',\n        'geree',\n        'gérés',\n        'geres',\n        'gérées',\n        'gerees',\n        'gestionnaire',\n        'gestionnaires',\n        'organisation',\n        'organisations',\n        'organiser',\n        'organisé',\n        'organise',\n        'organisée',\n        'organisee',\n        'organisés',\n        'organises',\n        'organisées',\n        'organisees',\n        'organisateur',\n        'organisateurs',\n        'organisatrice',\n        'organisatrices',\n        'système',\n        'systeme',\n        'systèmes',\n        'systemes',\n        'systématique',\n        'systematique',\n        'systématiques',\n        'systematiques',\n        'méthode',\n        'methode',\n        'méthodes',\n        'methodes',\n        'méthodologie',\n        'methodologie',\n        'méthodologies',\n        'methodologies',\n        'processus',\n        'traiter',\n        'traité',\n        'traite',\n        'traitée',\n        'traitee',\n        'traités',\n        'traites',\n        'traitées',\n        'traitees',\n        'procédure',\n        'procedure',\n        'procédures',\n        'procedures',\n        'procéder',\n        'proceder',\n        'protocole',\n        'protocoles',\n        'norme',\n        'normes',\n        'normal',\n        'normaux',\n        'normale',\n        'normales',\n        'normalité',\n        'normalite',\n        'normalités',\n        'normalites',\n        'normaliser',\n        'normalisé',\n        'normalise',\n        'normalisée',\n        'normalisee',\n        'normalisés',\n        'normalises',\n        'normalisées',\n        'normalisees',\n        'standard',\n        'standards',\n        'standardiser',\n        'standardisé',\n        'standardise',\n        'standardisée',\n        'standardisee',\n        'standardisés',\n        'standardises',\n        'standardisées',\n        'standardisees',\n        'règle',\n        'regle',\n        'règles',\n        'regles',\n        'règlement',\n        'reglement',\n        'règlements',\n        'reglements',\n        'réglementer',\n        'reglementer',\n        'réglementaire',\n        'reglementaire',\n        'réglementaires',\n        'reglementaires',\n        'réguler',\n        'reguler',\n        'régulier',\n        'regulier',\n        'réguliers',\n        'reguliers',\n        'régulière',\n        'reguliere',\n        'régulières',\n        'regulieres',\n        'régularité',\n        'regularite',\n        'régularités',\n        'regularites',\n        'régulariser',\n        'regulariser',\n        'régularisé',\n        'regularise',\n        'régularisée',\n        'regularisee',\n        'régularisés',\n        'regularises',\n        'régularisées',\n        'regularisees',\n        'loi',\n        'lois',\n        'légal',\n        'legal',\n        'légaux',\n        'legaux',\n        'légale',\n        'legale',\n        'légales',\n        'legales',\n        'légalité',\n        'legalite',\n        'légalités',\n        'legalites',\n        'légaliser',\n        'legaliser',\n        'légalisé',\n        'legalise',\n        'légalisée',\n        'legalisee',\n        'légalisés',\n        'legalises',\n        'légalisées',\n        'legalisees',\n        'droit',\n        'droits',\n        'juridique',\n        'juridiques',\n        'justice',\n        'justices',\n        'juste',\n        'justes',\n        'injuste',\n        'injustes',\n        'injustice',\n        'injustices',\n        'tribunal',\n        'tribunaux',\n        'juge',\n        'juges',\n        'jugement',\n        'jugements',\n        'juger',\n        'jugé',\n        'juge',\n        'jugée',\n        'jugee',\n        'jugés',\n        'juges',\n        'jugées',\n        'jugees',\n        'sentence',\n        'sentences',\n        'condamnation',\n        'condamnations',\n        'condamner',\n        'condamné',\n        'condamne',\n        'condamnée',\n        'condamnee',\n        'condamnés',\n        'condamnes',\n        'condamnées',\n        'condamnees',\n        'punition',\n        'punitions',\n        'punir',\n        'puni',\n        'punie',\n        'punis',\n        'punies',\n        'peine',\n        'peines',\n        'prison',\n        'prisons',\n        'emprisonner',\n        'emprisonné',\n        'emprisonne',\n        'emprisonnée',\n        'emprisonnee',\n        'emprisonnés',\n        'emprisonnes',\n        'emprisonnées',\n        'emprisonnees',\n        'prisonnier',\n        'prisonniers',\n        'prisonnière',\n        'prisonniere',\n        'prisonnières',\n        'prisonnieres',\n        'détenu',\n        'detenu',\n        'détenus',\n        'detenus',\n        'détenue',\n        'detenue',\n        'détenues',\n        'detenues',\n        'pénitencier',\n        'penitencier',\n        'pénitenciers',\n        'penitenciers',\n        'maison',\n        'maisons',\n        'arrêt',\n        'arret',\n        'arrêts',\n        'arrets',\n    ],\n    \n    'substitutions' => [\n        '/à/' => ['à', 'a', '@', '4'],\n        '/â/' => ['â', 'a', '@', '4'],\n        '/ä/' => ['ä', 'a', '@', '4'],\n        '/á/' => ['á', 'a', '@', '4'],\n        '/ã/' => ['ã', 'a', '@', '4'],\n        '/å/' => ['å', 'a', '@', '4'],\n        '/æ/' => ['æ', 'ae', 'a'],\n        '/è/' => ['è', 'e', '3', '€'],\n        '/é/' => ['é', 'e', '3', '€'],\n        '/ê/' => ['ê', 'e', '3', '€'],\n        '/ë/' => ['ë', 'e', '3', '€'],\n        '/ì/' => ['ì', 'i', '1', '!', '|'],\n        '/í/' => ['í', 'i', '1', '!', '|'],\n        '/î/' => ['î', 'i', '1', '!', '|'],\n        '/ï/' => ['ï', 'i', '1', '!', '|'],\n        '/ò/' => ['ò', 'o', '0', 'ø'],\n        '/ó/' => ['ó', 'o', '0', 'ø'],\n        '/ô/' => ['ô', 'o', '0', 'ø'],\n        '/ö/' => ['ö', 'o', '0', 'ø'],\n        '/õ/' => ['õ', 'o', '0', 'ø'],\n        '/ø/' => ['ø', 'o', '0'],\n        '/œ/' => ['œ', 'oe', 'o'],\n        '/ù/' => ['ù', 'u', 'ü'],\n        '/ú/' => ['ú', 'u', 'ü'],\n        '/û/' => ['û', 'u', 'ü'],\n        '/ü/' => ['ü', 'u', 'ù'],\n        '/u/' => ['u', 'ù', 'ú', 'û', 'ü', '@', '*'],\n        '/ÿ/' => ['ÿ', 'y', 'i'],\n        '/ç/' => ['ç', 'c', 's'],\n        '/ñ/' => ['ñ', 'n', '~n'],\n        '/c/' => ['c', 'k', 'ç', 's'],\n        '/k/' => ['k', 'c', 'q'],\n        '/ph/' => ['ph', 'f'],\n        '/qu/' => ['qu', 'k', 'q'],\n        '/x/' => ['x', 'ks', 'gs'],\n        '/z/' => ['z', 's'],\n        '/j/' => ['j', 'g'],\n        '/g/' => ['g', 'j'],\n    ]\n];"
  },
  {
    "path": "config/languages/german.php",
    "content": "<?php\n\nreturn [\n    'severity' => [\n        'mild' => [\n            'mist', 'kacke', 'verdammt', 'verdammte', 'verdammter', 'verdammtes',\n            'blöd', 'bloed', 'blöde', 'bloede', 'blöder', 'bloeder', 'blödes', 'bloedes',\n            'doof', 'doofe', 'doofer', 'doofes',\n            'dumm', 'dumme', 'dummer', 'dummes',\n            'albern', 'alberne', 'alberner', 'albernes',\n            'peinlich', 'peinliche', 'peinlicher', 'peinliches',\n        ],\n        'moderate' => [\n            'arsch', 'arschloch', 'arschlöcher', 'arschlocher',\n            'schlampe', 'nutte', 'hure',\n            'wichser', 'depp', 'trottel',\n            'idiot', 'vollidiot',\n            'bescheuert', 'bescheuerte', 'bescheuerter', 'bescheuertes',\n            'bekloppt', 'bekloppte', 'bekloppter', 'beklopptes',\n            'schwanz', 'pimmel',\n            'hintern', 'po', 'popo',\n            'schwul', 'schwuler', 'schwule', 'schwules',\n        ],\n        'high' => [\n            'scheiße', 'scheisse', 'ficken', 'fick', 'gefickt',\n            'verfickt', 'fotze', 'muschi', 'möse', 'moese',\n            'hurensohn', 'hurenkind', 'arschficker',\n            'vögeln', 'voegeln', 'bumsen',\n        ],\n        'extreme' => [\n            'tunte', 'tuntig',\n            'kampflesbe', 'kampflesben',\n            'kanake', 'kanaken',\n            'neger', 'negerin',\n            'zigeuner', 'zigeunerin',\n            'retardiert', 'retardierte', 'retardierter',\n        ],\n    ],\n\n    'profanities' => [\n        // Common German profanities and vulgar expressions\n        'scheiße',\n        'scheisse',\n        'scheiß',\n        'scheiss',\n        'kacke',\n        'mist',\n        'arsch',\n        'arschloch',\n        'arschlöcher',\n        'arschlocher',\n        'ficken',\n        'fick',\n        'gefickt',\n        'verfickt',\n        'verfickte',\n        'verfickter',\n        'verficktes',\n        'verdammt',\n        'verdammte',\n        'verdammter',\n        'verdammtes',\n        'hurensohn',\n        'hurenkind',\n        'hure',\n        'nutte',\n        'schlampe',\n        'fotze',\n        'muschi',\n        'möse',\n        'moese',\n        'schwanz',\n        'pimmel',\n        'dödel',\n        'doedel',\n        'lümmel',\n        'luemmel',\n        'rute',\n        'zipfel',\n        'glied',\n        'eier',\n        'hoden',\n        'klöten',\n        'kloeten',\n        'sack',\n        'hodensack',\n        'nüsse',\n        'nuesse',\n        'kugeln',\n        'beutel',\n        'titten',\n        'brüste',\n        'brueste',\n        'busen',\n        'möpse',\n        'moepse',\n        'hupen',\n        'vorbau',\n        'körbchen',\n        'koerbchen',\n        'milchdrüsen',\n        'milchdruesen',\n        'warzen',\n        'nippel',\n        'brustwarzen',\n        'zitzen',\n        'hintern',\n        'po',\n        'popo',\n        'gesäß',\n        'gesaess',\n        'kehrseite',\n        'vier buchstaben',\n        'allerwertester',\n        'rückseite',\n        'rueckseite',\n        'backen',\n        'pobacken',\n        'arschbacken',\n        'speck',\n        'hinterteil',\n        'schwul',\n        'schwuler',\n        'schwule',\n        'schwules',\n        'homo',\n        'homos',\n        'homosexuell',\n        'homosexuelle',\n        'homosexueller',\n        'homosexuelles',\n        'tuntig',\n        'tunte',\n        'warm',\n        'warmer',\n        'warme',\n        'warmes',\n        'lesbe',\n        'lesben',\n        'lesbisch',\n        'lesbische',\n        'lesbischer',\n        'lesbisches',\n        'kampflesbe',\n        'kampflesben',\n        'butze',\n        'butzen',\n        'wichser',\n        'wichsen',\n        'wichst',\n        'gewichst',\n        'onanieren',\n        'onaniert',\n        'masturbieren',\n        'masturbiert',\n        'selbstbefriedigung',\n        'handjob',\n        'blasen',\n        'bläst',\n        'blaest',\n        'blowjob',\n        'oral',\n        'lecken',\n        'leckt',\n        'geleckt',\n        'cunnilingus',\n        'fellatio',\n        'lutschen',\n        'lutscht',\n        'gelutscht',\n        'saugen',\n        'saugt',\n        'gesaugt',\n        'pusten',\n        'pustet',\n        'gepustet',\n        'vögeln',\n        'voegeln',\n        'vögelt',\n        'voegelt',\n        'gevögelt',\n        'gevoegelt',\n        'bumsen',\n        'bumst',\n        'gebumst',\n        'poppen',\n        'poppt',\n        'gepoppt',\n        'knallen',\n        'knallt',\n        'geknallt',\n        'nageln',\n        'nagelt',\n        'genagelt',\n        'rammeln',\n        'rammelt',\n        'gerammelt',\n        'durchnageln',\n        'durchnagelt',\n        'durchgenagelt',\n        'durchficken',\n        'durchfickt',\n        'durchgefickt',\n        'rannehmen',\n        'rannimmt',\n        'rangenommen',\n        'besteigen',\n        'besteigt',\n        'bestiegen',\n        'bespringen',\n        'bespringt',\n        'besprungen',\n        'penis',\n        'vagina',\n        'vulva',\n        'klitoris',\n        'kitzler',\n        'schamlippen',\n        'venushügel',\n        'venushuegel',\n        'scham',\n        'geschlecht',\n        'geschlechtsteil',\n        'geschlechtsteile',\n        'genitalien',\n        'intimbereich',\n        'unterkörper',\n        'unterkoerper',\n        'lenden',\n        'lendengegend',\n        'schritt',\n        'schrittbereich',\n        'unterleib',\n        'becken',\n        'beckenboden',\n        'damm',\n        'perineum',\n        'anus',\n        'after',\n        'poloch',\n        'arschloch',\n        'rosette',\n        'poperze',\n        'hintertür',\n        'hintertuer',\n        'ausgang',\n        'darmausgang',\n        'enddarm',\n        'rektum',\n        'mastdarm',\n        'analbereich',\n        'afterbereich',\n        'hinterlader',\n        'arschficker',\n        'arschficken',\n        'arschgefickt',\n        'analverkehr',\n        'analsex',\n        'sodomie',\n        'sodomist',\n        'sodomistin',\n        'pervers',\n        'perverse',\n        'perverser',\n        'perverses',\n        'perversling',\n        'pervertiert',\n        'pervertierte',\n        'pervertierter',\n        'pervertiertes',\n        'versaut',\n        'versaute',\n        'versauter',\n        'versautes',\n        'schmutzig',\n        'schmutzige',\n        'schmutziger',\n        'schmutziges',\n        'dreckig',\n        'dreckige',\n        'dreckiger',\n        'dreckiges',\n        'dreck',\n        'unrat',\n        'abschaum',\n        'pack',\n        'gesindel',\n        'pöbel',\n        'poebel',\n        'mob',\n        'kanaille',\n        'lumpen',\n        'lump',\n        'schuft',\n        'schurke',\n        'halunke',\n        'gauner',\n        'ganove',\n        'gangster',\n        'verbrecher',\n        'kriminell',\n        'kriminelle',\n        'krimineller',\n        'kriminelles',\n        'asozial',\n        'asoziale',\n        'asozialer',\n        'asoziales',\n        'asi',\n        'prollig',\n        'prollige',\n        'prolliger',\n        'prolliges',\n        'proll',\n        'prolet',\n        'unterschicht',\n        'prekariat',\n        'hartz',\n        'hartzer',\n        'arbeitslos',\n        'arbeitslose',\n        'arbeitsloser',\n        'arbeitsloses',\n        'sozialhilfe',\n        'sozialschmarotzer',\n        'schmarotzer',\n        'parasit',\n        'parasiten',\n        'ungeziefer',\n        'schädling',\n        'schaedling',\n        'schädlinge',\n        'schaedlinge',\n        'plage',\n        'pest',\n        'seuche',\n        'krankheit',\n        'leiden',\n        'übel',\n        'uebel',\n        'böse',\n        'boese',\n        'schlecht',\n        'schlimm',\n        'schrecklich',\n        'furchtbar',\n        'entsetzlich',\n        'grauenhaft',\n        'grausam',\n        'brutal',\n        'roh',\n        'primitiv',\n        'primitive',\n        'primitiver',\n        'primitives',\n        'barbarisch',\n        'barbarische',\n        'barbarischer',\n        'barbarisches',\n        'wild',\n        'wilde',\n        'wilder',\n        'wildes',\n        'ungezähmt',\n        'ungebildet',\n        'ungebildete',\n        'ungebildeter',\n        'ungebildetes',\n        'dumm',\n        'dumme',\n        'dummer',\n        'dummes',\n        'doof',\n        'doofe',\n        'doofer',\n        'doofes',\n        'blöd',\n        'bloed',\n        'blöde',\n        'bloede',\n        'blöder',\n        'bloeder',\n        'blödes',\n        'bloedes',\n        'bescheuert',\n        'bescheuerte',\n        'bescheuerter',\n        'bescheuertes',\n        'bekloppt',\n        'bekloppte',\n        'bekloppter',\n        'beklopptes',\n        'verrückt',\n        'verrueckt',\n        'verrückte',\n        'verrueckte',\n        'verrückter',\n        'verrueckter',\n        'verrücktes',\n        'verruecktes',\n        'irre',\n        'irrer',\n        'irres',\n        'wahnsinnig',\n        'wahnsinnige',\n        'wahnsinniger',\n        'wahnsinniges',\n        'gestört',\n        'gestoert',\n        'gestörte',\n        'gestoerte',\n        'gestörter',\n        'gestoerter',\n        'gestörtes',\n        'gestoertes',\n        'krank',\n        'kranke',\n        'kranker',\n        'krankes',\n        'pathologisch',\n        'pathologische',\n        'pathologischer',\n        'pathologisches',\n        'abnormal',\n        'abnormale',\n        'abnormaler',\n        'abnormales',\n        'unnormal',\n        'unnormale',\n        'unnormaler',\n        'unnormales',\n        'abartig',\n        'abartige',\n        'abartiger',\n        'abartiges',\n        'widerlich',\n        'widerliche',\n        'widerlicher',\n        'widerliches',\n        'ekelhaft',\n        'ekelhafte',\n        'ekelhafter',\n        'ekelhaftes',\n        'eklig',\n        'eklige',\n        'ekliger',\n        'ekliges',\n        'widerwertig',\n        'widerwertige',\n        'widerwertiger',\n        'widerwertiges',\n        'abstoßend',\n        'abstossend',\n        'abstoßende',\n        'abstossende',\n        'abstoßender',\n        'abstossender',\n        'abstoßendes',\n        'abstossendes',\n        'absurd',\n        'absurde',\n        'absurder',\n        'absurdes',\n        'lächerlich',\n        'laecherlich',\n        'lächerliche',\n        'laecherliche',\n        'lächerlicher',\n        'laecherlicher',\n        'lächerliches',\n        'laecherliches',\n        'albern',\n        'alberne',\n        'alberner',\n        'albernes',\n        'affig',\n        'affige',\n        'affiger',\n        'affiges',\n        'närrisch',\n        'naerrisch',\n        'närrische',\n        'naerrische',\n        'närrischer',\n        'naerrischer',\n        'närrisches',\n        'naerrisches',\n        'töricht',\n        'toericht',\n        'törichte',\n        'toerichte',\n        'törichter',\n        'toerichter',\n        'törichtes',\n        'toerichtes',\n        'blamabel',\n        'blamable',\n        'blamabler',\n        'blamables',\n        'peinlich',\n        'peinliche',\n        'peinlicher',\n        'peinliches',\n        'beschämend',\n        'beschaemend',\n        'beschämende',\n        'beschaemende',\n        'beschämender',\n        'beschaemender',\n        'beschämendes',\n        'beschaemendes',\n        'schmählich',\n        'schmaehlich',\n        'schmähliche',\n        'schmaehliche',\n        'schmählicher',\n        'schmaehlicher',\n        'schmähliches',\n        'schmaehliches',\n        'schändlich',\n        'schaendlich',\n        'schändliche',\n        'schaendliche',\n        'schändlicher',\n        'schaendlicher',\n        'schändliches',\n        'schaendliches',\n        'gemein',\n        'gemeine',\n        'gemeiner',\n        'gemeines',\n        'niederträchtig',\n        'niedertraechtig',\n        'niederträchtige',\n        'niedertraechtige',\n        'niederträchtiger',\n        'niedertraechtiger',\n        'niederträchtiges',\n        'niedertraechtiges',\n        'hinterhältig',\n        'hinterhaeltig',\n        'hinterhältige',\n        'hinterhaeltige',\n        'hinterhältiger',\n        'hinterhaeltiger',\n        'hinterhältiges',\n        'hinterhaeltiges',\n        'heimtückisch',\n        'heimtueckisch',\n        'heimtückische',\n        'heimtueckische',\n        'heimtückischer',\n        'heimtueckischer',\n        'heimtückisches',\n        'heimtueckisches',\n        'falsch',\n        'falsche',\n        'falscher',\n        'falsches',\n        'verlogen',\n        'verlogene',\n        'verlogener',\n        'verlogenes',\n        'heuchlerisch',\n        'heuchlerische',\n        'heuchlerischer',\n        'heuchlerisches',\n        'scheinheilig',\n        'scheinheilige',\n        'scheinheiliger',\n        'scheinheiliges',\n        'doppelzüngig',\n        'doppelzuengig',\n        'doppelzüngige',\n        'doppelzuengige',\n        'doppelzüngiger',\n        'doppelzuengiger',\n        'doppelzüngiges',\n        'doppelzuengiges',\n        'verlogen',\n        'verlogene',\n        'verlogener',\n        'verlogenes',\n        'unaufrichtig',\n        'unaufrichtige',\n        'unaufrichtiger',\n        'unaufrichtiges',\n        'unehrlich',\n        'unehrliche',\n        'unehrlicher',\n        'unehrliches',\n        'betrügerisch',\n        'betruegerisch',\n        'betrügerische',\n        'betruegerische',\n        'betrügerischer',\n        'betruegerischer',\n        'betrügerisches',\n        'betruegerisches',\n        'schwindelhaft',\n        'schwindlerisch',\n        'schwindlerische',\n        'schwindlerischer',\n        'schwindlerisches',\n        'unredlich',\n        'unredliche',\n        'unredlicher',\n        'unredliches',\n        'unlauter',\n        'unlautere',\n        'unlauterer',\n        'unlauteres',\n        'unseriös',\n        'unserioes',\n        'unseriöse',\n        'unserioes',\n        'unseriöser',\n        'unserioeser',\n        'unseriöses',\n        'unserioes',\n        'dubios',\n        'dubiose',\n        'dubioser',\n        'dubioses',\n        'fragwürdig',\n        'fragwuerdig',\n        'fragwürdige',\n        'fragwuerdige',\n        'fragwürdiger',\n        'fragwuerdiger',\n        'fragwürdiges',\n        'fragwuerdiges',\n        'zweifelhaft',\n        'zweifelhafte',\n        'zweifelhafter',\n        'zweifelhaftes',\n        'suspekt',\n        'suspekte',\n        'suspekter',\n        'suspektes',\n        'verdächtig',\n        'verdaechtig',\n        'verdächtige',\n        'verdaechtige',\n        'verdächtiger',\n        'verdaechtiger',\n        'verdächtiges',\n        'verdaechtiges',\n        'obskur',\n        'obskure',\n        'obskurer',\n        'obskures',\n        'dunkel',\n        'dunkle',\n        'dunkler',\n        'dunkles',\n        'finster',\n        'finstere',\n        'finsterer',\n        'finsteres',\n        'schwarz',\n        'schwarze',\n        'schwarzer',\n        'schwarzes',\n        'düster',\n        'duester',\n        'düstere',\n        'duestere',\n        'düsterer',\n        'duesterer',\n        'düsteres',\n        'duesteres',\n        'trüb',\n        'trueb',\n        'trübe',\n        'truebe',\n        'trüber',\n        'trueber',\n        'trübes',\n        'truebes',\n        'matt',\n        'matte',\n        'matter',\n        'mattes',\n        'fahl',\n        'fahle',\n        'fahler',\n        'fahles',\n        'blass',\n        'blasse',\n        'blasser',\n        'blasses',\n        'bleich',\n        'bleiche',\n        'bleicher',\n        'bleiches',\n        'käsig',\n        'kaesig',\n        'käsige',\n        'kaesige',\n        'käsiger',\n        'kaesiger',\n        'käsiges',\n        'kaesiges',\n        'kränklich',\n        'kraenklich',\n        'kränkliche',\n        'kraenkliche',\n        'kränklicher',\n        'kraenklicher',\n        'kränkliches',\n        'kraenkliches',\n        'schwächlich',\n        'schwaechlich',\n        'schwächliche',\n        'schwaechliche',\n        'schwächlicher',\n        'schwaechlicher',\n        'schwächliches',\n        'schwaechliches',\n        'schwach',\n        'schwache',\n        'schwacher',\n        'schwaches',\n        'kraftlos',\n        'kraftlose',\n        'kraftloser',\n        'kraftloses',\n        'energielos',\n        'energielose',\n        'energieloser',\n        'energieloses',\n        'müde',\n        'muede',\n        'müder',\n        'mueder',\n        'müdes',\n        'muedes',\n        'erschöpft',\n        'erschoepft',\n        'erschöpfte',\n        'erschoepfte',\n        'erschöpfter',\n        'erschoepfter',\n        'erschöpftes',\n        'erschoepftes',\n        'ausgepowert',\n        'ausgepowerte',\n        'ausgepower',\n        'ausgepowertes',\n        'kaputt',\n        'kaputte',\n        'kaputter',\n        'kaputtes',\n        'defekt',\n        'defekte',\n        'defekter',\n        'defektes',\n        'hinüber',\n        'hinueber',\n        'im arsch',\n        'futsch',\n        'dahin',\n        'ruiniert',\n        'ruinierte',\n        'ruinierter',\n        'ruiniertes',\n        'zerstört',\n        'zerstoert',\n        'zerstörte',\n        'zerstoerte',\n        'zerstörter',\n        'zerstoerter',\n        'zerstörtes',\n        'zerstoertes',\n        'zerbrochen',\n        'zerbrochene',\n        'zerbrochener',\n        'zerbrochenes',\n        'zerschmettert',\n        'zerschmetterte',\n        'zerschmetterter',\n        'zerschmettertes',\n        'demoliert',\n        'demolierte',\n        'demolierter',\n        'demoliertes',\n        'vernichtet',\n        'vernichtete',\n        'vernichteter',\n        'vernichtetes',\n        'ausgelöscht',\n        'ausgeloescht',\n        'ausgelöschte',\n        'ausgeloeschte',\n        'ausgelöschter',\n        'ausgeloeschter',\n        'ausgelöschtes',\n        'ausgeloeschtes',\n        'eliminiert',\n        'eliminierte',\n        'eliminierter',\n        'eliminiertes',\n        'getötet',\n        'getoetet',\n        'getötete',\n        'getoetete',\n        'getöteter',\n        'getoeteter',\n        'getötetes',\n        'getoetetes',\n        'umgebracht',\n        'umgebrachte',\n        'umgebrachter',\n        'umgebrachtes',\n        'ermordet',\n        'ermordete',\n        'ermordeter',\n        'ermordetes',\n        'hingerichtet',\n        'hingerichtete',\n        'hingerichteter',\n        'hingerichtetes',\n        'exekutiert',\n        'exekutierte',\n        'exekutierter',\n        'exekutiertes',\n        'liquidiert',\n        'liquidierte',\n        'liquidierter',\n        'liquidiertes',\n        'abgemurkst',\n        'abgemurks',\n        'abgemurkstes',\n        'kaltgemacht',\n        'kaltgemachte',\n        'kaltgemachter',\n        'kaltgemachtes',\n        'plattgemacht',\n        'plattgemachte',\n        'plattgemachter',\n        'plattgemachtes',\n        'fertiggemacht',\n        'fertiggemachte',\n        'fertiggemachter',\n        'fertiggemachtes',\n        'kaputtgemacht',\n        'kaputtgemachte',\n        'kaputtgemachter',\n        'kaputtgemachtes',\n        'totgemacht',\n        'totgemachte',\n        'totgemachter',\n        'totgemachtes',\n        'totgeschlagen',\n        'totgeschlagene',\n        'totgeschlagener',\n        'totgeschlagenes',\n        'totgeprügelt',\n        'totgeprügelte',\n        'totgeprügelter',\n        'totgeprügeltes',\n        'totgetrampelt',\n        'totgetrampelte',\n        'totgetrampelter',\n        'totgetrampe',\n        'totgefahren',\n        'totgefahrene',\n        'totgefahrener',\n        'totgefahrenes',\n        'überfahren',\n        'ueberfahren',\n        'überfahrene',\n        'ueberfahrene',\n        'überfahrener',\n        'ueberfahrener',\n        'überfahrenes',\n        'ueberfahrenes',\n        'totgefahren',\n        'totgefahrene',\n        'totgefahrener',\n        'totgefahrenes',\n        'erstickt',\n        'erstickte',\n        'erstickter',\n        'ersticktes',\n        'erwürgt',\n        'erwuergt',\n        'erwürgte',\n        'erwuergte',\n        'erwürgter',\n        'erwuergter',\n        'erwürgtes',\n        'erwuergtes',\n        'erdrosselt',\n        'erdrosselte',\n        'erdrosselter',\n        'erdrosseltes',\n        'stranguliert',\n        'strangulierte',\n        'strangulierter',\n        'stranguliertes',\n        'gehängt',\n        'gehaengt',\n        'gehängte',\n        'gehaengte',\n        'gehängter',\n        'gehaengter',\n        'gehängtes',\n        'gehaengtes',\n        'aufgehängt',\n        'aufgehaengt',\n        'aufgehängte',\n        'aufgehaengte',\n        'aufgehängter',\n        'aufgehaengter',\n        'aufgehängtes',\n        'aufgehaengtes',\n        'erhängt',\n        'erhaengt',\n        'erhängte',\n        'erhaengte',\n        'erhängter',\n        'erhaengter',\n        'erhängtes',\n        'erhaengtes',\n        'verbrannt',\n        'verbrannte',\n        'verbrannter',\n        'verbranntes',\n        'angezündet',\n        'angezuendet',\n        'angezündete',\n        'angezuendete',\n        'angezündeter',\n        'angezuendeter',\n        'angezündetes',\n        'angezuendetes',\n        'abgefackelt',\n        'abgefackelte',\n        'abgefackelter',\n        'abgefackeltes',\n        'niedergebrannt',\n        'niedergebrannte',\n        'niedergebrannter',\n        'niedergebranntes',\n        'eingeäschert',\n        'eingeaeschert',\n        'eingeäscherte',\n        'eingeaescherte',\n        'eingeäscherter',\n        'eingeaescherter',\n        'eingeäschertes',\n        'eingeaeschertes',\n        'verbrannt',\n        'verbrannte',\n        'verbrannter',\n        'verbranntes',\n        'verkohlt',\n        'verkohlte',\n        'verkohlter',\n        'verkohltes',\n        'verkocht',\n        'verkochte',\n        'verkochter',\n        'verkochtes',\n        'versotten',\n        'versottene',\n        'versottener',\n        'versottenes',\n        'versotten',\n        'versoffene',\n        'versoffener',\n        'versoffenes',\n        'besoffen',\n        'besoffene',\n        'besoffener',\n        'besoffenes',\n        'betrunken',\n        'betrunkene',\n        'betrunkener',\n        'betrunkenes',\n        'angetrunken',\n        'angetrunkene',\n        'angetrunkener',\n        'angetrunkenes',\n        'alkoholisiert',\n        'alkoholisierte',\n        'alkoholisierter',\n        'alkoholisiertes',\n        'breit',\n        'breite',\n        'breiter',\n        'breites',\n        'zu',\n        'zugedröhnt',\n        'zugedroehnt',\n        'zugedröhnte',\n        'zugedroehnte',\n        'zugedröhnter',\n        'zugedroeh',\n        'zugedröhntes',\n        'zugedroeh',\n        'dicht',\n        'dichte',\n        'dichter',\n        'dichtes',\n        'voll',\n        'volle',\n        'voller',\n        'volles',\n        'hinüber',\n        'hinueber',\n        'weggetreten',\n        'weggetretene',\n        'weggetretener',\n        'weggetretenes',\n        'weg',\n        'wege',\n        'weger',\n        'weges',\n        'drauf',\n        'high',\n        'highe',\n        'higher',\n        'highes',\n        'stoned',\n        'stoner',\n        'stones',\n        'bekifft',\n        'bekiffte',\n        'bekiffter',\n        'bekifftes',\n        'zugekifft',\n        'zugekiffte',\n        'zugekiffter',\n        'zugekifftes',\n        'zugeraucht',\n        'zugerauchte',\n        'zugerauchter',\n        'zugerauchtes',\n        'stramm',\n        'stramme',\n        'strammer',\n        'strammes',\n        'dicht',\n        'dichte',\n        'dichter',\n        'dichtes',\n        'platt',\n        'platte',\n        'platter',\n        'plattes',\n        'depp',\n        'trottel',\n        'idiot',\n        'vollidiot',\n        'kanake',\n        'kanaken',\n        'neger',\n        'negerin',\n        'zigeuner',\n        'zigeunerin',\n        'retardiert',\n        'retardierte',\n        'retardierter',\n    ],\n    \n    'false_positives' => [\n        // Common German words that might be detected as false positives\n        'analyse',\n        'analysen',\n        'analysieren',\n        'analysiert',\n        'analysierte',\n        'analysierter',\n        'analysiertes',\n        'klasse',\n        'klassen',\n        'klassisch',\n        'klassische',\n        'klassischer',\n        'klassisches',\n        'passen',\n        'passt',\n        'gepasst',\n        'passage',\n        'passagen',\n        'ausdruck',\n        'ausdrücke',\n        'ausdruecke',\n        'ausdrücklich',\n        'ausdruecklich',\n        'ausdrückliche',\n        'ausdrueckliche',\n        'ausdrücklicher',\n        'ausdruecklicher',\n        'ausdrückliches',\n        'ausdrueckliches',\n        'mörder',\n        'moerder',\n        'morden',\n        'mordet',\n        'gemordet',\n        'mord',\n        'morde',\n        'mordtat',\n        'mordtaten',\n        'unternehmen',\n        'unternehmens',\n        'unternehmung',\n        'unternehmungen',\n        'unternehmer',\n        'unternehmerin',\n        'geschäft',\n        'geschaeft',\n        'geschäfte',\n        'geschaefte',\n        'geschäftlich',\n        'geschaeftlich',\n        'geschäftliche',\n        'geschaeftliche',\n        'geschäftlicher',\n        'geschaeftlicher',\n        'geschäftliches',\n        'geschaeftliches',\n        'arbeit',\n        'arbeiten',\n        'arbeiter',\n        'arbeiterin',\n        'arbeiterinnen',\n        'arbeitsplatz',\n        'arbeitsplätze',\n        'arbeitsplaetze',\n        'anstellung',\n        'anstellungen',\n        'angestellt',\n        'angestellte',\n        'angestellter',\n        'angestelltes',\n        'arbeitgeber',\n        'arbeitgeberin',\n        'arbeitnehmer',\n        'arbeitnehmerin',\n        'büro',\n        'buero',\n        'büros',\n        'bueros',\n        'computer',\n        'computers',\n        'rechner',\n        'maschine',\n        'maschinen',\n        'maschinell',\n        'maschinelle',\n        'maschineller',\n        'maschinelles',\n        'gerät',\n        'geraet',\n        'geräte',\n        'geraete',\n        'apparat',\n        'apparate',\n        'vorrichtung',\n        'vorrichtungen',\n        'instrument',\n        'instrumente',\n        'werkzeug',\n        'werkzeuge',\n        'hilfsmittel',\n        'nutzen',\n        'nützen',\n        'nuetzen',\n        'nützlich',\n        'nuetzlich',\n        'nützliche',\n        'nuetzliche',\n        'nützlicher',\n        'nuetzlicher',\n        'nützliches',\n        'nuetzliches',\n        'funktion',\n        'funktionen',\n        'funktionieren',\n        'funktioniert',\n        'funktionierte',\n        'funktioniertes',\n        'eigenschaft',\n        'eigenschaften',\n        'charakteristikum',\n        'charakteristika',\n        'charakteristisch',\n        'charakteristische',\n        'charakteristischer',\n        'charakteristisches',\n        'spezialität',\n        'spezialitaet',\n        'spezialitäten',\n        'spezialitaeten',\n        'spezialist',\n        'spezialisten',\n        'spezialistin',\n        'spezialistinnen',\n        'spezialisieren',\n        'spezialisiert',\n        'spezialisierte',\n        'spezialisierter',\n        'spezialisiertes',\n        'spezialisierung',\n        'spezialisierungen',\n        'beruflich',\n        'berufliche',\n        'beruflicher',\n        'berufliches',\n        'beruf',\n        'berufe',\n        'lehrer',\n        'lehrerin',\n        'lehrerinnen',\n        'unterrichten',\n        'unterrichtet',\n        'unterrichtete',\n        'unterrichtetes',\n        'unterricht',\n        'lehre',\n        'lehren',\n        'lehrte',\n        'gelehrt',\n        'gelehrte',\n        'gelehrter',\n        'gelehrtes',\n        'bildung',\n        'bildungen',\n        'bildungswesen',\n        'ausbildung',\n        'ausbildungen',\n        'erziehung',\n        'erziehen',\n        'erzieht',\n        'erzogen',\n        'erzogene',\n        'erzogener',\n        'erzogenes',\n        'erzieher',\n        'erzieherin',\n        'erzieherinnen',\n        'student',\n        'studenten',\n        'studentin',\n        'studentinnen',\n        'studieren',\n        'studiert',\n        'studierte',\n        'studierter',\n        'studiertes',\n        'studium',\n        'studien',\n        'studie',\n        'forschung',\n        'forschungen',\n        'forschen',\n        'forscht',\n        'forschte',\n        'geforscht',\n        'forscher',\n        'forscherin',\n        'forscherinnen',\n        'wissenschaft',\n        'wissenschaften',\n        'wissenschaftlich',\n        'wissenschaftliche',\n        'wissenschaftlicher',\n        'wissenschaftliches',\n        'wissenschaftler',\n        'wissenschaftlerin',\n        'wissenschaftlerinnen',\n        'wissen',\n        'weiss',\n        'weiß',\n        'gewusst',\n        'wusste',\n        'wissend',\n        'wissende',\n        'wissender',\n        'wissendes',\n        'kenntnis',\n        'kenntnisse',\n        'kennen',\n        'kennt',\n        'kannte',\n        'gekannt',\n        'bekannt',\n        'bekannte',\n        'bekannter',\n        'bekanntes',\n        'erkennen',\n        'erkennt',\n        'erkannte',\n        'erkannt',\n        'erkannte',\n        'erkannter',\n        'erkanntes',\n        'erkenntnis',\n        'erkenntnisse',\n        'weisheit',\n        'weise',\n        'weisen',\n        'wies',\n        'gewiesen',\n        'intelligent',\n        'intelligente',\n        'intelligenter',\n        'intelligentes',\n        'intelligenz',\n        'talent',\n        'talente',\n        'talentiert',\n        'talentierte',\n        'talentierter',\n        'talentiertes',\n        'fähigkeit',\n        'faehigkeit',\n        'fähigkeiten',\n        'faehigkeiten',\n        'fähig',\n        'faehig',\n        'fähige',\n        'faehige',\n        'fähiger',\n        'faehiger',\n        'fähiges',\n        'faehiges',\n        'geschick',\n        'geschickt',\n        'geschickte',\n        'geschickter',\n        'geschicktes',\n        'geschicklichkeit',\n        'geschicklichkeiten',\n        'fertigkeit',\n        'fertigkeiten',\n        'können',\n        'koennen',\n        'kann',\n        'konnte',\n        'gekonnt',\n        'meister',\n        'meisterin',\n        'meisterinnen',\n        'meisterschaft',\n        'meisterschaften',\n        'meistern',\n        'meistert',\n        'meisterte',\n        'gemeistert',\n        'bereich',\n        'bereiche',\n        'gebiet',\n        'gebiete',\n        'domain',\n        'domäne',\n        'domaene',\n        'domänen',\n        'domaenen',\n        'beherrschen',\n        'beherrscht',\n        'beherrschte',\n        'beherrschtes',\n        'beherrschung',\n        'kontrolle',\n        'kontrollieren',\n        'kontrolliert',\n        'kontrollierte',\n        'kontrolliertes',\n        'verwaltung',\n        'verwaltungen',\n        'verwalten',\n        'verwaltet',\n        'verwaltete',\n        'verwaltetes',\n        'verwalter',\n        'verwalterin',\n        'verwalterinnen',\n        'management',\n        'managements',\n        'managen',\n        'gemanagt',\n        'manager',\n        'managerin',\n        'managerinnen',\n        'führung',\n        'fuehrung',\n        'führungen',\n        'fuehrungen',\n        'führen',\n        'fuehren',\n        'führt',\n        'fuehrt',\n        'führte',\n        'fuehrte',\n        'geführt',\n        'gefuehrt',\n        'führer',\n        'fuehrer',\n        'führerin',\n        'fuehrerin',\n        'führerinnen',\n        'fuehrerinnen',\n        'leitung',\n        'leitungen',\n        'leiten',\n        'leitet',\n        'leitete',\n        'geleitet',\n        'leiter',\n        'leiterin',\n        'leiterinnen',\n        'organisation',\n        'organisationen',\n        'organisieren',\n        'organisiert',\n        'organisierte',\n        'organisierter',\n        'organisiertes',\n        'system',\n        'systeme',\n        'systematisch',\n        'systematische',\n        'systematischer',\n        'systematisches',\n        'methode',\n        'methoden',\n        'methodisch',\n        'methodische',\n        'methodischer',\n        'methodisches',\n        'verfahren',\n        'prozess',\n        'prozesse',\n        'prozessieren',\n        'prozessiert',\n        'prozessierte',\n        'prozessiertes',\n        'ablauf',\n        'abläufe',\n        'ablaeufe',\n        'vorgang',\n        'vorgänge',\n        'vorgaenge',\n        'procedere',\n        'protokoll',\n        'protokolle',\n        'norm',\n        'normen',\n        'normieren',\n        'normiert',\n        'normierte',\n        'normierter',\n        'normiertes',\n        'normal',\n        'normale',\n        'normaler',\n        'normales',\n        'normalität',\n        'normalitaet',\n        'standard',\n        'standards',\n        'standardisieren',\n        'standardisiert',\n        'standardisierte',\n        'standardisierter',\n        'standardisiertes',\n        'regel',\n        'regeln',\n        'reglement',\n        'reglements',\n        'reglementieren',\n        'reglementiert',\n        'reglementierte',\n        'reglementiertes',\n        'regulieren',\n        'reguliert',\n        'regulierte',\n        'reguliertes',\n        'regular',\n        'reguläre',\n        'regulaere',\n        'regulärer',\n        'regulaerer',\n        'reguläres',\n        'regulaeres',\n        'regelmäßig',\n        'regelmaessig',\n        'regelmäßige',\n        'regelmaessige',\n        'regelmäßiger',\n        'regelmaessiger',\n        'regelmäßiges',\n        'regelmaessiges',\n        'gesetz',\n        'gesetze',\n        'gesetzlich',\n        'gesetzliche',\n        'gesetzlicher',\n        'gesetzliches',\n        'legal',\n        'legale',\n        'legaler',\n        'legales',\n        'legalität',\n        'legalitaet',\n        'legitimität',\n        'legitimiatet',\n        'legitim',\n        'legitime',\n        'legitimer',\n        'legitimes',\n        'legitimieren',\n        'legitimiert',\n        'legitimierte',\n        'legitimiertes',\n        'recht',\n        'rechte',\n        'rechtlich',\n        'rechtliche',\n        'rechtlicher',\n        'rechtliches',\n        'rechtmäßig',\n        'rechtmaessig',\n        'rechtmäßige',\n        'rechtmaessige',\n        'rechtmäßiger',\n        'rechtmaessiger',\n        'rechtmäßiges',\n        'rechtmaessiges',\n        'gerechtigkeit',\n        'gerecht',\n        'gerechte',\n        'gerechter',\n        'gerechtes',\n        'ungerecht',\n        'ungerechte',\n        'ungerechter',\n        'ungerechtes',\n        'ungerechtigkeit',\n        'ungerechtigkeiten',\n        'gericht',\n        'gerichte',\n        'richter',\n        'richterin',\n        'richterinnen',\n        'richten',\n        'richtet',\n        'richtete',\n        'gerichtet',\n        'urteil',\n        'urteile',\n        'urteilen',\n        'beurteilen',\n        'beurteilt',\n        'beurteilte',\n        'beurteiltes',\n        'beurteilung',\n        'beurteilungen',\n        'verurteilung',\n        'verurteilungen',\n        'verurteilen',\n        'verurteilt',\n        'verurteilte',\n        'verurteiltes',\n        'schuld',\n        'schuldig',\n        'schuldige',\n        'schuldiger',\n        'schuldiges',\n        'strafe',\n        'strafen',\n        'bestrafen',\n        'bestraft',\n        'bestrafte',\n        'bestrafter',\n        'bestrafftes',\n        'bestrafung',\n        'bestrafungen',\n        'gefängnis',\n        'gefaengnis',\n        'gefängnisse',\n        'gefaengnisse',\n        'knast',\n        'einsperren',\n        'eingesperrt',\n        'eingesperrte',\n        'eingesperrter',\n        'eingesperrtes',\n        'häftling',\n        'haeftling',\n        'häftlinge',\n        'haeftlinge',\n        'sträfling',\n        'straefling',\n        'sträflinge',\n        'straeflinge',\n        'inhaftiert',\n        'inhaftierte',\n        'inhaftierter',\n        'inhaftiertes',\n        'inhaftierung',\n        'inhaftierungen',\n        'festnahme',\n        'festnahmen',\n        'festnehmen',\n        'festgenommen',\n        'verhaftung',\n        'verhaftungen',\n        'verhaften',\n        'verhaftet',\n        'verhaftete',\n        'verhafteter',\n        'verhaftetes',\n        'arrest',\n        'arrestieren',\n        'arrestiert',\n        'arrestierte',\n        'arrestiertes',\n    ],\n    \n    'substitutions' => [\n        '/ä/' => ['ä', 'a', 'ae', '@', '4'],\n        '/ö/' => ['ö', 'o', 'oe', '0', 'ø'],\n        '/ü/' => ['ü', 'u', 'ue'],\n        '/ß/' => ['ß', 'ss', 's'],\n        '/á/' => ['á', 'a', '@', '4'],\n        '/à/' => ['à', 'a', '@', '4'],\n        '/â/' => ['â', 'a', '@', '4'],\n        '/ã/' => ['ã', 'a', '@', '4'],\n        '/å/' => ['å', 'a', '@', '4'],\n        '/æ/' => ['æ', 'ae', 'a'],\n        '/é/' => ['é', 'e', '3', '€'],\n        '/è/' => ['è', 'e', '3', '€'],\n        '/ê/' => ['ê', 'e', '3', '€'],\n        '/ë/' => ['ë', 'e', '3', '€'],\n        '/í/' => ['í', 'i', '1', '!', '|'],\n        '/ì/' => ['ì', 'i', '1', '!', '|'],\n        '/î/' => ['î', 'i', '1', '!', '|'],\n        '/ï/' => ['ï', 'i', '1', '!', '|'],\n        '/ó/' => ['ó', 'o', '0', 'ø'],\n        '/ò/' => ['ò', 'o', '0', 'ø'],\n        '/ô/' => ['ô', 'o', '0', 'ø'],\n        '/õ/' => ['õ', 'o', '0', 'ø'],\n        '/ø/' => ['ø', 'o', '0'],\n        '/ú/' => ['ú', 'u', 'ü'],\n        '/ù/' => ['ù', 'u', 'ü'],\n        '/û/' => ['û', 'u', 'ü'],\n        '/u/' => ['u', 'ü', 'ù', 'ú', 'û', '@', '*'],\n        '/c/' => ['c', 'k', 's', 'z'],\n        '/k/' => ['k', 'c', 'ck'],\n        '/ck/' => ['ck', 'k', 'c'],\n        '/z/' => ['z', 's', 'tz'],\n        '/tz/' => ['tz', 'z', 's'],\n        '/pf/' => ['pf', 'f', 'p'],\n        '/ph/' => ['ph', 'f'],\n        '/sch/' => ['sch', 'sh', 'ch'],\n        '/ch/' => ['ch', 'sh', 'x'],\n        '/ie/' => ['ie', 'i', 'y'],\n        '/ei/' => ['ei', 'ai', 'ey'],\n        '/ai/' => ['ai', 'ei', 'ay'],\n        '/au/' => ['au', 'aw', 'ou'],\n        '/eu/' => ['eu', 'oi', 'oy'],\n        '/äu/' => ['äu', 'aeu', 'oy'],\n        '/dt/' => ['dt', 't', 'd'],\n        '/st/' => ['st', 's', 't'],\n    ]\n];"
  },
  {
    "path": "config/languages/spanish.php",
    "content": "<?php\n\nreturn [\n    'severity' => [\n        'mild' => [\n            'maldito', 'maldita', 'maldición', 'maldicion', 'carajo',\n            'hostia', 'hostias', 'jolines', 'joline', 'jobar', 'joroba',\n            'caca', 'mear', 'meada', 'peo', 'pedorro', 'pedorra', 'pedos',\n            'tonto', 'tonta', 'bobo', 'boba', 'baboso', 'babosa',\n            'cursi', 'pesado', 'pesada', 'latoso', 'latosa',\n        ],\n        'moderate' => [\n            'cabrón', 'cabron', 'cabrona', 'cabrones', 'cabronazo',\n            'perra', 'zorra', 'gilipollas', 'gilipolla',\n            'imbécil', 'imbecil', 'idiota', 'estúpido', 'estupido', 'estúpida', 'estupida',\n            'pendejo', 'pendeja', 'mamón', 'mamon',\n            'boludo', 'boluda', 'pelotudo', 'pelotuda',\n            'culo', 'ojete', 'putilla', 'putita',\n            'capullo', 'coñazo', 'conazo', 'putada',\n        ],\n        'high' => [\n            'mierda', 'joder', 'coño', 'puta', 'puto',\n            'chingar', 'chingado', 'chingada', 'pinche',\n            'verga', 'follar', 'follada', 'follando',\n            'hijo de puta', 'hijoputa', 'concha', 'cojones',\n        ],\n        'extreme' => [\n            'maricón', 'maricon', 'marica', 'maricona', 'mariconazo',\n            'tortillera', 'bollera',\n            'retrasado', 'retrasada', 'retardado', 'retardada',\n            'mongoloide', 'subnormal',\n        ],\n    ],\n\n    'profanities' => [\n        // Common Spanish profanities and vulgar expressions\n        'mierda',\n        'joder',\n        'coño',\n        'cabrón',\n        'cabron',\n        'puta',\n        'puto',\n        'jodido',\n        'jodida',\n        'hijo de puta',\n        'hijoputa',\n        'gilipollas',\n        'gilipolla',\n        'imbécil',\n        'imbecil',\n        'idiota',\n        'estúpido',\n        'estupido',\n        'pendejo',\n        'pendeja',\n        'mamón',\n        'mamon',\n        'mamada',\n        'chingar',\n        'chingas',\n        'chingado',\n        'chingada',\n        'pinche',\n        'verga',\n        'carajo',\n        'cojones',\n        'huevos',\n        'huevón',\n        'huevon',\n        'maricón',\n        'maricon',\n        'marica',\n        'homosexual',\n        'tortillera',\n        'bollera',\n        'follar',\n        'folla',\n        'follada',\n        'follando',\n        'culiar',\n        'culear',\n        'culo',\n        'ojete',\n        'concha',\n        'chocha',\n        'chochito',\n        'chucha',\n        'almeja',\n        'zorra',\n        'zorro',\n        'putilla',\n        'putita',\n        'perra',\n        'perro',\n        'cabrona',\n        'cabrones',\n        'puton',\n        'putón',\n        'putona',\n        'putañero',\n        'putanero',\n        'polla',\n        'picha',\n        'rabo',\n        'nabo',\n        'cipote',\n        'chorizo',\n        'salchicha',\n        'salchichón',\n        'salchichon',\n        'miembro',\n        'pene',\n        'pijo',\n        'capullo',\n        'caput',\n        'gusano',\n        'rata',\n        'caca',\n        'mear',\n        'meada',\n        'orín',\n        'orin',\n        'orina',\n        'orinarse',\n        'cagar',\n        'cagada',\n        'cagarse',\n        'cagón',\n        'cagon',\n        'cagona',\n        'culiacan',\n        'culiao',\n        'culiado',\n        'culero',\n        'culera',\n        'nalgas',\n        'trasero',\n        'pompis',\n        'pompas',\n        'tetona',\n        'tetuda',\n        'tetas',\n        'pechos',\n        'chichonas',\n        'chichona',\n        'zángano',\n        'zangano',\n        'cabronazo',\n        'hijoelagranputa',\n        'hijoeputa',\n        'malparido',\n        'malparida',\n        'desgraciado',\n        'desgraciada',\n        'sinvergüenza',\n        'sinverguenza',\n        'cochino',\n        'cochina',\n        'guarro',\n        'guarra',\n        'sucio',\n        'sucia',\n        'asqueroso',\n        'asquerosa',\n        'repugnante',\n        'vomitivo',\n        'vomitiva',\n        'nauseabundo',\n        'nauseabunda',\n        'escoria',\n        'basura',\n        'porquería',\n        'porqueria',\n        'maldito',\n        'maldita',\n        'condenado',\n        'condenada',\n        'jodón',\n        'jodon',\n        'jodona',\n        'molesto',\n        'molesta',\n        'fastidioso',\n        'fastidiosa',\n        'cabronazo',\n        'maricona',\n        'mariconazo',\n        'bolludo',\n        'bolluda',\n        'boludo',\n        'boluda',\n        'pelotudo',\n        'pelotuda',\n        'tarado',\n        'tarada',\n        'retrasado',\n        'retrasada',\n        'retardado',\n        'retardada',\n        'mongoloide',\n        'subnormal',\n        'anormal',\n        'deficiente',\n        'tonto',\n        'tonta',\n        'bobo',\n        'boba',\n        'baboso',\n        'babosa',\n        'babas',\n        'ñoño',\n        'ñona',\n        'cursi',\n        'ridículo',\n        'ridiculo',\n        'ridícula',\n        'ridicula',\n        'estúpida',\n        'estupida',\n        'gorda',\n        'gordo',\n        'gordinflas',\n        'gordinfla',\n        'ballena',\n        'vaca',\n        'cerda',\n        'cerdo',\n        'chancho',\n        'chancha',\n        'marrano',\n        'marrana',\n        'cochino',\n        'cochina',\n        'puerco',\n        'puerca',\n        'animal',\n        'bestia',\n        'salvaje',\n        'bárbaro',\n        'barbaro',\n        'bárbara',\n        'barbara',\n        'bruto',\n        'bruta',\n        'burro',\n        'burra',\n        'asno',\n        'asna',\n        'mula',\n        'mulo',\n        'bestia',\n        'fiera',\n        'demonio',\n        'diablo',\n        'diabla',\n        'satanás',\n        'satanas',\n        'lucifer',\n        'maldición',\n        'maldicion',\n        'carajo',\n        'hostia',\n        'hostias',\n        'jolines',\n        'joline',\n        'jobar',\n        'joroba',\n        'cojonudo',\n        'cojonuda',\n        'cojudo',\n        'cojuda',\n        'acojonante',\n        'descojonarse',\n        'descojonar',\n        'tocapelotas',\n        'tocacojones',\n        'rompepelotas',\n        'rompecojones',\n        'pelmazos',\n        'pelmazo',\n        'pelma',\n        'plasta',\n        'pesado',\n        'pesada',\n        'pesao',\n        'pesá',\n        'latoso',\n        'latosa',\n        'coñazo',\n        'conazo',\n        'putada',\n        'jodienda',\n        'follón',\n        'follon',\n        'lío',\n        'lio',\n        'marrón',\n        'marron',\n        'peo',\n        'pedorro',\n        'pedorra',\n        'pedos',\n        'ventosidad',\n        'flatulencia',\n        'gases',\n        'tirarse pedos',\n        'echar pedos',\n        'soltar pedos',\n        'heder',\n        'apestar',\n        'oler mal',\n        'tufo',\n        'peste',\n        'pestilencia',\n        'putrefacción',\n        'putrefaccion',\n        'putrefacto',\n        'putrefacta',\n        'podrido',\n        'podrida',\n        'rancio',\n        'rancia',\n        'agrio',\n        'agria',\n        'amargo',\n        'amarga',\n        'salado',\n        'salada',\n        'soso',\n        'sosa',\n        'insípido',\n        'insipido',\n        'insípida',\n        'insipida',\n        'desabrido',\n        'desabrida',\n        'malo',\n        'mala',\n        'malísimo',\n        'malisimo',\n        'malísima',\n        'malisima',\n        'pésimo',\n        'pesimo',\n        'pésima',\n        'pesima',\n        'horrible',\n        'horroroso',\n        'horrorosa',\n        'terrorífico',\n        'terrorifico',\n        'terrorífica',\n        'terrorifica',\n        'espantoso',\n        'espantosa',\n        'horripilante',\n        'espeluznante',\n        'escalofriante',\n        'siniestro',\n        'siniestra',\n        'tenebroso',\n        'tenebrosa',\n        'lúgubre',\n        'lugubre',\n        'sombrío',\n        'sombrio',\n        'sombría',\n        'sombria',\n        'triste',\n        'melancólico',\n        'melancolico',\n        'melancólica',\n        'melancolica',\n        'deprimido',\n        'deprimida',\n        'depresivo',\n        'depresiva',\n        'suicida',\n        'morir',\n        'muerte',\n        'muerto',\n        'muerta',\n        'cadáver',\n        'cadaver',\n        'difunto',\n        'difunta',\n        'finado',\n        'finada',\n        'fallecido',\n        'fallecida',\n        'occiso',\n        'occisa',\n        'fiambre',\n        'estirar la pata',\n        'diñar',\n        'dinar',\n        'palmar',\n        'pelar',\n        'espichar',\n        'fenecer',\n        'expirar',\n        'perecer',\n        'sucumbir',\n        'fallecer',\n        'óbito',\n        'obito',\n        'defunción',\n        'defuncion',\n        'deceso',\n        'tránsito',\n        'transito',\n        'partida',\n        'despedida',\n        'adiós',\n        'adios',\n        'hasta la vista',\n        'hasta luego',\n        'hasta pronto',\n        'hasta mañana',\n        'hasta manana',\n        'chau',\n        'chao',\n        'bye',\n        'goodbye',\n    ],\n    \n    'false_positives' => [\n        // Common Spanish words that might be detected as false positives\n        'análisis',\n        'analisis',\n        'clase',\n        'clases',\n        'paso',\n        'pasos',\n        'expresión',\n        'expresion',\n        'expresiones',\n        'asesino',\n        'asesina',\n        'asesinar',\n        'asesinato',\n        'empresa',\n        'empresas',\n        'empresario',\n        'empresaria',\n        'negocio',\n        'negocios',\n        'trabajo',\n        'trabajos',\n        'trabajar',\n        'trabajador',\n        'trabajadora',\n        'empleo',\n        'empleos',\n        'empleado',\n        'empleada',\n        'empleador',\n        'empleadora',\n        'oficina',\n        'oficinas',\n        'oficinista',\n        'escritorio',\n        'escritorios',\n        'computadora',\n        'computadoras',\n        'computador',\n        'computadores',\n        'ordenador',\n        'ordenadores',\n        'máquina',\n        'maquina',\n        'máquinas',\n        'maquinas',\n        'aparato',\n        'aparatos',\n        'dispositivo',\n        'dispositivos',\n        'instrumento',\n        'instrumentos',\n        'herramienta',\n        'herramientas',\n        'útil',\n        'util',\n        'útiles',\n        'utiles',\n        'utilidad',\n        'utilidades',\n        'función',\n        'funcion',\n        'funciones',\n        'funcional',\n        'funcionalidad',\n        'funcionalidades',\n        'característico',\n        'caracteristico',\n        'característica',\n        'caracteristica',\n        'características',\n        'caracteristicas',\n        'especialidad',\n        'especialidades',\n        'especialista',\n        'especialistas',\n        'especializar',\n        'especializado',\n        'especializada',\n        'especialización',\n        'especializacion',\n        'profesional',\n        'profesionales',\n        'profesión',\n        'profesion',\n        'profesiones',\n        'profesor',\n        'profesora',\n        'profesores',\n        'profesoras',\n        'enseñar',\n        'ensenar',\n        'enseñanza',\n        'ensenanza',\n        'enseñanzas',\n        'ensenanzas',\n        'educación',\n        'educacion',\n        'educativo',\n        'educativa',\n        'educativos',\n        'educativas',\n        'educar',\n        'educado',\n        'educada',\n        'educador',\n        'educadora',\n        'educadores',\n        'educadoras',\n        'estudiante',\n        'estudiantes',\n        'estudiar',\n        'estudio',\n        'estudios',\n        'estudiado',\n        'estudiada',\n        'investigación',\n        'investigacion',\n        'investigaciones',\n        'investigar',\n        'investigador',\n        'investigadora',\n        'investigadores',\n        'investigadoras',\n        'científico',\n        'cientifico',\n        'científica',\n        'cientifica',\n        'científicos',\n        'cientificos',\n        'científicas',\n        'cientificas',\n        'ciencia',\n        'ciencias',\n        'conocimiento',\n        'conocimientos',\n        'conocer',\n        'conocido',\n        'conocida',\n        'conocidos',\n        'conocidas',\n        'saber',\n        'sabido',\n        'sabida',\n        'sabidos',\n        'sabidas',\n        'sabiduría',\n        'sabiduria',\n        'sabio',\n        'sabia',\n        'sabios',\n        'sabias',\n        'inteligente',\n        'inteligentes',\n        'inteligencia',\n        'inteligencias',\n        'talento',\n        'talentos',\n        'talentoso',\n        'talentosa',\n        'talentosos',\n        'talentosas',\n        'habilidad',\n        'habilidades',\n        'hábil',\n        'habil',\n        'hábiles',\n        'habiles',\n        'destreza',\n        'destrezas',\n        'destro',\n        'destra',\n        'diestro',\n        'diestra',\n        'diestros',\n        'diestras',\n        'maestro',\n        'maestra',\n        'maestros',\n        'maestras',\n        'maestría',\n        'maestria',\n        'maestrías',\n        'maestrias',\n        'dominio',\n        'dominios',\n        'dominar',\n        'dominado',\n        'dominada',\n        'dominados',\n        'dominadas',\n        'control',\n        'controles',\n        'controlar',\n        'controlado',\n        'controlada',\n        'controlados',\n        'controladas',\n        'administración',\n        'administracion',\n        'administrar',\n        'administrador',\n        'administradora',\n        'administradores',\n        'administradoras',\n        'gestión',\n        'gestion',\n        'gestiones',\n        'gestionar',\n        'gestor',\n        'gestora',\n        'gestores',\n        'gestoras',\n        'organización',\n        'organizacion',\n        'organizaciones',\n        'organizar',\n        'organizador',\n        'organizadora',\n        'organizadores',\n        'organizadoras',\n        'sistema',\n        'sistemas',\n        'sistemático',\n        'sistematico',\n        'sistemática',\n        'sistematica',\n        'sistemáticos',\n        'sistematicos',\n        'sistemáticas',\n        'sistematicas',\n        'método',\n        'metodo',\n        'métodos',\n        'metodos',\n        'metodología',\n        'metodologia',\n        'metodologías',\n        'metodologias',\n        'proceso',\n        'procesos',\n        'procesar',\n        'procesado',\n        'procesada',\n        'procesados',\n        'procesadas',\n        'procedimiento',\n        'procedimientos',\n        'proceder',\n        'protocolo',\n        'protocolos',\n        'norma',\n        'normas',\n        'normal',\n        'normales',\n        'normalidad',\n        'normalidades',\n        'normalizar',\n        'normalizado',\n        'normalizada',\n        'normalizados',\n        'normalizadas',\n        'estándar',\n        'estandar',\n        'estándares',\n        'estandares',\n        'estandarizar',\n        'estandarizado',\n        'estandarizada',\n        'estandarizados',\n        'estandarizadas',\n        'regla',\n        'reglas',\n        'reglamento',\n        'reglamentos',\n        'reglamentar',\n        'reglamentario',\n        'reglamentaria',\n        'reglamentarios',\n        'reglamentarias',\n        'regular',\n        'regulares',\n        'regularidad',\n        'regularidades',\n        'regularizar',\n        'regularizado',\n        'regularizada',\n        'regularizados',\n        'regularizadas',\n        'ley',\n        'leyes',\n        'legal',\n        'legales',\n        'legalidad',\n        'legalidades',\n        'legalizar',\n        'legalizado',\n        'legalizada',\n        'legalizados',\n        'legalizadas',\n        'derecho',\n        'derechos',\n        'jurídico',\n        'juridico',\n        'jurídica',\n        'juridica',\n        'jurídicos',\n        'juridicos',\n        'jurídicas',\n        'juridicas',\n        'justicia',\n        'justicias',\n        'justo',\n        'justa',\n        'justos',\n        'justas',\n        'injusto',\n        'injusta',\n        'injustos',\n        'injustas',\n        'injusticia',\n        'injusticias',\n        'tribunal',\n        'tribunales',\n        'juez',\n        'jueces',\n        'jueza',\n        'juezas',\n        'juzgar',\n        'juzgado',\n        'juzgada',\n        'juzgados',\n        'juzgadas',\n        'sentencia',\n        'sentencias',\n        'sentenciar',\n        'sentenciado',\n        'sentenciada',\n        'sentenciados',\n        'sentenciadas',\n        'condena',\n        'condenas',\n        'condenar',\n        'condenado',\n        'condenada',\n        'condenados',\n        'condenadas',\n        'castigo',\n        'castigos',\n        'castigar',\n        'castigado',\n        'castigada',\n        'castigados',\n        'castigadas',\n        'pena',\n        'penas',\n        'penar',\n        'penado',\n        'penada',\n        'penados',\n        'penadas',\n        'prisión',\n        'prision',\n        'prisiones',\n        'cárcel',\n        'carcel',\n        'cárceles',\n        'carceles',\n        'encarcelar',\n        'encarcelado',\n        'encarcelada',\n        'encarcelados',\n        'encarceladas',\n        'preso',\n        'presa',\n        'presos',\n        'presas',\n        'presidio',\n        'presidios',\n        'penitenciaría',\n        'penitenciaria',\n        'penitenciarías',\n        'penitenciarias',\n        'reformatorio',\n        'reformatorios',\n    ],\n    \n    'substitutions' => [\n        '/ñ/' => ['ñ', 'n', '~n', 'ni'],\n        '/á/' => ['á', 'a', '@', '4'],\n        '/é/' => ['é', 'e', '3', '€'],\n        '/í/' => ['í', 'i', '1', '!', '|'],\n        '/ó/' => ['ó', 'o', '0', 'ø'],\n        '/ú/' => ['ú', 'u', 'ü'],\n        '/ü/' => ['ü', 'u', 'ú'],\n        '/u/' => ['u', 'ú', 'ü', '@', '*'],\n        '/c/' => ['c', 'k', 'ç'],\n        '/ll/' => ['ll', 'y', 'i'],\n        '/rr/' => ['rr', 'r'],\n        '/ch/' => ['ch', 'x'],\n        '/z/' => ['z', 's', 'c'],\n        '/j/' => ['j', 'x', 'h'],\n        '/g/' => ['g', 'j', 'h'],\n        '/b/' => ['b', 'v', 'w'],\n        '/v/' => ['v', 'b', 'w'],\n    ]\n];"
  },
  {
    "path": "phpunit.xml",
    "content": "<phpunit bootstrap=\"vendor/autoload.php\" colors=\"true\">\n    <testsuites>\n        <testsuite name=\"Application Tests\">\n            <directory>./tests</directory>\n        </testsuite>\n    </testsuites>\n\n    <php>\n        <env name=\"APP_ENV\" value=\"testing\"/>\n    </php>\n</phpunit>\n"
  },
  {
    "path": "src/BlaspManager.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp;\n\nuse Closure;\nuse Illuminate\\Contracts\\Foundation\\Application;\nuse Blaspsoft\\Blasp\\Core\\Contracts\\DriverInterface;\nuse Blaspsoft\\Blasp\\Drivers\\RegexDriver;\nuse Blaspsoft\\Blasp\\Drivers\\PatternDriver;\nuse Blaspsoft\\Blasp\\Drivers\\PhoneticDriver;\nuse Blaspsoft\\Blasp\\Drivers\\PipelineDriver;\nuse InvalidArgumentException;\n\nclass BlaspManager\n{\n    protected Application $app;\n    protected array $drivers = [];\n    protected array $customCreators = [];\n\n    public function __construct(Application $app)\n    {\n        $this->app = $app;\n    }\n\n    public function driver(?string $driver = null): PendingCheck\n    {\n        return $this->newPendingCheck()->driver($driver ?? $this->getDefaultDriver());\n    }\n\n    public function resolveDriver(string $name): DriverInterface\n    {\n        if (!isset($this->drivers[$name])) {\n            $this->drivers[$name] = $this->createDriver($name);\n        }\n\n        return $this->drivers[$name];\n    }\n\n    protected function createDriver(string $name): DriverInterface\n    {\n        if (isset($this->customCreators[$name])) {\n            return ($this->customCreators[$name])($this->app);\n        }\n\n        $method = 'create' . ucfirst($name) . 'Driver';\n        if (method_exists($this, $method)) {\n            return $this->$method();\n        }\n\n        throw new InvalidArgumentException(\"Driver [{$name}] not supported.\");\n    }\n\n    public function createRegexDriver(): DriverInterface\n    {\n        return new RegexDriver();\n    }\n\n    public function createPatternDriver(): DriverInterface\n    {\n        return new PatternDriver();\n    }\n\n    public function createPhoneticDriver(): DriverInterface\n    {\n        $config = $this->app['config']->get('blasp.drivers.phonetic', []);\n\n        return new PhoneticDriver(\n            phonemes: $config['phonemes'] ?? 4,\n            minWordLength: $config['min_word_length'] ?? 3,\n            maxDistanceRatio: $config['max_distance_ratio'] ?? 0.6,\n            phoneticFalsePositives: $config['false_positives'] ?? [],\n            supportedLanguages: $config['supported_languages'] ?? ['english'],\n        );\n    }\n\n    public function createPipelineDriver(): DriverInterface\n    {\n        $config = $this->app['config']->get('blasp.drivers.pipeline', []);\n        $driverNames = $config['drivers'] ?? ['regex', 'phonetic'];\n\n        if (!is_array($driverNames)) {\n            throw new InvalidArgumentException('blasp.drivers.pipeline.drivers must be an array of driver names.');\n        }\n\n        foreach ($driverNames as $name) {\n            if (!is_string($name) || trim($name) === '') {\n                throw new InvalidArgumentException('Each pipeline driver name must be a non-empty string.');\n            }\n\n            if (strtolower(trim($name)) === 'pipeline') {\n                throw new InvalidArgumentException('Pipeline driver cannot contain itself. Remove \"pipeline\" from blasp.drivers.pipeline.drivers.');\n            }\n        }\n\n        $resolvedDrivers = array_map(\n            fn (string $name) => $this->resolveDriver($name),\n            $driverNames,\n        );\n\n        return new PipelineDriver($resolvedDrivers);\n    }\n\n    public function extend(string $driver, Closure $callback): self\n    {\n        $this->customCreators[$driver] = $callback;\n        return $this;\n    }\n\n    public function getDefaultDriver(): string\n    {\n        return $this->app['config']->get('blasp.default', 'regex');\n    }\n\n    public function newPendingCheck(): PendingCheck\n    {\n        return new PendingCheck($this);\n    }\n\n    public function pipeline(string ...$drivers): PendingCheck\n    {\n        return $this->newPendingCheck()->pipeline(...$drivers);\n    }\n\n    // --- Shortcut methods that create PendingCheck ---\n\n    public function check(?string $text): \\Blaspsoft\\Blasp\\Core\\Result\n    {\n        return $this->newPendingCheck()->check($text);\n    }\n\n    public function checkMany(array $texts): array\n    {\n        return $this->newPendingCheck()->checkMany($texts);\n    }\n\n    public function __call(string $method, array $parameters): mixed\n    {\n        return $this->newPendingCheck()->$method(...$parameters);\n    }\n\n    public function getApp(): Application\n    {\n        return $this->app;\n    }\n}\n"
  },
  {
    "path": "src/BlaspServiceProvider.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp;\n\nuse Illuminate\\Support\\Facades\\Blade;\nuse Illuminate\\Support\\ServiceProvider;\nuse Illuminate\\Support\\Str;\nuse Illuminate\\Support\\Stringable;\nclass BlaspServiceProvider extends ServiceProvider\n{\n    public function boot(): void\n    {\n        if ($this->app->runningInConsole()) {\n            $this->publishes([\n                __DIR__ . '/../config/blasp.php' => config_path('blasp.php'),\n            ], 'blasp-config');\n\n            $this->publishes([\n                __DIR__ . '/../config/languages' => config_path('languages'),\n            ], 'blasp-languages');\n\n            $this->publishes([\n                __DIR__ . '/../config/blasp.php' => config_path('blasp.php'),\n                __DIR__ . '/../config/languages' => config_path('languages'),\n            ], 'blasp');\n\n            $this->commands([\n                Console\\ClearCommand::class,\n                Console\\TestCommand::class,\n                Console\\LanguagesCommand::class,\n            ]);\n        }\n\n        $this->registerValidationRule();\n        $this->registerMiddlewareAlias();\n        $this->registerBladeDirectives();\n        $this->registerStringMacros();\n    }\n\n    public function register(): void\n    {\n        $this->mergeConfigFrom(__DIR__ . '/../config/blasp.php', 'blasp');\n\n        $this->app->singleton('blasp', function ($app) {\n            return new BlaspManager($app);\n        });\n\n        $this->app->alias('blasp', BlaspManager::class);\n    }\n\n    protected function registerValidationRule(): void\n    {\n        $this->app['validator']->extend('blasp_check', function ($attribute, $value, $parameters) {\n            if (!is_string($value) || $value === '') {\n                return true;\n            }\n\n            $language = $parameters[0] ?? config('blasp.language', config('blasp.default_language', 'english'));\n\n            $manager = $this->app->make('blasp');\n\n            $result = $manager->in($language)->check($value);\n\n            return !$result->isOffensive();\n        }, 'The :attribute contains profanity.');\n    }\n\n    protected function registerMiddlewareAlias(): void\n    {\n        $this->app['router']->aliasMiddleware('blasp', Middleware\\CheckProfanity::class);\n    }\n\n    protected function registerBladeDirectives(): void\n    {\n        Blade::directive('clean', function (string $expression) {\n            return \"<?php echo e(app('blasp')->check({$expression})->clean()); ?>\";\n        });\n    }\n\n    protected function registerStringMacros(): void\n    {\n        Str::macro('isProfane', function (string $text): bool {\n            return app('blasp')->check($text)->isOffensive();\n        });\n\n        Str::macro('cleanProfanity', function (string $text): string {\n            return app('blasp')->check($text)->clean();\n        });\n\n        Stringable::macro('isProfane', function (): bool {\n            return app('blasp')->check((string) $this)->isOffensive();\n        });\n\n        Stringable::macro('cleanProfanity', function (): Stringable {\n            return new Stringable(app('blasp')->check((string) $this)->clean());\n        });\n    }\n}\n"
  },
  {
    "path": "src/Blaspable.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp;\n\nuse Closure;\nuse Blaspsoft\\Blasp\\Core\\Result;\nuse Blaspsoft\\Blasp\\Events\\ModelProfanityDetected;\nuse Blaspsoft\\Blasp\\Exceptions\\ProfanityRejectedException;\nuse Illuminate\\Database\\Eloquent\\Model;\n\n/**\n * @mixin \\Illuminate\\Database\\Eloquent\\Model\n *\n * @property array $blaspable\n * @property string $blaspMode\n * @property string|null $blaspLanguage\n * @property string|null $blaspMask\n */\ntrait Blaspable\n{\n    protected static bool $blaspCheckingDisabled = false;\n\n    /** @var array<string, Result> */\n    protected array $blaspResultsCache = [];\n\n    public static function bootBlaspable(): void\n    {\n        static::saving(function (Model $model) {\n            if (static::$blaspCheckingDisabled) {\n                return;\n            }\n\n            $model->blaspResultsCache = [];\n\n            $attributes = $model->blaspable ?? [];\n            $dirty = $model->getDirty();\n            $mode = $model->blaspMode ?? config('blasp.model.mode', 'sanitize');\n\n            foreach ($attributes as $attr) {\n                if (!isset($dirty[$attr]) || !is_string($dirty[$attr])) {\n                    continue;\n                }\n\n                /** @var PendingCheck $check */\n                $check = app('blasp')->newPendingCheck();\n\n                if ($lang = ($model->blaspLanguage ?? null)) {\n                    $check = $check->in($lang);\n                }\n\n                if ($mask = ($model->blaspMask ?? null)) {\n                    $check = $check->mask($mask);\n                }\n\n                $result = $check->check($dirty[$attr]);\n                $model->blaspResultsCache[$attr] = $result;\n\n                if ($result->isOffensive()) {\n                    event(new ModelProfanityDetected($model, $attr, $result));\n\n                    if ($mode === 'reject') {\n                        throw ProfanityRejectedException::forModel($model, $attr, $result);\n                    }\n\n                    $model->setAttribute($attr, $result->clean());\n                }\n            }\n        });\n    }\n\n    public function hadProfanity(): bool\n    {\n        foreach ($this->blaspResultsCache as $result) {\n            if ($result->isOffensive()) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /** @return array<string, Result> */\n    public function blaspResults(): array\n    {\n        return $this->blaspResultsCache;\n    }\n\n    public function blaspResult(string $attribute): ?Result\n    {\n        return $this->blaspResultsCache[$attribute] ?? null;\n    }\n\n    public static function withoutBlaspChecking(Closure $callback): mixed\n    {\n        $previousState = static::$blaspCheckingDisabled;\n        static::$blaspCheckingDisabled = true;\n\n        try {\n            return $callback();\n        } finally {\n            static::$blaspCheckingDisabled = $previousState;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Console/ClearCommand.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Console;\n\nuse Illuminate\\Console\\Command;\nuse Blaspsoft\\Blasp\\Core\\Dictionary;\n\nclass ClearCommand extends Command\n{\n    protected $signature = 'blasp:clear';\n    protected $description = 'Clear the Blasp profanity cache';\n\n    public function handle(): void\n    {\n        Dictionary::clearCache();\n        $this->info('Blasp cache cleared successfully!');\n    }\n}\n"
  },
  {
    "path": "src/Console/LanguagesCommand.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Console;\n\nuse Illuminate\\Console\\Command;\nuse Blaspsoft\\Blasp\\Core\\Dictionary;\n\nclass LanguagesCommand extends Command\n{\n    protected $signature = 'blasp:languages';\n    protected $description = 'List available languages and their word counts';\n\n    public function handle(): void\n    {\n        $languages = Dictionary::getAvailableLanguages();\n\n        $rows = [];\n        foreach ($languages as $language) {\n            $config = Dictionary::loadLanguageConfig($language);\n            $profanityCount = count($config['profanities'] ?? []);\n            $falsePositiveCount = count($config['false_positives'] ?? []);\n            $hasSeverity = isset($config['severity']) ? 'Yes' : 'No';\n\n            $rows[] = [\n                ucfirst($language),\n                $profanityCount,\n                $falsePositiveCount,\n                $hasSeverity,\n            ];\n        }\n\n        $this->table(['Language', 'Profanities', 'False Positives', 'Severity Map'], $rows);\n    }\n}\n"
  },
  {
    "path": "src/Console/TestCommand.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Console;\n\nuse Illuminate\\Console\\Command;\n\nclass TestCommand extends Command\n{\n    protected $signature = 'blasp:test {text} {--lang= : Language to check against} {--detail}';\n    protected $description = 'Test profanity detection on a given text';\n\n    public function handle(): void\n    {\n        $text = $this->argument('text');\n        $language = $this->option('lang') ?? config('blasp.language', config('blasp.default_language', 'english'));\n\n        $manager = app('blasp');\n        $result = $manager->in($language)->check($text);\n\n        $this->info(\"Input: {$text}\");\n        $this->info(\"Language: {$language}\");\n        $this->newLine();\n\n        if ($result->isOffensive()) {\n            $this->error('Profanity detected!');\n            $this->table(\n                ['Property', 'Value'],\n                [\n                    ['Clean text', $result->clean()],\n                    ['Score', $result->score()],\n                    ['Count', $result->count()],\n                    ['Severity', $result->severity()?->value ?? 'n/a'],\n                    ['Unique words', implode(', ', $result->uniqueWords())],\n                ]\n            );\n\n            if ($this->option('detail')) {\n                $this->newLine();\n                $this->info('Matched words:');\n                $rows = [];\n                foreach ($result->words() as $word) {\n                    $rows[] = [\n                        $word->text,\n                        $word->base,\n                        $word->severity->value,\n                        $word->position,\n                        $word->length,\n                    ];\n                }\n                $this->table(['Text', 'Base', 'Severity', 'Position', 'Length'], $rows);\n            }\n        } else {\n            $this->info('No profanity detected. Text is clean.');\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Analyzer.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core;\n\nuse Blaspsoft\\Blasp\\Core\\Contracts\\DriverInterface;\nuse Blaspsoft\\Blasp\\Core\\Contracts\\MaskStrategyInterface;\nuse Blaspsoft\\Blasp\\Core\\Masking\\CharacterMask;\n\nclass Analyzer\n{\n    public function analyze(\n        string $text,\n        DriverInterface $driver,\n        Dictionary $dictionary,\n        ?MaskStrategyInterface $mask = null,\n        array $options = [],\n    ): Result {\n        $mask = $mask ?? new CharacterMask(config('blasp.mask', config('blasp.mask_character', '*')));\n\n        // Strip invisible Unicode format characters (zero-width spaces, invisible separators, etc.)\n        // before any driver sees the text, ensuring consistent positions across pipeline drivers\n        $text = preg_replace('/\\p{Cf}/u', '', $text) ?? $text;\n\n        return $driver->detect($text, $dictionary, $mask, $options);\n    }\n}\n"
  },
  {
    "path": "src/Core/Contracts/DriverInterface.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core\\Contracts;\n\nuse Blaspsoft\\Blasp\\Core\\Dictionary;\nuse Blaspsoft\\Blasp\\Core\\Result;\n\ninterface DriverInterface\n{\n    public function detect(string $text, Dictionary $dictionary, MaskStrategyInterface $mask, array $options = []): Result;\n}\n"
  },
  {
    "path": "src/Core/Contracts/MaskStrategyInterface.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core\\Contracts;\n\ninterface MaskStrategyInterface\n{\n    public function mask(string $word, int $length): string;\n}\n"
  },
  {
    "path": "src/Core/Dictionary.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core;\n\nuse Blaspsoft\\Blasp\\Enums\\Severity;\nuse Blaspsoft\\Blasp\\Core\\Matchers\\RegexMatcher;\nuse Blaspsoft\\Blasp\\Core\\Normalizers\\StringNormalizer;\nuse Blaspsoft\\Blasp\\Core\\Normalizers\\EnglishNormalizer;\nuse Blaspsoft\\Blasp\\Core\\Normalizers\\SpanishNormalizer;\nuse Blaspsoft\\Blasp\\Core\\Normalizers\\GermanNormalizer;\nuse Blaspsoft\\Blasp\\Core\\Normalizers\\FrenchNormalizer;\nuse Illuminate\\Support\\Facades\\Cache;\n\nclass Dictionary\n{\n    private const CACHE_TTL = 86400;\n\n    private array $profanities;\n    private array $falsePositives;\n    private array $separators;\n    private array $substitutions;\n    private array $severityMap;\n    private array $profanityExpressions;\n    private StringNormalizer $normalizer;\n    private array $allowList;\n    private array $blockList;\n    private string $language;\n\n    private static array $normalizers = [];\n\n    public function __construct(\n        array $profanities,\n        array $falsePositives,\n        array $separators,\n        array $substitutions,\n        array $severityMap,\n        StringNormalizer $normalizer,\n        array $allowList = [],\n        array $blockList = [],\n        string $language = 'english',\n        ?array $profanityExpressions = null,\n    ) {\n        $this->profanities = $profanities;\n        $this->falsePositives = $falsePositives;\n        $this->separators = $separators;\n        $this->substitutions = $substitutions;\n        $this->severityMap = $severityMap;\n        $this->normalizer = $normalizer;\n        $this->allowList = array_map('strtolower', $allowList);\n        $this->blockList = array_map('strtolower', $blockList);\n        $this->language = $language;\n\n        // Apply block list — add extra words to profanities\n        foreach ($this->blockList as $word) {\n            if (!in_array($word, $this->profanities)) {\n                $this->profanities[] = $word;\n                $this->severityMap[$word] = Severity::High;\n            }\n        }\n\n        // Remove allow-listed words\n        if (!empty($this->allowList)) {\n            $this->profanities = array_values(array_filter(\n                $this->profanities,\n                fn($p) => !in_array(strtolower($p), $this->allowList)\n            ));\n        }\n\n        if ($profanityExpressions !== null) {\n            $this->profanityExpressions = $profanityExpressions;\n        } else {\n            $this->profanityExpressions = (new RegexMatcher())->generateExpressions(\n                $this->profanities,\n                $this->separators,\n                $this->substitutions\n            );\n        }\n    }\n\n    public static function forLanguage(string $language, array $options = []): self\n    {\n        if (!preg_match('/^[a-zA-Z0-9_-]+$/', $language)) {\n            return new self(\n                profanities: [],\n                falsePositives: [],\n                separators: [],\n                substitutions: [],\n                severityMap: [],\n                normalizer: new EnglishNormalizer(),\n                language: $language,\n            );\n        }\n\n        $config = self::loadLanguageConfig($language);\n        $globalConfig = self::loadGlobalConfig();\n\n        $profanities = $config['profanities'] ?? [];\n        $falsePositives = $config['false_positives'] ?? [];\n        $severityMap = self::buildSeverityMap($config);\n\n        $substitutions = $globalConfig['substitutions'] ?? [];\n        if (isset($config['substitutions']) && is_array($config['substitutions'])) {\n            foreach ($config['substitutions'] as $pattern => $values) {\n                if (is_array($values)) {\n                    $substitutions[$pattern] = array_values(array_unique(array_merge(\n                        $substitutions[$pattern] ?? [],\n                        $values\n                    )));\n                }\n            }\n        }\n\n        return new self(\n            profanities: $profanities,\n            falsePositives: $falsePositives,\n            separators: $globalConfig['separators'] ?? [],\n            substitutions: $substitutions,\n            severityMap: $severityMap,\n            normalizer: self::getNormalizerForLanguage($language),\n            allowList: $options['allow'] ?? [],\n            blockList: $options['block'] ?? [],\n            language: $language,\n        );\n    }\n\n    public static function forLanguages(array $languages, array $options = []): self\n    {\n        $allProfanities = [];\n        $allFalsePositives = [];\n        $allSeverityMap = [];\n        $globalConfig = self::loadGlobalConfig();\n        $substitutions = $globalConfig['substitutions'] ?? [];\n\n        foreach ($languages as $language) {\n            if (!preg_match('/^[a-zA-Z0-9_-]+$/', $language)) {\n                continue;\n            }\n            $config = self::loadLanguageConfig($language);\n            $allProfanities = array_merge($allProfanities, $config['profanities'] ?? []);\n            $allFalsePositives = array_merge($allFalsePositives, $config['false_positives'] ?? []);\n            $allSeverityMap = array_merge($allSeverityMap, self::buildSeverityMap($config));\n\n            // Merge accent/diacritic substitutions only\n            if (isset($config['substitutions']) && is_array($config['substitutions'])) {\n                foreach ($config['substitutions'] as $pattern => $values) {\n                    if (is_array($values)) {\n                        $plainKey = trim($pattern, '/');\n                        if (mb_strlen($plainKey, 'UTF-8') > 1 || preg_match('/^[a-zA-Z]$/', $plainKey)) {\n                            continue;\n                        }\n                        $substitutions[$pattern] = array_values(array_unique(array_merge(\n                            $substitutions[$pattern] ?? [],\n                            $values\n                        )));\n                    }\n                }\n            }\n        }\n\n        return new self(\n            profanities: array_values(array_unique($allProfanities)),\n            falsePositives: array_values(array_unique($allFalsePositives)),\n            separators: $globalConfig['separators'] ?? [],\n            substitutions: $substitutions,\n            severityMap: $allSeverityMap,\n            normalizer: self::getNormalizerForLanguage('english'),\n            allowList: $options['allow'] ?? [],\n            blockList: $options['block'] ?? [],\n            language: implode(',', $languages),\n        );\n    }\n\n    public static function forAllLanguages(array $options = []): self\n    {\n        $languages = self::getAvailableLanguages();\n        return self::forLanguages($languages, $options);\n    }\n\n    public function getProfanities(): array\n    {\n        return $this->profanities;\n    }\n\n    public function getFalsePositives(): array\n    {\n        return $this->falsePositives;\n    }\n\n    public function getProfanityExpressions(): array\n    {\n        return $this->profanityExpressions;\n    }\n\n    public function getSeverity(string $word): Severity\n    {\n        $lower = strtolower($word);\n        return $this->severityMap[$lower] ?? Severity::High;\n    }\n\n    public function getNormalizer(): StringNormalizer\n    {\n        return $this->normalizer;\n    }\n\n    public function getLanguage(): string\n    {\n        return $this->language;\n    }\n\n    public function getSeparators(): array\n    {\n        return $this->separators;\n    }\n\n    public function getSubstitutions(): array\n    {\n        return $this->substitutions;\n    }\n\n    // --- Static helpers ---\n\n    public static function getAvailableLanguages(): array\n    {\n        $possiblePaths = [\n            config_path('languages'),\n            __DIR__ . '/../../config/languages',\n            realpath(__DIR__ . '/../../config/languages'),\n        ];\n\n        $languagesPath = null;\n        foreach ($possiblePaths as $path) {\n            if ($path && is_dir($path)) {\n                $languagesPath = $path;\n                break;\n            }\n        }\n\n        if (!$languagesPath) {\n            return ['english'];\n        }\n\n        $languageFiles = glob($languagesPath . '/*.php');\n        $languages = [];\n\n        foreach ($languageFiles as $languageFile) {\n            $languages[] = basename($languageFile, '.php');\n        }\n\n        return empty($languages) ? ['english'] : $languages;\n    }\n\n    public static function loadLanguageConfig(string $language): array\n    {\n        if (!preg_match('/^[a-zA-Z0-9_-]+$/', $language)) {\n            return ['profanities' => [], 'false_positives' => []];\n        }\n\n        $possiblePaths = [\n            config_path(\"languages/{$language}.php\"),\n            __DIR__ . \"/../../config/languages/{$language}.php\",\n            realpath(__DIR__ . \"/../../config/languages/{$language}.php\"),\n        ];\n\n        $languageFile = null;\n        foreach ($possiblePaths as $path) {\n            if ($path && file_exists($path)) {\n                $languageFile = $path;\n                break;\n            }\n        }\n\n        if (!$languageFile) {\n            return ['profanities' => [], 'false_positives' => []];\n        }\n\n        $config = require $languageFile;\n\n        if (!is_array($config) || !isset($config['profanities'])) {\n            return ['profanities' => [], 'false_positives' => []];\n        }\n\n        return $config;\n    }\n\n    private static function loadGlobalConfig(): array\n    {\n        return [\n            'separators' => config('blasp.separators', config('blasp.drivers.regex.separators', [])),\n            'substitutions' => config('blasp.substitutions', config('blasp.drivers.regex.substitutions', [])),\n            'false_positives' => config('blasp.false_positives', []),\n        ];\n    }\n\n    private static function buildSeverityMap(array $config): array\n    {\n        $map = [];\n\n        if (isset($config['severity']) && is_array($config['severity'])) {\n            foreach ($config['severity'] as $level => $words) {\n                $severity = Severity::tryFrom($level) ?? Severity::High;\n                foreach ($words as $word) {\n                    $map[strtolower($word)] = $severity;\n                }\n            }\n        }\n\n        // Words only in profanities (not in severity map) default to High\n        if (isset($config['profanities'])) {\n            foreach ($config['profanities'] as $word) {\n                $lower = strtolower($word);\n                if (!isset($map[$lower])) {\n                    $map[$lower] = Severity::High;\n                }\n            }\n        }\n\n        return $map;\n    }\n\n    public static function getNormalizerForLanguage(string $language): StringNormalizer\n    {\n        if (!isset(self::$normalizers[$language])) {\n            self::$normalizers[$language] = match (strtolower($language)) {\n                'english' => new EnglishNormalizer(),\n                'spanish' => new SpanishNormalizer(),\n                'german' => new GermanNormalizer(),\n                'french' => new FrenchNormalizer(),\n                default => new EnglishNormalizer(),\n            };\n        }\n\n        return self::$normalizers[$language];\n    }\n\n    // --- Caching ---\n\n    public static function clearCache(): void\n    {\n        $cache = self::getCache();\n        $keys = $cache->get('blasp_cache_keys', []);\n\n        foreach ($keys as $key) {\n            $cache->forget($key);\n        }\n\n        $cache->forget('blasp_cache_keys');\n\n        // Also clear result cache keys\n        $resultKeys = $cache->get('blasp_result_cache_keys', []);\n\n        foreach ($resultKeys as $key) {\n            $cache->forget($key);\n        }\n\n        $cache->forget('blasp_result_cache_keys');\n    }\n\n    private static function getCache(): \\Illuminate\\Contracts\\Cache\\Repository\n    {\n        $driver = config('blasp.cache.driver', config('blasp.cache_driver'));\n\n        return $driver !== null ? Cache::store($driver) : Cache::store();\n    }\n}\n"
  },
  {
    "path": "src/Core/Masking/CallbackMask.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core\\Masking;\n\nuse Closure;\nuse Blaspsoft\\Blasp\\Core\\Contracts\\MaskStrategyInterface;\n\nclass CallbackMask implements MaskStrategyInterface\n{\n    public function __construct(\n        private Closure $callback\n    ) {}\n\n    public function mask(string $word, int $length): string\n    {\n        return ($this->callback)($word, $length);\n    }\n}\n"
  },
  {
    "path": "src/Core/Masking/CharacterMask.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core\\Masking;\n\nuse Blaspsoft\\Blasp\\Core\\Contracts\\MaskStrategyInterface;\n\nclass CharacterMask implements MaskStrategyInterface\n{\n    public function __construct(\n        private string $character = '*'\n    ) {\n        $this->character = mb_substr($character, 0, 1);\n    }\n\n    public function mask(string $word, int $length): string\n    {\n        return str_repeat($this->character, $length);\n    }\n}\n"
  },
  {
    "path": "src/Core/Masking/GrawlixMask.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core\\Masking;\n\nuse Blaspsoft\\Blasp\\Core\\Contracts\\MaskStrategyInterface;\n\nclass GrawlixMask implements MaskStrategyInterface\n{\n    private const CHARS = ['!', '@', '#', '$', '%'];\n\n    public function mask(string $word, int $length): string\n    {\n        $result = '';\n        for ($i = 0; $i < $length; $i++) {\n            $result .= self::CHARS[$i % count(self::CHARS)];\n        }\n        return $result;\n    }\n}\n"
  },
  {
    "path": "src/Core/MatchedWord.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core;\n\nuse Blaspsoft\\Blasp\\Enums\\Severity;\nuse JsonSerializable;\n\nreadonly class MatchedWord implements JsonSerializable\n{\n    public function __construct(\n        public string $text,\n        public string $base,\n        public Severity $severity,\n        public int $position,\n        public int $length,\n        public string $language = 'english',\n    ) {}\n\n    public function toArray(): array\n    {\n        return [\n            'text' => $this->text,\n            'base' => $this->base,\n            'severity' => $this->severity->value,\n            'position' => $this->position,\n            'length' => $this->length,\n            'language' => $this->language,\n        ];\n    }\n\n    public function jsonSerialize(): mixed\n    {\n        return $this->toArray();\n    }\n}\n"
  },
  {
    "path": "src/Core/Matchers/CompoundWordDetector.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core\\Matchers;\n\nclass CompoundWordDetector\n{\n    private const SUFFIXES = ['s', 'es', 'ed', 'er', 'ers', 'est', 'ing', 'ings', 'ly', 'y'];\n\n    public function isPureAlphaSubstring(string $matchedText, string $fullWord, string $profanityKey, array $profanityExpressions): bool\n    {\n        if (!preg_match('/^[a-zA-Z]+$/', $matchedText)) {\n            return false;\n        }\n\n        if (!preg_match('/^[a-zA-Z]+$/', $fullWord)) {\n            return false;\n        }\n\n        if (strlen($fullWord) <= strlen($matchedText)) {\n            return false;\n        }\n\n        if (strlen($matchedText) > strlen($profanityKey)) {\n            return false;\n        }\n\n        $matchLower = strtolower($matchedText);\n        $wordLower = strtolower($fullWord);\n\n        foreach (self::SUFFIXES as $suffix) {\n            if ($wordLower === $matchLower . $suffix) {\n                return false;\n            }\n        }\n\n        $pos = strpos($wordLower, $matchLower);\n        if ($pos !== false) {\n            $remainder = substr($wordLower, 0, $pos) . substr($wordLower, $pos + strlen($matchLower));\n            foreach ($profanityExpressions as $profanity => $_) {\n                if (strlen($profanity) >= 3 && stripos($remainder, $profanity) !== false) {\n                    return false;\n                }\n            }\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/Core/Matchers/FalsePositiveFilter.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core\\Matchers;\n\nclass FalsePositiveFilter\n{\n    private array $falsePositivesMap;\n\n    public function __construct(array $falsePositives)\n    {\n        $this->falsePositivesMap = array_flip(array_map('strtolower', $falsePositives));\n    }\n\n    public function isFalsePositive(string $word): bool\n    {\n        return isset($this->falsePositivesMap[strtolower($word)]);\n    }\n\n    public function isInsideHexToken(string $string, int $start, int $length): bool\n    {\n        $end = $start + $length;\n        $strLen = strlen($string);\n\n        $tokenStart = $start;\n        while ($tokenStart > 0 && preg_match('/[0-9a-fA-F\\-]/', $string[$tokenStart - 1])) {\n            $tokenStart--;\n        }\n\n        $tokenEnd = $end;\n        while ($tokenEnd < $strLen && preg_match('/[0-9a-fA-F\\-]/', $string[$tokenEnd])) {\n            $tokenEnd++;\n        }\n\n        $token = substr($string, $tokenStart, $tokenEnd - $tokenStart);\n        $token = trim($token, '-');\n\n        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)) {\n            return true;\n        }\n\n        $stripped = str_replace('-', '', $token);\n        if (strlen($stripped) >= 8 && preg_match('/^[0-9a-fA-F]+$/', $stripped) && preg_match('/[0-9]/', $stripped)) {\n            return true;\n        }\n\n        return false;\n    }\n\n    public function isSpanningWordBoundary(string $matchedText, string $fullString, int $matchStart): bool\n    {\n        if (!preg_match('/\\s+/', $matchedText)) {\n            return false;\n        }\n\n        $parts = preg_split('/\\s+/', $matchedText);\n\n        if (count($parts) <= 1) {\n            return false;\n        }\n\n        $singleCharCount = 0;\n        foreach ($parts as $part) {\n            if (mb_strlen($part, 'UTF-8') === 1 && preg_match('/[a-z]/iu', $part)) {\n                $singleCharCount++;\n            }\n        }\n\n        if ($singleCharCount === count($parts)) {\n            return false;\n        }\n\n        $matchStartChar = mb_strlen(substr($fullString, 0, $matchStart), 'UTF-8');\n        $matchEndChar = $matchStartChar + mb_strlen($matchedText, 'UTF-8');\n\n        $embeddedAtStart = false;\n        $embeddedAtEnd = false;\n\n        if ($matchStartChar > 0) {\n            $charBefore = mb_substr($fullString, $matchStartChar - 1, 1, 'UTF-8');\n            if (preg_match('/\\w/u', $charBefore)) {\n                $embeddedAtStart = true;\n            }\n        }\n\n        if ($matchEndChar < mb_strlen($fullString, 'UTF-8')) {\n            $charAfter = mb_substr($fullString, $matchEndChar, 1, 'UTF-8');\n            if (preg_match('/\\w/u', $charAfter)) {\n                $embeddedAtEnd = true;\n            }\n        }\n\n        if ($embeddedAtStart && $embeddedAtEnd) {\n            return true;\n        }\n\n        if ($embeddedAtStart && !$embeddedAtEnd) {\n            $standaloneParts = array_slice($parts, 1);\n            $standalonePortion = implode(' ', $standaloneParts);\n\n            $hasLetter = preg_match('/[a-z]/iu', $standalonePortion);\n            $hasNonLetter = preg_match('/[^a-z\\s]/iu', $standalonePortion);\n\n            if ($hasLetter && $hasNonLetter) {\n                return false;\n            }\n            return true;\n        }\n\n        if (!$embeddedAtStart && $embeddedAtEnd) {\n            $standaloneParts = array_slice($parts, 0, -1);\n            $standalonePortion = implode(' ', $standaloneParts);\n\n            $hasLetter = preg_match('/[a-z]/iu', $standalonePortion);\n            $hasNonLetter = preg_match('/[^a-z\\s]/iu', $standalonePortion);\n\n            if ($hasLetter && $hasNonLetter) {\n                return false;\n            }\n            return true;\n        }\n\n        return false;\n    }\n\n    public function getFullWordContext(string $string, int $start, int $length): string\n    {\n        $left = $start;\n        $right = $start + $length;\n\n        while ($left > 0 && preg_match('/\\w/', $string[$left - 1])) {\n            $left--;\n        }\n\n        while ($right < strlen($string) && preg_match('/\\w/', $string[$right])) {\n            $right++;\n        }\n\n        return substr($string, $left, $right - $left);\n    }\n}\n"
  },
  {
    "path": "src/Core/Matchers/PhoneticMatcher.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core\\Matchers;\n\nclass PhoneticMatcher\n{\n    /** @var array<string, array<string>> metaphone code → list of profanity words */\n    private array $index = [];\n\n    public function __construct(\n        array $profanities,\n        private int $phonemes = 4,\n        private int $minWordLength = 3,\n        private float $maxDistanceRatio = 0.6,\n        private array $phoneticFalsePositives = [],\n    ) {\n        $this->phoneticFalsePositives = array_map(fn($fp) => mb_strtolower($fp, 'UTF-8'), $this->phoneticFalsePositives);\n        $this->buildIndex($profanities);\n    }\n\n    private function buildIndex(array $profanities): void\n    {\n        foreach ($profanities as $word) {\n            $lower = mb_strtolower($word, 'UTF-8');\n            if (mb_strlen($lower, 'UTF-8') < $this->minWordLength) {\n                continue;\n            }\n\n            $code = metaphone($lower, $this->phonemes);\n            if ($code === '') {\n                continue;\n            }\n\n            $this->index[$code][] = $lower;\n        }\n\n        // Deduplicate\n        foreach ($this->index as $code => $words) {\n            $this->index[$code] = array_values(array_unique($words));\n        }\n    }\n\n    public function match(string $word): ?string\n    {\n        $lower = strtolower($word);\n\n        if (mb_strlen($lower, 'UTF-8') < $this->minWordLength) {\n            return null;\n        }\n\n        if (in_array($lower, $this->phoneticFalsePositives, true)) {\n            return null;\n        }\n\n        $code = metaphone($lower, $this->phonemes);\n        if ($code === '' || !isset($this->index[$code])) {\n            return null;\n        }\n\n        $bestMatch = null;\n        $bestDistance = PHP_INT_MAX;\n\n        foreach ($this->index[$code] as $profanity) {\n            $distance = levenshtein($lower, $profanity);\n            $maxLen = max(mb_strlen($lower, 'UTF-8'), mb_strlen($profanity, 'UTF-8'));\n            $threshold = (int) ceil($this->maxDistanceRatio * $maxLen);\n\n            if ($distance <= $threshold && $distance < $bestDistance) {\n                $bestDistance = $distance;\n                $bestMatch = $profanity;\n            }\n        }\n\n        return $bestMatch;\n    }\n}\n"
  },
  {
    "path": "src/Core/Matchers/RegexMatcher.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core\\Matchers;\n\nclass RegexMatcher\n{\n    private const SEPARATOR_PLACEHOLDER = '{!!}';\n    private const ESCAPED_SEPARATOR_CHARACTERS = ['\\s'];\n\n    public function generateExpressions(array $profanities, array $separators, array $substitutions): array\n    {\n        $separatorExpression = $this->generateSeparatorExpression($separators);\n        $substitutionExpressions = $this->generateSubstitutionExpressions($substitutions);\n\n        $profanityExpressions = [];\n\n        foreach ($profanities as $profanity) {\n            $profanityExpressions[$profanity] = $this->generateProfanityExpression(\n                $profanity,\n                $substitutionExpressions,\n                $separatorExpression\n            );\n        }\n\n        return $profanityExpressions;\n    }\n\n    public function generateSeparatorExpression(array $separators): string\n    {\n        $normalSeparators = array_filter($separators, fn($sep) => $sep !== '.');\n\n        $pattern = $this->generateEscapedExpression($normalSeparators, self::ESCAPED_SEPARATOR_CHARACTERS, '');\n\n        return '(?:' . $pattern . '|\\.(?=\\w)){0,3}?';\n    }\n\n    public function generateSubstitutionExpressions(array $substitutions): array\n    {\n        $characterExpressions = [];\n\n        foreach ($substitutions as $character => $substitutionOptions) {\n            $hasMultiChar = false;\n            foreach ($substitutionOptions as $option) {\n                if (mb_strlen($option, 'UTF-8') > 1 && !preg_match('/^\\\\\\\\.$/u', $option)) {\n                    $hasMultiChar = true;\n                    break;\n                }\n            }\n\n            if ($hasMultiChar) {\n                $escaped = array_map(function ($opt) {\n                    if (preg_match('/^\\\\\\\\.$/u', $opt)) {\n                        return $opt;\n                    }\n                    return preg_quote($opt, '/');\n                }, $substitutionOptions);\n                $characterExpressions[$character] = '(?:' . implode('|', $escaped) . ')+' . self::SEPARATOR_PLACEHOLDER;\n            } else {\n                $characterExpressions[$character] = $this->generateEscapedExpression($substitutionOptions, [], '+') . self::SEPARATOR_PLACEHOLDER;\n            }\n        }\n\n        return $characterExpressions;\n    }\n\n    public function generateProfanityExpression(string $profanity, array $substitutionExpressions, string $separatorExpression): string\n    {\n        $plainSubstitutions = [];\n        foreach ($substitutionExpressions as $pattern => $replacement) {\n            $plainKey = trim($pattern, '/');\n            $plainSubstitutions[$plainKey] = $replacement;\n        }\n\n        uksort($plainSubstitutions, fn($a, $b) => mb_strlen($b, 'UTF-8') - mb_strlen($a, 'UTF-8'));\n\n        $expression = '';\n        $i = 0;\n        $len = mb_strlen($profanity, 'UTF-8');\n\n        while ($i < $len) {\n            $matched = false;\n            foreach ($plainSubstitutions as $key => $replacement) {\n                $keyLen = mb_strlen($key, 'UTF-8');\n                if ($i + $keyLen <= $len && mb_substr($profanity, $i, $keyLen, 'UTF-8') === $key) {\n                    $expression .= $replacement;\n                    $i += $keyLen;\n                    $matched = true;\n                    break;\n                }\n            }\n            if (!$matched) {\n                $expression .= preg_quote(mb_substr($profanity, $i, 1, 'UTF-8'), '/');\n                $i++;\n            }\n        }\n\n        $expression = str_replace(self::SEPARATOR_PLACEHOLDER, $separatorExpression, $expression);\n        $expression = '/' . $expression . '/iu';\n\n        return $expression;\n    }\n\n    private function generateEscapedExpression(array $characters = [], array $escapedCharacters = [], string $quantifier = '*?'): string\n    {\n        $regex = $escapedCharacters;\n\n        foreach ($characters as $character) {\n            $regex[] = preg_quote($character, '/');\n        }\n\n        return '[' . implode('', $regex) . ']' . $quantifier;\n    }\n}\n"
  },
  {
    "path": "src/Core/Normalizers/EnglishNormalizer.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core\\Normalizers;\n\nclass EnglishNormalizer implements StringNormalizer\n{\n    public function normalize(string $string): string\n    {\n        return $string;\n    }\n}\n"
  },
  {
    "path": "src/Core/Normalizers/FrenchNormalizer.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core\\Normalizers;\n\nclass FrenchNormalizer implements StringNormalizer\n{\n    public function normalize(string $string): string\n    {\n        $frenchAccents = [\n            'à' => 'a', 'â' => 'a', 'ä' => 'a', 'á' => 'a',\n            'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e',\n            'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i',\n            'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'ö' => 'o',\n            'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u',\n            'ý' => 'y', 'ÿ' => 'y',\n            'À' => 'A', 'Â' => 'A', 'Ä' => 'A', 'Á' => 'A',\n            'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E',\n            'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I',\n            'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Ö' => 'O',\n            'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U',\n            'Ý' => 'Y', 'Ÿ' => 'Y',\n            'ç' => 'c', 'Ç' => 'C',\n            'œ' => 'oe', 'Œ' => 'OE',\n            'æ' => 'ae', 'Æ' => 'AE',\n        ];\n\n        return strtr($string, $frenchAccents);\n    }\n}\n"
  },
  {
    "path": "src/Core/Normalizers/GermanNormalizer.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core\\Normalizers;\n\nclass GermanNormalizer implements StringNormalizer\n{\n    public function normalize(string $string): string\n    {\n        $germanMappings = [\n            'ä' => 'ae', 'Ä' => 'AE',\n            'ö' => 'oe', 'Ö' => 'OE',\n            'ü' => 'ue', 'Ü' => 'UE',\n            'ß' => 'ss',\n        ];\n\n        $normalizedString = strtr($string, $germanMappings);\n\n        $normalizedString = preg_replace_callback('/sch/i', function ($matches) {\n            $match = $matches[0];\n            if ($match === 'SCH') return 'SH';\n            if ($match === 'Sch') return 'Sh';\n            return 'sh';\n        }, $normalizedString);\n\n        return $normalizedString;\n    }\n}\n"
  },
  {
    "path": "src/Core/Normalizers/NullNormalizer.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core\\Normalizers;\n\nclass NullNormalizer implements StringNormalizer\n{\n    public function normalize(string $string): string\n    {\n        return $string;\n    }\n}\n"
  },
  {
    "path": "src/Core/Normalizers/SpanishNormalizer.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core\\Normalizers;\n\nclass SpanishNormalizer implements StringNormalizer\n{\n    public function normalize(string $string): string\n    {\n        $spanishMappings = [\n            'á' => 'a', 'Á' => 'A',\n            'é' => 'e', 'É' => 'E',\n            'í' => 'i', 'Í' => 'I',\n            'ó' => 'o', 'Ó' => 'O',\n            'ú' => 'u', 'Ú' => 'U',\n            'ü' => 'u', 'Ü' => 'U',\n            'ñ' => 'n', 'Ñ' => 'N',\n        ];\n\n        $normalizedString = strtr($string, $spanishMappings);\n\n        $normalizedString = preg_replace_callback('/\\bll(?=[aeiouáéíóúü])/i', function ($matches) {\n            $match = $matches[0];\n            if ($match === 'LL') return 'Y';\n            if ($match === 'Ll') return 'Y';\n            return 'y';\n        }, $normalizedString);\n\n        $normalizedString = preg_replace_callback('/rr/i', function ($matches) {\n            $match = $matches[0];\n            if ($match === 'RR') return 'R';\n            if ($match === 'Rr') return 'R';\n            return 'r';\n        }, $normalizedString);\n\n        return $normalizedString;\n    }\n}\n"
  },
  {
    "path": "src/Core/Normalizers/StringNormalizer.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core\\Normalizers;\n\ninterface StringNormalizer\n{\n    public function normalize(string $string): string;\n}\n"
  },
  {
    "path": "src/Core/Result.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core;\n\nuse Blaspsoft\\Blasp\\Enums\\Severity;\nuse Illuminate\\Support\\Collection;\nuse JsonSerializable;\nuse Stringable;\nuse Countable;\n\nclass Result implements JsonSerializable, Stringable, Countable\n{\n    private readonly Collection $matchedWords;\n\n    public function __construct(\n        private readonly string $originalText,\n        private readonly string $cleanText,\n        array $matchedWords,\n        private readonly int $scoreValue,\n    ) {\n        $this->matchedWords = new Collection($matchedWords);\n    }\n\n    // --- New v4 API ---\n\n    public function isClean(): bool\n    {\n        return $this->matchedWords->isEmpty();\n    }\n\n    public function isOffensive(): bool\n    {\n        return $this->matchedWords->isNotEmpty();\n    }\n\n    public function clean(): string\n    {\n        return $this->cleanText;\n    }\n\n    public function original(): string\n    {\n        return $this->originalText;\n    }\n\n    public function score(): int\n    {\n        return $this->scoreValue;\n    }\n\n    public function count(): int\n    {\n        return $this->matchedWords->count();\n    }\n\n    public function uniqueWords(): array\n    {\n        return $this->matchedWords->pluck('base')->unique()->values()->all();\n    }\n\n    public function severity(): ?Severity\n    {\n        if ($this->matchedWords->isEmpty()) {\n            return null;\n        }\n\n        return $this->matchedWords\n            ->sortByDesc(fn (MatchedWord $w) => $w->severity->weight())\n            ->first()\n            ->severity;\n    }\n\n    public function words(): Collection\n    {\n        return $this->matchedWords;\n    }\n\n    // --- Deprecated v3 backward-compat methods ---\n\n    /** @deprecated Use isOffensive() instead */\n    public function hasProfanity(): bool\n    {\n        return $this->isOffensive();\n    }\n\n    /** @deprecated Use clean() instead */\n    public function getCleanString(): string\n    {\n        return $this->clean();\n    }\n\n    /** @deprecated Use original() instead */\n    public function getSourceString(): string\n    {\n        return $this->original();\n    }\n\n    /** @deprecated Use count() instead */\n    public function getProfanitiesCount(): int\n    {\n        return $this->count();\n    }\n\n    /** @deprecated Use uniqueWords() instead */\n    public function getUniqueProfanitiesFound(): array\n    {\n        return $this->uniqueWords();\n    }\n\n    // --- Static constructors ---\n\n    public static function none(string $text): self\n    {\n        return new self($text, $text, [], 0);\n    }\n\n    public static function fromArray(array $data): self\n    {\n        $matchedWords = [];\n        foreach ($data['words'] ?? [] as $wordData) {\n            $matchedWords[] = new MatchedWord(\n                text: $wordData['text'],\n                base: $wordData['base'],\n                severity: Severity::tryFrom($wordData['severity']) ?? Severity::High,\n                position: $wordData['position'],\n                length: $wordData['length'],\n                language: $wordData['language'] ?? 'english',\n            );\n        }\n\n        return new self(\n            $data['original'] ?? '',\n            $data['clean'] ?? '',\n            $matchedWords,\n            $data['score'] ?? 0,\n        );\n    }\n\n    public static function withMatches(array $words, string $originalText = '', string $cleanText = ''): self\n    {\n        $matchedWords = [];\n        foreach ($words as $word) {\n            if ($word instanceof MatchedWord) {\n                $matchedWords[] = $word;\n            } else {\n                $matchedWords[] = new MatchedWord(\n                    text: $word,\n                    base: $word,\n                    severity: Severity::High,\n                    position: 0,\n                    length: mb_strlen($word),\n                );\n            }\n        }\n\n        $totalWords = max(1, count(preg_split('/\\s+/u', trim($originalText ?: implode(' ', $words)), -1, PREG_SPLIT_NO_EMPTY)));\n        $score = Score::calculate($matchedWords, $totalWords);\n\n        return new self($originalText, $cleanText ?: $originalText, $matchedWords, $score);\n    }\n\n    // --- Serialization ---\n\n    public function toArray(): array\n    {\n        return [\n            'original' => $this->originalText,\n            'clean' => $this->cleanText,\n            'is_offensive' => $this->isOffensive(),\n            'score' => $this->scoreValue,\n            'count' => $this->count(),\n            'unique_words' => $this->uniqueWords(),\n            'severity' => $this->severity()?->value,\n            'words' => $this->matchedWords->map->toArray()->all(),\n        ];\n    }\n\n    public function toJson(int $options = 0): string\n    {\n        return json_encode($this->toArray(), $options);\n    }\n\n    public function jsonSerialize(): mixed\n    {\n        return $this->toArray();\n    }\n\n    public function __toString(): string\n    {\n        return $this->cleanText;\n    }\n}\n"
  },
  {
    "path": "src/Core/Score.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Core;\n\nclass Score\n{\n    public static function calculate(array $matchedWords, int $totalWordCount): int\n    {\n        if (empty($matchedWords)) {\n            return 0;\n        }\n\n        $rawScore = 0;\n        foreach ($matchedWords as $word) {\n            $rawScore += $word->severity->weight();\n        }\n\n        $density = count($matchedWords) / max(1, $totalWordCount);\n        $normalized = (int) ($rawScore * (1 + $density));\n\n        return min(100, $normalized);\n    }\n}\n"
  },
  {
    "path": "src/Drivers/PatternDriver.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Drivers;\n\nuse Blaspsoft\\Blasp\\Core\\Contracts\\DriverInterface;\nuse Blaspsoft\\Blasp\\Core\\Contracts\\MaskStrategyInterface;\nuse Blaspsoft\\Blasp\\Core\\Dictionary;\nuse Blaspsoft\\Blasp\\Core\\MatchedWord;\nuse Blaspsoft\\Blasp\\Core\\Result;\nuse Blaspsoft\\Blasp\\Core\\Score;\nuse Blaspsoft\\Blasp\\Enums\\Severity;\n\nclass PatternDriver implements DriverInterface\n{\n    public function detect(string $text, Dictionary $dictionary, MaskStrategyInterface $mask, array $options = []): Result\n    {\n        if (empty($text)) {\n            return new Result($text ?? '', $text ?? '', [], 0);\n        }\n\n        if (!mb_check_encoding($text, 'UTF-8')) {\n            $text = mb_convert_encoding($text, 'UTF-8', 'UTF-8');\n        }\n\n        $matchedWords = [];\n        $lowerText = mb_strtolower($text, 'UTF-8');\n        $profanities = $dictionary->getProfanities();\n        $falsePositives = array_map(fn($fp) => mb_strtolower($fp, 'UTF-8'), $dictionary->getFalsePositives());\n\n        // Sort profanities by length descending for longest-match-first\n        usort($profanities, fn($a, $b) => mb_strlen($b) - mb_strlen($a));\n\n        foreach ($profanities as $profanity) {\n            $lowerProfanity = mb_strtolower($profanity, 'UTF-8');\n            $pattern = '/\\b' . preg_quote($lowerProfanity, '/') . '\\b/iu';\n\n            if (preg_match_all($pattern, $lowerText, $matches, PREG_OFFSET_CAPTURE)) {\n                foreach ($matches[0] as $match) {\n                    $start = mb_strlen(substr($lowerText, 0, $match[1]), 'UTF-8');\n                    $length = mb_strlen($match[0], 'UTF-8');\n                    $originalMatch = mb_substr($text, $start, $length);\n\n                    // Skip false positives\n                    if (in_array($lowerProfanity, $falsePositives)) {\n                        continue;\n                    }\n\n                    $matchedWords[] = new MatchedWord(\n                        text: $originalMatch,\n                        base: $profanity,\n                        severity: $dictionary->getSeverity($profanity),\n                        position: $start,\n                        length: $length,\n                        language: $dictionary->getLanguage(),\n                    );\n                }\n            }\n        }\n\n        // Apply severity filter before dedup so shorter high-severity matches aren't swallowed\n        $minimumSeverity = $options['severity'] ?? null;\n        if ($minimumSeverity instanceof Severity) {\n            $matchedWords = array_values(array_filter(\n                $matchedWords,\n                fn(MatchedWord $w) => $w->severity->isAtLeast($minimumSeverity)\n            ));\n        }\n\n        // Deduplicate overlapping matches (longest-first already recorded)\n        usort($matchedWords, fn($a, $b) => $a->position - $b->position ?: $b->length - $a->length);\n        $deduplicated = [];\n        $coveredEnd = -1;\n        foreach ($matchedWords as $mw) {\n            if ($mw->position >= $coveredEnd) {\n                $deduplicated[] = $mw;\n                $coveredEnd = $mw->position + $mw->length;\n            }\n        }\n        $matchedWords = $deduplicated;\n\n        // Rebuild cleanText from surviving matches (right-to-left)\n        $cleanText = $text;\n        $sorted = $matchedWords;\n        usort($sorted, fn($a, $b) => $b->position - $a->position);\n        foreach ($sorted as $word) {\n            $replacement = $mask->mask($word->text, $word->length);\n            $cleanText = mb_substr($cleanText, 0, $word->position)\n                . $replacement\n                . mb_substr($cleanText, $word->position + $word->length);\n        }\n\n        $totalWords = max(1, count(preg_split('/\\s+/u', trim($text), -1, PREG_SPLIT_NO_EMPTY)));\n        $scoreValue = Score::calculate($matchedWords, $totalWords);\n\n        return new Result($text, $cleanText, $matchedWords, $scoreValue);\n    }\n}\n"
  },
  {
    "path": "src/Drivers/PhoneticDriver.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Drivers;\n\nuse Blaspsoft\\Blasp\\Core\\Contracts\\DriverInterface;\nuse Blaspsoft\\Blasp\\Core\\Contracts\\MaskStrategyInterface;\nuse Blaspsoft\\Blasp\\Core\\Dictionary;\nuse Blaspsoft\\Blasp\\Core\\MatchedWord;\nuse Blaspsoft\\Blasp\\Core\\Result;\nuse Blaspsoft\\Blasp\\Core\\Score;\nuse Blaspsoft\\Blasp\\Core\\Matchers\\FalsePositiveFilter;\nuse Blaspsoft\\Blasp\\Core\\Matchers\\PhoneticMatcher;\nuse Blaspsoft\\Blasp\\Enums\\Severity;\n\nclass PhoneticDriver implements DriverInterface\n{\n    public function __construct(\n        private int $phonemes = 4,\n        private int $minWordLength = 3,\n        private float $maxDistanceRatio = 0.6,\n        private array $phoneticFalsePositives = [],\n        private array $supportedLanguages = ['english'],\n    ) {}\n\n    public function detect(string $text, Dictionary $dictionary, MaskStrategyInterface $mask, array $options = []): Result\n    {\n        if (empty($text)) {\n            return new Result($text ?? '', $text ?? '', [], 0);\n        }\n\n        if (!mb_check_encoding($text, 'UTF-8')) {\n            $text = mb_convert_encoding($text, 'UTF-8', 'UTF-8');\n        }\n\n        // Phonetic matching (metaphone) is English-oriented — skip unsupported languages\n        $language = $dictionary->getLanguage();\n        $languages = array_map('strtolower', explode(',', $language));\n        $supported = array_map('strtolower', $this->supportedLanguages);\n\n        $isSupported = false;\n        foreach ($languages as $lang) {\n            if (in_array(trim($lang), $supported, true)) {\n                $isSupported = true;\n                break;\n            }\n        }\n\n        if (!$isSupported) {\n            return new Result($text, $text, [], 0);\n        }\n\n        $filter = new FalsePositiveFilter($dictionary->getFalsePositives());\n\n        $matcher = new PhoneticMatcher(\n            profanities: $dictionary->getProfanities(),\n            phonemes: $this->phonemes,\n            minWordLength: $this->minWordLength,\n            maxDistanceRatio: $this->maxDistanceRatio,\n            phoneticFalsePositives: $this->phoneticFalsePositives,\n        );\n\n        $normalizer = $dictionary->getNormalizer();\n        $normalized = $normalizer->normalize($text);\n\n        // Tokenize\n        preg_match_all('/\\b[\\w\\']+\\b/u', $normalized, $matches, PREG_OFFSET_CAPTURE);\n        $tokens = $matches[0] ?? [];\n\n        $matchedWords = [];\n\n        foreach ($tokens as $token) {\n            $word = $token[0];\n            $byteStart = $token[1];\n            $byteLength = strlen($word);\n            $start = mb_strlen(substr($normalized, 0, $byteStart), 'UTF-8');\n            $length = mb_strlen($word, 'UTF-8');\n\n            // Skip dictionary false positives\n            if ($filter->isFalsePositive($word)) {\n                continue;\n            }\n\n            // Skip hex/UUID tokens (filter uses byte-level operations)\n            if ($filter->isInsideHexToken($normalized, $byteStart, $byteLength)) {\n                continue;\n            }\n\n            $baseWord = $matcher->match($word);\n            if ($baseWord === null) {\n                continue;\n            }\n\n            $originalWord = mb_substr($text, $start, $length);\n\n            $matchedWords[] = new MatchedWord(\n                text: $originalWord,\n                base: $baseWord,\n                severity: $dictionary->getSeverity($baseWord),\n                position: $start,\n                length: $length,\n                language: $dictionary->getLanguage(),\n            );\n        }\n\n        // Apply severity filter\n        $minimumSeverity = $options['severity'] ?? null;\n        if ($minimumSeverity instanceof Severity) {\n            $matchedWords = array_values(array_filter(\n                $matchedWords,\n                fn(MatchedWord $w) => $w->severity->isAtLeast($minimumSeverity)\n            ));\n        }\n\n        // Rebuild cleanText from surviving matches (right-to-left)\n        $cleanText = $text;\n        $sorted = $matchedWords;\n        usort($sorted, fn($a, $b) => $b->position - $a->position);\n        foreach ($sorted as $word) {\n            $replacement = $mask->mask($word->text, $word->length);\n            $cleanText = mb_substr($cleanText, 0, $word->position)\n                . $replacement\n                . mb_substr($cleanText, $word->position + $word->length);\n        }\n\n        $totalWords = max(1, count(preg_split('/\\s+/u', trim($text), -1, PREG_SPLIT_NO_EMPTY)));\n        $scoreValue = Score::calculate($matchedWords, $totalWords);\n\n        return new Result($text, $cleanText, $matchedWords, $scoreValue);\n    }\n}\n"
  },
  {
    "path": "src/Drivers/PipelineDriver.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Drivers;\n\nuse Blaspsoft\\Blasp\\Core\\Contracts\\DriverInterface;\nuse Blaspsoft\\Blasp\\Core\\Contracts\\MaskStrategyInterface;\nuse Blaspsoft\\Blasp\\Core\\Dictionary;\nuse Blaspsoft\\Blasp\\Core\\MatchedWord;\nuse Blaspsoft\\Blasp\\Core\\Result;\nuse Blaspsoft\\Blasp\\Core\\Score;\n\nclass PipelineDriver implements DriverInterface\n{\n    /** @param DriverInterface[] $drivers */\n    public function __construct(private array $drivers) {}\n\n    public function detect(string $text, Dictionary $dictionary, MaskStrategyInterface $mask, array $options = []): Result\n    {\n        if (empty($text)) {\n            return new Result($text ?? '', $text ?? '', [], 0);\n        }\n\n        // 1. Run each sub-driver, collecting all Result objects\n        $allMatches = [];\n        foreach ($this->drivers as $driver) {\n            $result = $driver->detect($text, $dictionary, $mask, $options);\n            foreach ($result->words() as $match) {\n                $allMatches[] = $match;\n            }\n        }\n\n        if (empty($allMatches)) {\n            return new Result($text, $text, [], 0);\n        }\n\n        // 2. Sort by position ascending, then length descending\n        usort($allMatches, function (MatchedWord $a, MatchedWord $b) {\n            if ($a->position !== $b->position) {\n                return $a->position <=> $b->position;\n            }\n            return $b->length <=> $a->length;\n        });\n\n        // 3. Deduplicate overlapping position ranges (greedy, longest-first at each position)\n        $kept = [];\n        foreach ($allMatches as $match) {\n            $overlaps = false;\n            foreach ($kept as $existing) {\n                $existingEnd = $existing->position + $existing->length;\n                $matchEnd = $match->position + $match->length;\n\n                if ($match->position < $existingEnd && $matchEnd > $existing->position) {\n                    $overlaps = true;\n                    break;\n                }\n            }\n\n            if (!$overlaps) {\n                $kept[] = $match;\n            }\n        }\n\n        // 4. Build clean text by applying masks right-to-left (preserves positions)\n        $cleanText = $text;\n        $reversed = array_reverse($kept);\n        foreach ($reversed as $match) {\n            $replacement = $mask->mask($match->text, $match->length);\n            $cleanText = mb_substr($cleanText, 0, $match->position, 'UTF-8') . $replacement . mb_substr($cleanText, $match->position + $match->length, null, 'UTF-8');\n        }\n\n        // 5. Recalculate score from merged matches\n        $totalWords = max(1, count(preg_split('/\\s+/u', trim($text), -1, PREG_SPLIT_NO_EMPTY)));\n        $scoreValue = Score::calculate($kept, $totalWords);\n\n        return new Result($text, $cleanText, $kept, $scoreValue);\n    }\n}\n"
  },
  {
    "path": "src/Drivers/RegexDriver.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Drivers;\n\nuse Blaspsoft\\Blasp\\Core\\Contracts\\DriverInterface;\nuse Blaspsoft\\Blasp\\Core\\Contracts\\MaskStrategyInterface;\nuse Blaspsoft\\Blasp\\Core\\Dictionary;\nuse Blaspsoft\\Blasp\\Core\\MatchedWord;\nuse Blaspsoft\\Blasp\\Core\\Result;\nuse Blaspsoft\\Blasp\\Core\\Score;\nuse Blaspsoft\\Blasp\\Core\\Matchers\\FalsePositiveFilter;\nuse Blaspsoft\\Blasp\\Core\\Matchers\\CompoundWordDetector;\nuse Blaspsoft\\Blasp\\Enums\\Severity;\n\nclass RegexDriver implements DriverInterface\n{\n    private FalsePositiveFilter $filter;\n    private CompoundWordDetector $compoundDetector;\n\n    public function detect(string $text, Dictionary $dictionary, MaskStrategyInterface $mask, array $options = []): Result\n    {\n        if (empty($text)) {\n            return new Result($text ?? '', $text ?? '', [], 0);\n        }\n\n        if (!mb_check_encoding($text, 'UTF-8')) {\n            $text = mb_convert_encoding($text, 'UTF-8', 'UTF-8');\n        }\n\n        $this->filter = new FalsePositiveFilter($dictionary->getFalsePositives());\n        $this->compoundDetector = new CompoundWordDetector();\n\n        $profanityExpressions = $dictionary->getProfanityExpressions();\n\n        // Sort by key length descending (longest profanity first)\n        uksort($profanityExpressions, fn($a, $b) => strlen($b) - strlen($a));\n\n        $normalizer = $dictionary->getNormalizer();\n        $normalizedString = $normalizer->normalize($text);\n        $originalNormalized = preg_replace('/\\s+/', ' ', $normalizedString);\n\n        // Immutable copy for position lookups — never mutated\n        $immutableNormalized = $originalNormalized;\n\n        $matchedWords = [];\n        $uniqueMap = [];\n        $profanitiesCount = 0;\n        $continue = true;\n\n        // Track masked character ranges so we don't re-match them\n        $maskedRanges = [];\n\n        while ($continue) {\n            $continue = false;\n            $normalizedString = preg_replace('/\\s+/', ' ', $normalizedString);\n\n            foreach ($profanityExpressions as $profanity => $expression) {\n                preg_match_all($expression, $normalizedString, $matches, PREG_OFFSET_CAPTURE);\n\n                if (!empty($matches[0])) {\n                    foreach ($matches[0] as $match) {\n                        $byteStart = $match[1];\n                        $byteLength = strlen($match[0]);\n                        $start = mb_strlen(substr($normalizedString, 0, $byteStart), 'UTF-8');\n                        $length = mb_strlen($match[0], 'UTF-8');\n                        $matchedText = $match[0];\n\n                        // Skip if this range overlaps with an already-masked range\n                        $matchEnd = $start + $length;\n                        $alreadyMasked = false;\n                        foreach ($maskedRanges as [$mStart, $mEnd]) {\n                            if ($start < $mEnd && $matchEnd > $mStart) {\n                                $alreadyMasked = true;\n                                break;\n                            }\n                        }\n                        if ($alreadyMasked) {\n                            continue;\n                        }\n\n                        // Check word boundary spanning (filter uses byte-level operations)\n                        if ($this->filter->isSpanningWordBoundary($matchedText, $normalizedString, $byteStart)) {\n                            continue;\n                        }\n\n                        // Check hex/UUID token (filter uses byte-level operations)\n                        if ($this->filter->isInsideHexToken($normalizedString, $byteStart, $byteLength)) {\n                            continue;\n                        }\n\n                        // Full word context for false positive check (filter uses byte-level operations)\n                        $fullWord = $this->filter->getFullWordContext($normalizedString, $byteStart, $byteLength);\n\n                        // Check pure alpha substring against original (unmasked) normalized\n                        $originalFullWord = $this->filter->getFullWordContext($immutableNormalized, $byteStart, $byteLength);\n                        if ($this->compoundDetector->isPureAlphaSubstring($matchedText, $originalFullWord, $profanity, $profanityExpressions)) {\n                            continue;\n                        }\n\n                        // False positive check\n                        if ($this->filter->isFalsePositive($fullWord)) {\n                            continue;\n                        }\n\n                        $continue = true;\n\n                        // Mask in normalizedString only (needed for loop termination)\n                        // Use SOH control char internally to avoid re-matching when '*' is\n                        // a valid substitution character in profanity patterns\n                        $normalizedString = mb_substr($normalizedString, 0, $start) . str_repeat(\"\\x01\", $length) .\n                            mb_substr($normalizedString, $start + $length);\n\n                        // Record masked range using character positions from immutable string\n                        $maskedRanges[] = [$start, $matchEnd];\n\n                        // Track match — use position derived from immutable normalized string\n                        $profanitiesCount++;\n\n                        // Get the original text at this position from the original input\n                        $originalMatchText = mb_substr($text, $start, $length);\n\n                        $matchedWords[] = new MatchedWord(\n                            text: $originalMatchText,\n                            base: $profanity,\n                            severity: $dictionary->getSeverity($profanity),\n                            position: $start,\n                            length: $length,\n                            language: $dictionary->getLanguage(),\n                        );\n\n                        if (!isset($uniqueMap[$profanity])) {\n                            $uniqueMap[$profanity] = true;\n                        }\n                    }\n                }\n            }\n        }\n\n        // Apply severity filter before masking so low-severity matches don't suppress overlapping ones\n        $minimumSeverity = $options['severity'] ?? null;\n        if ($minimumSeverity instanceof Severity) {\n            $matchedWords = array_values(array_filter(\n                $matchedWords,\n                fn(MatchedWord $w) => $w->severity->isAtLeast($minimumSeverity)\n            ));\n        }\n\n        // Rebuild cleanText from surviving matches (right-to-left)\n        $workingCleanString = $text;\n        $sorted = $matchedWords;\n        usort($sorted, fn($a, $b) => $b->position - $a->position);\n        foreach ($sorted as $word) {\n            $replacement = $mask->mask($word->text, $word->length);\n            $workingCleanString = mb_substr($workingCleanString, 0, $word->position)\n                . $replacement\n                . mb_substr($workingCleanString, $word->position + $word->length);\n        }\n\n        $totalWords = max(1, count(preg_split('/\\s+/u', trim($text), -1, PREG_SPLIT_NO_EMPTY)));\n        $scoreValue = Score::calculate($matchedWords, $totalWords);\n\n        return new Result($text, $workingCleanString, $matchedWords, $scoreValue);\n    }\n}\n"
  },
  {
    "path": "src/Enums/Severity.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Enums;\n\nenum Severity: string\n{\n    case Mild = 'mild';\n    case Moderate = 'moderate';\n    case High = 'high';\n    case Extreme = 'extreme';\n\n    public function weight(): int\n    {\n        return match ($this) {\n            self::Mild => 5,\n            self::Moderate => 15,\n            self::High => 30,\n            self::Extreme => 50,\n        };\n    }\n\n    public function isAtLeast(self $minimum): bool\n    {\n        return $this->weight() >= $minimum->weight();\n    }\n}\n"
  },
  {
    "path": "src/Events/ContentBlocked.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Events;\n\nuse Blaspsoft\\Blasp\\Core\\Result;\nuse Illuminate\\Http\\Request;\n\nclass ContentBlocked\n{\n    public function __construct(\n        public readonly Result $result,\n        public readonly Request $request,\n        public readonly string $field,\n        public readonly string $action,\n    ) {}\n}\n"
  },
  {
    "path": "src/Events/ModelProfanityDetected.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Events;\n\nuse Blaspsoft\\Blasp\\Core\\Result;\nuse Illuminate\\Database\\Eloquent\\Model;\n\nclass ModelProfanityDetected\n{\n    public function __construct(\n        public readonly Model $model,\n        public readonly string $attribute,\n        public readonly Result $result,\n    ) {}\n}\n"
  },
  {
    "path": "src/Events/ProfanityDetected.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Events;\n\nuse Blaspsoft\\Blasp\\Core\\Result;\n\nclass ProfanityDetected\n{\n    public function __construct(\n        public readonly Result $result,\n        public readonly string $originalText,\n    ) {}\n}\n"
  },
  {
    "path": "src/Exceptions/ProfanityRejectedException.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Exceptions;\n\nuse Blaspsoft\\Blasp\\Core\\Result;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse RuntimeException;\n\nclass ProfanityRejectedException extends RuntimeException\n{\n    public function __construct(\n        public readonly Model $model,\n        public readonly string $attribute,\n        public readonly Result $result,\n    ) {\n        parent::__construct(\"Profanity detected in '{$attribute}': \" . implode(', ', $result->uniqueWords()));\n    }\n\n    public static function forModel(Model $model, string $attribute, Result $result): static\n    {\n        return new static($model, $attribute, $result);\n    }\n}\n"
  },
  {
    "path": "src/Facades/Blasp.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Facades;\n\nuse Blaspsoft\\Blasp\\BlaspManager;\nuse Blaspsoft\\Blasp\\Core\\Result;\nuse Blaspsoft\\Blasp\\Enums\\Severity;\nuse Blaspsoft\\Blasp\\PendingCheck;\nuse Blaspsoft\\Blasp\\Testing\\BlaspFake;\nuse Closure;\nuse Illuminate\\Support\\Facades\\Facade as BaseFacade;\n\n/**\n * @method static Result check(?string $text)\n * @method static array checkMany(array $texts)\n * @method static PendingCheck in(string ...$languages)\n * @method static PendingCheck inAllLanguages()\n * @method static PendingCheck mask(string|Closure $mask)\n * @method static PendingCheck allow(string ...$words)\n * @method static PendingCheck block(string ...$words)\n * @method static PendingCheck withSeverity(Severity $severity)\n * @method static PendingCheck strict()\n * @method static PendingCheck lenient()\n * @method static PendingCheck driver(string $driver)\n * @method static PendingCheck pipeline(string ...$drivers)\n * @method static PendingCheck english()\n * @method static PendingCheck spanish()\n * @method static PendingCheck german()\n * @method static PendingCheck french()\n * @method static PendingCheck maskWith(string $character)\n * @method static PendingCheck allLanguages()\n * @method static PendingCheck language(string $language)\n * @method static PendingCheck configure(?array $profanities = null, ?array $falsePositives = null)\n * @method static BlaspManager extend(string $driver, Closure $callback)\n *\n * @see \\Blaspsoft\\Blasp\\BlaspManager\n */\nclass Blasp extends BaseFacade\n{\n    protected static function getFacadeAccessor(): string\n    {\n        return 'blasp';\n    }\n\n    public static function fake(array $responses = []): BlaspFake\n    {\n        $fake = new BlaspFake($responses);\n        static::swap($fake);\n        return $fake;\n    }\n\n    public static function withoutFiltering(Closure $callback): mixed\n    {\n        $fake = new BlaspFake();\n        static::swap($fake);\n\n        try {\n            return $callback();\n        } finally {\n            static::clearResolvedInstance('blasp');\n        }\n    }\n\n    public static function assertChecked(): void\n    {\n        $instance = static::getFacadeRoot();\n        if (!$instance instanceof BlaspFake) {\n            throw new \\RuntimeException('Blasp::assertChecked() requires Blasp::fake() to be called first.');\n        }\n        $instance->assertChecked();\n    }\n\n    public static function assertCheckedTimes(int $times): void\n    {\n        $instance = static::getFacadeRoot();\n        if (!$instance instanceof BlaspFake) {\n            throw new \\RuntimeException('Blasp::assertCheckedTimes() requires Blasp::fake() to be called first.');\n        }\n        $instance->assertCheckedTimes($times);\n    }\n}\n"
  },
  {
    "path": "src/Middleware/CheckProfanity.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Middleware;\n\nuse Closure;\nuse Illuminate\\Http\\Request;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Blaspsoft\\Blasp\\BlaspManager;\nuse Blaspsoft\\Blasp\\Events\\ContentBlocked;\nuse Blaspsoft\\Blasp\\Enums\\Severity;\n\nclass CheckProfanity\n{\n    public function __construct(\n        protected BlaspManager $manager\n    ) {}\n\n    public function handle(Request $request, Closure $next, ?string $action = null, ?string $severity = null): Response\n    {\n        $action = $action ?? config('blasp.middleware.action', 'reject');\n        $minimumSeverity = $severity ? (Severity::tryFrom($severity) ?? Severity::Mild) : Severity::tryFrom(config('blasp.middleware.severity', 'mild'));\n        $fields = config('blasp.middleware.fields', ['*']);\n        $except = config('blasp.middleware.except', ['password', 'email', '_token']);\n\n        if ($fields !== ['*']) {\n            $input = collect($request->only($fields))->except($except)->all();\n        } else {\n            $input = $request->except($except);\n        }\n\n        $textFields = $this->extractTextFields($input);\n\n        foreach ($textFields as $field => $value) {\n            $pendingCheck = $this->manager->newPendingCheck();\n\n            if ($minimumSeverity) {\n                $pendingCheck = $pendingCheck->withSeverity($minimumSeverity);\n            }\n\n            $result = $pendingCheck->check($value);\n\n            if ($result->isOffensive()) {\n                if (config('blasp.events', false)) {\n                    event(new ContentBlocked($result, $request, $field, $action));\n                }\n\n                if ($action === 'reject') {\n                    return response()->json([\n                        'message' => 'The request contains inappropriate content.',\n                        'errors' => [$field => ['The ' . $field . ' field contains profanity.']],\n                    ], 422);\n                }\n\n                if ($action === 'sanitize') {\n                    $request->merge([$field => $result->clean()]);\n                }\n            }\n        }\n\n        return $next($request);\n    }\n\n    protected function extractTextFields(array $input): array\n    {\n        $fields = [];\n        foreach ($input as $key => $value) {\n            if (is_string($value) && !empty(trim($value))) {\n                $fields[$key] = $value;\n            }\n        }\n        return $fields;\n    }\n}\n"
  },
  {
    "path": "src/PendingCheck.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp;\n\nuse Closure;\nuse Blaspsoft\\Blasp\\Core\\Analyzer;\nuse Blaspsoft\\Blasp\\Core\\Dictionary;\nuse Blaspsoft\\Blasp\\Core\\Result;\nuse Blaspsoft\\Blasp\\Core\\Contracts\\MaskStrategyInterface;\nuse Blaspsoft\\Blasp\\Core\\Masking\\CharacterMask;\nuse Blaspsoft\\Blasp\\Core\\Masking\\GrawlixMask;\nuse Blaspsoft\\Blasp\\Core\\Masking\\CallbackMask;\nuse Blaspsoft\\Blasp\\Drivers\\PipelineDriver;\nuse Blaspsoft\\Blasp\\Enums\\Severity;\nuse Blaspsoft\\Blasp\\Events\\ProfanityDetected;\nuse Illuminate\\Support\\Facades\\Cache;\n\nclass PendingCheck\n{\n    protected BlaspManager $manager;\n    protected ?string $driverName = null;\n    protected array $languages = [];\n    protected bool $allLanguages = false;\n    protected ?MaskStrategyInterface $maskStrategy = null;\n    protected array $allowList = [];\n    protected array $blockList = [];\n    protected ?Severity $minimumSeverity = null;\n    protected bool $strictMode = false;\n    protected bool $lenientMode = false;\n    protected ?array $pipelineDrivers = null;\n\n    public function __construct(BlaspManager $manager)\n    {\n        $this->manager = $manager;\n    }\n\n    // --- Fluent builder methods ---\n\n    public function driver(string $driver): self\n    {\n        $this->driverName = $driver;\n        return $this;\n    }\n\n    public function in(string ...$languages): self\n    {\n        $this->languages = $languages;\n        return $this;\n    }\n\n    public function inAllLanguages(): self\n    {\n        $this->allLanguages = true;\n        return $this;\n    }\n\n    public function mask(string|Closure $mask): self\n    {\n        if ($mask instanceof Closure) {\n            $this->maskStrategy = new CallbackMask($mask);\n        } elseif ($mask === 'grawlix') {\n            $this->maskStrategy = new GrawlixMask();\n        } else {\n            $this->maskStrategy = new CharacterMask($mask);\n        }\n        return $this;\n    }\n\n    public function allow(string ...$words): self\n    {\n        $this->allowList = array_merge($this->allowList, $words);\n        return $this;\n    }\n\n    public function block(string ...$words): self\n    {\n        $this->blockList = array_merge($this->blockList, $words);\n        return $this;\n    }\n\n    public function withSeverity(Severity $severity): self\n    {\n        $this->minimumSeverity = $severity;\n        return $this;\n    }\n\n    public function strict(): self\n    {\n        $this->strictMode = true;\n        $this->lenientMode = false;\n        return $this;\n    }\n\n    public function lenient(): self\n    {\n        $this->lenientMode = true;\n        $this->strictMode = false;\n        return $this;\n    }\n\n    public function pipeline(string ...$drivers): self\n    {\n        $this->pipelineDrivers = $drivers;\n        return $this;\n    }\n\n    // --- Deprecated backward-compat builder methods ---\n\n    /** @deprecated Use mask() instead */\n    public function maskWith(string $character): self\n    {\n        return $this->mask($character);\n    }\n\n    /** @deprecated Use inAllLanguages() instead */\n    public function allLanguages(): self\n    {\n        return $this->inAllLanguages();\n    }\n\n    /** @deprecated Use in() instead */\n    public function language(string $language): self\n    {\n        return $this->in($language);\n    }\n\n    // --- Language shortcuts ---\n\n    public function english(): self\n    {\n        return $this->in('english');\n    }\n\n    public function spanish(): self\n    {\n        return $this->in('spanish');\n    }\n\n    public function german(): self\n    {\n        return $this->in('german');\n    }\n\n    public function french(): self\n    {\n        return $this->in('french');\n    }\n\n    // --- Configure (backward-compat) ---\n\n    public function configure(?array $profanities = null, ?array $falsePositives = null): self\n    {\n        if ($profanities !== null) {\n            $this->blockList = array_merge($this->blockList, $profanities);\n        }\n        return $this;\n    }\n\n    // --- Execute ---\n\n    public function check(?string $text): Result\n    {\n        $text = $text ?? '';\n\n        if ($this->shouldCache()) {\n            $cacheKey = $this->buildCacheKey($text);\n            $cache = $this->getCache();\n            $ttl = config('blasp.cache.ttl', 86400);\n\n            $cached = $cache->get($cacheKey);\n            if ($cached !== null) {\n                return Result::fromArray($cached);\n            }\n\n            $result = $this->performCheck($text);\n\n            $cache->put($cacheKey, $result->toArray(), $ttl);\n            $this->trackCacheKey($cacheKey);\n\n            return $result;\n        }\n\n        return $this->performCheck($text);\n    }\n\n    protected function performCheck(string $text): Result\n    {\n        $dictionary = $this->buildDictionary();\n        $driver = $this->resolveDriver();\n        $mask = $this->resolveMask();\n\n        $options = [];\n        if ($this->minimumSeverity !== null) {\n            $options['severity'] = $this->minimumSeverity;\n        }\n\n        $analyzer = new Analyzer();\n        $result = $analyzer->analyze($text, $driver, $dictionary, $mask, $options);\n\n        // Fire event if configured\n        if ($result->isOffensive() && config('blasp.events', false)) {\n            event(new ProfanityDetected($result, $text));\n        }\n\n        return $result;\n    }\n\n    public function checkMany(array $texts): array\n    {\n        $results = [];\n        foreach ($texts as $key => $text) {\n            $results[$key] = $this->check($text);\n        }\n        return $results;\n    }\n\n    // --- Internal ---\n\n    protected function buildDictionary(): Dictionary\n    {\n        $options = [\n            'allow' => array_merge(config('blasp.allow', []), $this->allowList),\n            'block' => array_merge(config('blasp.block', []), $this->blockList),\n        ];\n\n        if ($this->allLanguages) {\n            return Dictionary::forAllLanguages($options);\n        }\n\n        if (!empty($this->languages)) {\n            if (count($this->languages) === 1) {\n                return Dictionary::forLanguage($this->languages[0], $options);\n            }\n            return Dictionary::forLanguages($this->languages, $options);\n        }\n\n        $defaultLanguage = config('blasp.language', config('blasp.default_language', 'english'));\n        return Dictionary::forLanguage($defaultLanguage, $options);\n    }\n\n    protected function resolveDriver(): \\Blaspsoft\\Blasp\\Core\\Contracts\\DriverInterface\n    {\n        if ($this->pipelineDrivers !== null) {\n            $resolved = array_map(\n                fn (string $name) => $this->manager->resolveDriver($name),\n                $this->pipelineDrivers,\n            );\n\n            return new PipelineDriver($resolved);\n        }\n\n        $driverName = $this->driverName ?? $this->manager->getDefaultDriver();\n\n        if ($this->lenientMode) {\n            $driverName = 'pattern';\n        }\n\n        return $this->manager->resolveDriver($driverName);\n    }\n\n    protected function resolveMask(): MaskStrategyInterface\n    {\n        if ($this->maskStrategy !== null) {\n            return $this->maskStrategy;\n        }\n\n        $maskConfig = config('blasp.mask', config('blasp.mask_character', '*'));\n        return new CharacterMask($maskConfig);\n    }\n\n    // --- Caching ---\n\n    protected function shouldCache(): bool\n    {\n        if (!config('blasp.cache.enabled', true)) {\n            return false;\n        }\n\n        if (!config('blasp.cache.results', true)) {\n            return false;\n        }\n\n        if ($this->maskStrategy instanceof CallbackMask) {\n            return false;\n        }\n\n        return true;\n    }\n\n    protected function buildCacheKey(string $text): string\n    {\n        $parts = [\n            'text' => $text,\n            'driver' => $this->driverName ?? config('blasp.default', 'regex'),\n            'pipeline' => $this->pipelineDrivers,\n            'languages' => $this->languages,\n            'all_languages' => $this->allLanguages,\n            'allow' => $this->allowList,\n            'block' => $this->blockList,\n            'severity' => $this->minimumSeverity?->value,\n            'strict' => $this->strictMode,\n            'lenient' => $this->lenientMode,\n            'mask' => $this->maskStrategy ? serialize($this->maskStrategy) : null,\n        ];\n\n        return 'blasp_result_' . md5(serialize($parts));\n    }\n\n    protected function getCache(): \\Illuminate\\Contracts\\Cache\\Repository\n    {\n        $driver = config('blasp.cache.driver', config('blasp.cache_driver'));\n\n        return $driver !== null ? Cache::store($driver) : Cache::store();\n    }\n\n    protected function trackCacheKey(string $key): void\n    {\n        $cache = $this->getCache();\n        $keys = $cache->get('blasp_result_cache_keys', []);\n        $keys[] = $key;\n        $keys = array_unique($keys);\n\n        // Evict oldest keys when exceeding the configured limit\n        $maxKeys = config('blasp.cache.max_tracked_keys', 1000);\n        if (count($keys) > $maxKeys) {\n            $keys = array_slice($keys, -$maxKeys);\n        }\n\n        $cache->forever('blasp_result_cache_keys', $keys);\n    }\n}\n"
  },
  {
    "path": "src/Rules/Profanity.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Rules;\n\nuse Closure;\nuse Illuminate\\Contracts\\Validation\\ValidationRule;\nuse Blaspsoft\\Blasp\\Enums\\Severity;\n\nclass Profanity implements ValidationRule\n{\n    protected ?string $language = null;\n    protected ?int $maxScore = null;\n    protected ?Severity $minimumSeverity = null;\n\n    public static function make(): self\n    {\n        return new self();\n    }\n\n    public function in(string $language): self\n    {\n        $this->language = $language;\n        return $this;\n    }\n\n    public function maxScore(int $score): self\n    {\n        $this->maxScore = $score;\n        return $this;\n    }\n\n    public function severity(Severity $severity): self\n    {\n        $this->minimumSeverity = $severity;\n        return $this;\n    }\n\n    public static function __callStatic(string $name, array $arguments): self\n    {\n        return (new self())->$name(...$arguments);\n    }\n\n    public function validate(string $attribute, mixed $value, Closure $fail): void\n    {\n        if (!is_string($value)) {\n            return;\n        }\n\n        $manager = app('blasp');\n        $pendingCheck = $manager->newPendingCheck();\n\n        if ($this->language) {\n            $pendingCheck = $pendingCheck->in($this->language);\n        }\n\n        if ($this->minimumSeverity) {\n            $pendingCheck = $pendingCheck->withSeverity($this->minimumSeverity);\n        }\n\n        $result = $pendingCheck->check($value);\n\n        if ($this->maxScore !== null) {\n            if ($result->score() > $this->maxScore) {\n                $fail('The :attribute contains profanity.');\n            }\n            return;\n        }\n\n        if ($result->isOffensive()) {\n            $fail('The :attribute contains profanity.');\n        }\n    }\n}\n"
  },
  {
    "path": "src/Testing/BlaspFake.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Testing;\n\nuse Blaspsoft\\Blasp\\Core\\Result;\nuse Blaspsoft\\Blasp\\PendingCheck;\nuse PHPUnit\\Framework\\Assert;\n\nclass BlaspFake\n{\n    protected array $fakeResults;\n    protected array $checksPerformed = [];\n\n    public function __construct(array $fakeResults = [])\n    {\n        $this->fakeResults = $fakeResults;\n    }\n\n    public function check(?string $text): Result\n    {\n        $text = $text ?? '';\n        $this->checksPerformed[] = $text;\n\n        if (isset($this->fakeResults[$text])) {\n            return $this->fakeResults[$text];\n        }\n\n        return Result::none($text);\n    }\n\n    public function checkMany(array $texts): array\n    {\n        $results = [];\n        foreach ($texts as $key => $text) {\n            $results[$key] = $this->check($text);\n        }\n        return $results;\n    }\n\n    public function assertChecked(): void\n    {\n        Assert::assertNotEmpty($this->checksPerformed, 'Expected at least one check to be performed.');\n    }\n\n    public function assertCheckedTimes(int $times): void\n    {\n        Assert::assertCount(\n            $times,\n            $this->checksPerformed,\n            \"Expected {$times} checks but \" . count($this->checksPerformed) . ' were performed.'\n        );\n    }\n\n    public function assertCheckedWith(string $text): void\n    {\n        Assert::assertContains($text, $this->checksPerformed, \"Expected check with text: {$text}\");\n    }\n\n    // Builder methods return self (no-op in fake mode, just pass through to check)\n    public function __call(string $method, array $parameters): self\n    {\n        return $this;\n    }\n\n    public function in(string ...$languages): self\n    {\n        return $this;\n    }\n\n    public function inAllLanguages(): self\n    {\n        return $this;\n    }\n\n    public function allLanguages(): self\n    {\n        return $this;\n    }\n\n    public function english(): self\n    {\n        return $this;\n    }\n\n    public function spanish(): self\n    {\n        return $this;\n    }\n\n    public function german(): self\n    {\n        return $this;\n    }\n\n    public function french(): self\n    {\n        return $this;\n    }\n\n    public function mask(string $mask): self\n    {\n        return $this;\n    }\n\n    public function maskWith(string $character): self\n    {\n        return $this;\n    }\n\n    public function language(string $language): self\n    {\n        return $this;\n    }\n\n    public function driver(string $driver): self\n    {\n        return $this;\n    }\n\n    public function configure(?array $profanities = null, ?array $falsePositives = null): self\n    {\n        return $this;\n    }\n}\n"
  },
  {
    "path": "tests/AllLanguagesApiTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Facades\\Blasp;\n\nclass AllLanguagesApiTest extends TestCase\n{\n    public function test_all_languages_detection()\n    {\n        $result = Blasp::allLanguages()->check('This is fucking amazing');\n        $this->assertTrue($result->hasProfanity());\n        $this->assertEquals('This is ******* amazing', $result->getCleanString());\n\n        $result = Blasp::allLanguages()->check('esto es una mierda');\n        $this->assertTrue($result->hasProfanity());\n        $this->assertEquals('esto es una ******', $result->getCleanString());\n\n        $result = Blasp::allLanguages()->check('das ist scheiße');\n        $this->assertTrue($result->hasProfanity());\n        $this->assertEquals('das ist *******', $result->getCleanString());\n\n        $result = Blasp::allLanguages()->check('c\\'est de la merde');\n        $this->assertTrue($result->hasProfanity());\n        $this->assertEquals('c\\'est de la *****', $result->getCleanString());\n    }\n\n    public function test_mixed_language_content()\n    {\n        $result = Blasp::allLanguages()->check('This shit is mierda and scheiße');\n        $this->assertTrue($result->hasProfanity());\n        $this->assertEquals('This **** is ****** and *******', $result->getCleanString());\n        $this->assertEquals(3, $result->getProfanitiesCount());\n    }\n\n    public function test_chainable_all_languages()\n    {\n        $result = Blasp::allLanguages()->check('damn merde');\n        $this->assertTrue($result->hasProfanity());\n    }\n\n    public function test_language_shortcuts_vs_all()\n    {\n        $text = 'fucking merde scheiße mierda';\n\n        $englishResult = Blasp::english()->check($text);\n        $this->assertEquals(1, $englishResult->getProfanitiesCount());\n\n        $allResult = Blasp::allLanguages()->check($text);\n        $this->assertEquals(4, $allResult->getProfanitiesCount());\n\n        $this->assertStringNotContainsString('fucking', $allResult->getCleanString());\n        $this->assertStringNotContainsString('merde', $allResult->getCleanString());\n        $this->assertStringNotContainsString('scheiße', $allResult->getCleanString());\n        $this->assertStringContainsString('*******', $allResult->getCleanString());\n    }\n\n    public function test_direct_manager_all_languages()\n    {\n        $manager = app('blasp');\n        $result = $manager->inAllLanguages()->check('This fuck is merde');\n        $this->assertTrue($result->hasProfanity());\n        $this->assertEquals(2, $result->getProfanitiesCount());\n    }\n\n    public function test_configure_with_all_languages()\n    {\n        $result = Blasp::allLanguages()\n            ->block('customword')\n            ->check('customword and fuck');\n\n        $this->assertTrue($result->hasProfanity());\n        $this->assertStringContainsString('*', $result->getCleanString());\n    }\n}\n"
  },
  {
    "path": "tests/AllLanguagesDetectionTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Facades\\Blasp;\n\nclass AllLanguagesDetectionTest extends TestCase\n{\n    public function test_all_languages_profanity_detection()\n    {\n        $testCases = [\n            'english' => [\n                'text' => 'You are a fucking cunt',\n                'expected_profanities' => ['fucking', 'cunt'],\n                'min_count' => 2\n            ],\n            'german' => [\n                'text' => 'Du bist eine verdammte Fotze',\n                'expected_profanities' => ['verdammte', 'fotze'],\n                'min_count' => 2\n            ],\n            'french' => [\n                'text' => 'Tu es un putain de connard',\n                'expected_profanities' => ['putain', 'connard'],\n                'min_count' => 2\n            ],\n            'spanish' => [\n                'text' => 'Eres un maldito hijo de puta',\n                'expected_profanities' => ['maldito', 'hijo de puta', 'puta'],\n                'min_count' => 2\n            ]\n        ];\n\n        foreach ($testCases as $language => $testCase) {\n            $result = Blasp::in($language)->check($testCase['text']);\n\n            $this->assertTrue(\n                $result->isOffensive(),\n                \"[$language] Failed to detect profanities in: {$testCase['text']}\"\n            );\n\n            $this->assertGreaterThanOrEqual(\n                $testCase['min_count'],\n                $result->count(),\n                \"[$language] Expected at least {$testCase['min_count']} profanities, got {$result->count()}\"\n            );\n\n            foreach ($testCase['expected_profanities'] as $profanity) {\n                $this->assertStringNotContainsString(\n                    $profanity,\n                    strtolower($result->clean()),\n                    \"[$language] '$profanity' was not censored\"\n                );\n            }\n\n            $this->assertStringContainsString(\n                '*',\n                $result->clean(),\n                \"[$language] No asterisks found in censored string\"\n            );\n        }\n    }\n\n    public function test_language_variations()\n    {\n        $variations = [\n            'german' => [\n                'verdammte' => ['VERDAMMTE', 'Verdammte', 'verdammte', 'VeRdAmMtE'],\n                'scheisse' => ['SCHEISSE', 'Scheisse', 'scheisse', 'ScHeIsSe', 'scheisse']\n            ],\n            'french' => [\n                'merde' => ['MERDE', 'Merde', 'merde', 'MeRdE'],\n                'putain' => ['PUTAIN', 'Putain', 'putain', 'PuTaIn']\n            ],\n            'spanish' => [\n                'mierda' => ['MIERDA', 'Mierda', 'mierda', 'MiErDa'],\n                'joder' => ['JODER', 'Joder', 'joder', 'JoDeR']\n            ],\n            'english' => [\n                'fuck' => ['FUCK', 'Fuck', 'fuck', 'FuCk', 'f@ck', 'f*ck'],\n                'shit' => ['SHIT', 'Shit', 'shit', 'ShIt', 'sh1t', 'sh!t']\n            ]\n        ];\n\n        foreach ($variations as $language => $words) {\n            foreach ($words as $base => $variants) {\n                foreach ($variants as $variant) {\n                    $testText = \"This contains $variant here\";\n                    $result = Blasp::in($language)->check($testText);\n\n                    $this->assertTrue(\n                        $result->isOffensive(),\n                        \"[$language] Failed to detect variant '$variant' of '$base'\"\n                    );\n                }\n            }\n        }\n    }\n\n    public function test_language_normalizers()\n    {\n        // German-specific: umlauts and eszett\n        $germanTests = ['scheisse', 'Scheisse', 'SCHEISSE'];\n\n        foreach ($germanTests as $input) {\n            $result = Blasp::german()->check(\"Das ist $input test\");\n            $this->assertTrue(\n                $result->isOffensive(),\n                \"German normalizer failed for '$input'\"\n            );\n        }\n\n        // French-specific: accents\n        $frenchTests = ['connard', 'CONNARD', 'Connard'];\n\n        foreach ($frenchTests as $input) {\n            $result = Blasp::french()->check(\"C'est un $input ici\");\n            $this->assertTrue(\n                $result->isOffensive(),\n                \"French normalizer failed for '$input'\"\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "tests/BladeDirectiveTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Illuminate\\Support\\Facades\\Blade;\n\nclass BladeDirectiveTest extends TestCase\n{\n    protected function renderBlade(string $template, array $data = []): string\n    {\n        $compiled = Blade::compileString($template);\n\n        ob_start();\n        extract($data);\n        eval('?>' . $compiled);\n        return ob_get_clean();\n    }\n\n    public function test_clean_directive_masks_profane_text()\n    {\n        $output = $this->renderBlade('@clean($text)', ['text' => 'This is a fucking sentence']);\n\n        $this->assertStringNotContainsString('fucking', $output);\n        $this->assertStringContainsString('*', $output);\n    }\n\n    public function test_clean_directive_passes_clean_text_unchanged()\n    {\n        $output = $this->renderBlade('@clean($text)', ['text' => 'This is a clean sentence']);\n\n        $this->assertSame('This is a clean sentence', $output);\n    }\n\n    public function test_clean_directive_escapes_html_for_xss_safety()\n    {\n        $output = $this->renderBlade('@clean($text)', ['text' => '<script>alert(\"xss\")</script>']);\n\n        $this->assertStringNotContainsString('<script>', $output);\n        $this->assertStringContainsString('&lt;script&gt;', $output);\n    }\n}\n"
  },
  {
    "path": "tests/BlaspCheckTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Facades\\Blasp;\n\nclass BlaspCheckTest extends TestCase\n{\n    public function test_real_blasp_service()\n    {\n        $result = Blasp::check('This is a fuck!ng sentence');\n        $this->assertTrue($result->isOffensive());\n    }\n\n    public function test_straight_match()\n    {\n        $result = Blasp::check('This is a fucking sentence');\n\n        $this->assertTrue($result->isOffensive());\n        $this->assertSame(1, $result->count());\n        $this->assertCount(1, $result->uniqueWords());\n        $this->assertSame('This is a ******* sentence', $result->clean());\n    }\n\n    public function test_substitution_match()\n    {\n        $result = Blasp::check('This is a fÛck!ng sentence');\n\n        $this->assertTrue($result->isOffensive());\n        $this->assertSame(1, $result->count());\n        $this->assertCount(1, $result->uniqueWords());\n        $this->assertSame('This is a ******* sentence', $result->clean());\n    }\n\n    public function test_obscured_match()\n    {\n        $result = Blasp::check('This is a f-u-c-k-i-n-g sentence');\n\n        $this->assertTrue($result->isOffensive());\n        $this->assertSame(1, $result->count());\n        $this->assertCount(1, $result->uniqueWords());\n        $this->assertSame('This is a ************* sentence', $result->clean());\n    }\n\n    public function test_doubled_match()\n    {\n        $result = Blasp::check('This is a ffuucckkiinngg sentence');\n\n        $this->assertTrue($result->isOffensive());\n        $this->assertSame(1, $result->count());\n        $this->assertCount(1, $result->uniqueWords());\n        $this->assertSame('This is a ************** sentence', $result->clean());\n    }\n\n    public function test_combination_match()\n    {\n        $result = Blasp::check('This is a f-uuck!ng sentence');\n\n        $this->assertTrue($result->isOffensive());\n        $this->assertSame(1, $result->count());\n        $this->assertCount(1, $result->uniqueWords());\n        $this->assertSame('This is a ********* sentence', $result->clean());\n    }\n\n    public function test_multiple_profanities_no_spaces()\n    {\n        $result = Blasp::check('cuntfuck shit');\n\n        $this->assertTrue($result->isOffensive());\n        $this->assertSame(3, $result->count());\n        $this->assertCount(3, $result->uniqueWords());\n        $this->assertSame('******** ****', $result->clean());\n    }\n\n    public function test_multiple_profanities()\n    {\n        $result = Blasp::check('This is a fuuckking sentence you fucking cunt!');\n        $this->assertTrue($result->isOffensive());\n        $this->assertSame(3, $result->count());\n        $this->assertCount(2, $result->uniqueWords());\n        $this->assertSame('This is a ********* sentence you ******* ****!', $result->clean());\n    }\n\n    public function test_scunthorpe_problem()\n    {\n        $result = Blasp::check('I live in a town called Scunthorpe');\n\n        $this->assertFalse($result->isOffensive());\n        $this->assertSame(0, $result->count());\n        $this->assertCount(0, $result->uniqueWords());\n        $this->assertSame('I live in a town called Scunthorpe', $result->clean());\n    }\n\n    public function test_penistone_problem()\n    {\n        $result = Blasp::check('I live in a town called Penistone');\n\n        $this->assertFalse($result->isOffensive());\n        $this->assertSame(0, $result->count());\n        $this->assertCount(0, $result->uniqueWords());\n        $this->assertSame('I live in a town called Penistone', $result->clean());\n    }\n\n    public function test_false_positives()\n    {\n        $words = [\n            'Blackcocktail', 'Scunthorpe', 'Cockburn', 'Penistone', 'Lightwater',\n            'Assume', 'Bass', 'Class', 'Compass', 'Pass',\n            'Dickinson', 'Middlesex', 'Cockerel', 'Butterscotch', 'Blackcock',\n            'Countryside', 'Arsenal', 'Flick', 'Flicker', 'Analyst',\n        ];\n\n        foreach ($words as $word) {\n            $result = Blasp::check($word);\n            $this->assertFalse($result->isOffensive(), \"False positive detected for: $word\");\n            $this->assertSame(0, $result->count());\n            $this->assertCount(0, $result->uniqueWords());\n            $this->assertSame($word, $result->clean());\n        }\n    }\n\n    public function test_cuntfuck_fuckcunt()\n    {\n        $result = Blasp::check('cuntfuck fuckcunt');\n        $this->assertTrue($result->isOffensive());\n        $this->assertSame(4, $result->count());\n        $this->assertCount(2, $result->uniqueWords());\n        $this->assertSame('******** ********', $result->clean());\n    }\n\n    public function test_fucking_shit_cunt_fuck()\n    {\n        $result = Blasp::check('fuckingshitcuntfuck');\n        $this->assertTrue($result->isOffensive());\n        $this->assertSame(3, $result->count());\n        $this->assertCount(3, $result->uniqueWords());\n        $this->assertSame('*******************', $result->clean());\n    }\n\n    public function test_billy_butcher()\n    {\n        $result = Blasp::check('oi! cunt!');\n        $this->assertTrue($result->isOffensive());\n        $this->assertSame(1, $result->count());\n        $this->assertCount(1, $result->uniqueWords());\n        $this->assertSame('oi! ****!', $result->clean());\n    }\n\n    public function test_paragraph()\n    {\n        $paragraph = \"This damn project is such a pain in the ass. I can't believe I have to deal with this bullshit every single day. It's like everything is completely fucked up, and nobody gives a shit. Sometimes I just want to scream, 'What the hell is going on?' Honestly, it's a total clusterfuck, and I'm so fucking done with this crap.\";\n\n        $result = Blasp::check($paragraph);\n\n        $expectedOutcome = \"This **** project is such a pain in the ***. I can't believe I have to deal with this ******** every single day. It's like everything is completely ****** up, and nobody gives a ****. Sometimes I just want to scream, 'What the **** is going on?' Honestly, it's a total ***********, and I'm so ******* done with this ****.\";\n\n        $this->assertTrue($result->isOffensive());\n        $this->assertSame(9, $result->count());\n        $this->assertCount(9, $result->uniqueWords());\n        $this->assertSame($expectedOutcome, $result->clean());\n    }\n\n    public function test_word_boudary()\n    {\n        $result = Blasp::check('afuckb');\n        $this->assertFalse($result->isOffensive());\n\n        $result = Blasp::check('a f u c k b');\n        $this->assertTrue($result->isOffensive());\n\n        $result = Blasp::check('af@ckb');\n        $this->assertTrue($result->isOffensive());\n    }\n\n    public function test_pural_profanity()\n    {\n        $result = Blasp::check('fuckings');\n        $this->assertTrue($result->isOffensive());\n        $this->assertSame(1, $result->count());\n        $this->assertCount(1, $result->uniqueWords());\n        $this->assertSame('*******s', $result->clean());\n    }\n\n    public function test_this_musicals_hit()\n    {\n        $result = Blasp::check('This musicals hit');\n        $this->assertFalse($result->isOffensive());\n        $this->assertSame(0, $result->count());\n        $this->assertCount(0, $result->uniqueWords());\n        $this->assertSame('This musicals hit', $result->clean());\n    }\n\n    public function test_ass_subtitution()\n    {\n        $result = Blasp::check('a$$');\n        $this->assertTrue($result->isOffensive());\n        $this->assertSame(1, $result->count());\n        $this->assertCount(1, $result->uniqueWords());\n        $this->assertSame('***', $result->clean());\n    }\n\n    public function test_embedded_profanities()\n    {\n        $result = Blasp::check('abcdtwatefghshitijklmfuckeropqrccuunntt');\n        $this->assertTrue($result->isOffensive());\n        $this->assertSame(4, $result->count());\n        $this->assertCount(4, $result->uniqueWords());\n        $this->assertSame('abcd****efgh****ijklm******opqr********', $result->clean());\n    }\n\n    public function test_multiple_profanities_with_spaces()\n    {\n        $result = Blasp::check('This is a fucking shit sentence');\n        $this->assertTrue($result->isOffensive());\n        $this->assertSame(2, $result->count());\n        $this->assertCount(2, $result->uniqueWords());\n        $this->assertSame('This is a ******* **** sentence', $result->clean());\n    }\n\n    public function test_spaced_profanity_with_substitution()\n    {\n        $result = Blasp::check('This is f u c k 1 n g awesome!');\n        $this->assertTrue($result->isOffensive());\n        $this->assertStringContainsString('*', $result->clean());\n    }\n\n    public function test_spaced_profanity_without_substitution()\n    {\n        $result = Blasp::check('f u c k i n g');\n        $this->assertTrue($result->isOffensive());\n    }\n\n    public function test_partial_spacing_s_hit()\n    {\n        $result = Blasp::check('s hit');\n        $this->assertTrue($result->isOffensive());\n        $this->assertContains('shit', $result->uniqueWords());\n    }\n\n    public function test_partial_spacing_f_uck()\n    {\n        $result = Blasp::check('f uck');\n        $this->assertTrue($result->isOffensive());\n        $this->assertContains('fuck', $result->uniqueWords());\n    }\n\n    public function test_partial_spacing_t_wat()\n    {\n        $result = Blasp::check('t wat');\n        $this->assertTrue($result->isOffensive());\n        $this->assertContains('twat', $result->uniqueWords());\n    }\n\n    public function test_partial_spacing_fu_c_k()\n    {\n        $result = Blasp::check('fu c k');\n        $this->assertTrue($result->isOffensive());\n        $this->assertContains('fuck', $result->uniqueWords());\n    }\n\n    public function test_partial_spacing_tw_a_t()\n    {\n        $result = Blasp::check('tw a t');\n        $this->assertTrue($result->isOffensive());\n        $this->assertContains('twat', $result->uniqueWords());\n    }\n\n    public function test_no_false_positive_musicals_hit_embedded()\n    {\n        $result = Blasp::check('This musicals hit');\n        $this->assertFalse($result->isOffensive());\n        $this->assertSame('This musicals hit', $result->clean());\n    }\n\n    public function test_no_false_positive_an_alert()\n    {\n        $result = Blasp::check('an alert');\n        $this->assertFalse($result->isOffensive());\n        $this->assertSame('an alert', $result->clean());\n    }\n\n    public function test_no_false_positive_has_5_faces()\n    {\n        $result = Blasp::check('the user has 5 faces');\n        $this->assertFalse($result->isOffensive());\n        $this->assertSame('the user has 5 faces', $result->clean());\n    }\n\n    public function test_detects_at_ss_obfuscation()\n    {\n        $result = Blasp::check('This has @ss in it');\n        $this->assertTrue($result->isOffensive());\n    }\n\n    public function test_no_false_positive_space_words()\n    {\n        $words = [\n            'This product provides ample space for storage.',\n            'The spacious design offers great workspace.',\n            'Perfect for aerospace applications.',\n            'Use the backspace key to delete.',\n            'The spacecraft landed safely.',\n        ];\n\n        foreach ($words as $sentence) {\n            $result = Blasp::check($sentence);\n            $this->assertFalse(\n                $result->isOffensive(),\n                \"\\\"$sentence\\\" should not be flagged but got: \" . implode(', ', $result->uniqueWords())\n            );\n        }\n\n        $result = Blasp::check('you spac');\n        $this->assertTrue($result->isOffensive());\n\n        $result = Blasp::check('you sp@c');\n        $this->assertTrue($result->isOffensive());\n    }\n}\n"
  },
  {
    "path": "tests/BlaspCheckValidationTest.php",
    "content": "<?php\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Illuminate\\Support\\Facades\\Validator;\n\nclass BlaspCheckValidationTest extends TestCase\n{\n    /**\n     * Test validation passes with clean text.\n     *\n     * @return void\n     */\n    public function test_blasp_check_validation_passes_with_clean_text()\n    {\n        $data = ['message' => 'This is a clean message.'];\n\n        $rules = ['message' => 'blasp_check'];\n\n        $validator = Validator::make($data, $rules);\n\n        $this->assertTrue($validator->passes());\n    }\n\n    /**\n     * Test validation fails with profane text.\n     *\n     * @return void\n     */\n    public function test_blasp_check_validation_fails_with_profanity()\n    {\n        $data = ['message' => 'This is a fucking message.'];\n\n        $rules = ['message' => 'blasp_check'];\n\n        $validator = Validator::make($data, $rules);\n\n        $this->assertTrue($validator->fails());\n    }\n}\n"
  },
  {
    "path": "tests/BlaspableTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Core\\Result;\nuse Blaspsoft\\Blasp\\Blaspable;\nuse Blaspsoft\\Blasp\\Events\\ModelProfanityDetected;\nuse Blaspsoft\\Blasp\\Exceptions\\ProfanityRejectedException;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Event;\nuse Illuminate\\Support\\Facades\\Schema;\n\nclass BlaspableTestModel extends Model\n{\n    use Blaspable;\n\n    protected $table = 'comments';\n    protected $guarded = [];\n    public $timestamps = false;\n\n    protected array $blaspable = ['body', 'title'];\n}\n\nclass BlaspableRejectModel extends Model\n{\n    use Blaspable;\n\n    protected $table = 'comments';\n    protected $guarded = [];\n    public $timestamps = false;\n\n    protected array $blaspable = ['body', 'title'];\n    protected string $blaspMode = 'reject';\n}\n\nclass BlaspableSpanishModel extends Model\n{\n    use Blaspable;\n\n    protected $table = 'comments';\n    protected $guarded = [];\n    public $timestamps = false;\n\n    protected array $blaspable = ['body'];\n    protected string $blaspLanguage = 'spanish';\n}\n\nclass BlaspableCustomMaskModel extends Model\n{\n    use Blaspable;\n\n    protected $table = 'comments';\n    protected $guarded = [];\n    public $timestamps = false;\n\n    protected array $blaspable = ['body'];\n    protected string $blaspMask = '#';\n}\n\nclass BlaspableTest extends TestCase\n{\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        Schema::create('comments', function (Blueprint $table) {\n            $table->id();\n            $table->string('title')->nullable();\n            $table->text('body')->nullable();\n            $table->string('email')->nullable();\n        });\n    }\n\n    protected function tearDown(): void\n    {\n        Schema::dropIfExists('comments');\n        parent::tearDown();\n    }\n\n    public function test_sanitize_mode_masks_profanity_on_save()\n    {\n        $model = BlaspableTestModel::create([\n            'body' => 'This is a fucking sentence',\n            'title' => 'Clean title',\n        ]);\n\n        $this->assertStringNotContainsString('fucking', $model->body);\n        $this->assertStringContainsString('*', $model->body);\n        $this->assertSame('Clean title', $model->title);\n        $this->assertTrue($model->exists);\n    }\n\n    public function test_reject_mode_throws_exception()\n    {\n        $this->expectException(ProfanityRejectedException::class);\n        $this->expectExceptionMessage(\"Profanity detected in 'body'\");\n\n        BlaspableRejectModel::create([\n            'body' => 'This is a fucking sentence',\n            'title' => 'Clean title',\n        ]);\n    }\n\n    public function test_reject_mode_does_not_persist_model()\n    {\n        try {\n            BlaspableRejectModel::create([\n                'body' => 'This is a fucking sentence',\n            ]);\n        } catch (ProfanityRejectedException) {\n            // expected\n        }\n\n        $this->assertSame(0, BlaspableRejectModel::count());\n    }\n\n    public function test_clean_text_passes_through_untouched()\n    {\n        $model = BlaspableTestModel::create([\n            'body' => 'This is a perfectly clean sentence',\n            'title' => 'Nice title',\n        ]);\n\n        $this->assertSame('This is a perfectly clean sentence', $model->body);\n        $this->assertSame('Nice title', $model->title);\n    }\n\n    public function test_only_dirty_attributes_are_checked()\n    {\n        $model = BlaspableTestModel::create([\n            'body' => 'Clean body',\n            'title' => 'Clean title',\n        ]);\n\n        // Update only body — title should not be re-checked\n        $model->body = 'Still clean';\n        $model->save();\n\n        $this->assertArrayNotHasKey('title', $model->blaspResults());\n        $this->assertArrayHasKey('body', $model->blaspResults());\n    }\n\n    public function test_non_blaspable_attributes_are_ignored()\n    {\n        $model = BlaspableTestModel::create([\n            'body' => 'Clean body',\n            'email' => 'fucking@example.com',\n        ]);\n\n        $this->assertSame('fucking@example.com', $model->email);\n    }\n\n    public function test_per_model_language_override()\n    {\n        $model = BlaspableSpanishModel::create([\n            'body' => 'Esto es una mierda',\n        ]);\n\n        $this->assertStringNotContainsString('mierda', $model->body);\n        $this->assertStringContainsString('*', $model->body);\n    }\n\n    public function test_per_model_mask_override()\n    {\n        $model = BlaspableCustomMaskModel::create([\n            'body' => 'This is a fucking sentence',\n        ]);\n\n        $this->assertStringNotContainsString('fucking', $model->body);\n        $this->assertStringContainsString('#', $model->body);\n        $this->assertStringNotContainsString('*', $model->body);\n    }\n\n    public function test_had_profanity_returns_true_when_profanity_detected()\n    {\n        $model = BlaspableTestModel::create([\n            'body' => 'This is a fucking sentence',\n        ]);\n\n        $this->assertTrue($model->hadProfanity());\n    }\n\n    public function test_had_profanity_returns_false_for_clean_text()\n    {\n        $model = BlaspableTestModel::create([\n            'body' => 'This is a clean sentence',\n        ]);\n\n        $this->assertFalse($model->hadProfanity());\n    }\n\n    public function test_blasp_results_returns_results_array()\n    {\n        $model = BlaspableTestModel::create([\n            'body' => 'This is a fucking sentence',\n            'title' => 'Clean title',\n        ]);\n\n        $results = $model->blaspResults();\n\n        $this->assertArrayHasKey('body', $results);\n        $this->assertArrayHasKey('title', $results);\n        $this->assertInstanceOf(Result::class, $results['body']);\n        $this->assertTrue($results['body']->isOffensive());\n        $this->assertFalse($results['title']->isOffensive());\n    }\n\n    public function test_blasp_result_returns_single_attribute_result()\n    {\n        $model = BlaspableTestModel::create([\n            'body' => 'This is a fucking sentence',\n        ]);\n\n        $result = $model->blaspResult('body');\n\n        $this->assertInstanceOf(Result::class, $result);\n        $this->assertTrue($result->isOffensive());\n    }\n\n    public function test_blasp_result_returns_null_for_unknown_attribute()\n    {\n        $model = BlaspableTestModel::create([\n            'body' => 'Clean body',\n        ]);\n\n        $this->assertNull($model->blaspResult('nonexistent'));\n    }\n\n    public function test_without_blasp_checking_disables_profanity_check()\n    {\n        $model = BlaspableTestModel::withoutBlaspChecking(function () {\n            return BlaspableTestModel::create([\n                'body' => 'This is a fucking sentence',\n            ]);\n        });\n\n        $this->assertSame('This is a fucking sentence', $model->body);\n        $this->assertTrue($model->exists);\n    }\n\n    public function test_model_profanity_detected_event_fires_in_sanitize_mode()\n    {\n        Event::fake([ModelProfanityDetected::class]);\n\n        BlaspableTestModel::create([\n            'body' => 'This is a fucking sentence',\n        ]);\n\n        Event::assertDispatched(ModelProfanityDetected::class, function ($event) {\n            return $event->attribute === 'body'\n                && $event->result->isOffensive()\n                && $event->model instanceof BlaspableTestModel;\n        });\n    }\n\n    public function test_model_profanity_detected_event_fires_in_reject_mode()\n    {\n        Event::fake([ModelProfanityDetected::class]);\n\n        try {\n            BlaspableRejectModel::create([\n                'body' => 'This is a fucking sentence',\n            ]);\n        } catch (ProfanityRejectedException) {\n            // expected\n        }\n\n        Event::assertDispatched(ModelProfanityDetected::class, function ($event) {\n            return $event->attribute === 'body';\n        });\n    }\n\n    public function test_event_not_fired_for_clean_text()\n    {\n        Event::fake([ModelProfanityDetected::class]);\n\n        BlaspableTestModel::create([\n            'body' => 'This is a clean sentence',\n        ]);\n\n        Event::assertNotDispatched(ModelProfanityDetected::class);\n    }\n\n    public function test_update_triggers_sanitization()\n    {\n        $model = BlaspableTestModel::create([\n            'body' => 'Clean body',\n        ]);\n\n        $model->body = 'This is a fucking update';\n        $model->save();\n\n        $this->assertStringNotContainsString('fucking', $model->body);\n        $this->assertStringContainsString('*', $model->body);\n    }\n\n    public function test_multiple_profane_attributes_are_sanitized()\n    {\n        $model = BlaspableTestModel::create([\n            'body' => 'This is a fucking sentence',\n            'title' => 'What the shit',\n        ]);\n\n        $this->assertStringNotContainsString('fucking', $model->body);\n        $this->assertStringNotContainsString('shit', $model->title);\n        $this->assertTrue($model->hadProfanity());\n    }\n\n    public function test_null_attributes_are_skipped()\n    {\n        $model = BlaspableTestModel::create([\n            'body' => null,\n            'title' => 'Clean title',\n        ]);\n\n        $this->assertNull($model->body);\n        $this->assertSame('Clean title', $model->title);\n    }\n\n    public function test_profanity_rejected_exception_contains_model_and_attribute()\n    {\n        try {\n            BlaspableRejectModel::create([\n                'body' => 'This is a fucking sentence',\n            ]);\n            $this->fail('Expected ProfanityRejectedException was not thrown');\n        } catch (ProfanityRejectedException $e) {\n            $this->assertSame('body', $e->attribute);\n            $this->assertInstanceOf(BlaspableRejectModel::class, $e->model);\n            $this->assertInstanceOf(Result::class, $e->result);\n            $this->assertTrue($e->result->isOffensive());\n        }\n    }\n}\n"
  },
  {
    "path": "tests/BypassVulnerabilityTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Facades\\Blasp;\n\nclass BypassVulnerabilityTest extends TestCase\n{\n    // -------------------------------------------------------\n    // Invisible Unicode Characters (U+2063, U+200B, etc.)\n    // -------------------------------------------------------\n\n    public function test_invisible_separator_in_fuck()\n    {\n        $result = Blasp::check(\"f\\u{2063}uck\");\n        $this->assertTrue($result->isOffensive());\n        $this->assertContains('fuck', $result->uniqueWords());\n    }\n\n    public function test_zero_width_space_in_shit()\n    {\n        $result = Blasp::check(\"s\\u{200B}hit\");\n        $this->assertTrue($result->isOffensive());\n        $this->assertContains('shit', $result->uniqueWords());\n    }\n\n    public function test_multiple_invisible_chars_in_profanity()\n    {\n        $result = Blasp::check(\"f\\u{200B}\\u{2063}uck\");\n        $this->assertTrue($result->isOffensive());\n        $this->assertContains('fuck', $result->uniqueWords());\n    }\n\n    public function test_invisible_chars_in_clean_text_no_false_positive()\n    {\n        $result = Blasp::check(\"he\\u{2063}llo\");\n        $this->assertFalse($result->isOffensive());\n    }\n\n    public function test_invisible_separator_clean_output_masks_profanity()\n    {\n        $result = Blasp::check(\"f\\u{2063}uck this\");\n        $this->assertTrue($result->isOffensive());\n        $this->assertSame('**** this', $result->clean());\n    }\n\n    // -------------------------------------------------------\n    // Censored Profanity (asterisk as letter replacement)\n    // -------------------------------------------------------\n\n    public function test_asterisk_censored_fag()\n    {\n        $result = Blasp::check('f*g');\n        $this->assertTrue($result->isOffensive());\n        $this->assertContains('fag', $result->uniqueWords());\n    }\n\n    public function test_asterisk_censored_fuck()\n    {\n        $result = Blasp::check('f**k');\n        $this->assertTrue($result->isOffensive());\n    }\n\n    public function test_asterisk_censored_shit()\n    {\n        $result = Blasp::check('s**t');\n        $this->assertTrue($result->isOffensive());\n    }\n\n    public function test_asterisk_fully_censored_fuck()\n    {\n        $result = Blasp::check('f***');\n        $this->assertTrue($result->isOffensive());\n    }\n\n    public function test_asterisk_in_non_profane_word_no_false_positive()\n    {\n        $result = Blasp::check('b*g');\n        $this->assertFalse($result->isOffensive());\n    }\n\n    // -------------------------------------------------------\n    // Combined: invisible + wildcard\n    // -------------------------------------------------------\n\n    public function test_invisible_char_plus_asterisk_censoring()\n    {\n        $result = Blasp::check(\"f\\u{2063}*g\");\n        $this->assertTrue($result->isOffensive());\n    }\n}\n"
  },
  {
    "path": "tests/CacheDriverConfigurationTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Core\\Dictionary;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Config;\n\nclass CacheDriverConfigurationTest extends TestCase\n{\n    public function setUp(): void\n    {\n        parent::setUp();\n        $this->app['config']->set('cache.default', 'array');\n        Cache::flush();\n    }\n\n    public function test_dictionary_can_be_created_without_cache(): void\n    {\n        Config::set('blasp.cache.driver', null);\n\n        $dictionary = Dictionary::forLanguage('english');\n\n        $this->assertNotNull($dictionary);\n        $this->assertNotEmpty($dictionary->getProfanities());\n    }\n\n    public function test_clear_cache_works(): void\n    {\n        Dictionary::clearCache();\n        $this->assertFalse(Cache::has('blasp_cache_keys'));\n    }\n\n    public function test_dictionary_loads_consistently(): void\n    {\n        $dict1 = Dictionary::forLanguage('english');\n        $dict2 = Dictionary::forLanguage('english');\n\n        $this->assertEquals($dict1->getProfanities(), $dict2->getProfanities());\n        $this->assertEquals($dict1->getFalsePositives(), $dict2->getFalsePositives());\n    }\n\n    public function test_different_languages_have_different_profanities(): void\n    {\n        $english = Dictionary::forLanguage('english');\n        $spanish = Dictionary::forLanguage('spanish');\n\n        $this->assertNotEquals($english->getProfanities(), $spanish->getProfanities());\n    }\n\n    public function test_clear_cache_with_custom_driver(): void\n    {\n        Config::set('blasp.cache.driver', 'array');\n\n        Dictionary::clearCache();\n\n        $keys = Cache::store('array')->get('blasp_cache_keys', []);\n        $this->assertEmpty($keys);\n    }\n}\n"
  },
  {
    "path": "tests/ConfigurationLoaderLanguageTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Core\\Dictionary;\nuse Blaspsoft\\Blasp\\Core\\Normalizers\\EnglishNormalizer;\nuse Blaspsoft\\Blasp\\Core\\Normalizers\\SpanishNormalizer;\nuse Blaspsoft\\Blasp\\Core\\Normalizers\\GermanNormalizer;\nuse Blaspsoft\\Blasp\\Core\\Normalizers\\FrenchNormalizer;\n\nclass ConfigurationLoaderLanguageTest extends TestCase\n{\n    public function test_get_available_languages()\n    {\n        $languages = Dictionary::getAvailableLanguages();\n\n        $this->assertIsArray($languages);\n        $this->assertContains('english', $languages);\n        $this->assertContains('spanish', $languages);\n        $this->assertContains('french', $languages);\n        $this->assertContains('german', $languages);\n    }\n\n    public function test_load_specific_language_english()\n    {\n        $englishConfig = Dictionary::loadLanguageConfig('english');\n\n        $this->assertIsArray($englishConfig);\n        $this->assertArrayHasKey('profanities', $englishConfig);\n        $this->assertArrayHasKey('false_positives', $englishConfig);\n        $this->assertIsArray($englishConfig['profanities']);\n        $this->assertIsArray($englishConfig['false_positives']);\n        $this->assertContains('fuck', $englishConfig['profanities']);\n        $this->assertContains('shit', $englishConfig['profanities']);\n        $this->assertContains('class', $englishConfig['false_positives']);\n        $this->assertContains('pass', $englishConfig['false_positives']);\n    }\n\n    public function test_load_specific_language_spanish()\n    {\n        $spanishConfig = Dictionary::loadLanguageConfig('spanish');\n\n        $this->assertIsArray($spanishConfig);\n        $this->assertArrayHasKey('profanities', $spanishConfig);\n        $this->assertArrayHasKey('false_positives', $spanishConfig);\n        $this->assertArrayHasKey('substitutions', $spanishConfig);\n        $this->assertContains('mierda', $spanishConfig['profanities']);\n        $this->assertContains('joder', $spanishConfig['profanities']);\n        $this->assertContains('cabrón', $spanishConfig['profanities']);\n        $this->assertContains('clase', $spanishConfig['false_positives']);\n        $this->assertContains('análisis', $spanishConfig['false_positives']);\n        $this->assertArrayHasKey('/ñ/', $spanishConfig['substitutions']);\n        $this->assertArrayHasKey('/á/', $spanishConfig['substitutions']);\n    }\n\n    public function test_load_specific_language_french()\n    {\n        $frenchConfig = Dictionary::loadLanguageConfig('french');\n\n        $this->assertIsArray($frenchConfig);\n        $this->assertArrayHasKey('profanities', $frenchConfig);\n        $this->assertArrayHasKey('false_positives', $frenchConfig);\n        $this->assertArrayHasKey('substitutions', $frenchConfig);\n        $this->assertContains('merde', $frenchConfig['profanities']);\n        $this->assertContains('putain', $frenchConfig['profanities']);\n        $this->assertContains('connard', $frenchConfig['profanities']);\n        $this->assertContains('classe', $frenchConfig['false_positives']);\n        $this->assertContains('analyse', $frenchConfig['false_positives']);\n        $this->assertArrayHasKey('/à/', $frenchConfig['substitutions']);\n        $this->assertArrayHasKey('/é/', $frenchConfig['substitutions']);\n        $this->assertArrayHasKey('/ç/', $frenchConfig['substitutions']);\n    }\n\n    public function test_load_specific_language_german()\n    {\n        $germanConfig = Dictionary::loadLanguageConfig('german');\n\n        $this->assertIsArray($germanConfig);\n        $this->assertArrayHasKey('profanities', $germanConfig);\n        $this->assertArrayHasKey('false_positives', $germanConfig);\n        $this->assertArrayHasKey('substitutions', $germanConfig);\n        $this->assertContains('scheiße', $germanConfig['profanities']);\n        $this->assertContains('ficken', $germanConfig['profanities']);\n        $this->assertContains('arsch', $germanConfig['profanities']);\n        $this->assertContains('klasse', $germanConfig['false_positives']);\n        $this->assertContains('analyse', $germanConfig['false_positives']);\n        $this->assertArrayHasKey('/ä/', $germanConfig['substitutions']);\n        $this->assertArrayHasKey('/ö/', $germanConfig['substitutions']);\n        $this->assertArrayHasKey('/ü/', $germanConfig['substitutions']);\n        $this->assertArrayHasKey('/ß/', $germanConfig['substitutions']);\n    }\n\n    public function test_load_nonexistent_language()\n    {\n        $result = Dictionary::loadLanguageConfig('nonexistent');\n        $this->assertEmpty($result['profanities']);\n    }\n\n    public function test_normalizer_for_languages()\n    {\n        $this->assertInstanceOf(EnglishNormalizer::class, Dictionary::getNormalizerForLanguage('english'));\n        $this->assertInstanceOf(SpanishNormalizer::class, Dictionary::getNormalizerForLanguage('spanish'));\n        $this->assertInstanceOf(GermanNormalizer::class, Dictionary::getNormalizerForLanguage('german'));\n        $this->assertInstanceOf(FrenchNormalizer::class, Dictionary::getNormalizerForLanguage('french'));\n    }\n\n    public function test_language_substitutions_are_merged()\n    {\n        $dictionary = Dictionary::forLanguage('french');\n        $substitutions = $dictionary->getSubstitutions();\n\n        // Main config base patterns should be present\n        $this->assertArrayHasKey('/a/', $substitutions);\n        $this->assertArrayHasKey('/z/', $substitutions);\n\n        // Verify detection works with merged substitutions\n        $result = \\Blaspsoft\\Blasp\\Facades\\Blasp::french()->check('connard');\n        $this->assertTrue($result->isOffensive());\n    }\n}\n"
  },
  {
    "path": "tests/ConfigurationLoaderTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Core\\Dictionary;\nuse Illuminate\\Support\\Facades\\Cache;\n\nclass ConfigurationLoaderTest extends TestCase\n{\n    public function setUp(): void\n    {\n        parent::setUp();\n        $this->app['config']->set('cache.default', 'array');\n        Cache::flush();\n    }\n\n    public function test_for_language_returns_dictionary()\n    {\n        $dictionary = Dictionary::forLanguage('english');\n\n        $this->assertInstanceOf(Dictionary::class, $dictionary);\n        $this->assertIsArray($dictionary->getProfanities());\n        $this->assertIsArray($dictionary->getFalsePositives());\n    }\n\n    public function test_dictionary_has_profanity_expressions()\n    {\n        $dictionary = Dictionary::forLanguage('english');\n        $expressions = $dictionary->getProfanityExpressions();\n\n        $this->assertIsArray($expressions);\n        $this->assertNotEmpty($expressions);\n        $this->assertArrayHasKey('fuck', $expressions);\n        $this->assertArrayHasKey('shit', $expressions);\n    }\n\n    public function test_for_languages_returns_multi_language_dictionary()\n    {\n        $dictionary = Dictionary::forLanguages(['english', 'spanish']);\n\n        $profanities = $dictionary->getProfanities();\n        $this->assertContains('fuck', $profanities);\n        $this->assertContains('mierda', $profanities);\n    }\n\n    public function test_for_all_languages_returns_all_language_dictionary()\n    {\n        $dictionary = Dictionary::forAllLanguages();\n\n        $profanities = $dictionary->getProfanities();\n        $this->assertContains('fuck', $profanities);\n        $this->assertContains('mierda', $profanities);\n        $this->assertContains('merde', $profanities);\n        $this->assertContains('scheiße', $profanities);\n    }\n\n    public function test_allow_list_removes_words()\n    {\n        $dictionary = Dictionary::forLanguage('english', ['allow' => ['fuck']]);\n\n        $this->assertNotContains('fuck', $dictionary->getProfanities());\n        $this->assertContains('shit', $dictionary->getProfanities());\n    }\n\n    public function test_block_list_adds_words()\n    {\n        $dictionary = Dictionary::forLanguage('english', ['block' => ['customword']]);\n\n        $this->assertContains('customword', $dictionary->getProfanities());\n    }\n\n    public function test_severity_map_is_populated()\n    {\n        $dictionary = Dictionary::forLanguage('english');\n\n        $severity = $dictionary->getSeverity('fuck');\n        $this->assertNotNull($severity);\n    }\n\n    public function test_clear_cache()\n    {\n        Dictionary::clearCache();\n        $this->assertFalse(Cache::has('blasp_cache_keys'));\n    }\n\n    public function test_get_available_languages()\n    {\n        $languages = Dictionary::getAvailableLanguages();\n\n        $this->assertIsArray($languages);\n        $this->assertContains('english', $languages);\n        $this->assertContains('spanish', $languages);\n        $this->assertContains('french', $languages);\n        $this->assertContains('german', $languages);\n    }\n\n    public function test_load_language_config()\n    {\n        $config = Dictionary::loadLanguageConfig('english');\n\n        $this->assertIsArray($config);\n        $this->assertArrayHasKey('profanities', $config);\n        $this->assertContains('fuck', $config['profanities']);\n    }\n\n    public function test_load_nonexistent_language_config()\n    {\n        $config = Dictionary::loadLanguageConfig('nonexistent');\n\n        $this->assertIsArray($config);\n        $this->assertEmpty($config['profanities']);\n    }\n\n    public function test_normalizer_is_set()\n    {\n        $dictionary = Dictionary::forLanguage('english');\n\n        $this->assertNotNull($dictionary->getNormalizer());\n    }\n\n    public function test_separators_and_substitutions_loaded()\n    {\n        $dictionary = Dictionary::forLanguage('english');\n\n        $this->assertNotEmpty($dictionary->getSeparators());\n        $this->assertNotEmpty($dictionary->getSubstitutions());\n    }\n}\n"
  },
  {
    "path": "tests/CustomMaskCharacterTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Facades\\Blasp;\n\nclass CustomMaskCharacterTest extends TestCase\n{\n    public function test_default_mask_character_is_asterisk()\n    {\n        $result = Blasp::check('This is fucking awesome');\n        $this->assertEquals('This is ******* awesome', $result->clean());\n    }\n\n    public function test_custom_mask_character_with_hash()\n    {\n        $result = Blasp::mask('#')->check('This is fucking awesome');\n        $this->assertEquals('This is ####### awesome', $result->clean());\n    }\n\n    public function test_custom_mask_character_with_dash()\n    {\n        $result = Blasp::mask('-')->check('This shit is bad');\n        $this->assertEquals('This ---- is bad', $result->clean());\n    }\n\n    public function test_custom_mask_character_with_underscore()\n    {\n        $result = Blasp::mask('_')->check('What the hell');\n        $this->assertEquals('What the ____', $result->clean());\n    }\n\n    public function test_custom_mask_character_with_unicode()\n    {\n        $result = Blasp::mask('●')->check('This is damn good');\n        $this->assertEquals('This is ●●●● good', $result->clean());\n    }\n\n    public function test_custom_mask_character_only_uses_first_character()\n    {\n        $result = Blasp::mask('###')->check('This is fucking awesome');\n        $this->assertEquals('This is ####### awesome', $result->clean());\n    }\n\n    public function test_mask_character_can_be_chained_with_language()\n    {\n        $result = Blasp::spanish()->mask('@')->check('Esto es mierda');\n        $this->assertEquals('Esto es @@@@@@', $result->clean());\n    }\n\n    public function test_mask_character_works_with_multiple_profanities()\n    {\n        $result = Blasp::mask('!')->check('fuck this shit damn');\n        $this->assertEquals('!!!! this !!!! !!!!', $result->clean());\n        $this->assertEquals(3, $result->count());\n    }\n\n    public function test_mask_character_with_block_list()\n    {\n        $result = Blasp::mask('#')->block('test')->check('This is a test');\n        $this->assertEquals('This is a ####', $result->clean());\n    }\n\n    public function test_different_mask_characters_can_be_used_independently()\n    {\n        $resultHash = Blasp::mask('#')->check('This is shit');\n        $resultDash = Blasp::mask('-')->check('This is shit');\n\n        $this->assertEquals('This is ####', $resultHash->clean());\n        $this->assertEquals('This is ----', $resultDash->clean());\n    }\n}\n"
  },
  {
    "path": "tests/DetectionStrategyRegistryTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\BlaspManager;\nuse Blaspsoft\\Blasp\\Core\\Contracts\\DriverInterface;\nuse Blaspsoft\\Blasp\\Core\\Dictionary;\nuse Blaspsoft\\Blasp\\Core\\Contracts\\MaskStrategyInterface;\nuse Blaspsoft\\Blasp\\Core\\Result;\nuse Blaspsoft\\Blasp\\Drivers\\RegexDriver;\nuse Blaspsoft\\Blasp\\Drivers\\PatternDriver;\nuse InvalidArgumentException;\n\nclass DetectionStrategyRegistryTest extends TestCase\n{\n    private BlaspManager $manager;\n\n    public function setUp(): void\n    {\n        parent::setUp();\n        $this->manager = app('blasp');\n    }\n\n    public function test_default_driver_is_regex()\n    {\n        $this->assertEquals('regex', $this->manager->getDefaultDriver());\n    }\n\n    public function test_resolve_regex_driver()\n    {\n        $driver = $this->manager->resolveDriver('regex');\n        $this->assertInstanceOf(RegexDriver::class, $driver);\n    }\n\n    public function test_resolve_pattern_driver()\n    {\n        $driver = $this->manager->resolveDriver('pattern');\n        $this->assertInstanceOf(PatternDriver::class, $driver);\n    }\n\n    public function test_resolve_unknown_driver_throws_exception()\n    {\n        $this->expectException(InvalidArgumentException::class);\n        $this->manager->resolveDriver('unknown');\n    }\n\n    public function test_extend_registers_custom_driver()\n    {\n        $this->manager->extend('custom', function ($app) {\n            return new class implements DriverInterface {\n                public function detect(string $text, Dictionary $dictionary, MaskStrategyInterface $mask, array $options = []): Result\n                {\n                    return new Result($text, $text, [], 0);\n                }\n            };\n        });\n\n        $driver = $this->manager->resolveDriver('custom');\n        $this->assertInstanceOf(DriverInterface::class, $driver);\n    }\n\n    public function test_manager_check_returns_result()\n    {\n        $result = $this->manager->check('fuck this');\n        $this->assertInstanceOf(Result::class, $result);\n        $this->assertTrue($result->isOffensive());\n    }\n\n    public function test_manager_creates_pending_check()\n    {\n        $pending = $this->manager->newPendingCheck();\n        $this->assertInstanceOf(\\Blaspsoft\\Blasp\\PendingCheck::class, $pending);\n    }\n\n    public function test_driver_method_returns_pending_check()\n    {\n        $pending = $this->manager->driver('regex');\n        $this->assertInstanceOf(\\Blaspsoft\\Blasp\\PendingCheck::class, $pending);\n    }\n}\n"
  },
  {
    "path": "tests/EdgeCaseTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Facades\\Blasp;\n\nclass EdgeCaseTest extends TestCase\n{\n    public function test_fuckme_not_detected_across_word_boundaries()\n    {\n        $result = Blasp::allLanguages()->check('fuck merde scheiße mierda');\n\n        $this->assertTrue($result->hasProfanity());\n        $this->assertNotContains('fuckme', $result->getUniqueProfanitiesFound());\n\n        $found = $result->getUniqueProfanitiesFound();\n        $this->assertContains('fuck', $found);\n        $this->assertContains('merde', $found);\n        $this->assertContains('scheiße', $found);\n        $this->assertContains('mierda', $found);\n    }\n\n    public function test_removed_compound_profanities_not_detected()\n    {\n        $result = Blasp::check('fuck me hard');\n        $this->assertTrue($result->hasProfanity());\n        $this->assertNotContains('fuckme', $result->getUniqueProfanitiesFound());\n        $this->assertNotContains('fuckmehard', $result->getUniqueProfanitiesFound());\n        $this->assertNotContains('fuckher', $result->getUniqueProfanitiesFound());\n\n        $this->assertContains('fuck', $result->getUniqueProfanitiesFound());\n    }\n\n    public function test_legitimate_compound_profanities_still_work()\n    {\n        $result = Blasp::check('fuckyou you fuckhead');\n        $this->assertTrue($result->hasProfanity());\n        $this->assertContains('fuckyou', $result->getUniqueProfanitiesFound());\n        $this->assertContains('fuckhead', $result->getUniqueProfanitiesFound());\n    }\n}\n"
  },
  {
    "path": "tests/EmptyInputTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Facades\\Blasp;\n\nclass EmptyInputTest extends TestCase\n{\n    public function test_empty_string_returns_no_profanity()\n    {\n        $result = Blasp::check('');\n\n        $this->assertFalse($result->isOffensive());\n        $this->assertEquals(0, $result->count());\n        $this->assertEmpty($result->uniqueWords());\n    }\n\n    public function test_empty_string_returns_empty_source_and_clean_strings()\n    {\n        $result = Blasp::check('');\n\n        $this->assertEquals('', $result->original());\n        $this->assertEquals('', $result->clean());\n    }\n\n    public function test_null_returns_no_profanity()\n    {\n        $result = Blasp::check(null);\n\n        $this->assertFalse($result->isOffensive());\n        $this->assertEquals('', $result->original());\n        $this->assertEquals('', $result->clean());\n    }\n\n    public function test_profanity_still_detected_after_empty_check()\n    {\n        Blasp::check('');\n        $result = Blasp::check('shit');\n\n        $this->assertTrue($result->isOffensive());\n    }\n}\n"
  },
  {
    "path": "tests/FrenchStringNormalizerTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Core\\Normalizers\\FrenchNormalizer;\n\nclass FrenchStringNormalizerTest extends TestCase\n{\n    private FrenchNormalizer $normalizer;\n\n    public function setUp(): void\n    {\n        parent::setUp();\n        $this->normalizer = new FrenchNormalizer();\n    }\n\n    public function test_normalize_accented_vowels()\n    {\n        $this->assertEquals('ecole eleve', $this->normalizer->normalize('école élève'));\n        $this->assertEquals('cafe the', $this->normalizer->normalize('café thé'));\n        $this->assertEquals('hotel foret', $this->normalizer->normalize('hôtel forêt'));\n        $this->assertEquals('ou deja', $this->normalizer->normalize('où déjà'));\n        $this->assertEquals('naive Noel', $this->normalizer->normalize('naïve Noël'));\n    }\n\n    public function test_normalize_cedilla()\n    {\n        $this->assertEquals('francais garcon', $this->normalizer->normalize('français garçon'));\n        $this->assertEquals('ca commence', $this->normalizer->normalize('ça commence'));\n        $this->assertEquals('FRANCAIS', $this->normalizer->normalize('FRANÇAIS'));\n    }\n\n    public function test_normalize_ligatures()\n    {\n        $this->assertEquals('oeuvre coeur', $this->normalizer->normalize('œuvre cœur'));\n        $this->assertEquals('soeur boeuf', $this->normalizer->normalize('sœur bœuf'));\n        $this->assertEquals('OEUVRE', $this->normalizer->normalize('ŒUVRE'));\n    }\n\n    public function test_normalize_french_profanity_variants()\n    {\n        $this->assertEquals('merde', $this->normalizer->normalize('mèrde'));\n        $this->assertEquals('encule', $this->normalizer->normalize('enculé'));\n        $this->assertEquals('connard', $this->normalizer->normalize('cônnard'));\n        $this->assertEquals('putain', $this->normalizer->normalize('putàin'));\n        $this->assertEquals('salope', $this->normalizer->normalize('sâlope'));\n    }\n\n    public function test_normalize_circumflex_accent()\n    {\n        $this->assertEquals('hopital', $this->normalizer->normalize('hôpital'));\n        $this->assertEquals('tete', $this->normalizer->normalize('tête'));\n        $this->assertEquals('etre', $this->normalizer->normalize('être'));\n        $this->assertEquals('chateaux', $this->normalizer->normalize('châteaux'));\n        $this->assertEquals('cote', $this->normalizer->normalize('côte'));\n    }\n\n    public function test_normalize_grave_accent()\n    {\n        $this->assertEquals('tres', $this->normalizer->normalize('très'));\n        $this->assertEquals('apres', $this->normalizer->normalize('après'));\n        $this->assertEquals('des', $this->normalizer->normalize('dès'));\n        $this->assertEquals('premiere', $this->normalizer->normalize('première'));\n        $this->assertEquals('deuxieme', $this->normalizer->normalize('deuxième'));\n    }\n\n    public function test_normalize_acute_accent()\n    {\n        $this->assertEquals('ete', $this->normalizer->normalize('été'));\n        $this->assertEquals('ecole', $this->normalizer->normalize('école'));\n        $this->assertEquals('eleve', $this->normalizer->normalize('élève'));\n        $this->assertEquals('general', $this->normalizer->normalize('général'));\n        $this->assertEquals('celebre', $this->normalizer->normalize('célèbre'));\n    }\n\n    public function test_normalize_diaeresis()\n    {\n        $this->assertEquals('naive', $this->normalizer->normalize('naïve'));\n        $this->assertEquals('heroine', $this->normalizer->normalize('héroïne'));\n        $this->assertEquals('mais', $this->normalizer->normalize('maïs'));\n        $this->assertEquals('Noel', $this->normalizer->normalize('Noël'));\n        $this->assertEquals('Israel', $this->normalizer->normalize('Israël'));\n    }\n\n    public function test_normalize_mixed_case_preservation()\n    {\n        $this->assertEquals('MERDE', $this->normalizer->normalize('MÈRDE'));\n        $this->assertEquals('Putain', $this->normalizer->normalize('Putàin'));\n        $this->assertEquals('CoNNaRD', $this->normalizer->normalize('CôNNaRD'));\n        $this->assertEquals('sAlOPe', $this->normalizer->normalize('sÂlOPe'));\n    }\n\n    public function test_normalize_preserves_non_french_characters()\n    {\n        $this->assertEquals('hello world 123', $this->normalizer->normalize('hello world 123'));\n        $this->assertEquals('test@email.com', $this->normalizer->normalize('test@email.com'));\n        $this->assertEquals('user_name-123', $this->normalizer->normalize('user_name-123'));\n    }\n\n    public function test_normalize_empty_and_special_strings()\n    {\n        $this->assertEquals('', $this->normalizer->normalize(''));\n        $this->assertEquals('   ', $this->normalizer->normalize('   '));\n        $this->assertEquals('eeee', $this->normalizer->normalize('éèêë'));\n        $this->assertEquals('aaaa', $this->normalizer->normalize('àâäá'));\n    }\n\n    public function test_normalize_complex_french_text()\n    {\n        $input = \"L'école française où les élèves étudient l'œuvre de Molière\";\n        $expected = \"L'ecole francaise ou les eleves etudient l'oeuvre de Moliere\";\n        $this->assertEquals($expected, $this->normalizer->normalize($input));\n    }\n\n    public function test_normalize_all_french_accents()\n    {\n        $accents = [\n            'à' => 'a', 'â' => 'a', 'ä' => 'a', 'á' => 'a',\n            'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e',\n            'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i',\n            'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'ö' => 'o',\n            'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u',\n            'ý' => 'y', 'ÿ' => 'y',\n            'ç' => 'c',\n            'œ' => 'oe', 'æ' => 'ae',\n            'À' => 'A', 'Â' => 'A', 'Ä' => 'A', 'Á' => 'A',\n            'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E',\n            'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I',\n            'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Ö' => 'O',\n            'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U',\n            'Ý' => 'Y', 'Ÿ' => 'Y',\n            'Ç' => 'C',\n            'Œ' => 'OE', 'Æ' => 'AE'\n        ];\n\n        foreach ($accents as $accented => $normalized) {\n            $this->assertEquals(\n                $normalized,\n                $this->normalizer->normalize($accented),\n                \"Failed to normalize '$accented' to '$normalized'\"\n            );\n        }\n    }\n\n    public function test_normalize_numbers_and_special_chars()\n    {\n        $this->assertEquals('123abc', $this->normalizer->normalize('123abc'));\n        $this->assertEquals('test!@#$%', $this->normalizer->normalize('test!@#$%'));\n        $this->assertEquals('hello_world-2024', $this->normalizer->normalize('hello_world-2024'));\n    }\n\n    public function test_normalize_french_profanities_from_config()\n    {\n        $config = require __DIR__ . '/../config/languages/french.php';\n        $profanities = array_slice($config['profanities'], 0, 20);\n\n        foreach ($profanities as $profanity) {\n            $normalized = $this->normalizer->normalize($profanity);\n            $this->assertDoesNotMatchRegularExpression(\n                '/[àâäáèéêëìíîïòóôöùúûüýÿçœæÀÂÄÁÈÉÊËÌÍÎÏÒÓÔÖÙÚÛÜÝŸÇŒÆ]/',\n                $normalized,\n                \"French profanity '$profanity' still contains accents after normalization: '$normalized'\"\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "tests/GermanStringNormalizerTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Core\\Normalizers\\GermanNormalizer;\n\nclass GermanStringNormalizerTest extends TestCase\n{\n    private GermanNormalizer $normalizer;\n\n    public function setUp(): void\n    {\n        parent::setUp();\n        $this->normalizer = new GermanNormalizer();\n    }\n\n    public function test_normalize_umlauts()\n    {\n        $this->assertEquals('maedchen', $this->normalizer->normalize('mädchen'));\n        $this->assertEquals('shoene', $this->normalizer->normalize('schöne'));\n        $this->assertEquals('gruessen', $this->normalizer->normalize('grüssen'));\n        $this->assertEquals('MAEDCHEN', $this->normalizer->normalize('MÄDCHEN'));\n        $this->assertEquals('SHOENE', $this->normalizer->normalize('SCHÖNE'));\n        $this->assertEquals('GRUESSEN', $this->normalizer->normalize('GRÜSSEN'));\n    }\n\n    public function test_normalize_eszett()\n    {\n        $this->assertEquals('weiss', $this->normalizer->normalize('weiß'));\n        $this->assertEquals('strasse', $this->normalizer->normalize('straße'));\n        $this->assertEquals('gross', $this->normalizer->normalize('groß'));\n        $this->assertEquals('heiss', $this->normalizer->normalize('heiß'));\n    }\n\n    public function test_normalize_sch_combinations()\n    {\n        $this->assertEquals('shule', $this->normalizer->normalize('schule'));\n        $this->assertEquals('mensh', $this->normalizer->normalize('mensch'));\n        $this->assertEquals('SHULE', $this->normalizer->normalize('SCHULE'));\n    }\n\n    public function test_normalize_german_profanity_variants()\n    {\n        $this->assertEquals('sheisse', $this->normalizer->normalize('scheiße'));\n        $this->assertEquals('arsh', $this->normalizer->normalize('arsch'));\n        $this->assertEquals('ficken', $this->normalizer->normalize('ficken'));\n        $this->assertEquals('maedchen', $this->normalizer->normalize('mädchen'));\n    }\n\n    public function test_normalize_preserves_non_german_characters()\n    {\n        $this->assertEquals('hello world 123', $this->normalizer->normalize('hello world 123'));\n        $this->assertEquals('test@email.com', $this->normalizer->normalize('test@email.com'));\n    }\n\n    public function test_normalize_mixed_case_preservation()\n    {\n        $this->assertEquals('MAEDCHEN', $this->normalizer->normalize('MÄDCHEN'));\n        $this->assertEquals('Shoene', $this->normalizer->normalize('Schöne'));\n        $this->assertEquals('gRUEssen', $this->normalizer->normalize('gRÜßen'));\n    }\n\n    public function test_normalize_empty_and_special_strings()\n    {\n        $this->assertEquals('', $this->normalizer->normalize(''));\n        $this->assertEquals('   ', $this->normalizer->normalize('   '));\n        $this->assertEquals('aeaeaeaeaeae', $this->normalizer->normalize('ääääää'));\n    }\n}\n"
  },
  {
    "path": "tests/Issue24Test.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Facades\\Blasp;\n\nclass Issue24Test extends TestCase\n{\n    public function test_etre_not_flagged_as_profanity()\n    {\n        $result = Blasp::check('Le cadre pourrait être un peu mieux');\n        $this->assertFalse($result->isOffensive(), 'être should not be flagged. Found: ' . implode(', ', $result->uniqueWords()));\n    }\n\n    public function test_are_accent_not_flagged()\n    {\n        $result = Blasp::check('aré');\n        $this->assertFalse($result->isOffensive(), 'aré should not be flagged. Found: ' . implode(', ', $result->uniqueWords()));\n    }\n\n    public function test_tete_not_flagged()\n    {\n        $result = Blasp::check('tête tete');\n        $this->assertFalse($result->isOffensive(), 'tête should not be flagged. Found: ' . implode(', ', $result->uniqueWords()));\n    }\n\n    public function test_actual_profanity_still_detected()\n    {\n        $result = Blasp::check('shit');\n        $this->assertTrue($result->isOffensive(), 'Actual profanity should still be detected after unicode fix');\n    }\n}\n"
  },
  {
    "path": "tests/Issue32FalsePositiveTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Blaspsoft\\Blasp\\Facades\\Blasp;\n\nclass Issue32FalsePositiveTest extends TestCase\n{\n    #[DataProvider('legitimateWordsProvider')]\n    public function test_legitimate_words_not_flagged(string $word)\n    {\n        $result = Blasp::check($word);\n        $this->assertFalse(\n            $result->hasProfanity(),\n            \"\\\"$word\\\" should not be flagged as profanity but got: \" . implode(', ', $result->getUniqueProfanitiesFound())\n        );\n    }\n\n    public static function legitimateWordsProvider(): array\n    {\n        return [\n            'assignment' => ['assignment'],\n            'passion' => ['passion'],\n            'classroom' => ['classroom'],\n            'passenger' => ['passenger'],\n            'assassin' => ['assassin'],\n            'massive' => ['massive'],\n            'embassy' => ['embassy'],\n            'harassment' => ['harassment'],\n            'compassion' => ['compassion'],\n            'association' => ['association'],\n        ];\n    }\n\n    public function test_actual_profanity_still_detected()\n    {\n        $result = Blasp::check('ass');\n        $this->assertTrue($result->hasProfanity());\n    }\n}\n"
  },
  {
    "path": "tests/MiddlewareAliasTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Middleware\\CheckProfanity;\n\nclass MiddlewareAliasTest extends TestCase\n{\n    public function test_blasp_alias_resolves_to_check_profanity_middleware()\n    {\n        $router = $this->app['router'];\n\n        $aliases = $router->getMiddleware();\n\n        $this->assertArrayHasKey('blasp', $aliases);\n        $this->assertSame(CheckProfanity::class, $aliases['blasp']);\n    }\n}\n"
  },
  {
    "path": "tests/MultiLanguageDetectionConfigTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Core\\Dictionary;\nuse Blaspsoft\\Blasp\\Enums\\Severity;\n\nclass MultiLanguageDetectionConfigTest extends TestCase\n{\n    public function test_for_language_sets_language()\n    {\n        $dictionary = Dictionary::forLanguage('spanish');\n        $this->assertEquals('spanish', $dictionary->getLanguage());\n    }\n\n    public function test_for_languages_merges_profanities()\n    {\n        $dictionary = Dictionary::forLanguages(['english', 'spanish']);\n\n        $profanities = $dictionary->getProfanities();\n        $this->assertContains('fuck', $profanities);\n        $this->assertContains('mierda', $profanities);\n    }\n\n    public function test_for_all_languages_includes_all()\n    {\n        $dictionary = Dictionary::forAllLanguages();\n\n        $profanities = $dictionary->getProfanities();\n        $this->assertContains('fuck', $profanities);\n        $this->assertContains('mierda', $profanities);\n        $this->assertContains('merde', $profanities);\n        $this->assertContains('scheiße', $profanities);\n    }\n\n    public function test_profanity_expressions_generated()\n    {\n        $dictionary = Dictionary::forLanguage('english');\n        $expressions = $dictionary->getProfanityExpressions();\n\n        $this->assertIsArray($expressions);\n        $this->assertNotEmpty($expressions);\n        $this->assertArrayHasKey('fuck', $expressions);\n    }\n\n    public function test_severity_map_populated()\n    {\n        $dictionary = Dictionary::forLanguage('english');\n\n        $severity = $dictionary->getSeverity('fuck');\n        $this->assertInstanceOf(Severity::class, $severity);\n    }\n\n    public function test_false_positives_loaded()\n    {\n        $dictionary = Dictionary::forLanguage('english');\n        $falsePositives = $dictionary->getFalsePositives();\n\n        $this->assertIsArray($falsePositives);\n        $this->assertContains('class', $falsePositives);\n        $this->assertContains('pass', $falsePositives);\n    }\n\n    public function test_allow_list_removes_profanities()\n    {\n        $dictionary = Dictionary::forLanguage('english', ['allow' => ['fuck']]);\n\n        $this->assertNotContains('fuck', $dictionary->getProfanities());\n    }\n\n    public function test_block_list_adds_profanities()\n    {\n        $dictionary = Dictionary::forLanguage('english', ['block' => ['customword']]);\n\n        $this->assertContains('customword', $dictionary->getProfanities());\n    }\n\n    public function test_block_list_gets_severity()\n    {\n        $dictionary = Dictionary::forLanguage('english', ['block' => ['customword']]);\n\n        $severity = $dictionary->getSeverity('customword');\n        $this->assertEquals(Severity::High, $severity);\n    }\n\n    public function test_separators_and_substitutions_present()\n    {\n        $dictionary = Dictionary::forLanguage('english');\n\n        $this->assertNotEmpty($dictionary->getSeparators());\n        $this->assertNotEmpty($dictionary->getSubstitutions());\n    }\n}\n"
  },
  {
    "path": "tests/MultiLanguageProfanityTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Facades\\Blasp;\nuse Blaspsoft\\Blasp\\Core\\Dictionary;\n\nclass MultiLanguageProfanityTest extends TestCase\n{\n    public function test_english_profanities()\n    {\n        $testCases = [\n            'fuck' => 'This fuck word',\n            'shit' => 'This shit happens',\n            'ass' => 'What an ass',\n            'bitch' => 'Stop being a bitch',\n            'damn' => 'Damn it all',\n        ];\n\n        foreach ($testCases as $profanity => $text) {\n            $result = Blasp::english()->check($text);\n            $this->assertTrue($result->isOffensive(), \"Failed to detect: $profanity\");\n        }\n    }\n\n    public function test_spanish_profanities()\n    {\n        $testCases = [\n            'mierda' => 'Esta es una mierda',\n            'joder' => 'No quiero joder',\n            'cabron' => 'Eres un cabron',\n            'puta' => 'La puta madre',\n        ];\n\n        foreach ($testCases as $profanity => $text) {\n            $result = Blasp::spanish()->check($text);\n            $this->assertTrue($result->isOffensive(), \"Failed to detect Spanish: $profanity\");\n        }\n    }\n\n    public function test_german_profanities()\n    {\n        $testCases = [\n            'scheisse' => 'Das ist scheisse',\n            'scheisse' => 'Das ist scheisse',\n            'arsch' => 'Du bist ein arsch',\n            'ficken' => 'Ich will ficken',\n            'verdammt' => 'Verdammt noch mal',\n        ];\n\n        foreach ($testCases as $profanity => $text) {\n            $result = Blasp::german()->check($text);\n            $this->assertTrue($result->isOffensive(), \"Failed to detect German: $profanity\");\n        }\n    }\n\n    public function test_french_profanities()\n    {\n        $testCases = [\n            'merde' => \"C'est de la merde\",\n            'putain' => 'Putain de merde',\n            'connard' => 'Quel connard',\n            'salope' => 'Une vraie salope',\n        ];\n\n        foreach ($testCases as $profanity => $text) {\n            $result = Blasp::french()->check($text);\n            $this->assertTrue($result->isOffensive(), \"Failed to detect French: $profanity\");\n        }\n    }\n\n    public function test_profanity_variations()\n    {\n        $testCases = [\n            'f-u-c-k' => 'obscuring with dashes',\n            'ffuucckk' => 'character doubling',\n            's.h.i.t' => 'obscuring with dots',\n            '@ss' => 'substitution',\n        ];\n\n        foreach ($testCases as $variation => $description) {\n            $result = Blasp::check(\"This has $variation in it\");\n            $this->assertTrue(\n                $result->isOffensive(),\n                \"Failed to detect variation ($description): $variation\"\n            );\n        }\n    }\n\n    public function test_case_insensitivity()\n    {\n        $testCases = [\n            'english' => ['FUCK', 'FuCk', 'fUcK'],\n            'spanish' => ['MIERDA', 'MiErDa', 'mIeRdA'],\n            'german' => ['SCHEISSE', 'ScHeIsSe', 'schEISSE'],\n            'french' => ['MERDE', 'MeRdE', 'mErDe'],\n        ];\n\n        foreach ($testCases as $language => $variations) {\n            foreach ($variations as $variation) {\n                $result = Blasp::in($language)->check(\"Word: $variation here\");\n                $this->assertTrue(\n                    $result->isOffensive(),\n                    \"Failed to detect $language case variation: $variation\"\n                );\n            }\n        }\n    }\n\n    public function test_false_positives_not_flagged()\n    {\n        $safeFalsePositives = ['class', 'pass', 'hello'];\n\n        foreach ($safeFalsePositives as $word) {\n            $result = Blasp::check(\"This contains $word word\");\n            $this->assertFalse(\n                $result->isOffensive(),\n                \"False positive incorrectly detected: $word\"\n            );\n        }\n    }\n\n    public function test_comprehensive_language_coverage()\n    {\n        $languages = ['english', 'spanish', 'german', 'french'];\n\n        foreach ($languages as $language) {\n            $config = Dictionary::loadLanguageConfig($language);\n            $profanities = $config['profanities'] ?? [];\n            $totalProfanities = count($profanities);\n            $detected = 0;\n            $failed = [];\n\n            foreach ($profanities as $profanity) {\n                $result = Blasp::in($language)->check($profanity);\n                if ($result->isOffensive()) {\n                    $detected++;\n                } else {\n                    $failed[] = $profanity;\n                }\n            }\n\n            $detectionRate = ($totalProfanities > 0) ? ($detected / $totalProfanities) * 100 : 0;\n\n            $this->assertGreaterThanOrEqual(\n                90,\n                $detectionRate,\n                sprintf(\n                    \"%s: Detection rate %.2f%% (detected %d/%d). Failed: %s\",\n                    ucfirst($language),\n                    $detectionRate,\n                    $detected,\n                    $totalProfanities,\n                    implode(', ', array_slice($failed, 0, 5))\n                )\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "tests/PhoneticDriverTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Core\\Matchers\\PhoneticMatcher;\nuse Blaspsoft\\Blasp\\Enums\\Severity;\nuse Blaspsoft\\Blasp\\Facades\\Blasp;\n\nclass PhoneticDriverTest extends TestCase\n{\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        // Ensure phonetic driver config is set\n        $this->app['config']->set('blasp.drivers.phonetic', [\n            'phonemes' => 4,\n            'min_word_length' => 3,\n            'max_distance_ratio' => 0.6,\n            'supported_languages' => ['english'],\n            'false_positives' => [\n                'fork', 'forked', 'forking',\n                'beach', 'beaches',\n                'witch', 'witches',\n                'sheet', 'sheets',\n                'deck', 'decks',\n                'count', 'counts', 'counter', 'county',\n                'ship', 'shipped', 'shipping',\n                'duck', 'ducked', 'ducking',\n                'fudge', 'fudging',\n                'buck', 'bucks',\n                'puck', 'pucks',\n                'bass',\n                'mass',\n                'pass', 'passed',\n                'heck',\n                'shoot', 'shot',\n                'what', 'white', 'while', 'whole',\n            ],\n        ]);\n    }\n\n    // -------------------------------------------------------\n    // PhoneticMatcher unit tests\n    // -------------------------------------------------------\n\n    public function test_matcher_exact_profanity_match()\n    {\n        $matcher = new PhoneticMatcher(['fuck', 'shit', 'ass']);\n\n        $this->assertSame('fuck', $matcher->match('fuck'));\n        $this->assertSame('shit', $matcher->match('shit'));\n    }\n\n    public function test_matcher_phonetic_variant_detection()\n    {\n        $matcher = new PhoneticMatcher(['fuck', 'shit']);\n\n        $this->assertSame('fuck', $matcher->match('phuck'));\n        $this->assertSame('fuck', $matcher->match('fuk'));\n        $this->assertSame('shit', $matcher->match('sheit'));\n    }\n\n    public function test_matcher_short_word_skipping()\n    {\n        $matcher = new PhoneticMatcher(['fuck', 'shit'], minWordLength: 3);\n\n        $this->assertNull($matcher->match('fu'));\n        $this->assertNull($matcher->match('sh'));\n    }\n\n    public function test_matcher_phonetic_false_positive_respected()\n    {\n        $matcher = new PhoneticMatcher(\n            ['fuck'],\n            phoneticFalsePositives: ['fork'],\n        );\n\n        $this->assertNull($matcher->match('fork'));\n    }\n\n    public function test_matcher_high_levenshtein_distance_rejection()\n    {\n        $matcher = new PhoneticMatcher(\n            ['fuck'],\n            maxDistanceRatio: 0.3,\n        );\n\n        // \"phucking\" has high edit distance from \"fuck\" with strict ratio\n        $this->assertNull($matcher->match('phucking'));\n    }\n\n    // -------------------------------------------------------\n    // PhoneticDriver integration tests\n    // -------------------------------------------------------\n\n    public function test_resolves_from_manager()\n    {\n        $result = Blasp::driver('phonetic')->check('hello world');\n\n        $this->assertTrue($result->isClean());\n    }\n\n    public function test_detects_standard_profanity()\n    {\n        $result = Blasp::driver('phonetic')->check('What the fuck');\n\n        $this->assertTrue($result->isOffensive());\n        $this->assertSame(1, $result->count());\n    }\n\n    public function test_detects_phonetic_evasion()\n    {\n        $result = Blasp::driver('phonetic')->check('This is phucking awful');\n\n        $this->assertTrue($result->isOffensive());\n        // Base word may be \"fucking\", \"phuking\", etc. depending on dictionary\n        $matched = false;\n        foreach ($result->uniqueWords() as $word) {\n            if (str_contains($word, 'fuck') || str_contains($word, 'phuk')) {\n                $matched = true;\n                break;\n            }\n        }\n        $this->assertTrue($matched, 'Expected a fuck/phuk variant in uniqueWords: ' . implode(', ', $result->uniqueWords()));\n    }\n\n    public function test_returns_correct_clean_text_with_masking()\n    {\n        $result = Blasp::driver('phonetic')->check('What the fuck');\n\n        $this->assertTrue($result->isOffensive());\n        $this->assertSame('What the ****', $result->clean());\n    }\n\n    public function test_handles_empty_text()\n    {\n        $result = Blasp::driver('phonetic')->check('');\n\n        $this->assertTrue($result->isClean());\n        $this->assertSame('', $result->clean());\n        $this->assertSame(0, $result->count());\n    }\n\n    public function test_respects_severity_filter()\n    {\n        $result = Blasp::driver('phonetic')\n            ->withSeverity(Severity::Extreme)\n            ->check('What the fuck');\n\n        // \"fuck\" is typically High severity, not Extreme, so should be filtered out\n        $this->assertTrue($result->isClean());\n    }\n\n    public function test_respects_dictionary_false_positives()\n    {\n        $result = Blasp::driver('phonetic')->check('I live in scunthorpe');\n\n        $this->assertTrue($result->isClean());\n    }\n\n    public function test_multiple_profanities_in_one_text()\n    {\n        $result = Blasp::driver('phonetic')->check('fuck this shit');\n\n        $this->assertTrue($result->isOffensive());\n        $this->assertGreaterThanOrEqual(2, $result->count());\n    }\n\n    public function test_unsupported_language_returns_clean_result()\n    {\n        $result = Blasp::driver('phonetic')\n            ->in('spanish')\n            ->check('mierda');\n\n        $this->assertTrue($result->isClean());\n    }\n\n    // -------------------------------------------------------\n    // False positive regression tests\n    // -------------------------------------------------------\n\n    public function test_fork_is_not_flagged()\n    {\n        $result = Blasp::driver('phonetic')->check('Use a fork to eat');\n\n        $this->assertTrue($result->isClean());\n    }\n\n    public function test_beach_is_not_flagged()\n    {\n        $result = Blasp::driver('phonetic')->check('Let us go to the beach');\n\n        $this->assertTrue($result->isClean());\n    }\n\n    public function test_sheet_is_not_flagged()\n    {\n        $result = Blasp::driver('phonetic')->check('Print the sheet');\n\n        $this->assertTrue($result->isClean());\n    }\n\n    public function test_duck_is_not_flagged()\n    {\n        $result = Blasp::driver('phonetic')->check('Look at that duck');\n\n        $this->assertTrue($result->isClean());\n    }\n\n    public function test_count_is_not_flagged()\n    {\n        $result = Blasp::driver('phonetic')->check('Count the items');\n\n        $this->assertTrue($result->isClean());\n    }\n}\n"
  },
  {
    "path": "tests/PipelineDriverTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Drivers\\PipelineDriver;\nuse Blaspsoft\\Blasp\\Enums\\Severity;\nuse Blaspsoft\\Blasp\\Facades\\Blasp;\n\nclass PipelineDriverTest extends TestCase\n{\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        $this->app['config']->set('blasp.drivers.pipeline', [\n            'drivers' => ['regex', 'phonetic'],\n        ]);\n\n        $this->app['config']->set('blasp.drivers.phonetic', [\n            'phonemes' => 4,\n            'min_word_length' => 3,\n            'max_distance_ratio' => 0.6,\n            'supported_languages' => ['english'],\n            'false_positives' => [\n                'fork', 'forked', 'forking',\n                'beach', 'beaches',\n                'witch', 'witches',\n                'sheet', 'sheets',\n                'deck', 'decks',\n                'count', 'counts', 'counter', 'county',\n                'ship', 'shipped', 'shipping',\n                'duck', 'ducked', 'ducking',\n                'fudge', 'fudging',\n                'buck', 'bucks',\n                'puck', 'pucks',\n                'bass',\n                'mass',\n                'pass', 'passed',\n                'heck',\n                'shoot', 'shot',\n                'what', 'white', 'while', 'whole',\n            ],\n        ]);\n    }\n\n    // -------------------------------------------------------\n    // Resolution\n    // -------------------------------------------------------\n\n    public function test_resolves_from_manager_via_driver_name()\n    {\n        $result = Blasp::driver('pipeline')->check('hello world');\n\n        $this->assertTrue($result->isClean());\n    }\n\n    public function test_ad_hoc_pipeline_via_facade()\n    {\n        $result = Blasp::pipeline('regex', 'phonetic')->check('hello world');\n\n        $this->assertTrue($result->isClean());\n    }\n\n    // -------------------------------------------------------\n    // Union merge — catches matches from different drivers\n    // -------------------------------------------------------\n\n    public function test_catches_obfuscated_text_via_regex()\n    {\n        $result = Blasp::driver('pipeline')->check('This is f-u-c-k-i-n-g awful');\n\n        $this->assertTrue($result->isOffensive());\n        $this->assertGreaterThanOrEqual(1, $result->count());\n    }\n\n    public function test_catches_phonetic_evasion()\n    {\n        $result = Blasp::pipeline('regex', 'phonetic')->check('This is phucking awful');\n\n        $this->assertTrue($result->isOffensive());\n    }\n\n    public function test_catches_exact_match_via_pattern()\n    {\n        $result = Blasp::pipeline('pattern', 'phonetic')->check('What the fuck');\n\n        $this->assertTrue($result->isOffensive());\n    }\n\n    // -------------------------------------------------------\n    // Deduplication\n    // -------------------------------------------------------\n\n    public function test_same_word_at_same_position_only_counted_once()\n    {\n        // \"fuck\" will be detected by both regex and phonetic at the same position\n        $result = Blasp::pipeline('regex', 'phonetic')->check('What the fuck');\n\n        $this->assertTrue($result->isOffensive());\n        // Both drivers detect \"fuck\" at the same position — should be deduplicated to 1\n        $this->assertSame(1, $result->count());\n    }\n\n    // -------------------------------------------------------\n    // Clean text\n    // -------------------------------------------------------\n\n    public function test_clean_text_masks_applied_correctly()\n    {\n        $result = Blasp::driver('pipeline')->check('What the fuck');\n\n        $this->assertTrue($result->isOffensive());\n        $this->assertStringNotContainsString('fuck', $result->clean());\n        $this->assertStringContainsString('What the', $result->clean());\n    }\n\n    public function test_clean_text_with_multiple_matches()\n    {\n        $result = Blasp::driver('pipeline')->check('fuck this shit');\n\n        $this->assertTrue($result->isOffensive());\n        $this->assertStringNotContainsString('fuck', $result->clean());\n        $this->assertStringNotContainsString('shit', $result->clean());\n    }\n\n    // -------------------------------------------------------\n    // Score\n    // -------------------------------------------------------\n\n    public function test_score_recalculated_from_merged_matches()\n    {\n        $result = Blasp::driver('pipeline')->check('fuck this shit');\n\n        $this->assertTrue($result->isOffensive());\n        $this->assertGreaterThan(0, $result->score());\n    }\n\n    // -------------------------------------------------------\n    // Empty text\n    // -------------------------------------------------------\n\n    public function test_empty_text_returns_clean_result()\n    {\n        $result = Blasp::driver('pipeline')->check('');\n\n        $this->assertTrue($result->isClean());\n        $this->assertSame('', $result->clean());\n        $this->assertSame(0, $result->count());\n        $this->assertSame(0, $result->score());\n    }\n\n    // -------------------------------------------------------\n    // Single-driver pipeline\n    // -------------------------------------------------------\n\n    public function test_single_driver_pipeline_matches_standalone()\n    {\n        $standalone = Blasp::driver('regex')->check('This is a fucking sentence');\n        $pipeline = Blasp::pipeline('regex')->check('This is a fucking sentence');\n\n        $this->assertSame($standalone->isOffensive(), $pipeline->isOffensive());\n        $this->assertSame($standalone->count(), $pipeline->count());\n        $this->assertSame($standalone->clean(), $pipeline->clean());\n    }\n\n    // -------------------------------------------------------\n    // Severity filter\n    // -------------------------------------------------------\n\n    public function test_severity_filter_applies_across_merged_result()\n    {\n        $result = Blasp::driver('pipeline')\n            ->withSeverity(Severity::Extreme)\n            ->check('What the fuck');\n\n        // \"fuck\" is High severity, not Extreme — should be filtered out by sub-drivers\n        $this->assertTrue($result->isClean());\n    }\n\n    // -------------------------------------------------------\n    // Language selection\n    // -------------------------------------------------------\n\n    public function test_works_with_language_selection()\n    {\n        $result = Blasp::pipeline('regex', 'phonetic')\n            ->in('english')\n            ->check('What the fuck');\n\n        $this->assertTrue($result->isOffensive());\n    }\n\n    // -------------------------------------------------------\n    // Mask strategies\n    // -------------------------------------------------------\n\n    public function test_works_with_custom_mask_character()\n    {\n        $result = Blasp::pipeline('regex', 'phonetic')\n            ->mask('#')\n            ->check('What the fuck');\n\n        $this->assertTrue($result->isOffensive());\n        $this->assertStringContainsString('####', $result->clean());\n        $this->assertStringNotContainsString('fuck', $result->clean());\n    }\n\n    // -------------------------------------------------------\n    // Allow / block lists\n    // -------------------------------------------------------\n\n    public function test_works_with_allow_list()\n    {\n        // Use regex-only pipeline to avoid phonetic matching variants\n        $result = Blasp::pipeline('regex')\n            ->allow('fuck')\n            ->check('What the fuck');\n\n        $this->assertTrue($result->isClean());\n    }\n\n    public function test_works_with_block_list()\n    {\n        $result = Blasp::pipeline('regex', 'phonetic')\n            ->block('banana')\n            ->check('I like banana');\n\n        $this->assertTrue($result->isOffensive());\n    }\n\n    // -------------------------------------------------------\n    // Original text preserved\n    // -------------------------------------------------------\n\n    public function test_original_text_preserved()\n    {\n        $text = 'What the fuck';\n        $result = Blasp::driver('pipeline')->check($text);\n\n        $this->assertSame($text, $result->original());\n    }\n\n    // -------------------------------------------------------\n    // Pipeline with pattern + phonetic\n    // -------------------------------------------------------\n\n    public function test_pipeline_with_pattern_and_phonetic()\n    {\n        $result = Blasp::pipeline('pattern', 'phonetic')\n            ->in('english')\n            ->mask('#')\n            ->check('fuck this phucking thing');\n\n        $this->assertTrue($result->isOffensive());\n        $this->assertGreaterThanOrEqual(2, $result->count());\n    }\n\n    // -------------------------------------------------------\n    // Clean text on clean input\n    // -------------------------------------------------------\n\n    public function test_clean_input_returns_unchanged()\n    {\n        $text = 'Hello world this is fine';\n        $result = Blasp::driver('pipeline')->check($text);\n\n        $this->assertTrue($result->isClean());\n        $this->assertSame($text, $result->clean());\n    }\n}\n"
  },
  {
    "path": "tests/ProfanityExpressionGeneratorTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Core\\Matchers\\RegexMatcher;\n\nclass ProfanityExpressionGeneratorTest extends TestCase\n{\n    private RegexMatcher $matcher;\n\n    public function setUp(): void\n    {\n        parent::setUp();\n        $this->matcher = new RegexMatcher();\n    }\n\n    public function test_generate_separator_expression()\n    {\n        $separators = ['-', '_', '.', ' '];\n        $result = $this->matcher->generateSeparatorExpression($separators);\n\n        $this->assertIsString($result);\n    }\n\n    public function test_generate_substitution_expressions()\n    {\n        $substitutions = [\n            '/a/' => ['a', '@', '4'],\n            '/e/' => ['e', '3'],\n            '/o/' => ['o', '0']\n        ];\n\n        $result = $this->matcher->generateSubstitutionExpressions($substitutions);\n\n        $this->assertIsArray($result);\n        $this->assertArrayHasKey('/a/', $result);\n        $this->assertArrayHasKey('/e/', $result);\n        $this->assertArrayHasKey('/o/', $result);\n    }\n\n    public function test_generate_profanity_expression_simple()\n    {\n        $profanity = 'test';\n        $substitutionExpressions = [\n            '/t/' => '[t\\+]+{!!}',\n            '/e/' => '[e3]+{!!}',\n            '/s/' => '[s$]+{!!}'\n        ];\n        $separatorExpression = '[\\-\\s]*?';\n\n        $result = $this->matcher->generateProfanityExpression(\n            $profanity,\n            $substitutionExpressions,\n            $separatorExpression\n        );\n\n        $this->assertIsString($result);\n        $this->assertStringStartsWith('/', $result);\n        $this->assertStringEndsWith('/iu', $result);\n    }\n\n    public function test_generate_expressions_full_flow()\n    {\n        $profanities = ['fuck', 'shit'];\n        $separators = ['-', '_', '.'];\n        $substitutions = [\n            '/f/' => ['f', 'ƒ'],\n            '/u/' => ['u', 'υ', 'µ'],\n            '/c/' => ['c', 'ç', '¢'],\n            '/s/' => ['s', '5', '$'],\n            '/h/' => ['h'],\n            '/i/' => ['i', '!', '|'],\n            '/t/' => ['t']\n        ];\n\n        $result = $this->matcher->generateExpressions($profanities, $separators, $substitutions);\n\n        $this->assertIsArray($result);\n        $this->assertArrayHasKey('fuck', $result);\n        $this->assertArrayHasKey('shit', $result);\n\n        foreach ($result as $profanity => $expression) {\n            $this->assertIsString($expression);\n            $this->assertStringStartsWith('/', $expression);\n            $this->assertStringEndsWith('/iu', $expression);\n\n            $testResult = @preg_match($expression, $profanity);\n            $this->assertNotFalse($testResult, \"Invalid regex generated for '$profanity': $expression\");\n        }\n    }\n\n    public function test_generated_expressions_match_profanities()\n    {\n        $profanities = ['fuck'];\n        $separators = ['-', '_'];\n        $substitutions = [\n            '/f/' => ['f', 'ƒ'],\n            '/u/' => ['u', 'υ', 'µ'],\n            '/c/' => ['c', 'ç', '¢'],\n            '/k/' => ['k']\n        ];\n\n        $expressions = $this->matcher->generateExpressions($profanities, $separators, $substitutions);\n        $expression = $expressions['fuck'];\n\n        $this->assertEquals(1, preg_match($expression, 'fuck'));\n        $this->assertEquals(1, preg_match($expression, 'FUCK'));\n        $this->assertEquals(1, preg_match($expression, 'ƒuck'));\n        $this->assertEquals(1, preg_match($expression, 'fuçk'));\n        $this->assertEquals(1, preg_match($expression, 'f-u-c-k'));\n        $this->assertEquals(1, preg_match($expression, 'f_u_c_k'));\n        $this->assertEquals(0, preg_match($expression, 'hello'));\n        $this->assertEquals(0, preg_match($expression, 'world'));\n    }\n\n    public function test_separator_expression_with_various_chars()\n    {\n        $separators = ['-', '_', '.', ' ', '*', '!'];\n        $result = $this->matcher->generateSeparatorExpression($separators);\n\n        $this->assertIsString($result);\n\n        $testExpression = '/f' . $result . 'u' . $result . 'c' . $result . 'k/i';\n\n        $this->assertEquals(1, preg_match($testExpression, 'f-u-c-k'));\n        $this->assertEquals(1, preg_match($testExpression, 'f_u_c_k'));\n        $this->assertEquals(1, preg_match($testExpression, 'f u c k'));\n        $this->assertEquals(1, preg_match($testExpression, 'f*u*c*k'));\n        $this->assertEquals(1, preg_match($testExpression, 'f!u!c!k'));\n        $this->assertEquals(1, preg_match($testExpression, 'fuck'));\n    }\n\n    public function test_generate_expressions_with_multi_char_substitutions()\n    {\n        $profanities = ['ass'];\n        $separators = ['-'];\n        $substitutions = [\n            '/a/' => ['a', '@', '4'],\n            '/s/' => ['s', '$', '5']\n        ];\n\n        $expressions = $this->matcher->generateExpressions($profanities, $separators, $substitutions);\n        $expression = $expressions['ass'];\n\n        $this->assertEquals(1, preg_match($expression, 'ass'));\n        $this->assertEquals(1, preg_match($expression, '@ss'));\n        $this->assertEquals(1, preg_match($expression, '4ss'));\n        $this->assertEquals(1, preg_match($expression, 'a$s'));\n        $this->assertEquals(1, preg_match($expression, 'a55'));\n        $this->assertEquals(1, preg_match($expression, '@$$'));\n        $this->assertEquals(1, preg_match($expression, '455'));\n    }\n\n    public function test_expressions_are_case_insensitive()\n    {\n        $profanities = ['test'];\n        $separators = [];\n        $substitutions = [\n            '/t/' => ['t'],\n            '/e/' => ['e', '3'],\n            '/s/' => ['s', '$']\n        ];\n\n        $expressions = $this->matcher->generateExpressions($profanities, $separators, $substitutions);\n        $expression = $expressions['test'];\n\n        $this->assertEquals(1, preg_match($expression, 'test'));\n        $this->assertEquals(1, preg_match($expression, 'TEST'));\n        $this->assertEquals(1, preg_match($expression, 'Test'));\n        $this->assertEquals(1, preg_match($expression, 'TeSt'));\n        $this->assertEquals(1, preg_match($expression, 't3st'));\n        $this->assertEquals(1, preg_match($expression, 'T3ST'));\n        $this->assertEquals(1, preg_match($expression, 'te$t'));\n    }\n\n    public function test_empty_arrays_handling()\n    {\n        $result = $this->matcher->generateExpressions([], [], []);\n        $this->assertIsArray($result);\n        $this->assertEmpty($result);\n\n        $separatorResult = $this->matcher->generateSeparatorExpression([]);\n        $this->assertIsString($separatorResult);\n\n        $substitutionResult = $this->matcher->generateSubstitutionExpressions([]);\n        $this->assertIsArray($substitutionResult);\n        $this->assertEmpty($substitutionResult);\n    }\n\n    public function test_complex_profanity_patterns()\n    {\n        $profanities = ['fucking', 'bullshit'];\n        $separators = ['-', '_', ' ', '.'];\n        $substitutions = [\n            '/f/' => ['f'],\n            '/u/' => ['u', 'ü', 'ū'],\n            '/c/' => ['c', 'ç'],\n            '/k/' => ['k'],\n            '/i/' => ['i', '!', '1'],\n            '/n/' => ['n', 'ñ'],\n            '/g/' => ['g'],\n            '/b/' => ['b', 'ß'],\n            '/l/' => ['l'],\n            '/s/' => ['s', '$'],\n            '/h/' => ['h'],\n            '/t/' => ['t']\n        ];\n\n        $expressions = $this->matcher->generateExpressions($profanities, $separators, $substitutions);\n\n        $fuckingExpression = $expressions['fucking'];\n        $this->assertEquals(1, preg_match($fuckingExpression, 'fucking'));\n        $this->assertEquals(1, preg_match($fuckingExpression, 'füçk1ng'));\n        $this->assertEquals(1, preg_match($fuckingExpression, 'f-u-c-k-i-n-g'));\n\n        $bullshitExpression = $expressions['bullshit'];\n        $this->assertEquals(1, preg_match($bullshitExpression, 'bullshit'));\n        $this->assertEquals(1, preg_match($bullshitExpression, 'ßull$h1t'));\n        $this->assertEquals(1, preg_match($bullshitExpression, 'b.u.l.l.s.h.i.t'));\n    }\n\n    public function test_circular_substitutions_produce_valid_regex()\n    {\n        $substitutions = [\n            '/c/' => ['c', 'k', 'ç'],\n            '/k/' => ['k', 'c', 'q'],\n        ];\n        $subExpressions = $this->matcher->generateSubstitutionExpressions($substitutions);\n        $separatorExpr = $this->matcher->generateSeparatorExpression([]);\n        $regex = $this->matcher->generateProfanityExpression('cock', $subExpressions, $separatorExpr);\n\n        $this->assertNotFalse(@preg_match($regex, ''));\n        $this->assertMatchesRegularExpression($regex, 'cock');\n        $this->assertMatchesRegularExpression($regex, 'kokk');\n        $this->assertMatchesRegularExpression($regex, 'çoçk');\n    }\n\n    public function test_basic_profanity_matching()\n    {\n        $profanities = ['damn', 'hell'];\n        $separators = ['-', '_'];\n        $substitutions = [\n            '/a/' => ['a', '@'],\n            '/e/' => ['e', '3'],\n            '/l/' => ['l', '1']\n        ];\n\n        $expressions = $this->matcher->generateExpressions($profanities, $separators, $substitutions);\n        $damnExpression = $expressions['damn'];\n        $hellExpression = $expressions['hell'];\n\n        $this->assertEquals(1, preg_match($damnExpression, 'damn'));\n        $this->assertEquals(1, preg_match($damnExpression, 'd@mn'));\n        $this->assertEquals(1, preg_match($hellExpression, 'hell'));\n        $this->assertEquals(1, preg_match($hellExpression, 'h3ll'));\n        $this->assertEquals(1, preg_match($hellExpression, 'he11'));\n\n        $this->assertEquals(0, preg_match($damnExpression, 'hello'));\n        $this->assertEquals(0, preg_match($hellExpression, 'damn'));\n    }\n}\n"
  },
  {
    "path": "tests/ResultCachingTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Core\\Dictionary;\nuse Blaspsoft\\Blasp\\Enums\\Severity;\nuse Blaspsoft\\Blasp\\Facades\\Blasp;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Config;\n\nclass ResultCachingTest extends TestCase\n{\n    protected function setUp(): void\n    {\n        parent::setUp();\n        $this->app['config']->set('cache.default', 'array');\n        $this->app['config']->set('blasp.cache.enabled', true);\n        $this->app['config']->set('blasp.cache.results', true);\n        $this->app['config']->set('blasp.cache.driver', null);\n        Cache::flush();\n    }\n\n    public function test_results_are_cached(): void\n    {\n        $result1 = Blasp::check('This is a fucking sentence');\n        $result2 = Blasp::check('This is a fucking sentence');\n\n        $this->assertTrue($result1->isOffensive());\n        $this->assertTrue($result2->isOffensive());\n        $this->assertSame($result1->clean(), $result2->clean());\n        $this->assertSame($result1->score(), $result2->score());\n        $this->assertSame($result1->count(), $result2->count());\n\n        // Verify cache keys were tracked\n        $keys = Cache::get('blasp_result_cache_keys', []);\n        $this->assertNotEmpty($keys);\n    }\n\n    public function test_cache_key_varies_by_language(): void\n    {\n        $englishResult = Blasp::in('english')->check('damn');\n        $spanishResult = Blasp::in('spanish')->check('damn');\n\n        // English should detect 'damn', Spanish should not\n        $this->assertTrue($englishResult->isOffensive());\n        $this->assertFalse($spanishResult->isOffensive());\n\n        // Both should be cached as separate entries\n        $keys = Cache::get('blasp_result_cache_keys', []);\n        $this->assertCount(2, $keys);\n    }\n\n    public function test_cache_key_varies_by_severity(): void\n    {\n        $result1 = Blasp::withSeverity(Severity::Mild)->check('damn this');\n        $result2 = Blasp::withSeverity(Severity::Extreme)->check('damn this');\n\n        // Mild severity catches 'damn', Extreme does not\n        $this->assertTrue($result1->isOffensive());\n        $this->assertFalse($result2->isOffensive());\n    }\n\n    public function test_cache_key_varies_by_allow_list(): void\n    {\n        $result1 = Blasp::check('damn this');\n        $result2 = Blasp::allow('damn')->check('damn this');\n\n        $this->assertTrue($result1->isOffensive());\n        $this->assertFalse($result2->isOffensive());\n    }\n\n    public function test_cache_key_varies_by_block_list(): void\n    {\n        $result1 = Blasp::check('foobar');\n        $result2 = Blasp::block('foobar')->check('foobar');\n\n        $this->assertFalse($result1->isOffensive());\n        $this->assertTrue($result2->isOffensive());\n    }\n\n    public function test_cache_key_varies_by_driver(): void\n    {\n        $result1 = Blasp::driver('regex')->check('fuck this');\n        $result2 = Blasp::driver('pattern')->check('fuck this');\n\n        // Both should detect it, but they should be separate cache entries\n        $this->assertTrue($result1->isOffensive());\n        $this->assertTrue($result2->isOffensive());\n\n        $keys = Cache::get('blasp_result_cache_keys', []);\n        $this->assertCount(2, $keys);\n    }\n\n    public function test_callback_mask_bypasses_cache(): void\n    {\n        Blasp::mask(fn($word, $len) => '[CENSORED]')->check('fuck this');\n\n        // No cache keys should be tracked for CallbackMask\n        $keys = Cache::get('blasp_result_cache_keys', []);\n        $this->assertEmpty($keys);\n    }\n\n    public function test_clear_cache_wipes_result_cache(): void\n    {\n        Blasp::check('This is a fucking sentence');\n\n        $keys = Cache::get('blasp_result_cache_keys', []);\n        $this->assertNotEmpty($keys);\n\n        Dictionary::clearCache();\n\n        $keys = Cache::get('blasp_result_cache_keys', []);\n        $this->assertNull(Cache::get('blasp_result_cache_keys'));\n\n        // Verify the cached result data was also cleared\n        foreach ($keys as $key) {\n            $this->assertNull(Cache::get($key));\n        }\n    }\n\n    public function test_disabling_results_config_skips_caching(): void\n    {\n        Config::set('blasp.cache.results', false);\n\n        Blasp::check('This is a fucking sentence');\n\n        $keys = Cache::get('blasp_result_cache_keys', []);\n        $this->assertEmpty($keys);\n    }\n\n    public function test_disabling_cache_entirely_skips_caching(): void\n    {\n        Config::set('blasp.cache.enabled', false);\n\n        Blasp::check('This is a fucking sentence');\n\n        $keys = Cache::get('blasp_result_cache_keys', []);\n        $this->assertEmpty($keys);\n    }\n\n    public function test_cached_results_deserialize_correctly(): void\n    {\n        $result1 = Blasp::check('This is a fucking sentence');\n\n        // Clear PHP state but keep cache\n        // Second call should come from cache\n        $result2 = Blasp::check('This is a fucking sentence');\n\n        $this->assertSame($result1->isOffensive(), $result2->isOffensive());\n        $this->assertSame($result1->clean(), $result2->clean());\n        $this->assertSame($result1->original(), $result2->original());\n        $this->assertSame($result1->score(), $result2->score());\n        $this->assertSame($result1->count(), $result2->count());\n        $this->assertSame($result1->uniqueWords(), $result2->uniqueWords());\n    }\n\n    public function test_clean_text_is_not_cached_incorrectly(): void\n    {\n        $result = Blasp::check('hello world');\n        $this->assertFalse($result->isOffensive());\n        $this->assertSame('hello world', $result->clean());\n\n        // Second call\n        $result2 = Blasp::check('hello world');\n        $this->assertFalse($result2->isOffensive());\n        $this->assertSame('hello world', $result2->clean());\n    }\n}\n"
  },
  {
    "path": "tests/SeverityMapTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Facades\\Blasp;\nuse Blaspsoft\\Blasp\\Enums\\Severity;\n\nclass SeverityMapTest extends TestCase\n{\n    // --- Spanish ---\n\n    public function test_spanish_mild_words_filtered_by_moderate_severity(): void\n    {\n        // 'tonto' is mild — should be ignored when filtering at Moderate\n        $result = Blasp::in('spanish')->withSeverity(Severity::Moderate)->check('eres tonto');\n        $this->assertFalse($result->isOffensive(), 'Mild word \"tonto\" should be ignored at Moderate severity');\n    }\n\n    public function test_spanish_moderate_words_caught_at_moderate_severity(): void\n    {\n        // 'cabrón' is moderate — should be caught when filtering at Moderate\n        $result = Blasp::in('spanish')->withSeverity(Severity::Moderate)->check('eres cabrón');\n        $this->assertTrue($result->isOffensive(), 'Moderate word \"cabrón\" should be caught at Moderate severity');\n    }\n\n    public function test_spanish_moderate_words_filtered_by_high_severity(): void\n    {\n        // 'gilipollas' is moderate — should be ignored when filtering at High\n        $result = Blasp::in('spanish')->withSeverity(Severity::High)->check('eres gilipollas');\n        $this->assertFalse($result->isOffensive(), 'Moderate word \"gilipollas\" should be ignored at High severity');\n    }\n\n    public function test_spanish_default_high_words_caught(): void\n    {\n        // 'mierda' is not in severity map — defaults to High\n        $result = Blasp::in('spanish')->withSeverity(Severity::High)->check('esto es mierda');\n        $this->assertTrue($result->isOffensive(), 'Default High word \"mierda\" should be caught at High severity');\n    }\n\n    public function test_spanish_extreme_words_caught_at_extreme(): void\n    {\n        $result = Blasp::in('spanish')->withSeverity(Severity::Extreme)->check('maricón');\n        $this->assertTrue($result->isOffensive(), 'Extreme word \"maricón\" should be caught at Extreme severity');\n    }\n\n    public function test_spanish_high_words_filtered_by_extreme(): void\n    {\n        // 'mierda' defaults to High — should be ignored at Extreme\n        $result = Blasp::in('spanish')->withSeverity(Severity::Extreme)->check('mierda');\n        $this->assertFalse($result->isOffensive(), 'High word \"mierda\" should be ignored at Extreme severity');\n    }\n\n    // --- French ---\n\n    public function test_french_mild_words_filtered_by_moderate_severity(): void\n    {\n        // 'idiot' is mild — should be ignored at Moderate\n        $result = Blasp::in('french')->withSeverity(Severity::Moderate)->check('quel idiot');\n        $this->assertFalse($result->isOffensive(), 'Mild word \"idiot\" should be ignored at Moderate severity');\n    }\n\n    public function test_french_moderate_words_caught_at_moderate_severity(): void\n    {\n        // 'connard' is moderate — should be caught at Moderate\n        $result = Blasp::in('french')->withSeverity(Severity::Moderate)->check('espèce de connard');\n        $this->assertTrue($result->isOffensive(), 'Moderate word \"connard\" should be caught at Moderate severity');\n    }\n\n    public function test_french_moderate_words_filtered_by_high_severity(): void\n    {\n        // 'salaud' is moderate — should be ignored at High\n        $result = Blasp::in('french')->withSeverity(Severity::High)->check('quel salaud');\n        $this->assertFalse($result->isOffensive(), 'Moderate word \"salaud\" should be ignored at High severity');\n    }\n\n    public function test_french_default_high_words_caught(): void\n    {\n        // 'merde' is not in severity map — defaults to High\n        $result = Blasp::in('french')->withSeverity(Severity::High)->check('merde alors');\n        $this->assertTrue($result->isOffensive(), 'Default High word \"merde\" should be caught at High severity');\n    }\n\n    public function test_french_extreme_words_caught_at_extreme(): void\n    {\n        $result = Blasp::in('french')->withSeverity(Severity::Extreme)->check('sale pédé');\n        $this->assertTrue($result->isOffensive(), 'Extreme word \"pédé\" should be caught at Extreme severity');\n    }\n\n    public function test_french_high_words_filtered_by_extreme(): void\n    {\n        // 'merde' defaults to High — should be ignored at Extreme\n        $result = Blasp::in('french')->withSeverity(Severity::Extreme)->check('merde');\n        $this->assertFalse($result->isOffensive(), 'High word \"merde\" should be ignored at Extreme severity');\n    }\n\n    // --- German ---\n\n    public function test_german_mild_words_filtered_by_moderate_severity(): void\n    {\n        // 'mist' is mild — should be ignored at Moderate\n        $result = Blasp::in('german')->withSeverity(Severity::Moderate)->check('so ein mist');\n        $this->assertFalse($result->isOffensive(), 'Mild word \"mist\" should be ignored at Moderate severity');\n    }\n\n    public function test_german_moderate_words_caught_at_moderate_severity(): void\n    {\n        // 'arschloch' is moderate — should be caught at Moderate\n        $result = Blasp::in('german')->withSeverity(Severity::Moderate)->check('du arschloch');\n        $this->assertTrue($result->isOffensive(), 'Moderate word \"arschloch\" should be caught at Moderate severity');\n    }\n\n    public function test_german_moderate_words_filtered_by_high_severity(): void\n    {\n        // 'wichser' is moderate — should be ignored at High\n        $result = Blasp::in('german')->withSeverity(Severity::High)->check('du wichser');\n        $this->assertFalse($result->isOffensive(), 'Moderate word \"wichser\" should be ignored at High severity');\n    }\n\n    public function test_german_default_high_words_caught(): void\n    {\n        // 'ficken' is not in severity map — defaults to High\n        $result = Blasp::in('german')->withSeverity(Severity::High)->check('ficken');\n        $this->assertTrue($result->isOffensive(), 'Default High word \"ficken\" should be caught at High severity');\n    }\n\n    public function test_german_extreme_words_caught_at_extreme(): void\n    {\n        $result = Blasp::in('german')->withSeverity(Severity::Extreme)->check('du tunte');\n        $this->assertTrue($result->isOffensive(), 'Extreme word \"tunte\" should be caught at Extreme severity');\n    }\n\n    public function test_german_high_words_filtered_by_extreme(): void\n    {\n        // 'scheiße' defaults to High — should be ignored at Extreme\n        $result = Blasp::in('german')->withSeverity(Severity::Extreme)->check('scheiße');\n        $this->assertFalse($result->isOffensive(), 'High word \"scheiße\" should be ignored at Extreme severity');\n    }\n\n    // --- Cross-cutting ---\n\n    public function test_unmapped_words_default_to_high_across_languages(): void\n    {\n        // Words not in any severity map should default to High\n        $spanishResult = Blasp::in('spanish')->check('joder');\n        $frenchResult = Blasp::in('french')->check('putain');\n        $germanResult = Blasp::in('german')->check('fotze');\n\n        $this->assertTrue($spanishResult->isOffensive());\n        $this->assertTrue($frenchResult->isOffensive());\n        $this->assertTrue($germanResult->isOffensive());\n\n        // All should be at least High severity\n        $this->assertTrue($spanishResult->severity()->isAtLeast(Severity::High));\n        $this->assertTrue($frenchResult->severity()->isAtLeast(Severity::High));\n        $this->assertTrue($germanResult->severity()->isAtLeast(Severity::High));\n    }\n}\n"
  },
  {
    "path": "tests/SpanishStringNormalizerTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Core\\Normalizers\\SpanishNormalizer;\n\nclass SpanishStringNormalizerTest extends TestCase\n{\n    private SpanishNormalizer $normalizer;\n\n    public function setUp(): void\n    {\n        parent::setUp();\n        $this->normalizer = new SpanishNormalizer();\n    }\n\n    public function test_normalize_accented_vowels()\n    {\n        $this->assertEquals('hola como estas', $this->normalizer->normalize('hóla cómo estás'));\n        $this->assertEquals('feliz cumpleanos', $this->normalizer->normalize('felíz cumpleañós'));\n        $this->assertEquals('nino pequeno', $this->normalizer->normalize('niño pequeño'));\n    }\n\n    public function test_normalize_enye_character()\n    {\n        $this->assertEquals('espanol', $this->normalizer->normalize('español'));\n        $this->assertEquals('manana', $this->normalizer->normalize('mañana'));\n        $this->assertEquals('NINO', $this->normalizer->normalize('NIÑO'));\n    }\n\n    public function test_normalize_double_l()\n    {\n        $this->assertEquals('yamo', $this->normalizer->normalize('llamo'));\n        $this->assertEquals('yegar', $this->normalizer->normalize('llegar'));\n        $this->assertEquals('YOVER', $this->normalizer->normalize('LLOVER'));\n    }\n\n    public function test_normalize_double_r()\n    {\n        $this->assertEquals('pero', $this->normalizer->normalize('perro'));\n        $this->assertEquals('caro', $this->normalizer->normalize('carro'));\n        $this->assertEquals('RUN', $this->normalizer->normalize('RRUN'));\n    }\n\n    public function test_normalize_spanish_profanity_variants()\n    {\n        $this->assertEquals('mierda', $this->normalizer->normalize('miérda'));\n        $this->assertEquals('cabron', $this->normalizer->normalize('cabrón'));\n        $this->assertEquals('joder', $this->normalizer->normalize('jodér'));\n    }\n\n    public function test_normalize_preserves_non_spanish_characters()\n    {\n        $this->assertEquals('hello world 123', $this->normalizer->normalize('hello world 123'));\n        $this->assertEquals('test@email.com', $this->normalizer->normalize('test@email.com'));\n    }\n\n    public function test_normalize_mixed_case_preservation()\n    {\n        $this->assertEquals('MANANA', $this->normalizer->normalize('MAÑANA'));\n        $this->assertEquals('Espanol', $this->normalizer->normalize('Español'));\n        $this->assertEquals('hOLA', $this->normalizer->normalize('hÓLA'));\n    }\n\n    public function test_normalize_empty_and_special_strings()\n    {\n        $this->assertEquals('', $this->normalizer->normalize(''));\n        $this->assertEquals('   ', $this->normalizer->normalize('   '));\n        $this->assertEquals('nnn', $this->normalizer->normalize('ñññ'));\n    }\n}\n"
  },
  {
    "path": "tests/StrMacroTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Illuminate\\Support\\Str;\nuse Illuminate\\Support\\Stringable;\n\nclass StrMacroTest extends TestCase\n{\n    public function test_str_is_profane_returns_true_for_profane_text()\n    {\n        $this->assertTrue(Str::isProfane('fuck'));\n    }\n\n    public function test_str_is_profane_returns_false_for_clean_text()\n    {\n        $this->assertFalse(Str::isProfane('hello'));\n    }\n\n    public function test_str_clean_profanity_masks_profane_text()\n    {\n        $result = Str::cleanProfanity('fuck this');\n\n        $this->assertStringContainsString('*', $result);\n        $this->assertStringNotContainsString('fuck', $result);\n    }\n\n    public function test_str_clean_profanity_returns_clean_text_unchanged()\n    {\n        $this->assertSame('hello', Str::cleanProfanity('hello'));\n    }\n\n    public function test_stringable_is_profane_returns_true_for_profane_text()\n    {\n        $this->assertTrue(Str::of('fuck')->isProfane());\n    }\n\n    public function test_stringable_is_profane_returns_false_for_clean_text()\n    {\n        $this->assertFalse(Str::of('hello')->isProfane());\n    }\n\n    public function test_stringable_clean_profanity_returns_stringable_instance()\n    {\n        $result = Str::of('fuck this')->cleanProfanity();\n\n        $this->assertInstanceOf(Stringable::class, $result);\n        $this->assertStringContainsString('*', (string) $result);\n        $this->assertStringNotContainsString('fuck', (string) $result);\n    }\n\n    public function test_stringable_clean_profanity_returns_clean_text_unchanged()\n    {\n        $this->assertSame('hello', (string) Str::of('hello')->cleanProfanity());\n    }\n}\n"
  },
  {
    "path": "tests/TestCase.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\BlaspServiceProvider;\nuse Illuminate\\Support\\Facades\\Config;\nuse Orchestra\\Testbench\\TestCase as BaseTestCase;\n\nabstract class TestCase extends BaseTestCase\n{\n\n    protected function getPackageProviders($app)\n    {\n        return [\n            BlaspServiceProvider::class,\n        ];\n    }\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n        Config::set('cache.default', 'array');\n        Config::set('blasp.separators', config('blasp.separators'));\n        Config::set('blasp.profanities', config('blasp.profanities'));\n        Config::set('blasp.false_positives', config('blasp.false_positives', []));\n        Config::set('blasp.substitutions', config('blasp.substitutions', []));\n        Config::set('blasp.mask', '*');\n        Config::set('blasp.mask_character', '*');\n        Config::set('blasp.cache.driver', config('blasp.cache.driver'));\n        Config::set('blasp.cache_driver', config('blasp.cache_driver'));\n    }\n}\n"
  },
  {
    "path": "tests/UuidFalsePositiveTest.php",
    "content": "<?php\n\nnamespace Blaspsoft\\Blasp\\Tests;\n\nuse Blaspsoft\\Blasp\\Facades\\Blasp;\n\nclass UuidFalsePositiveTest extends TestCase\n{\n    public function test_uuid_not_flagged_as_profanity()\n    {\n        $result = Blasp::check('6ec3e80f-11ad-3d5c-809f-144a2ef5800b');\n        $this->assertFalse($result->hasProfanity());\n    }\n\n    public function test_hex_string_not_flagged()\n    {\n        $result = Blasp::check('a55e7b3f9c1d2e4f');\n        $this->assertFalse($result->hasProfanity());\n    }\n\n    public function test_profanity_alongside_uuid_still_detected()\n    {\n        $result = Blasp::check('fuck 6ec3e80f-11ad-3d5c-809f-144a2ef5800b');\n        $this->assertTrue($result->hasProfanity());\n        $this->assertContains('fuck', $result->getUniqueProfanitiesFound());\n    }\n\n    public function test_standalone_profanity_still_detected()\n    {\n        $result = Blasp::check('boob');\n        $this->assertTrue($result->hasProfanity());\n    }\n\n    public function test_normal_profanity_detection_unaffected()\n    {\n        $result = Blasp::check('shit ass damn');\n        $this->assertTrue($result->hasProfanity());\n    }\n\n    public function test_uuid_in_sentence_not_flagged()\n    {\n        $result = Blasp::check('User 6ec3e80f-11ad-3d5c-809f-144a2ef5800b logged in');\n        $this->assertFalse($result->hasProfanity());\n    }\n\n    public function test_short_hex_does_not_suppress_profanity()\n    {\n        $result = Blasp::check('800b');\n        $this->assertTrue($result->hasProfanity());\n    }\n\n    public function test_pure_letter_hex_does_not_suppress_profanity()\n    {\n        $result = Blasp::check('fuck deadbeef');\n        $this->assertTrue($result->hasProfanity());\n        $this->assertContains('fuck', $result->getUniqueProfanitiesFound());\n    }\n\n    public function test_md5_hash_not_flagged()\n    {\n        $result = Blasp::check('a55e7b3f9c1d2e4f8a0b1c2d3e4f5a6b');\n        $this->assertFalse($result->hasProfanity());\n    }\n\n    public function test_multiple_uuids_not_flagged()\n    {\n        $result = Blasp::check('6ec3e80f-11ad-3d5c-809f-144a2ef5800b and 550e8400-e29b-41d4-a716-446655440000');\n        $this->assertFalse($result->hasProfanity());\n    }\n}\n"
  }
]