UNPKG

glin-profanity

Version:

Glin-Profanity is a lightweight and efficient npm package designed to detect and filter profane language in text inputs across multiple languages. Whether you’re building a chat application, a comment section, or any platform where user-generated content

6,927 lines • 171 kB
'use strict';

// src/ml/ToxicityDetector.ts
var _ToxicityDetector = class _ToxicityDetector {
  /**
   * Creates a new ToxicityDetector instance.
   *
   * @param config - Configuration options
   *
   * @example
   * ```typescript
   * // Basic usage with default threshold (0.85)
   * const detector = new ToxicityDetector();
   *
   * // Custom threshold for higher precision
   * const strictDetector = new ToxicityDetector({ threshold: 0.95 });
   *
   * // Check only specific categories
   * const customDetector = new ToxicityDetector({
   *   threshold: 0.8,
   *   labels: ['insult', 'threat', 'obscene'],
   * });
   * ```
   */
  constructor(config = {}) {
    this.model = null;
    this.loadingPromise = null;
    this.isAvailable = null;
    this.config = {
      threshold: config.threshold ?? 0.85,
      labels: config.labels ?? _ToxicityDetector.ALL_LABELS,
      preloadModel: config.preloadModel ?? false
    };
    if (this.config.preloadModel) {
      this.loadModel().catch(() => {
      });
    }
  }
  /**
   * Dynamic import wrapper to avoid TypeScript static analysis issues.
   * Uses Function constructor to bypass module resolution at compile time.
   * @internal
   */
  dynamicImport(moduleName) {
    return new Function("m", "return import(m)")(moduleName);
  }
  /**
   * Checks if TensorFlow.js and the toxicity model are available.
   * This performs a lazy check on first call and caches the result.
   *
   * @returns True if ML dependencies are available
   */
  async checkAvailability() {
    if (this.isAvailable !== null) {
      return this.isAvailable;
    }
    try {
      await this.dynamicImport("@tensorflow/tfjs");
      await this.dynamicImport("@tensorflow-models/toxicity");
      this.isAvailable = true;
    } catch {
      this.isAvailable = false;
    }
    return this.isAvailable;
  }
  /**
   * Loads the toxicity model.
   * This is called automatically on first analyze() call if not called explicitly.
   *
   * @returns The loaded model
   * @throws Error if TensorFlow.js dependencies are not installed
   *
   * @example
   * ```typescript
   * const detector = new ToxicityDetector();
   *
   * // Explicitly preload model (optional)
   * await detector.loadModel();
   *
   * // Or let it load automatically on first use
   * const result = await detector.analyze('text');
   * ```
   */
  async loadModel() {
    if (this.model) {
      return this.model;
    }
    if (this.loadingPromise) {
      return this.loadingPromise;
    }
    this.loadingPromise = this.doLoadModel();
    try {
      this.model = await this.loadingPromise;
      return this.model;
    } finally {
      this.loadingPromise = null;
    }
  }
  async doLoadModel() {
    try {
      const toxicityModule = await this.dynamicImport(
        "@tensorflow-models/toxicity"
      );
      const loadFn = toxicityModule.load;
      const model = await loadFn(
        this.config.threshold,
        this.config.labels
      );
      return model;
    } catch (error) {
      const message = error instanceof Error ? error.message : "Unknown error";
      if (message.includes("Cannot find module") || message.includes("MODULE_NOT_FOUND")) {
        throw new Error(
          "TensorFlow.js dependencies not installed. Install with: npm install @tensorflow/tfjs @tensorflow-models/toxicity"
        );
      }
      throw new Error(`Failed to load toxicity model: ${message}`);
    }
  }
  /**
   * Analyzes text for toxicity using the ML model.
   *
   * @param text - Text to analyze
   * @returns Analysis result with predictions and scores
   *
   * @example
   * ```typescript
   * const detector = new ToxicityDetector();
   * const result = await detector.analyze('you are stupid');
   *
   * console.log(result.isToxic);           // true
   * console.log(result.overallScore);      // 0.92
   * console.log(result.matchedCategories); // ['insult', 'toxicity']
   * ```
   */
  async analyze(text) {
    const startTime = performance.now();
    const model = await this.loadModel();
    const predictions = await model.classify([text]);
    const processedPredictions = predictions.map(
      (pred) => ({
        label: pred.label,
        match: pred.results[0].match,
        probabilities: [
          pred.results[0].probabilities[0],
          pred.results[0].probabilities[1]
        ]
      })
    );
    const matchedCategories = processedPredictions.filter((p) => p.match === true).map((p) => p.label);
    const overallScore = Math.max(
      ...processedPredictions.map((p) => p.probabilities[1])
    );
    const processingTimeMs = performance.now() - startTime;
    return {
      isToxic: matchedCategories.length > 0,
      overallScore,
      predictions: processedPredictions,
      matchedCategories,
      processingTimeMs
    };
  }
  /**
   * Analyzes multiple texts in a batch for better performance.
   *
   * @param texts - Array of texts to analyze
   * @returns Array of analysis results
   *
   * @example
   * ```typescript
   * const detector = new ToxicityDetector();
   * const results = await detector.analyzeBatch([
   *   'hello friend',
   *   'you are terrible',
   *   'great work!',
   * ]);
   *
   * results.forEach((result, i) => {
   *   console.log(`Text ${i}: ${result.isToxic ? 'toxic' : 'clean'}`);
   * });
   * ```
   */
  async analyzeBatch(texts) {
    if (texts.length === 0) {
      return [];
    }
    const startTime = performance.now();
    const model = await this.loadModel();
    const predictions = await model.classify(texts);
    const totalTimeMs = performance.now() - startTime;
    const perTextTimeMs = totalTimeMs / texts.length;
    return texts.map((_, textIndex) => {
      const processedPredictions = predictions.map(
        (pred) => ({
          label: pred.label,
          match: pred.results[textIndex].match,
          probabilities: [
            pred.results[textIndex].probabilities[0],
            pred.results[textIndex].probabilities[1]
          ]
        })
      );
      const matchedCategories = processedPredictions.filter((p) => p.match === true).map((p) => p.label);
      const overallScore = Math.max(
        ...processedPredictions.map((p) => p.probabilities[1])
      );
      return {
        isToxic: matchedCategories.length > 0,
        overallScore,
        predictions: processedPredictions,
        matchedCategories,
        processingTimeMs: perTextTimeMs
      };
    });
  }
  /**
   * Simple boolean check for toxicity.
   *
   * @param text - Text to check
   * @returns True if text is detected as toxic
   *
   * @example
   * ```typescript
   * const detector = new ToxicityDetector();
   *
   * if (await detector.isToxic('some user input')) {
   *   console.log('Content flagged as toxic');
   * }
   * ```
   */
  async isToxic(text) {
    const result = await this.analyze(text);
    return result.isToxic;
  }
  /**
   * Gets the toxicity score for text (0-1).
   *
   * @param text - Text to score
   * @returns Toxicity score from 0 (clean) to 1 (highly toxic)
   */
  async getScore(text) {
    const result = await this.analyze(text);
    return result.overallScore;
  }
  /**
   * Disposes of the model to free memory.
   * The model will be reloaded on next analyze() call.
   */
  dispose() {
    this.model = null;
    this.loadingPromise = null;
  }
  /**
   * Gets the current configuration.
   */
  getConfig() {
    return { ...this.config };
  }
  /**
   * Checks if the model is currently loaded.
   */
  isModelLoaded() {
    return this.model !== null;
  }
};
/**
 * All available toxicity labels.
 */
_ToxicityDetector.ALL_LABELS = [
  "identity_attack",
  "insult",
  "obscene",
  "severe_toxicity",
  "sexual_explicit",
  "threat",
  "toxicity"
];
var ToxicityDetector = _ToxicityDetector;

// ../../shared/dictionaries/arabic.json
var arabic_default = {
  words: [
    "\u0633\u0643\u0633",
    "\u0637\u064A\u0632",
    "\u0634\u0631\u062C",
    "\u0644\u0639\u0642",
    "\u0644\u062D\u0633",
    "\u062A\u0645\u0635",
    "\u0628\u064A\u0636\u0627\u0646",
    "\u062B\u062F\u064A",
    "\u0628\u0632",
    "\u062D\u0644\u0645\u0629",
    "\u0645\u0641\u0644\u0642\u0633\u0629",
    "\u0628\u0638\u0631",
    "\u0643\u0633",
    "\u0645\u0628\u0627\u062F\u0644",
    "\u0639\u0627\u0647\u0631\u0629",
    "\u062C\u0645\u0627\u0639",
    "\u0642\u0636\u064A\u0628",
    "\u0632\u0628",
    "\u0644\u0648\u0637\u064A",
    "\u0644\u0648\u0627\u0637",
    "\u0633\u062D\u0627\u0642",
    "\u0627\u063A\u062A\u0635\u0627\u0628",
    "\u062E\u0646\u062B\u064A",
    "\u0627\u062D\u062A\u0644\u0627\u0645",
    "\u0646\u064A\u0643",
    "\u0645\u062A\u0646\u0627\u0643",
    "\u0634\u0631\u0645\u0648\u0637\u0629",
    "\u0639\u0631\u0635",
    "\u062E\u0648\u0644",
    "\u0642\u062D\u0628\u0629",
    "\u0644\u0628\u0648\u0629",
    "\u0648\u0633\u062E",
    "\u0639\u0644\u0642",
    "\u0646\u0627\u0643",
    "\u0645\u0646\u064A\u0643",
    "\u0634\u0631\u0645\u0637",
    "\u0642\u062D\u0628",
    "\u0634\u0631\u0645\u0648\u0637",
    "\u062E\u0648\u0644",
    "\u0647\u0646\u064A\u0643",
    "\u0641\u062D\u0644",
    "\u062F\u064A\u0648\u062B",
    "\u0645\u062A\u0646\u0627\u0643",
    "\u0647\u0627\u064A\u062C",
    "\u0632\u0628\u0631",
    "bdsm",
    "\u0632\u0627\u0646",
    "\u0633\u0627\u062F\u064A",
    "\u0633\u0627\u062F\u0649",
    "\u0643\u0633\u0647\u0627",
    "\u0643\u0633\u064A",
    "\u0643\u0633\u0643",
    "\u0643\u0633\u0645",
    "\u0628\u0646\u062A\u0627\u0643",
    "\u0628\u062A\u0646\u0627\u0643",
    "\u0646\u064A\u0643\u0646\u064A",
    "\u062F\u064A\u0648\u062B",
    "\u0646\u064A\u0627\u0643",
    "\u0641\u0634\u0627\u062E",
    "\u0641\u0634\u062E",
    "\u0647\u0641\u0634\u062E\u0643",
    "\u0634\u0630\u0648\u0630",
    "\u0644\u0648\u0627\u0637",
    "\u0644\u0648\u0637\u064A",
    "\u0644\u0648\u0637\u0649",
    "\u0639\u0627\u0647\u0631",
    "\u0644\u062D\u0633",
    "\u0645\u062A\u0639",
    "\u062D\u0645\u0627\u0645",
    "\u0647\u0631\u0628\u0637",
    "\u062F\u064A\u0627\u062B",
    "\u0633\u062D\u0627\u0642",
    "\u062C\u0646\u0633",
    "\u062C\u0646\u0633",
    "\u062C\u0648\u0632\u064A",
    "\u0645\u0631\u0627\u062A\u064A",
    "\u0645\u0631\u0627\u062A\u0649",
    "\u0645\u0631\u0627\u062A",
    "\u0648\u0631\u0627",
    "\u0634\u0639\u0631",
    "\u0645\u0634\u0639\u0631",
    "\u0627\u0631\u0643\u0628",
    "\u0641\u0631\u0633",
    "\u062C\u0627\u062F",
    "\u0645\u0646\u064A\u0648\u0643",
    "\u0632\u0648\u0628\u0631",
    "\u0637\u064A\u0627\u0632",
    "\u0634\u0631\u0627\u0645\u064A\u0637",
    "\u0641\u0627\u062C\u0631",
    "\u0645\u062D\u0631\u0648\u0645",
    "\u0648\u0627\u0642\u0641",
    "\u0628\u064A\u062D\u0643",
    "\u0645\u0645\u062D\u0648\u0646\u0629",
    "\u0644\u0628\u0646",
    "\u0644\u0628\u0648",
    "\u0648\u0627\u0642\u0641",
    "\u0634\u0631\u0642\u0627\u0646",
    "\u0627\u0641\u0636\u064A",
    "\u062F\u0647\u0648\u0644\u062A",
    "bzaz",
    "nek",
    "nik",
    "mtnak",
    "zeb",
    "zep",
    "zobr",
    "zopr",
    "kos",
    "a7a",
    "hayg",
    "\u0644\u0628\u0648\u0647",
    "\u0644\u0628\u0648\u0629",
    "\u0644\u0628\u0648\u062A",
    "sharma",
    "banoty",
    "\u0628\u0646\u0648\u062A\u064A",
    "\u0628\u0646\u0648\u062A\u0649",
    "\u0633\u0644\u0628",
    "\u0639\u0627\u064A\u0632",
    "\u062F\u0643\u0631",
    "\u0646\u062A\u0627\u064A\u0629",
    "\u0646\u062A\u0627\u064A\u0647",
    "\u0632\u0646\u062C",
    "\u0645\u0645\u062D\u0648\u0646",
    "\u062A\u0639\u0628\u0627\u0646",
    "\u0627\u0642\u0639\u062F",
    "\u0628\u0632\u0627\u0632",
    "\u0647\u064A\u062C\u0627\u0646",
    "\u0633\u0627\u0641\u0644",
    "\u0645\u0635\u064A\u0647\u0648\u0644\u064A",
    "\u0647\u062A\u0645\u0635",
    "\u0645\u0635\u064A",
    "\u0628\u0632",
    "hayg",
    "\u0643\u0644\u0648\u062A",
    "\u0627\u0646\u062F\u0631",
    "\u0633\u0646\u062A\u064A\u0627",
    "\u0628\u0648\u0643\u0633\u0631",
    "\u0628\u062A\u0627\u0639",
    "\u0645\u0634\u062A\u0647\u064A",
    "\u0632\u0628\u0649",
    "\u0646\u0643\u062A\u0647",
    "\u0627\u062A\u0646\u0627\u0643",
    "nayak",
    "nyak",
    "neeek",
    "\u0627\u063A\u062A\u0635\u0627",
    "\u0632\u0628",
    "\u0643\u0633",
    "\u0628\u0644\u0643\u064A\u0634\u0646",
    "\u0643\u064A\u0634\u0646",
    "\u0644\u064A\u0633\u0628\u064A\u0627\u0646",
    "\u0633\u0628\u064A\u0627\u0646"
  ]
};

// ../../shared/dictionaries/chinese.json
var chinese_default = {
  words: [
    "13.",
    "13\u70B9",
    "\u4E09\u7EA7\u7247",
    "\u4E0B\u4E09\u70C2",
    "\u4E0B\u8D31",
    "\u4E2A\u8001\u5B50\u7684",
    "\u4E5D\u6E38",
    "\u4E73",
    "\u4E73\u4EA4",
    "\u4E73\u5934",
    "\u4E73\u623F",
    "\u4E73\u6CE2\u81C0\u6D6A",
    "\u4EA4\u914D",
    "\u4EC6\u8857",
    "\u4ED6\u5976\u5976",
    "\u4ED6\u5976\u5976\u7684",
    "\u4ED6\u5976\u5A18\u7684",
    "\u4ED6\u5988",
    "\u4ED6\u5988\u3109\u738B\u516B\u86CB",
    "\u4ED6\u5988\u5730",
    "\u4ED6\u5988\u7684",
    "\u4ED6\u5A18",
    "\u4ED6\u9A6C\u7684",
    "\u4F60\u4E2A\u50BB\u6BD4",
    "\u4F60\u4ED6\u9A6C\u7684",
    "\u4F60\u5168\u5BB6",
    "\u4F60\u5976\u5976\u7684",
    "\u4F60\u5979\u9A6C\u7684",
    "\u4F60\u5988",
    "\u4F60\u5988\u7684",
    "\u4F60\u5A18",
    "\u4F60\u5A18\u5361\u597D",
    "\u4F60\u5A18\u54A7",
    "\u4F60\u5B83\u5988\u7684",
    "\u4F60\u5B83\u9A6C\u7684",
    "\u4F60\u662F\u9E21",
    "\u4F60\u662F\u9E2D",
    "\u4F60\u9A6C\u7684",
    "\u505A\u7231",
    "\u50BB\u6BD4",
    "\u50BB\u903C",
    "\u518C\u90A3",
    "\u519B\u5993",
    "\u51E0\u516B",
    "\u51E0\u53ED",
    "\u51E0\u5DF4",
    "\u51E0\u82AD",
    "\u521A\u5EA6",
    "\u521A\u762A\u4E09",
    "\u5305\u76AE",
    "\u5341\u4E09\u70B9",
    "\u5356B",
    "\u5356\u6BD4",
    "\u5356\u6DEB",
    "\u5375",
    "\u5375\u5B50",
    "\u53CC\u5CF0\u5FAE\u98A4",
    "\u53E3\u4EA4",
    "\u53E3\u80AF",
    "\u53EB\u5E8A",
    "\u5403\u5C4E",
    "\u540E\u5EAD",
    "\u5439\u7BAB",
    "\u585E\u4F60\u516C",
    "\u585E\u4F60\u5A18",
    "\u585E\u4F60\u6BCD",
    "\u585E\u4F60\u7238",
    "\u585E\u4F60\u8001\u5E08",
    "\u585E\u4F60\u8001\u6BCD",
    "\u5904\u5973",
    "\u5916\u9634",
    "\u5927\u5375\u5B50",
    "\u5927\u5375\u6CE1",
    "\u5927\u9E21\u5DF4",
    "\u5976",
    "\u5976\u5976\u7684\u718A",
    "\u5976\u5B50",
    "\u5978",
    "\u5978\u4F60",
    "\u5979\u5988\u5730",
    "\u5979\u5988\u7684",
    "\u5979\u9A6C\u7684",
    "\u5988B",
    "\u5988\u4E2AB",
    "\u5988\u4E2A\u6BD4",
    "\u5988\u4E2A\u8001\u6BD4",
    "\u5988\u5988\u7684",
    "\u5988\u6BD4",
    "\u5988\u7684",
    "\u5988\u7684B",
    "\u5988\u903C",
    "\u5993",
    "\u5993\u5973",
    "\u5993\u9662",
    "\u59B3\u5979\u5988\u7684",
    "\u59B3\u5988\u7684",
    "\u59B3\u5A18\u7684",
    "\u59B3\u8001\u6BCD\u7684",
    "\u59B3\u9A6C\u7684",
    "\u59D8\u5934",
    "\u59E3\u897F",
    "\u59E6",
    "\u5A18\u4E2A\u6BD4",
    "\u5A18\u7684",
    "\u5A4A\u5B50",
    "\u5A4A\u5B50\u517B\u7684",
    "\u5AD6\u5A3C",
    "\u5AD6\u5BA2",
    "\u5B83\u5988\u5730",
    "\u5B83\u5988\u7684",
    "\u5BC6\u6D1E",
    "\u5C04\u4F60",
    "\u5C04\u7CBE",
    "\u5C0F\u4E73\u5934",
    "\u5C0F\u5375\u5B50",
    "\u5C0F\u5375\u6CE1",
    "\u5C0F\u762A\u4E09",
    "\u5C0F\u8089\u7C92",
    "\u5C0F\u9A9A\u6BD4",
    "\u5C0F\u9A9A\u8D27",
    "\u5C0F\u9E21\u5DF4",
    "\u5C0F\u9E21\u9E21",
    "\u5C41\u773C",
    "\u5C41\u80A1",
    "\u5C44",
    "\u5C4C",
    "\u5DE8\u4E73",
    "\u5E72x\u5A18",
    "\u5E72\u4E03\u516B",
    "\u5E72\u4F60",
    "\u5E72\u4F60\u5988",
    "\u5E72\u4F60\u5A18",
    "\u5E72\u4F60\u8001\u6BCD",
    "\u5E72\u4F60\u826F",
    "\u5E72\u59B3\u5988",
    "\u5E72\u59B3\u5A18",
    "\u5E72\u59B3\u8001\u6BCD",
    "\u5E72\u59B3\u9A6C",
    "\u5E72\u60A8\u5A18",
    "\u5E72\u673A\u63B0",
    "\u5E72\u6B7BCS",
    "\u5E72\u6B7BGM",
    "\u5E72\u6B7B\u4F60",
    "\u5E72\u6B7B\u5BA2\u670D",
    "\u5E79",
    "\u5F3A\u5978",
    "\u5F3A\u5978\u4F60",
    "\u6027",
    "\u6027\u4EA4",
    "\u6027\u5668",
    "\u6027\u65E0\u80FD",
    "\u6027\u7231",
    "\u60C5\u8272",
    "\u60F3\u4E0A\u4F60",
    "\u61C6\u60A8\u5988",
    "\u61C6\u60A8\u5A18",
    "\u61D28",
    "\u61D2\u516B",
    "\u61D2\u53EB",
    "\u61D2\u6559",
    "\u6210\u4EBA",
    "\u6211\u64CD\u4F60\u7956\u5B97\u5341\u516B\u4EE3",
    "\u6252\u5149",
    "\u6253\u70AE",
    "\u6253\u98DE\u673A",
    "\u62BD\u63D2",
    "\u62DB\u5993",
    "\u63D2\u4F60",
    "\u63D2\u6B7B\u4F60",
    "\u6492\u5C3F",
    "\u64CD\u4F60",
    "\u64CD\u4F60\u5168\u5BB6",
    "\u64CD\u4F60\u5976\u5976",
    "\u64CD\u4F60\u5988",
    "\u64CD\u4F60\u5A18",
    "\u64CD\u4F60\u7956\u5B97",
    "\u64CD\u4F60\u8001\u5988",
    "\u64CD\u4F60\u8001\u6BCD",
    "\u64CD\u59B3",
    "\u64CD\u59B3\u5168\u5BB6",
    "\u64CD\u59B3\u5988",
    "\u64CD\u59B3\u5A18",
    "\u64CD\u59B3\u7956\u5B97",
    "\u64CD\u673A\u63B0",
    "\u64CD\u6BD4",
    "\u64CD\u903C",
    "\u653E\u8361",
    "\u65E5\u4ED6\u5A18",
    "\u65E5\u4F60",
    "\u65E5\u4F60\u5988",
    "\u65E5\u4F60\u8001\u5A18",
    "\u65E5\u4F60\u8001\u6BCD",
    "\u65E5\u6279",
    "\u6708\u7ECF",
    "\u673A\u516B",
    "\u673A\u5DF4",
    "\u673A\u673A\u6B6A\u6B6A",
    "\u6742\u79CD",
    "\u6D6A\u53EB",
    "\u6DEB",
    "\u6DEB\u4E71",
    "\u6DEB\u5987",
    "\u6DEB\u68CD",
    "\u6DEB\u6C34",
    "\u6DEB\u79FD",
    "\u6DEB\u8361",
    "\u6DEB\u897F",
    "\u6E7F\u900F\u7684\u5185\u88E4",
    "\u6FC0\u60C5",
    "\u7068\u4F60\u5A18",
    "\u70C2\u8D27",
    "\u70C2\u903C",
    "\u721B",
    "\u72D7\u5C41",
    "\u72D7\u65E5",
    "\u72D7\u72FC\u517B\u7684",
    "\u7389\u6775",
    "\u738B\u516B\u86CB",
    "\u74DC\u5A03\u5B50",
    "\u74DC\u5A46\u5A18",
    "\u74DC\u6279",
    "\u762A\u4E09",
    "\u767D\u70C2",
    "\u767D\u75F4",
    "\u767D\u7661",
    "\u7956\u5B97",
    "\u79C1\u670D",
    "\u7B28\u86CB",
    "\u7CBE\u5B50",
    "\u8001\u4E8C",
    "\u8001\u5473",
    "\u8001\u6BCD",
    "\u8001\u762A\u4E09",
    "\u8001\u9A9A\u6BD4",
    "\u8001\u9A9A\u8D27",
    "\u8089\u58C1",
    "\u8089\u68CD\u5B50",
    "\u8089\u68D2",
    "\u8089\u7F1D",
    "\u808F",
    "\u809B\u4EA4",
    "\u80A5\u897F",
    "\u8272\u60C5",
    "\u82B1\u67F3",
    "\u8361\u5987",
    "\u8CE4",
    "\u8D1D\u8089",
    "\u8D31B",
    "\u8D31\u4EBA",
    "\u8D31\u8D27",
    "\u8D3C\u4F60\u5988",
    "\u8D5B\u4F60\u8001\u6BCD",
    "\u8D5B\u59B3\u963F\u6BCD",
    "\u8D63\u60A8\u5A18",
    "\u8F6E\u5978",
    "\u8FF7\u836F",
    "\u903C",
    "\u903C\u6837",
    "\u91CE\u9E21",
    "\u9633\u5177",
    "\u9633\u840E",
    "\u9634\u5507",
    "\u9634\u6237",
    "\u9634\u6838",
    "\u9634\u6BDB",
    "\u9634\u830E",
    "\u9634\u9053",
    "\u9634\u90E8",
    "\u96DE\u5DF4",
    "\u9760\u5317",
    "\u9760\u6BCD",
    "\u9760\u7238",
    "\u9760\u80CC",
    "\u9760\u8170",
    "\u9A76\u4F60\u516C",
    "\u9A76\u4F60\u5A18",
    "\u9A76\u4F60\u6BCD",
    "\u9A76\u4F60\u7238",
    "\u9A76\u4F60\u8001\u5E08",
    "\u9A76\u4F60\u8001\u6BCD",
    "\u9A9A\u6BD4",
    "\u9A9A\u8D27",
    "\u9A9A\u903C",
    "\u9B3C\u516C",
    "\u9E218",
    "\u9E21\u516B",
    "\u9E21\u53ED",
    "\u9E21\u5427",
    "\u9E21\u5978",
    "\u9E21\u5DF4",
    "\u9E21\u82AD",
    "\u9E21\u9E21",
    "\u9F9F\u513F\u5B50",
    "\u9F9F\u5934"
  ]
};

// ../../shared/dictionaries/czech.json
var czech_default = {
  words: [
    "bordel",
    "buzna",
    "\u010Dum\u011Bt",
    "\u010Dur\xE1k",
    "debil",
    "do pi\u010De",
    "do prdele",
    "dr\u0161\u0165ka",
    "dr\u017Eka",
    "flundra",
    "hajzl",
    "hovno",
    "chcanky",
    "chuj",
    "jebat",
    "kokot",
    "kokotina",
    "ko\u0148omrd",
    "kunda",
    "kurva",
    "mamrd",
    "mrdat",
    "mrdka",
    "mrdn\xEDk",
    "oslo\u0161oust",
    "pi\u010Da",
    "p\xED\u010Dus",
    "p\xEDchat",
    "pizda",
    "prcat",
    "prdel",
    "prdelka",
    "sra\u010Dka",
    "sr\xE1t",
    "\u0161oustat",
    "\u0161ulin",
    "vyp\xED\u010Denec",
    "zkurvit",
    "zkurvysyn",
    "zmrd",
    "\u017Er\xE1t"
  ]
};

// ../../shared/dictionaries/danish.json
var danish_default = {
  words: [
    "anus",
    "b\xF8sser\xF8v",
    "cock",
    "fisse",
    "fisseh\xE5r",
    "fuck",
    "hestepik",
    "kussekryller",
    "lort",
    "luder",
    "pik",
    "pikh\xE5r",
    "pikslugeri",
    "piksutteri",
    "pis",
    "r\xF8v",
    "r\xF8vhul",
    "r\xF8vsk\xE6g",
    "r\xF8vspr\xE6ke",
    "shit"
  ]
};

// ../../shared/dictionaries/dutch.json
var dutch_default = {
  words: [
    "aardappels afgieten",
    "achter het raam zitten",
    "afberen",
    "aflebberen",
    "afrossen",
    "afrukken",
    "aftrekken",
    "afwerkplaats",
    "afzeiken",
    "afzuigen",
    "anderhalve man en een paardekop",
    "anita",
    "asbak",
    "aso",
    "bagger schijten",
    "balen",
    "bedonderen",
    "befborstel",
    "beffen",
    "bekken",
    "belazeren",
    "besodemieterd zijn",
    "besodemieteren",
    "beurt",
    "boemelen",
    "boerelul",
    "boerenpummel",
    "bokkelul",
    "botergeil",
    "broekhoesten",
    "brugpieper",
    "buffelen",
    "buiten de pot piesen",
    "da's kloten van de bok",
    "de ballen",
    "de hoer spelen",
    "de hond uitlaten",
    "de koffer induiken",
    "del",
    "de pijp aan maarten geven",
    "de pijp uitgaan",
    "dombo",
    "draaikont",
    "driehoog achter wonen",
    "drol",
    "drooggeiler",
    "droogkloot",
    "een beurt geven",
    "een nummertje maken",
    "een wip maken",
    "eikel",
    "engerd",
    "flamoes",
    "flikken",
    "flikker",
    "gadverdamme",
    "galbak",
    "gat",
    "gedoogzone",
    "geilneef",
    "gesodemieter",
    "godverdomme",
    "graftak",
    "gras maaien",
    "gratenkut",
    "greppeldel",
    "griet",
    "hoempert",
    "hoer",
    "hoerenbuurt",
    "hoerenloper",
    "hoerig",
    "hol",
    "hufter",
    "huisdealer",
    "johny",
    "kanen",
    "kettingzeug",
    "klaarkomen",
    "klerebeer",
    "klojo",
    "klooien",
    "klootjesvolk",
    "klootoog",
    "klootzak",
    "kloten",
    "knor",
    "kont",
    "kontneuken",
    "krentekakker",
    "kut",
    "kuttelikkertje",
    "kwakkie",
    "liefdesgrot",
    "lul",
    "lul-de-behanger",
    "lulhannes",
    "lummel",
    "mafketel",
    "matennaaier",
    "matje",
    "mof",
    "muts",
    "naaien",
    "naakt",
    "neuken",
    "neukstier",
    "nicht",
    "oetlul",
    "opgeilen",
    "opkankeren",
    "oprotten",
    "opsodemieteren",
    "op z'n hondjes",
    "op z'n sodemieter geven",
    "opzouten",
    "ouwehoer",
    "ouwehoeren",
    "ouwe rukker",
    "paal",
    "paardelul",
    "palen",
    "penoze",
    "piesen",
    "pijpbekkie",
    "pijpen",
    "pik",
    "pleurislaaier",
    "poep",
    "poepen",
    "poot",
    "portiekslet",
    "pot",
    "potverdorie",
    "publiciteitsgeil",
    "raaskallen",
    "reet",
    "reetridder",
    "reet trappen, voor zijn",
    "remsporen",
    "reutelen",
    "rothoer",
    "rotzak",
    "rukhond",
    "rukken",
    "schatje",
    "schijt",
    "schijten",
    "schoft",
    "schuinsmarcheerder",
    "shit",
    "slempen",
    "slet",
    "sletterig",
    "slik mijn zaad",
    "snol",
    "spuiten",
    "standje",
    "standje-69",
    "stoephoer",
    "stootje",
    "stront",
    "sufferd",
    "tapijtnek",
    "teef",
    "temeier",
    "teringlijer",
    "toeter",
    "tongzoen",
    "triootje",
    "trottoir prostituee",
    "trottoirteef",
    "vergallen",
    "verkloten",
    "verneuken",
    "viespeuk",
    "vingeren",
    "vleesroos",
    "voor jan lul",
    "voor jan-met-de-korte-achternaam",
    "watje",
    "welzijnsmafia",
    "wijf",
    "wippen",
    "wuftje",
    "zaadje",
    "zakkenwasser",
    "zeiken",
    "zeiker",
    "zuigen",
    "zuiplap"
  ]
};

// ../../shared/dictionaries/esperanto.json
var esperanto_default = {
  words: [
    "bugren",
    "bugri",
    "bugru",
    "\u0109iesulino",
    "\u0109iesulo",
    "diofek",
    "diofeka",
    "fek",
    "feken",
    "fekfikanto",
    "feklekulo",
    "fekulo",
    "fik",
    "fikado",
    "fikema",
    "fikfek",
    "fiki",
    "fiki\u011Di",
    "fiki\u011Du",
    "fikilo",
    "fikkla\u016Dno",
    "fikota",
    "fiku",
    "forfiki",
    "forfiki\u011Du",
    "forfiku",
    "forfurzu",
    "forpisi",
    "forpisu",
    "furzulo",
    "kacen",
    "kaco",
    "kacsu\u0109ulo",
    "kojono",
    "pi\u0109en",
    "pi\u0109o",
    "zamenfek"
  ]
};

// ../../shared/dictionaries/english.json
var english_default = {
  words: [
    "2 girls 1 cup",
    "2g1c",
    "a$$",
    "@ss",
    "4ss",
    "acrotomophilia",
    "alabama hot pocket",
    "alaskan pipeline",
    "anal",
    "anilingus",
    "anus",
    "apeshit",
    "arsehole",
    "ass",
    "asshat",
    "asshole",
    "assmunch",
    "asswipe",
    "auto erotic",
    "autoerotic",
    "b!tch",
    "babeland",
    "baby batter",
    "baby juice",
    "ball gag",
    "ball gravy",
    "ball kicking",
    "ball licking",
    "ball sack",
    "ball sucking",
    "bangbros",
    "bareback",
    "barely legal",
    "barenaked",
    "bastard",
    "bastardized",
    "bastardo",
    "bastinado",
    "bbw",
    "bdsm",
    "beaner",
    "beaners",
    "beaver cleaver",
    "beaver lips",
    "bestiality",
    "big black",
    "big breasts",
    "big knockers",
    "big tits",
    "bimbos",
    "birdlock",
    "bitch",
    "bitches",
    "black cock",
    "blonde action",
    "blonde on blonde action",
    "blow job",
    "blow your load",
    "blowjob",
    "blue waffle",
    "blumpkin",
    "bollocks",
    "bondage",
    "boner",
    "boob",
    "boobs",
    "booty call",
    "brown showers",
    "brunette action",
    "bs",
    "bukkake",
    "bulldyke",
    "bullet vibe",
    "bullshit",
    "bung hole",
    "bunghole",
    "busty",
    "butt",
    "buttcheeks",
    "butthole",
    "camel toe",
    "camgirl",
    "camslut",
    "camwhore",
    "carpet muncher",
    "carpetmuncher",
    "chocolate rosebuds",
    "circlejerk",
    "cleveland steamer",
    "clit",
    "clitoris",
    "clover clamps",
    "clusterfuck",
    "cock",
    "cocks",
    "cocksucker",
    "coon",
    "coons",
    "coprolagnia",
    "coprophilia",
    "cornhole",
    "creampie",
    "cum",
    "cumming",
    "cunnilingus",
    "cunt",
    "darkie",
    "date rape",
    "daterape",
    "deep throat",
    "deepthroat",
    "dendrophilia",
    "dick",
    "dildo",
    "dingleberries",
    "dingleberry",
    "dipshit",
    "dirty pillows",
    "dirty sanchez",
    "dog style",
    "doggie style",
    "doggiestyle",
    "doggy style",
    "doggystyle",
    "dolcett",
    "domination",
    "dominatrix",
    "dommes",
    "donkey punch",
    "double dong",
    "double penetration",
    "dp action",
    "dry hump",
    "dumbass",
    "dvda",
    "eat my ass",
    "ecchi",
    "ejaculation",
    "erotic",
    "erotism",
    "escort",
    "eunuch",
    "f*ck",
    "f@ck",
    "f4ck",
    "fvck",
    "phuck",
    "fuk",
    "faggot",
    "fecal",
    "felch",
    "fellatio",
    "feltch",
    "female squirting",
    "femdom",
    "figging",
    "fingerbang",
    "fingering",
    "fisting",
    "foot fetish",
    "footjob",
    "frotting",
    "fuck",
    "fuck buttons",
    "fucker",
    "fuckface",
    "fuckin",
    "fucking",
    "fucktards",
    "fudge packer",
    "fudgepacker",
    "fuq",
    "futanari",
    "g-spot",
    "gang bang",
    "gay sex",
    "genitals",
    "giant cock",
    "girl on",
    "girl on top",
    "girls gone wild",
    "goatcx",
    "goatse",
    "god damn",
    "gokkun",
    "golden shower",
    "goo girl",
    "goodpoop",
    "goregasm",
    "grope",
    "group sex",
    "guro",
    "hand job",
    "handjob",
    "hard core",
    "hardcore",
    "hentai",
    "hoe",
    "homoerotic",
    "honkey",
    "hooker",
    "hot carl",
    "hot chick",
    "how to kill",
    "how to murder",
    "huge fat",
    "humping",
    "incest",
    "intercourse",
    "jack off",
    "jackass",
    "jail bait",
    "jailbait",
    "jelly donut",
    "jerk off",
    "jigaboo",
    "jiggaboo",
    "jiggerboo",
    "jizz",
    "juggs",
    "kike",
    "kinbaku",
    "kinkster",
    "kinky",
    "knobbing",
    "leather restraint",
    "leather straight jacket",
    "lemon party",
    "lmfao",
    "lolita",
    "lovemaking",
    "make me come",
    "male squirting",
    "masturbate",
    "menage a trois",
    "mf",
    "milf",
    "missionary position",
    "motherfucker",
    "mound of venus",
    "mr hands",
    "muff diver",
    "muffdiving",
    "nambla",
    "nawashi",
    "negro",
    "neonazi",
    "nig nog",
    "nigga",
    "nigger",
    "nimphomania",
    "nipple",
    "nipples",
    "nsfw images",
    "nude",
    "nudity",
    "nympho",
    "nymphomania",
    "octopussy",
    "omfg",
    "omorashi",
    "one cup two girls",
    "one guy one jar",
    "orgasm",
    "orgy",
    "paedophile",
    "paki",
    "panties",
    "panty",
    "pedobear",
    "pedophile",
    "pegging",
    "penis",
    "phone sex",
    "piece of shit",
    "piss pig",
    "pissed",
    "pissing",
    "pisspig",
    "playboy",
    "pleasure chest",
    "pole smoker",
    "ponyplay",
    "poof",
    "poon",
    "poontang",
    "poop chute",
    "poopchute",
    "porn",
    "porno",
    "pornography",
    "prince albert piercing",
    "pthc",
    "pubes",
    "punany",
    "pussy",
    "queaf",
    "queef",
    "quim",
    "raghead",
    "raging boner",
    "rape",
    "raping",
    "rapist",
    "rectum",
    "retard",
    "retarded",
    "reverse cowgirl",
    "rimjob",
    "rimming",
    "rosy palm",
    "rosy palm and her 5 sisters",
    "rusty trombone",
    "s&m",
    "sadism",
    "santorum",
    "scat",
    "schlong",
    "scissoring",
    "semen",
    "sex",
    "sexo",
    "sexy",
    "sh!t",
    "shaved beaver",
    "shaved pussy",
    "shemale",
    "shibari",
    "shit",
    "shite",
    "sh1t",
    "$hit",
    "$h!t",
    "shitblimp",
    "shithead",
    "shitshow",
    "shitty",
    "shota",
    "shrimping",
    "simp",
    "skeet",
    "slanteye",
    "slut",
    "smut",
    "snatch",
    "snowballing",
    "sodomize",
    "sodomy",
    "spic",
    "splooge",
    "splooge moose",
    "spooge",
    "spread legs",
    "spunk",
    "stfu",
    "strap on",
    "strapon",
    "strappado",
    "strip club",
    "style doggy",
    "suck",
    "sucks",
    "suicide girls",
    "sultry women",
    "swastika",
    "swinger",
    "tainted love",
    "taste my",
    "tea bagging",
    "testbad01",
    "threesome",
    "throating",
    "tied up",
    "tight white",
    "tit",
    "tits",
    "titties",
    "titty",
    "tongue in a",
    "topless",
    "tosser",
    "towelhead",
    "tranny",
    "tribadism",
    "tub girl",
    "tubgirl",
    "tushy",
    "twat",
    "twink",
    "twinkie",
    "two girls one cup",
    "undressing",
    "upskirt",
    "urethra play",
    "urophilia",
    "vagina",
    "venus mound",
    "vibrator",
    "violet wand",
    "vorarephilia",
    "voyeur",
    "vulva",
    "wank",
    "wet dream",
    "wetback",
    "white power",
    "wrapping men",
    "wrinkled starfish",
    "wtf",
    "xx",
    "xxx",
    "yaoi",
    "yellow showers",
    "yiffy",
    "zoophilia",
    "\u{1F595}"
  ]
};

// ../../shared/dictionaries/finnish.json
var finnish_default = {
  words: [
    "alfred nussi",
    "bylsi\xE4",
    "haahka",
    "haista paska",
    "haista vittu",
    "hatullinen",
    "helvetisti",
    "hevonkuusi",
    "hevonpaska",
    "hevonperse",
    "hevonvittu",
    "hevonvitunperse",
    "hitosti",
    "hitto",
    "huorata",
    "h\xE4ssi\xE4",
    "juosten kustu",
    "jutku",
    "jutsku",
    "j\xE4tk\xE4",
    "kananpaska",
    "koiranpaska",
    "kuin esterin perseest\xE4",
    "kulli",
    "kullinluikaus",
    "kuppainen",
    "kusaista",
    "kuseksia",
    "kusettaa",
    "kusi",
    "kusip\xE4\xE4",
    "kusta",
    "kyrpiintynyt",
    "kyrpiinty\xE4",
    "kyrpi\xE4",
    "kyrp\xE4",
    "kyrp\xE4naama",
    "kyrvitys",
    "lahtari",
    "lutka",
    "molo",
    "molop\xE4\xE4",
    "mulkero",
    "mulkku",
    "mulkvisti",
    "muna",
    "munap\xE4\xE4",
    "munaton",
    "mutakuono",
    "mutiainen",
    "naida",
    "nainti",
    "narttu",
    "neekeri",
    "nekru",
    "nuolla persett\xE4",
    "nussia",
    "nussija",
    "nussinta",
    "paljaalla",
    "palli",
    "pallit",
    "paneskella",
    "panettaa",
    "panna",
    "pano",
    "pantava",
    "paska",
    "paskainen",
    "paskamainen",
    "paskanmarjat",
    "paskantaa",
    "paskapuhe",
    "paskap\xE4\xE4",
    "paskattaa",
    "paskiainen",
    "paskoa",
    "pehko",
    "pentele",
    "perkele",
    "perkeleesti",
    "persaukinen",
    "perse",
    "perseennuolija",
    "perseet olalla",
    "persereik\xE4",
    "perse\xE4\xE4li\xF6",
    "persl\xE4pi",
    "perspano",
    "persvako",
    "pilkunnussija",
    "pillu",
    "pillut",
    "pipari",
    "piru",
    "pist\xE4\xE4",
    "pyllyvako",
    "reik\xE4",
    "reva",
    "ripsipiirakka",
    "runkata",
    "runkkari",
    "runkkaus",
    "runkku",
    "ryss\xE4",
    "r\xE4ttip\xE4\xE4",
    "saatanasti",
    "suklaaosasto",
    "tavara",
    "toosa",
    "tuhkaluukku",
    "tumputtaa",
    "turpasauna",
    "tussu",
    "tussukka",
    "tussut",
    "vakipano",
    "vet\xE4\xE4 k\xE4teen",
    "viiksi",
    "vittu",
    "vittuilla",
    "vittuilu",
    "vittumainen",
    "vittuuntua",
    "vittuuntunut",
    "vitun",
    "vitusti",
    "vituttaa",
    "vitutus",
    "\xE4p\xE4r\xE4"
  ]
};

// ../../shared/dictionaries/german.json
var german_default = {
  words: [
    "analritter",
    "arsch",
    "arschficker",
    "arschlecker",
    "arschloch",
    "bimbo",
    "bratze",
    "bumsen",
    "bonze",
    "d\xF6del",
    "fick",
    "ficken",
    "flittchen",
    "fotze",
    "fratze",
    "hackfresse",
    "hure",
    "hurensohn",
    "ische",
    "kackbratze",
    "kacke",
    "kacken",
    "kackwurst",
    "kampflesbe",
    "kanake",
    "kimme",
    "l\xFCmmel",
    "MILF",
    "m\xF6pse",
    "morgenlatte",
    "m\xF6se",
    "mufti",
    "muschi",
    "nackt",
    "neger",
    "nigger",
    "nippel",
    "nutte",
    "onanieren",
    "orgasmus",
    "pimmel",
    "pimpern",
    "pinkeln",
    "pissen",
    "pisser",
    "popel",
    "poppen",
    "porno",
    "reudig",
    "rosette",
    "schabracke",
    "schlampe",
    "schei\xDFe",
    "scheisser",
    "schiesser",
    "schnackeln",
    "schwanzlutscher",
    "schwuchtel",
    "tittchen",
    "titten",
    "v\xF6geln",
    "vollpfosten",
    "wichse",
    "wichsen",
    "wichser"
  ]
};

// ../../shared/dictionaries/hindi.json
var hindi_default = {
  words: [
    "aand",
    "aandu",
    "balatkar",
    "beti chod",
    "bhadva",
    "bhadve",
    "bhandve",
    "bhootni ke",
    "bhosad",
    "bhosadi ke",
    "boobe",
    "chakke",
    "chinaal",
    "chinki",
    "chod",
    "chodu",
    "chodu bhagat",
    "chooche",
    "choochi",
    "choot",
    "choot ke baal",
    "chootia",
    "chootiya",
    "chuche",
    "chuchi",
    "chudai khanaa",
    "chudan chudai",
    "chut",
    "chut ke baal",
    "chut ke dhakkan",
    "chut maarli",
    "chutad",
    "chutadd",
    "chutan",
    "chutia",
    "chutiya",
    "gaand",
    "gaandfat",
    "gaandmasti",
    "gaandufad",
    "gandu",
    "gashti",
    "gasti",
    "ghassa",
    "ghasti",
    "harami",
    "haramzade",
    "hawas",
    "hawas ke pujari",
    "hijda",
    "hijra",
    "jhant",
    "jhant chaatu",
    "jhant ke baal",
    "jhantu",
    "kamine",
    "kaminey",
    "kanjar",
    "kutta",
    "kutta kamina",
    "kutte ki aulad",
    "kutte ki jat",
    "kuttiya",
    "loda",
    "lodu",
    "lund",
    "lund choos",
    "lund khajoor",
    "lundtopi",
    "lundure",
    "maa ki chut",
    "maal",
    "madar chod",
    "mooh mein le",
    "mutth",
    "najayaz",
    "najayaz aulaad",
    "najayaz paidaish",
    "paki",
    "pataka",
    "patakha",
    "raand",
    "randi",
    "saala",
    "saala kutta",
    "saali kutti",
    "saali randi",
    "suar",
    "suar ki aulad",
    "tatte",
    "tatti",
    "teri maa ka bhosada",
    "teri maa ka boba chusu",
    "teri maa ki chut",
    "tharak",
    "tharki"
  ]
};

// ../../shared/dictionaries/hungarian.json
var hungarian_default = {
  words: [
    "balfasz",
    "balfaszok",
    "balfaszokat",
    "balfaszt",
    "barmok",
    "barmokat",
    "barmot",
    "barom",
    "baszik",
    "bazmeg",
    "buksza",
    "buksz\xE1k",
    "buksz\xE1kat",
    "buksz\xE1t",
    "b\xFAr",
    "b\xFArok",
    "cs\xF6cs",
    "cs\xF6cs\xF6k",
    "cs\xF6cs\xF6ket",
    "cs\xF6cs\xF6t",
    "fasz",
    "faszfej",
    "faszfejek",
    "faszfejeket",
    "faszfejet",
    "faszok",
    "faszokat",
    "faszt",
    "fing",
    "fingok",
    "fingokat",
    "fingot",
    "franc",
    "francok",
    "francokat",
    "francot",
    "geci",
    "gecibb",
    "gecik",
    "geciket",
    "gecit",
    "kibaszott",
    "kibaszottabb",
    "k\xFAr",
    "kurafi",
    "kurafik",
    "kurafikat",
    "kurafit",
    "kurva",
    "kurv\xE1k",
    "kurv\xE1kat",
    "kurv\xE1t",
    "leggecibb",
    "legkibaszottabb",
    "legszarabb",
    "marha",
    "marh\xE1k",
    "marh\xE1kat",
    "marh\xE1t",
    "megd\xF6glik",
    "pele",
    "pel\xE9k",
    "picsa",
    "pics\xE1kat",
    "pics\xE1t",
    "pina",
    "pin\xE1k",
    "pin\xE1kat",
    "pin\xE1t",
    "pofa",
    "pof\xE1kat",
    "pof\xE1t",
    "p\xF6cs",
    "p\xF6cs\xF6k",
    "p\xF6cs\xF6ket",
    "p\xF6cs\xF6t",
    "punci",
    "puncik",
    "segg",
    "seggek",
    "seggeket",
    "segget",
    "seggfej",
    "seggfejek",
    "seggfejeket",
    "seggfejet",
    "szajha",
    "szajh\xE1k",
    "szajh\xE1kat",
    "szajh\xE1t",
    "szar",
    "szarabb",
    "szarik",
    "szarok",
    "szarokat",
    "szart"
  ]
};

// ../../shared/dictionaries/korean.json
var korean_default = {
  words: [
    "\uAC15\uAC04",
    "\uAC1C\uC0C8\uB07C",
    "\uAC1C\uC790\uC2DD",
    "\uAC1C\uC886",
    "\uAC1C\uCC28\uBC18",
    "\uAC70\uC720",
    "\uACC4\uC9D1\uB144",
    "\uACE0\uC790",
    "\uADFC\uCE5C",
    "\uB178\uBAA8",
    "\uB2C8\uAE30\uBBF8",
    "\uB4A4\uC9C8\uB798",
    "\uB538\uB538\uC774",
    "\uB54C\uC539",
    "\uB610\uB77C\uC774",
    "\uB664\uB188",
    "\uB85C\uB9AC\uD0C0",
    "\uB9DD\uAC00",
    "\uBAB0\uCE74",
    "\uBBF8\uCE5C",
    "\uBBF8\uCE5C\uC0C8\uB07C",
    "\uBC14\uBC14\uB9AC\uB9E8",
    "\uBCC0\uD0DC",
    "\uBCD1\uC2E0",
    "\uBCF4\uC9C0",
    "\uBD88\uC54C",
    "\uBE60\uAD6C\uB9AC",
    "\uC0AC\uAE4C\uC2DC",
    "\uC139\uC2A4",
    "\uC2A4\uC640\uD551",
    "\uC30D\uB188",
    "\uC528\uBC1C",
    "\uC528\uBC1C\uB188",
    "\uC528\uD314",
    "\uC539",
    "\uC539\uBB3C",
    "\uC539\uBE68",
    "\uC539\uC0C8\uB07C",
    "\uC539\uC54C",
    "\uC539\uCC3D",
    "\uC539\uD314",
    "\uC554\uCE90",
    "\uC560\uC790",
    "\uC57C\uB3D9",
    "\uC57C\uC0AC",
    "\uC57C\uC560\uB2C8",
    "\uC5C4\uCC3D",
    "\uC5D0\uB85C",
    "\uC5FC\uBCD1",
    "\uC618\uBCD1",
    "\uC720\uBAA8",
    "\uC721\uAC11",
    "\uC740\uAF34",
    "\uC790\uC704",
    "\uC790\uC9C0",
    "\uC7A1\uB144",
    "\uC885\uAC04\uB098",
    "\uC886",
    "\uC886\uB9CC",
    "\uC8FD\uC77C\uB144",
    "\uC950\uC886",
    "\uC9C1\uCD2C",
    "\uC9F1\uAE68",
    "\uCABD\uBC14\uB9AC",
    "\uCC3D\uB140",
    "\uD3EC\uB974\uB178",
    "\uD558\uB4DC\uCF54\uC5B4",
    "\uD638\uB85C",
    "\uD654\uB0E5\uB144",
    "\uD6C4\uB808\uC544\uB4E4",
    "\uD6C4\uC7A5",
    "\uD76C\uCB48\uADF8\uB9AC"
  ]
};

// ../../shared/dictionaries/norwegian.json
var norwegian_default = {
  words: [
    "drittsekk",
    "faen i helvete",
    "fitte",
    "j\xE6vla",
    "kuk",
    "kukene",
    "kuker",
    "nigger",
    "pikk",
    "sotr\xF8r",
    "st\xE5pikk",
    "st\xE5pikkene",
    "st\xE5pikker"
  ]
};

// ../../shared/dictionaries/persian.json
var persian_default = {
  words: [
    "\u0622\u0628 \u06A9\u06CC\u0631",
    "\u0627\u0631\u06AF\u0627\u0633\u0645",
    "\u0628\u0631\u0647\u0646\u0647",
    "\u067E\u0648\u0631\u0646",
    "\u067E\u0648\u0631\u0646\u0648",
    "\u062A\u062C\u0627\u0648\u0632",
    "\u062A\u062E\u0645\u06CC",
    "\u062C\u0642",
    "\u062C\u0642\u06CC",
    "\u062C\u0644\u0642",
    "\u062C\u0646\u062F\u0647",
    "\u0686\u0648\u0686\u0648\u0644",
    "\u062D\u0634\u0631",
    "\u062D\u0634\u0631\u06CC",
    "\u062F\u0627\u0641",
    "\u062F\u0648\u062F\u0648\u0644",
    "\u0633\u0627\u06A9 \u0632\u062F\u0646",
    "\u0633\u06A9\u0633",
    "\u0633\u06A9\u0633 \u06A9\u0631\u062F\u0646",
    "\u0633\u06A9\u0633\u06CC",
    "\u0633\u0648\u067E\u0631",
    "\u0634\u0642 \u06A9\u0631\u062F\u0646",
    "\u0634\u0647\u0648\u062A",
    "\u0634\u0647\u0648\u062A\u06CC",
    "\u0634\u0648\u0646\u0628\u0648\u0644",
    "\u0641\u06CC\u0644\u0645 \u0633\u0648\u067E\u0631",
    "\u06A9\u0633",
    "\u06A9\u0633 \u062F\u0627\u062F\u0646",
    "\u06A9\u0633 \u06A9\u0631\u062F\u0646",
    "\u06A9\u0633\u06A9\u0634",
    "\u06A9\u0648\u0633",
    "\u06A9\u0648\u0646",
    "\u06A9\u0648\u0646 \u062F\u0627\u062F\u0646",
    "\u06A9\u0648\u0646 \u06A9\u0631\u062F\u0646",
    "\u06A9\u0648\u0646\u06A9\u0634",
    "\u06A9\u0648\u0646\u06CC",
    "\u06A9\u06CC\u0631",
    "\u06A9\u06CC\u0631\u06CC",
    "\u0644\u0627\u067E\u0627",
    "\u0644\u0627\u067E\u0627\u06CC\u06CC",
    "\u0644\u0627\u0634\u06CC",
    "\u0644\u062E\u062A",
    "\u0644\u0634",
    "\u0645\u0646\u06CC",
    "\u0647\u0631\u0632\u0647"
  ]
};

// ../../shared/dictionaries/polish.json
var polish_default = {
  words: [
    "burdel",
    "burdelmama",
    "chuj",
    "chujnia",
    "ciota",
    "cipa",
    "cyc",
    "debil",
    "dmucha\u0107",
    "do kurwy n\u0119dzy",
    "dupa",
    "dupek",
    "duperele",
    "dziwka",
    "fiut",
    "g\xF3wno",
    "g\xF3wno prawda",
    "huj",
    "jajco",
    "jajeczko",
    "jajko",
    "jajo",
    "ja pierdol\u0119",
    "jeba\u0107",
    "jebany",
    "kurwa",
    "kurwy",
    "kutafon",
    "kutas",
    "liza\u0107 pa\u0142\u0119",
    "obci\u0105ga\u0107 chuja",
    "obci\u0105ga\u0107 fiuta",
    "obci\u0105ga\u0107 loda",
    "pieprzy\u0107",
    "pierdolec",
    "pierdoli\u0107",
    "pierdolni\u0119ty",
    "pierdo\u0142a",
    "pierdzie\u0107",
    "pizda",
    "pojeb",
    "popierdolony",
    "robic loda",
    "robi\u0107 loda",
    "rucha\u0107",
    "rzyga\u0107",
    "skurwysyn",
    "sraczka",
    "sra\u0107",
    "suka",
    "syf",
    "wkurwia\u0107",
    "zajebisty"
  ]
};

// ../../shared/dictionaries/portuguese.json
var portuguese_default = {
  words: [
    "aborto",
    "amador",
    "\xE2nus",
    "aranha",
    "ariano",
    "balalao",
    "bastardo",
    "bicha",
    "biscate",
    "bissexual",
    "boceta",
    "boob",
    "bosta",
    "braulio de borracha",
    "bumbum",
    "burro",
    "cabrao",
    "cacete",
    "cagar",
    "camisinha",
    "caralho",
    "cerveja",
    "chochota",
    "chupar",
    "clitoris",
    "coca\xEDna",
    "colhoes",
    "comer",
    "cona",
    "consolo",
    "corno",
    "cu",
    "dar o rabo",
    "dum raio",
    "esporra",
    "fecal",
    "filho da puta",
    "foda",
    "foda-se",
    "foder",
    "frango assado",
    "gozar",
    "grelho",
    "hero\xEDna",
    "heterosexual",
    "homem gay",
    "homoer\xF3tico",
    "homosexual",
    "inferno",
    "l\xE9sbica",
    "lolita",
    "mama",
    "merda",
    "paneleiro",
    "passar um cheque",
    "pau",
    "peidar",
    "p\xEAnis",
    "pinto",
    "porra",
    "puta",
    "puta que pariu",
    "puta que te pariu",
    "queca",
    "sacanagem",
    "saco",
    "torneira",
    "transar",
    "vai-te foder",
    "vai tomar no cu",
    "veado",
    "vibrador",
    "xana",
    "xochota"
  ]
};

// ../../shared/dictionaries/russian.json
var russian_default = {
  words: [
    "bychara",
    "byk",
    "chernozhopyi",
    "dolboy'eb",
    "ebalnik",
    "ebalo",
    "ebalom sch'elkat",
    "gol",
    "mudack",
    "opizdenet",
    "osto'eblo",
    "ostokhuitel'no",
    "ot'ebis",
    "otmudohat",
    "otpizdit",
    "otsosi",
    "padlo",
    "pedik",
    "perdet",
    "petuh",
    "pidar gnoinyj",
    "pizda",
    "pizdato",
    "pizdatyi",
    "piz'det",
    "pizdetc",
    "pizdoi nakryt'sja",
    "pizd'uk",
    "piz`dyulina",
    "podi ku'evo",
    "poeben",
    "po'imat' na konchik",
    "po'iti posrat",
    "po khuy",
    "poluchit pizdy",
    "pososi moyu konfetku",
    "prissat",
    "proebat",
    "promudobl'adsksya pizdopro'ebina",
    "propezdoloch",
    "prosrat",
    "raspeezdeyi",
    "raspizdatyi",
    "raz'yebuy",
    "raz'yoba",
    "s'ebat'sya",
    "shalava",
    "styervo",
    "sukin syn",
    "svodit posrat",
    "svoloch",
    "trakhat'sya",
    "trimandoblydskiy pizdoproyob",
    "ubl'yudok",
    "uboy",
    "u'ebitsche",
    "vafl'a",
    "vafli lovit",
    "v pizdu",
    "vyperdysh",
    "vzdrochennyi",
    "yeb vas",
    "za'ebat",
    "zaebis",
    "zalupa",
    "zalupat",
    "zasranetc",
    "zassat",
    "zlo'ebuchy",
    "\u0431\u0430\u0440\u0434\u0430\u043A",
    "\u0431\u0437\u0434\u0451\u043D\u043E\u043A",
    "\u0431\u043B\u044F\u0434\u043A\u0438",
    "\u0431\u043B\u044F\u0434\u043E\u0432\u0430\u0442\u044C",
    "\u0431\u043B\u044F\u0434\u0441\u0442\u0432\u043E",
    "\u0431\u043B\u044F\u0434\u044C",
    "\u0431\u0443\u0433\u043E\u0440",
    "\u0432\u043E \u043F\u0438\u0437\u0434\u0443",
    "\u0432\u0441\u0442\u0430\u0442\u044C \u0440\u0430\u043A\u043E\u043C",
    "\u0432\u044B\u0451\u0431\u044B\u0432\u0430\u0442\u044C\u0441\u044F",
    "\u0433\u0430\u043D\u0434\u043E\u043D",
    "\u0433\u043E\u0432\u043D\u043E",
    "\u0433\u043E\u0432\u043D\u044E\u043A",
    "\u0433\u043E\u043B\u044B\u0439",
    "\u0434\u0430\u0442\u044C \u043F\u0438\u0437\u0434\u044B",
    "\u0434\u0435\u0440\u044C\u043C\u043E",
    "\u0434\u0440\u043E\u0447\u0438\u0442\u044C",
    "\u0434\u0440\u0443\u0433\u043E\u0439 \u0434\u0440\u0430\u0437\u043D\u0438\u0442\u0441\u044F",
    "\u0451\u0431\u0430\u0440\u044C",
    "\u0435\u0431\u0430\u0442\u044C",
    "\u0435\u0431\u0430\u0442\u044C-\u043A\u043E\u043F\u0430\u0442\u044C",
    "\u0435\u0431\u043B\u043E",
    "\u0435\u0431\u043D\u0443\u0442\u044C",
    "\u0451\u0431 \u0442\u0432\u043E\u044E \u043C\u0430\u0442\u044C",
    "\u0436\u043E\u043F\u0430",
    "\u0436\u043E\u043F\u043E\u043B\u0438\u0437",
    "\u0438\u0433\u0440\u0430\u0442\u044C \u043D\u0430 \u043A\u043E\u0436\u0430\u043D\u043E\u0439 \u0444\u043B\u0435\u0439\u0442\u0435",
    "\u0438\u0437\u043C\u0443\u0434\u043E\u0445\u0430\u0442\u044C",
    "\u043A\u0430\u0436\u0434\u044B\u0439 \u0434\u0440\u043E\u0447\u0438\u0442 \u043A\u0430\u043A \u043E\u043D \u0445\u043E\u0447\u0435\u0442",
    "\u043A\u0430\u043A\u0430\u044F \u0440\u0430\u0437\u043D\u0438\u0446\u0430",
    "\u043A\u0430\u043A \u0434\u0432\u0430 \u043F\u0430\u043B\u044C\u0446\u0430 \u043E\u0431\u043E\u0441\u0441\u0430\u0442\u044C",
    "\u043A\u0443\u0440\u0438\u0442\u0435 \u043C\u043E\u044E \u0442\u0440\u0443\u0431\u043A\u0443",
    "\u043B\u044B\u0441\u043E\u0433\u043E \u0432 \u043A\u0443\u043B\u0430\u043A\u0435 \u0433\u043E\u043D\u044F\u0442\u044C",
    "\u043C\u0430\u043B\u043E\u0444\u044F",
    "\u043C\u0430\u043D\u0434\u0430",
    "\u043C\u0430\u043D\u0434\u0430\u0432\u043E\u0448\u043A\u0430",
    "\u043C\u0435\u043D\u0442",
    "\u043C\u0443\u0434\u0430",
    "\u043C\u0443\u0434\u0438\u043B\u043E",
    "\u043C\u0443\u0434\u043E\u0437\u043C\u043E\u043D",
    "\u043D\u0430\u0435\u0431\u0430\u0442\u044C",
    "\u043D\u0430\u0435\u0431\u0435\u043D\u0438\u0442\u044C\u0441\u044F",
    "\u043D\u0430\u0435\u0431\u043D\u0443\u0442\u044C\u0441\u044F",
    "\u043D\u0430 \u0444\u0438\u0433",
    "\u043D\u0430 \u0445\u0443\u0439",
    "\u043D\u0430 \u0445\u0443\u044E \u0432\u0435\u0440\u0442\u0435\u0442\u044C",
    "\u043D\u0430 \u0445\u0443\u044F",
    "\u043D\u0430\u0445\u0443\u044F\u0447\u0438\u0442\u044C\u0441\u044F",
    "\u043D\u0435\u0432\u0435\u0431\u0435\u043D\u043D\u044B\u0439",
    "\u043D\u0435 \u0435\u0431\u0435\u0442",
    "\u043D\u0438 \u0437\u0430 \u0445\u0443\u0439 \u0441\u043E\u0431\u0430\u0447\u0443",
    "\u043D\u0438 \u0445\u0443\u044F",
    "\u043E\u0431\u043D\u0430\u0436\u0435\u043D\u043D\u044B\u0439",
    "\u043E\u0431\u043E\u0441\u0441\u0430\u0442\u044C\u0441\u044F \u043C\u043E\u0436\u043D\u043E",
    "\u043E\u0434\u0438\u043D \u0435\u0431\u0451\u0442\u0441\u044F",
    "\u043E\u043F\u0435\u0441\u0434\u043E\u043B",
    "\u043E\u0444\u0438\u0433\u0435\u0442\u044C",
    "\u043E\u0445\u0443\u0435\u0442\u044C",
    "\u043E\u0445\u0443\u0439\u0442\u0435\u043B\u044C\u043D\u043E",
    "\u043F\u043E\u043B\u043E\u0432\u043E\u0435 \u0441\u043D\u043E\u0448\u0435\u043D\u0438\u0435",
    "\u0441\u0435\u043A\u0441",
    "\u0441\u0438\u0441\u043A\u0438",
    "\u0441\u043F\u0438\u0437\u0434\u0438\u0442\u044C",
    "\u0441\u0440\u0430\u0442\u044C",
    "\u0441\u0441\u0430\u0442\u044C",
    "\u0442\u0440\u0430x\u0430\u0442\u044C",
    "\u0442\u044B \u043C\u043D\u0435 \u0432\u0430\u043D\u044C\u043A\u0443 \u043D\u0435 \u0432\u0430\u043B\u044F\u0439",
    "\u0444\u0438\u0433\u0430",
    "\u0445\u0430\u043F\u0430\u0442\u044C",
    "\u0445\u0435\u0440 \u0441 \u043D\u0435\u0439",
    "\u0445\u0435\u0440 \u0441 \u043D\u0438\u043C",
    "\u0445\u043E\u0445\u043E\u043B",
    "\u0445\u0440\u0435\u043D",
    "\u0445\u0443\u0451\u0432\u043E",
    "\u0445\u0443\u0451\u0432\u044B\u0439",
    "\u0445\u0443\u0435\u043C \u0433\u0440\u0443\u0448\u0438 \u043E\u043A\u043E\u043B\u0430\u0447\u0438\u0432\u0430\u0442\u044C",
    "\u0445\u0443\u0435\u043F\u043B\u0435\u0442",
    "\u0445\u0443\u0438\u043B\u043E",
    "\u0445\u0443\u0438\u043D\u0435\u0439 \u0441\u0442\u0440\u0430\u0434\u0430\u0442\u044C",
    "\u0445\u0443\u0438\u043D\u044F",
    "\u0445\u0443\u0439",
    "\u0445\u0443\u0439\u043D\u0443\u0442\u044C",
    "\u0445\u0443\u0439 \u043F\u0438\u043D\u0430\u0442\u044C"
  ]
};

// ../../shared/dictionaries/french.json
var french_default = {
  words: [
    "baiser",
    "bander",
    "bigornette",
    "bite",
    "bitte",
    "bloblos",
    "bordel",
    "bosser",
    "bourr\xE9",
    "bourr\xE9e",
    "brackmard",
    "branlage",
    "branler",
    "branlette",
    "branleur",
    "branleuse",
    "brouter le cresson",
    "caca",
    "cailler",
    "chatte",
    "chiasse",
    "chier",
    "chiottes",
    "clito",
    "clitoris",
    "con",
    "connard",
    "connasse",
    "conne",
    "couilles",
    "cramouille",
    "cul",
    "d\xE9conne",
    "d\xE9conner",
    "drague",
    "emmerdant",
    "emmerder",
    "emmerdeur",
    "emmerdeuse",
    "encul\xE9",
    "encul\xE9e",
    "enculeur",
    "enculeurs",
    "enfoir\xE9",
    "enfoir\xE9e",
    "\xE9tron",
    "fille de pute",
    "fils de pute",
    "folle",
    "foutre",
    "gerbe",
    "gerber",
    "gouine",
    "grande folle",
    "grogniasse",
    "gueule",
    "jouir",
    "la putain de ta m\xE8re",
    "MALPT",
    "m\xE9nage \xE0 trois",
    "merde",
    "merdeuse",
    "merdeux",
    "meuf",
    "n\xE8gre",
    "nique ta m\xE8re",
    "nique ta race",
    "palucher",
    "p\xE9dale",
    "p\xE9d\xE9",
    "p\xE9ter",
    "pipi",
    "pisser",
    "pouffiasse",
    "pousse-crotte",
    "putain",
    "pute",
    "ramoner",
    "sac \xE0 foutre",
    "sac \xE0 merde",
    "salaud",
    "salope",
    "suce",
    "tapette",
    "tanche",
    "teuch",
    "teuf",
    "tringler",
    "trique",
    "troncher",
    "trou du cul",
    "turlute",
    "veuve",
    "zigounette",
    "zizi"
  ]
};

// ../../shared/dictionaries/italian.json
var italian_default = {
  words: [
    "allupato",
    "ammucchiata",
    "anale",
    "arrapato",
    "arrusa",
    "arruso",
    "assatanato",
    "bagascia",
    "bagassa",
    "bagnarsi",
    "baldracca",
    "balle",
    "battere",
    "battona",
    "belino",
    "biga",
    "bocchinara",
    "bocchino",
    "bofilo",
    "boiata",
    "bordello",
    "brinca",
    "bucaiolo",
    "budi\xF9lo",
    "buona donna",
    "busone",
    "cacca",
    "caccati in mano e prenditi a schiaffi",
    "caciocappella",
    "cadavere",
    "cagare",
    "cagata",
    "cagna",
    "cammello",
    "cappella",
    "carciofo",
    "carit\xE0",
    "casci",
    "cazzata",
    "cazzimma",
    "cazzo",
    "checca",
    "chiappa",
    "chiavare",
    "chiavata",
    "ciospo",
    "ciucciami il cazzo",
    "coglione",
    "coglioni",
    "cornuto",
    "cozza",
    "culattina",
    "culattone",
    "culo",
    "di merda",
    "ditalino",
    "duro",
    "fare una sega",
    "fava",
    "femminuccia",
    "fica",
    "figa",
    "figlio di buona donna",
    "figlio di puttana",
    "figone",
    "finocchio",
    "fottere",
    "fottersi",
    "fracicone",
    "fregna",
    "frocio",
    "froscio",
    "fuori come un balcone",
    "goldone",
    "grilletto",
    "guanto",
    "guardone",
    "incazzarsi",
    "incoglionirsi",
    "ingoio",
    "l'arte bolognese",
    "leccaculo",
    "lecchino",
    "lofare",
    "loffa",
    "loffare",
    "lumaca",
    "manico",
    "mannaggia",
    "merda",
    "merdata",
    "merdoso",
    "mignotta",
    "minchia",
    "minchione",
    "mona",
    "monta",
    "montare",
    "mussa",
    "nave scuola",
    "nerchia",
    "nudo",
    "padulo",
    "palle",
    "palloso",
    "patacca",
    "patonza",
    "pecorina",
    "pesce",
    "picio",
    "pincare",
    "pipa",
    "pip\xEC",
    "pippone",
    "pirla",
    "pisciare",
    "piscio",
    "pisello",
    "pistola",
    "pistolotto",
    "pomiciare",
    "pompa",
    "pompino",
    "porca",
    "porca madonna",
    "porca miseria",
    "porca puttana",
    "porco due",
    "porco zio",
    "potta",
    "puppami",
    "puttana",
    "quaglia",
    "recchione",
    "regina",
    "rincoglionire",
    "rizzarsi",
    "rompiballe",
    "ruffiano",
    "sbattere",
    "sbattersi",
    "sborra",
    "sborrata",
    "sborrone",
    "sbrodolata",
    "scopare",
    "scopata",
    "scorreggiare",
    "sega",
    "slinguare",
    "slinguata",
    "smandrappata",
    "soccia",
    "socmel",
    "sorca",
    "spagnola",
    "spompinare",
    "sticchio",
    "stronza",
    "stronzata",
    "stronzo",
    "succhiami",
    "sveltina",
    "sverginare",
    "tarzanello",
    "terrone",
    "testa di cazzo",
    "tette",
    "tirare",
    "topa",
    "troia",
    "trombare",
    "uccello",
    "vacca",
    "vaffanculo",
    "vangare",
    "venire",
    "zinne",
    "zio cantante",
    "zoccola"
  ]
};

// ../../shared/dictionaries/japanese.json
var japanese_default = {
  words: [
    "3p",
    "g \u30B9\u30DD\u30C3\u30C8",
    "s \uFF06 m",
    "sm",
    "sm\u5973\u738B",
    "xx",
    "\u30A2\u30B8\u30A2\u306E\u304B\u308F\u3044\u3044\u5973\u306E\u5B50",
    "\u30A2\u30B9\u30DB\u30FC\u30EB",
    "\u30A2\u30CA\u30EA\u30F3\u30B0\u30B9",
    "\u30A2\u30CA\u30EB",
    "\u3044\u305F\u305A\u3089",
    "\u30A4\u30E9\u30DE\u30C1\u30AA",
    "\u30A6\u30A7\u30D6\u30AB\u30E1\u30E9",
    "\u30A8\u30AF\u30B9\u30BF\u30B7\u30FC",
    "\u30A8\u30B9\u30B3\u30FC\u30C8",
    "\u30A8\u30C3\u30C1",
    "\u30A8\u30ED\u30C6\u30A3\u30BA\u30E0",
    "\u30A8\u30ED\u30C6\u30A3\u30C3\u30AF",
    "\u30AA\u30FC\u30AC\u30BA\u30E0",
    "\u30AA\u30AB\u30DE",
    "\u304A\u3057\u3063\u3053",
    "\u304A\u3057\u308A",
    "\u30AA\u30B7\u30EA",
    "\u304A\u3057\u308A\u306E\u3042\u306A",
    "\u304A\u3063\u3071\u3044",
    "\u30AA\u30C3\u30D1\u30A4",
    "\u30AA\u30CA\u30CB\u30FC",
    "\u30AA\u30DE\u30F3\u30B3",
    "\u304A\u3082\u3089\u3057",
    "\u304A\u5C3B",
    "\u30AB\u30FC\u30DE\u30B9\u30FC\u30C8\u30E9",
    "\u30AB\u30F3\u30C8",
    "\u30AF\u30EA\u30C8\u30EA\u30B9",
    "\u30B0\u30EB\u30FC\u30D7\u30FB\u30BB\u30C3\u30AF\u30B9",
    "\u30B0\u30ED",
    "\u30AF\u30F3\u30CB\u30EA\u30F3\u30B0\u30B9",
    "\u30B2\u30A4\u30FB\u30BB\u30C3\u30AF\u30B9",
    "\u30B2\u30A4\u306E\u7537\u6027",
    "\u30B2\u30A4\u30DC\u30FC\u30A4",
    "\u30B4\u30FC\u30EB\u30C7\u30F3\u30B7\u30E3\u30EF\u30FC",
    "\u30B3\u30AB\u30A4\u30F3",
    "\u30B4\u30C3\u30AF\u30F3",
    "\u30B5\u30C7\u30A3\u30BA\u30E0",
    "\u3057\u3070\u308A",
    "\u30B9\u30A6\u30A3\u30F3\u30AC\u30FC",
    "\u30B9\u30AB\u30FC\u30C8\u306E\u4E2D",
    "\u30B9\u30AB\u30C8\u30ED",
    "\u30B9\u30C8\u30E9\u30C3\u30D7\u30AA\u30F3",
    "\u30B9\u30C8\u30EA\u30C3\u30D7\u5287\u5834",
    "\u30B9\u30E9\u30C3\u30C8",
    "\u30B9\u30EA\u30C3\u30C8",
    "\u30BB\u30AF\u30B7\u30FC\u306A",
    "\u30BB\u30AF\u30B7\u30FC\u306A 10 \u4EE3",
    "\u30BB\u30C3\u30AF\u30B9",
    "\u30BD\u30C9\u30DF\u30FC",
    "\u3061\u3093\u3053",
    "\u30C7\u30A3\u30FC\u30D7\u30FB\u30B9\u30ED\u30FC\u30C8",
    "\u30C7\u30A3\u30C3\u30AF",
    "\u30C7\u30A3\u30EB\u30C9",
    "\u30C7\u30FC\u30C8\u30EC\u30A4\u30D7",
    "\u30C7\u30D6",
    "\u30C6\u30EC\u30D5\u30A9\u30F3\u30BB\u30C3\u30AF\u30B9",
    "\u30C9\u30C3\u30B0\u30B9\u30BF\u30A4\u30EB",
    "\u30C8\u30C3\u30D7\u30EC\u30B9",
    "\u306A\u3081",
    "\u30CB\u30AC\u30FC",
    "\u30CC\u30FC\u30C9",
    "\u30CD\u30AA\u30FB\u30CA\u30C1",
    "\u30CF\u30FC\u30C9\u30B3\u30A2",
    "\u30D1\u30A4\u30D1\u30F3",
    "\u30D0\u30A4\u30D6\u30EC\u30FC\u30BF\u30FC",
    "\u30D0\u30C3\u30AF\u30FB\u30B9\u30BF\u30A4\u30EB",
    "\u30D1\u30F3\u30C6\u30A3\u30FC",
    "\u30D3\u30C3\u30C1",
    "\u30D5\u30A1\u30C3\u30AF",
    "\u30D5\u30A1\u30F3\u30BF\u30B8\u30FC",
    "\u30D5\u30A3\u30B9\u30C8",
    "\u30D5\u30A7\u30C6\u30A3\u30C3\u30B7\u30E5",
    "\u30D5\u30A7\u30E9\u30C1\u30AA",
    "\u3075\u305F\u306A\u308A",
    "\u3076\u3063\u304B\u3051",
    "\u30D5\u30C3\u30AF",
    "\u30D7\u30EA\u30F3\u30B9 \u30A2\u30EB\u30D0\u30FC\u30C8 \u30D4\u30A2\u30B9",
    "\u30D7\u30EC\u30A4\u30DC\u30FC\u30A4",
    "\u30D9\u30A2\u30D0\u30C3\u30AF",
    "\u30DA\u30CB\u30B9",
    "\u30DA\u30CB\u30B9\u30D0\u30F3\u30C9",
    "\u30DC\u30FC\u30A4\u30BA\u30E9\u30D6",
    "\u30DC\u30FC\u30EB\u30AE\u30E3\u30B0",
    "\u30DC\u30FC\u30EB\u3092\u8E74\u308B",
    "\u307D\u3063\u3061\u3083\u308A",
    "\u30DB\u30E2",
    "\u30DD\u30EB\u30CE",
    "\u30DD\u30EB\u30CE\u30B0\u30E9\u30D5\u30A3\u30FC",
    "\u30DC\u30F3\u30C6\u30FC\u30B8",
    "\u30DE\u30B6\u30FC\u30FB\u30D5\u30A1\u30C3\u30AB\u30FC",
    "\u30DE\u30B9\u30BF\u30FC\u30D9\u30FC\u30B7\u30E7\u30F3",
    "\u307E\u3093\u3053",
    "\u3084\u304A\u3044",
    "\u3084\u308A\u307E\u3093",
    "\u30E6\u30C0\u30E4\u4EBA",
    "\u30E9\u30C6\u30A3\u30FC\u30CA",
    "\u30E9\u30D0\u30FC",
    "\u30E9\u30F3\u30B8\u30A7\u30EA\u30FC",
    "\u30EC\u30A4\u30D7",
    "\u30EC\u30BA\u30D3\u30A2\u30F3",
    "\u30ED\u30FC\u30BF\u30FC",
    "\u30ED\u30EA\u30FC\u30BF",
    "\u6DEB\u4E71",
    "\u9670\u6BDB",
    "\u9769\u6291\u5236",
    "\u9A0E\u4E0A\u4F4D",
    "\u5DE8\u6839",
    "\u5DE8\u4E73",
    "\u5F37\u59E6\u72AF",
    "\u7389\u306A\u3081",
    "\u7389\u8210\u3081",
    "\u7DCA\u7E1B",
    "\u8FD1\u89AA\u76F8\u59E6",
    "\u5F8C\u80CC\u4F4D",
    "\u5408\u610F\u306E\u6027\u4EA4",
    "\u62F7\u554F",
    "\u6BBA\u3057\u65B9",
    "\u6BBA\u4EBA\u4E8B\u4EF6",
    "\u6BBA\u4EBA\u65B9\u6CD5",
    "\u652F\u914D",
    "\u5150\u7AE5\u6027\u8650\u5F85",
    "\u81EA\u5DF1\u611B\u6027",
    "\u5C04\u7CBE",
    "\u624B\u30B3\u30AD",
    "\u7363\u59E6",
    "\u5973\u738B\u69D8",
    "\u5973\u5B50\u9AD8\u751F",
    "\u5973\u88C5",
    "\u65B0\u3057\u3044\u30DD\u30EB\u30CE",
    "\u4EBA\u59BB",
    "\u4EBA\u7A2E",
    "\u6027\u4EA4",
    "\u6B63\u5E38\u4F4D",
    "\u751F\u6B96\u5668",
    "\u7CBE\u6DB2",
    "\u633F\u5165",
    "\u8DB3\u30D5\u30A7\u30C1",
    "\u8DB3\u3092\u5E83\u3052\u308B",
    "\u5927\u9670\u5507",
    "\u8131\u8863",
    "\u8336\u8272\u306E\u30B7\u30E3\u30EF\u30FC",
    "\u4E2D\u51FA\u3057",
    "\u6F6E\u5439\u304D\u5973",
    "\u6F6E\u5439\u304D\u7537\u6027",
    "\u76F4\u8178",
    "\u5243\u6BDB",
    "\u8C9E\u64CD\u5E2F",
    "\u5974\u96B7",
    "\u4E8C\u7A74",
    "\u4E73\u9996",
    "\u5C3F\u9053\u30D7\u30EC\u30A4",
    "\u8997\u304D",
    "\u58F2\u6625\u5A66",
    "\u7E1B\u308A",
    "\u5674\u51FA",
    "\u7CDE",
    "\u7CDE\u5C3F\u611B\u597D\u75C7",
    "\u7CDE\u4FBF",
    "\u5E73\u624B\u6253\u3061",
    "\u5909\u614B",
    "\u52C3\u8D77\u3059\u308B",
    "\u5922\u7CBE",
    "\u6BDB\u6DF1\u3044",
    "\u8A98\u60D1",
    "\u5E7C\u5150",
    "\u5E7C\u5150\u6027\u611B\u8005",
    "\u88F8",
    "\u88F8\u306E\u5973\u6027",
    "\u4E71\u4EA4",
    "\u4E21\u6027",
    "\u4E21\u6027\u5177\u6709",
    "\u4E21\u5200",
    "\u8F2A\u59E6",
    "\u534D",
    "\u5BA6\u5B98",
    "\u809B\u9580",
    "\u81A3"
  ]
};

// ../../shared/dictionaries/turkish.json
var turkish_default = {
  words: [
    "18+",
    "30+1",
    "31",
    ":poop:",
    "a.g.",
    "a.mk",
    "a.q",
    "a.q.",
    "a.\u0131g",
    "abaza",
    "abazan",
    "adi",
    "ag",
    "ag.",
    "ahmak",
    "ak",
    "akp",
    "al a\u011Fz\u0131na",
    "allahs\u0131z",
    "am",
    "am biti",
    "am.k",
    "amar\u0131m",
    "ambiti",
    "amcik",
    "amck",
    "amckl",
    "amcklama",
    "amcklaryla",
    "amckta",
    "amcktan",
    "amcuk",
    "amc\u0131k",
    "amc\u0131k ho\u015Faf\u0131",
    "amc\u0131ka\u011F\u0131z",
    "amc\u0131klama",
    "amc\u0131kland\u0131",
    "amc\u0131klar\u0131",
    "amc\u0131\u011F\u0131 ",
    "amc\u0131\u011F\u0131n",
    "amc\u0131\u011F\u0131n\u0131",
    "amc\u0131\u011F\u0131n\u0131z\u0131",
    "amed",
    "amg",
    "amin oglu",
    "amina",
    "amina g",
    "amina k",
    "amina koyarim",
    "amina koyayim",
    "amina koyay\u0131m",
    "aminako",
    "aminakoyarim",
    "aminakoyim",
    "aminda",
    "amindan",
    "amindayken",
    "amini",
    "aminiyarraaniskiim",
    "aminoglu",
    "amiyum",
    "amk",
    "amk \xE7ocu\u011Fu",
    "amk.",
    "amkafa",
    "amlarnzn",
    "aml\u0131",
    "amm",
    "ammak",
    "ammna",
    "amm\u0131na",
    "amn",
    "amna",
    "amnda",
    "amndaki",
    "amngtn",
    "amnn",
    "amona",
    "amq",
    "amsiz",
    "amsz",
    "ams\u0131z",
    "amteri",
    "amugaa",
    "amuna",
    "amu\u011Fa",
    "am\u0131g",
    "am\u0131k",
    "am\u0131n",
    "am\u0131n feryad\u0131",
    "am\u0131n oglu",
    "am\u0131n o\u011Flu",
    "am\u0131na",
    "Am\u0131na",
    "am\u0131na g",
    "am\u0131na k",
    "am\u0131na koy",
    "am\u0131na koyar\u0131m",
    "am\u0131na koyay\u0131m",
    "am\u0131na koyim",
    "am\u0131na koyyim",
    "am\u0131na s",
    "am\u0131na sikem",
    "am\u0131na sokam",
    "am\u0131nad\xFCrz\xFC",
    "am\u0131nako",
    "am\u0131nakoyim",
    "am\u0131no\u011Flu",
    "am\u0131n\u0131",
    "am\u0131n\u0131 s",
    "am\u0131s\u0131na",
    "am\u0131s\u0131n\u0131",
    "anaaann",
    "anal",
    "analarn",
    "anam",
    "anamla",
    "anan",
    "anana",
    "anandan",
    "anani",
    "anani sikerim",
    "anani sikeyim",
    "ananin",
    "ananisikerim",
    "ananisikeyim",
    "anann",
    "ananz",
    "anan\u0131",
    "anan\u0131 ",
    "anan\u0131 sikerim",
    "anan\u0131 sikeyim",
    "anan\u0131n",
    "anan\u0131n am",
    "anan\u0131n am\u0131",
    "anan\u0131n d\xF6l\xFC",
    "anan\u0131nki",
    "anan\u0131sikerim",
    "anan\u0131sikeyim",
    "anan\u0131z\u0131",
    "anan\u0131z\u0131n",
    "anan\u0131z\u0131n am",
    "anas",
    "anasi",
    "anasinin",
    "anas\u0131 orospu",
    "anas\u0131n\u0131",
    "anas\u0131n\u0131 avrad\u0131n\u0131",
    "anas\u0131n\u0131n am",
    "anay",
    "anayin",
    "angut",
    "anl\u0131k be\u011Feni",
    "anneni",
    "annenin",
    "annesiz",
    "anuna",
    "aptal",
    "aq",
    "aq.",
    "ass",
    "atkafas\u0131",
    "atm\u0131k",
    "attrrm",
    "att\u0131rd\u0131\u011F\u0131m",
    "auzlu",
    "avon",
    "avrad\u0131n\u0131",
    "avrat",
    "ayklarmalrmsikerim",
    "azd\u0131m",
    "azd\u0131r",
    "azd\u0131r\u0131c\u0131",
    "a\u011Fz\u0131na",
    "a\u011Fz\u0131na s\u0131\xE7ay\u0131m",
    "a\u015Fa\u011Fl\u0131k",
    "a\u015Fa\u011Fl\u0131ksn\u0131z",
    "a\u015Fa\u011F\u0131l\u0131ks\u0131n",
    "a\u015Fa\u011F\u0131l\u0131ks\u0131n\u0131z",
    "b.k",
    "babaannesi ka\u015Far",
    "babafingo",
    "babani",
    "baban\u0131",
    "baban\u0131n",
    "babas\u0131 pezevenk",
    "babas\u0131 tornac\u0131 anas\u0131 orospu",
    "baca\u011F\u0131na s\u0131\xE7ay\u0131m",
    "bacini",
    "bacn",
    "bacndan",
    "bacy",
    "bac\u0131n",
    "bac\u0131na",
    "bac\u0131n\u0131",
    "bac\u0131n\u0131n",
    "bastard",
    "basur",
    "bayanlar",
    "beyinsiz",
    "be\u011Fen",
    "be\u011Fenirmisiniz",
    "bira",
    "biseksuel",
    "bisekuel",
    "bisexual",
    "bisikiverem",
    "bitch",
    "biting",
    "bok",
    "boka",
    "bokbok",
    "bokhu",
    "bokkkumu",
    "boklar",
    "boktan",
    "boku",
    "bokubokuna",
    "bokum",
    "bok\xE7a",
    "bombok",
    "boner",
    "bosalmak",
    "boycot",
    "boycott",
    "boykot",
    "bo\u015Falmak",
    "bo\u015Fluk",
    "browni",
    "b\u0131z\u0131r",
    "ccc",
    "ccc ccc",
    "ccc incisiker ccc",
    "cccincisikerccc",
    "ccs colgate ccc",
    "cenabet",
    "cibiliyetsiz",
    "cibilliyetini",
    "cibilliyetsiz",
    "cif",
    "cikar",
    "cim",
    "cm",
    "dalaks\u0131z",
    "dallama",
    "daltassak",
    "dalyarak",
    "dalyarrak",
    "dangalak",
    "dasak",
    "dassagi",
    "dassak",
    "da\u015Fak",
    "da\u015F\u015Fak",
    "da\u015F\u015Faks\u0131z",
    "dede",
    "destek",
    "destekk",
    "deste\u011Finizi",
    "devsol",
    "dhkp",
    "diktim",
    "dildo",
    "din",
    "dinci",
    "dingil",
    "dingilini",
    "dinsiz",
    "dkerim",
    "domal",
    "domalan",
    "domald\u0131",
    "domald\u0131n",
    "domalmak",
    "domalm\u0131\u015F",
    "domals\u0131n",
    "domalt",
    "domaltarak",
    "domaltip",
    "domaltmak",
    "domalt\u0131p",
    "domalt\u0131r",
    "domalt\u0131r\u0131m",
    "domal\u0131k",
    "domal\u0131yor",
    "domuz yag\u0131",
    "domuz ya\u011F\u0131",
    "domuzyag\u0131",
    "durum",
    "d\xF6l\xFC",
    "d\xF6nek",
    "d\xFCd\xFCk",
    "d\xFCrz\xFC",
    "eben",
    "ebeni",
    "ebenin",
    "ebeninki",
    "ebleh",
    "ecdadini",
    "ecdad\u0131n\u0131",
    "edit",
    "embesil",
    "emek server",
    "emi",
    "ensest",
    "erotik",
    "escinsel",
    "ev han\u0131mlar\u0131",
    "ezik",
    "e\u015Fcinsel",
    "fahise",
    "fahi\u015Fe",
    "farmasi",
    "fasist",
    "fasizm",
    "fa\u015Fist",
    "fa\u015Fizm",
    "feri\u015Ftah",
    "ferre",
    "fuck",
    "fucker",
    "fuckin",
    "fucking",
    "f\u0131rsat",
    "g*t",
    "g*t\xFC",
    "g*t\xFCn",
    "g*t\xFCne",
    "g.t",
    "gasp",
    "gavad",
    "gavat",
    "gavurun d\xF6l\xFC",
    "gay",
    "geber",
    "geberik",
    "gebermek",
    "gebermi\u015F",
    "gebersin",
    "gebertir",
    "geri zekal\u0131",
    "gerizekali",
    "gerizekal\u0131",
    "gerizekal\u0131d\u0131r",
    "gerzek",
    "ger\u0131 zekal\u0131d\u0131r",
    "ger\u0131zekal\u0131",
    "giberim",
    "giberler",
    "gibis",
    "gibi\u015F",
    "gibmek",
    "gibtiler",
    "giren \xE7\u0131kan",
    "goddamn",
    "godo\u015F",
    "godumun",
    "got",
    "gotelek",
    "gotlalesi",
    "gotlu",
    "gotten",
    "gotundeki",
    "gotunden",
    "gotune",
    "gotunu",
    "gotuze",
    "gotveren",
    "goyiim",
    "goyum",
    "goyuyim",
    "goyyim",
    "gtelek",
    "gtn",
    "gtnde",
    "gtnden",
    "gtne",
    "gtten",
    "gtveren",
    "guat",
    "g\xF6t",
    "g\xF6t deli\u011Fi",
    "g\xF6t herif",
    "g\xF6t o\u011Flan\u0131",
    "g\xF6t veren",
    "g\xF6t verir",
    "g\xF6te",
    "g\xF6telek",
    "g\xF6tlalesi",
    "g\xF6tlek",
    "g\xF6to\u011Flan\u0131",
    "g\xF6to\u015F",
    "g\xF6ts\xFCn",
    "g\xF6ts\xFCn\xFCz",
    "g\xF6tten",
    "g\xF6tveren",
    "g\xF6tvern",
    "g\xF6t\xFC",
    "g\xF6t\xFCn",
    "g\xF6t\xFCne",
    "g\xF6t\xFCne koyim",
    "g\xF6t\xFCnekoyim",
    "g\xF6t\xFCn\xFC",
    "g\xF6t\xFCn\xFCze",
    "g\xF6t\xFCyle",
    "g\xF6\u011F\xFCs",
    "hack",
    "han\u0131mlar",
    "has siktir",
    "hasiktir",
    "hasiktr",
    "hassikome",
    "hassiktir",
    "hassittir",
    "hastir",
    "haydar",
    "haysiyetsiz",
    "hayvan",
    "hayvan herif",
    "ha\u015Fgerya",
    "hile",
    "hizbullah",
    "homo",
    "ho\u015Faf\u0131",
    "hristiyan",
    "hsktr",
    "huur",
    "hz.muhammed",
    "hz.\xF6mer",
    "h\xF6d\xFCk",
    "h\u0131rs\u0131z",
    "h\u0131yar",
    "h\u0131yara\u011Fas\u0131",
    "i.ne",
    "ibina",
    "ibine",
    "ibinenin",
    "ibne",
    "ibnedir",
    "ibneler",
    "ibneleri",
    "ibnelik",
    "ibneli\u011Fi",
    "ibnelri",
    "ibneni",
    "ibnenin",
    "ibnerator",
    "ibnesi",
    "idiot",
    "idiyot",
    "igrenc",
    "ilk yorum",
    "imansz",
    "inci siker",
    "incisike",
    "incisiker",
    "ipne",
    "ipneler",
    "irsail",
    "iserim",
    "israil",
    "israli",
    "israyil",
    "isra\u0131l",
    "it",
    "iti",
    "itler",
    "ito\u011Flu it",
    "izrail",
    "i\u011Fren\xE7",
    "i\u015F f\u0131rsat\u0131",
    "i\u015Ferim",
    "k.o.c",
    "k.o.\xE7",
    "kadek",
    "kafam girsin",
    "kafasiz",
    "kafas\u0131z",
    "kahpe",
    "kahpenin",
    "kahpenin feryad\u0131",
    "kaka",
    "kaltak",
    "kancik",
    "kanc\u0131k",
    "kan\u0131",
    "kan\u0131 bozuk",
    "kappe",
    "karhane",
    "kariyer f\u0131rsat\u0131",
    "kavat",
    "kavatn",
    "kaypak",
    "kayyum",
    "kazik",
    "kazma",
    "kaz\u0131k",
    "ka\xE7\u0131rma",
    "ka\u015Far",
    "kerane",
    "kerhane",
    "kerhanelerde",
    "keriz",
    "kerizler",
    "kevase",
    "keva\u015Fe",
    "kevvase",
    "koca g\xF6t",
    "kodumun",
    "kodumunun",
    "koduumun",
    "kodu\u011Fmun",
    "kodu\u011Fmunun",
    "komonist",
    "komunist",
    "komunizm",
    "kom\xFCnist",
    "kom\xFCnizm",
    "kongragel",
    "kopegi",
    "kope\u011Fi",
    "koyarm",
    "koyay\u0131m",
    "koyiim",
    "koyiiym",
    "koyim",
    "koyum",
    "koyyim",
    "krar",
    "kro",
    "kro musunuz",
    "kromusunuz",
    "kuku",
    "kukudaym",
    "kurdistan",
    "k\xF6peginin",
    "k\xF6pekle\u015Fme",
    "k\xFCrdistan",
    "k\u0131ro",
    "k\u0131\xE7",
    "k\u0131\xE7\u0131n\u0131z",
    "k\u0131\xE7\u0131n\u0131za",
    "l.an",
    "laciye boyad\u0131m",
    "lan",
    "lanet",
    "lann",
    "lannn",
    "lavuk",
    "len",
    "leyn",
    "lezle",
    "lezler",
    "libo\u015F",
    "madafaka",
    "mal",
    "malafat",
    "malak",
    "mallar",
    "manda",
    "mankafa",
    "manyak",
    "mcik",
    "meme",
    "memelerini",
    "memi\u015F",
    "mezveleli",
    "minaamc\u0131k",
    "mincikliyim",
    "mk",
    "mna",
    "moloz",
    "monakkoluyum",
    "moron",
    "motherfucker",
    "mudik",
    "musevi",
    "musevi bozuk",
    "m\xFCsl\xFCman",
    "nah",
    "o \xE7",
    "o. \xE7ocu\u011Fu",
    "o.\xE7",
    "o.\xE7.",
    "oc",
    "ocuu",
    "ocuun",
    "odun",
    "okuz",
    "oriflame",
    "orosbu",
    "orosbucocuu",
    "orospu",
    "orospu cocugu",
    "orospu \xE7oc",
    "orospu \xE7ocuklar\u0131",
    "orospu \xE7ocu\u011Fu",
    "orospu \xE7ocu\u011Fudur",
    "orospucocugu",
    "orospudur",
    "orospular",
    "orospunun",
    "orospunun evlad\u0131",
    "orospuydu",
    "orospuyuz",
    "orospu\xE7ocu\u011Fu",
    "orostoban",
    "orostopol",
    "orrospu",
    "oruc",
    "oruspu",
    "oruspu \xE7ocu\u011Fu",
    "oruspu\xE7ocu\u011Fu",
    "oru\xE7",
    "osbir",
    "ossurduum",
    "ossurmak",
    "ossuruk",
    "osur",
    "osuram",
    "osurduu",
    "osuruk",
    "osururum",
    "otusbir",
    "otuzbir",
    "oyna",
    "O\xC7",
    "o\xE7",
    "o\xE7.",
    "o\u011Flan",
    "o\u011Flanc\u0131",
    "o\u011Flu it",
    "p kk",
    "patlak zar",
    "penis",
    "peygamber",
    "pezevek",
    "pezeven",
    "pezeveng",
    "pezevengi",
    "pezevengin evlad\u0131",
    "pezevenk",
    "pezevenkler",
    "pezo",
    "pic",
    "pici",
    "picler",
    "pipi",
    "pipi\u015F",
    "pis",
    "pisliktir",
    "pi\xE7",
    "pi\xE7 kurusu",
    "pi\xE7i",
    "pi\xE7in o\u011Flu",
    "pi\xE7inin",
    "pi\xE7ler",
    "pi\xE7sin",
    "pi\xE7siniz",
    "pkk",
    "pok",
    "pokunu",
    "pompalama",
    "pompalamak",
    "porn",
    "porno",
    "protesto",
    "pussy",
    "putpererst",
    "putperest",
    "pu\u015Ft",
    "pu\u015Fttur",
    "pvp",
    "rahminde",
    "rak",
    "rak\u0131",
    "revizyonist",
    "s.ikerim",
    "s.iktm",
    "s.ktir",
    "s.tir",
    "s1kerim",
    "s1kerm",
    "s1krm",
    "sahtekar",
    "sakso",
    "saksofon",
    "salaak",
    "salak",
    "sana ne",
    "sanane",
    "saxo",
    "sayfa",
    "sekis",
    "seks",
    "serefsiz",
    "serefsz",
    "serefszler",
    "sevgi koyar\u0131m",
    "sevi\u015F",
    "sevi\u015Felim",
    "sevi\u015Fmek",
    "sex",
    "sexs",
    "shit",
    "si.\xE7mak",
    "sicarsin",
    "sicmak",
    "sicti",
    "sie",
    "sigara",
    "sik",
    "sik k\u0131r\u0131\u011F\u0131",
    "sikdi",
    "sikdi\u011Fim",
    "sike",
    "sikecem",
    "sikem",
    "siken",
    "sikenin",
    "siker",
    "sikerim",
    "sikerler",
    "sikersin",
    "sikert",
    "sikertir",
    "sikertirler",
    "sikertmek",
    "sikesen",
    "sikesicenin",
    "sikey",
    "sikeydim",
    "sikeyim",
    "sikeym",
    "siki",
    "sikicem",
    "sikici",
    "sikien",
    "sikienler",
    "sikiiim",
    "sikiiimmm",
    "sikiim",
    "sikiir",
    "sikiirken",
    "sikik",
    "sikil",
    "sikildiini",
    "sikilesice",
    "sikilmi",
    "sikilmie",
    "sikilmis",
    "sikilmi\u015F",
    "sikilsin",
    "sikim",
    "sikimde",
    "sikimden",
    "sikime",
    "sikimi",
    "sikimiin",
    "sikimin",
    "sikimle",
    "sikimsonik",
    "sikimtrak",
    "sikin",
    "sikinde",
    "sikinden",
    "sikine",
    "sikini",
    "sikip",
    "sikis",
    "sikisek",
    "sikisen",
    "sikish",
    "sikismis",
    "sikitiin",
    "sikiyim",
    "sikiym",
    "sikiyorum",
    "siki\u015F",
    "siki\u015Fen",
    "siki\u015Fken",
    "siki\u015Fme",
    "siki\u015Fmek",
    "sikkim",
    "sikko",
    "sikleri",
    "sikleriii",
    "sikli",
    "sikm",
    "sikmek",
    "sikmem",
    "sikmeyi",
    "sikmiler",
    "sikmisligim",
    "siksem",
    "sikseydin",
    "sikseyidin",
    "siksin",
    "siksinbaya",
    "siksinler",
    "siksiz",
    "siksok",
    "siksz",
    "sikt",
    "sikti",
    "siktigimin",
    "siktigiminin",
    "siktii",
    "siktiim",
    "siktiimin",
    "siktiiminin",
    "siktiler",
    "siktim",
    "siktim ",
    "siktimin",
    "siktiminin",
    "siktin",
    "siktir",
    "siktir et",
    "siktir git",
    "siktir lan",
    "siktir ol git",
    "siktirgit",
    "siktirir",
    "siktiririm",
    "siktiriyor",
    "siktirolgit",
    "siktirsin",
    "sikti\u011Fim",
    "sikti\u011Fimin",
    "sikti\u011Fiminin",
    "siktr",
    "siqem",
    "sisme",
    "sittimin",
    "sittir",
    "skcem",
    "skecem",
    "skem",
    "sker",
    "skerim",
    "skerm",
    "skeyim",
    "skiim",
    "skik",
    "skim",
    "skime",
    "skiym",
    "skm",
    "skmek",
    "skrm",
    "sksin",
    "sksn",
    "sksz",
    "sktiimin",
    "sktim",
    "sktir",
    "sktirsin",
    "sktr",
    "sktroradan",
    "sktrr",
    "sktrsn",
    "skyim",
    "slaleni",
    "snane",
    "sokacak",
    "sokam",
    "sokarim",
    "sokarm",
    "sokarmkoduumun",
    "sokar\u0131m",
    "sokaym",
    "sokay\u0131m",
    "sokiim",
    "soktu\u011Fumunun",
    "sokuk",
    "sokum",
    "sokuyum",
    "soku\u015F",
    "soxum",
    "sperm",
    "sulaleni",
    "s\xE7kik",
    "s\xFClaleni",
    "s\xFClalenizi",
    "s\xFCrt\xFCk",
    "s\xFCt\xFC bozuk",
    "s\xFCzme",
    "s\u0131ecem",
    "s\u0131ker\u0131m",
    "s\u0131km",
    "s\u0131kt\u0131r",
    "s\u0131\xE7ar\u0131m",
    "s\u0131\xE7mak",
    "s\u0131\xE7t\u0131\u011F\u0131m",
    "taaklarn",
    "taaklarna",
    "takip",
    "takip et",
    "takma",
    "tarrakimin",
    "tasak",
    "tassak",
    "tayyip",
    "ta\u015Fak",
    "ta\u015F\u015Fak",
    "temas\u0131yla",
    "teror",
    "terorist",
    "ter\xF6r",
    "ter\xF6rist",
    "thumb?",
    "tipini s.k",
    "tipinizi s.keyim",
    "tiyniyat",
    "tokmaklama",
    "tokmaklamak",
    "top",
    "toplarm",
    "topsun",
    "toto\u015F",
    "transeksuel",
    "transeks\xFCel",
    "travesti",
    "ukler",
    "ulan",
    "ulen",
    "ulker",
    "vagina",
    "vajina",
    "vajinan\u0131",
    "va\u015Fina",
    "veled",
    "veled i zina",
    "veledizina",
    "verdiimin",
    "verem",
    "vodka",
    "votka",
    "weled",
    "weledizina",
    "whore",
    "wslik",
    "wslik server",
    "xikeyim",
    "yaaraaa",
    "yahudi",
    "yalaka",
    "yalama",
    "yalarun",
    "yalar\u0131m",
    "yaraaam",
    "yarak",
    "yaraks\u0131z",
    "yaraktr",
    "yaram",
    "yaraminbasi",
    "yaramn",
    "yararmorospunun",
    "yark",
    "yarra",
    "yarraaaa",
    "yarraak",
    "yarraam",
    "yarraam\u0131",
    "yarragi",
    "yarragimi",
    "yarragina",
    "yarragindan",
    "yarragm",
    "yarraimin",
    "yarrak",
    "yarram",
    "yarramin",
    "yarraminba\u015F\u0131",
    "yarramn",
    "yarram\u0131n",
    "yarran",
    "yarrana",
    "yarra\u011F",
    "yarra\u011F\u0131m",
    "yarra\u011F\u0131m\u0131",
    "yarrk",
    "yarro",
    "yarrrak",
    "yaudi",
    "yavak",
    "yavu\u015Fak",
    "yav\u015F",
    "yav\u015Fak",
    "yav\u015Fakt\u0131r",
    "yav\u015Famak",
    "ya\u011Fudi",
    "yilisik",
    "yogurtlayam",
    "yosma",
    "yo\u011Furtlayam",
    "yrak",
    "yrk",
    "yrrak",
    "y\u0131l\u0131\u015F\u0131k",
    "zibidi",
    "zigsin",
    "zikeyim",
    "zikiiim",
    "zikiim",
    "zikik",
    "zikim",
    "ziksiiin",
    "ziksiin",
    "zina",
    "zulliyetini",
    "zviyetini",
    "z\u0131kk\u0131m\u0131m",
    "\xE7al\u0131\u015Fmak istermisiniz?",
    "\xE7\xFCk",
    "\xF6k\xFCz",
    "\xF6l",
    "\xF6\u015Fex",
    "\xFCkler",
    "\xFClker",
    "\xFClk\xFCc\xFC",
    "\u0131bnel\u0131k",
    "\u015Ferefsiz",
    "\u015Ferefsizler",
    "\u015Ferefsizlerin",
    "\u015Ferefsizlik",
    "\u015Firret",
    "\u015Fi\u015Fme",
    "\u015Fi\u015Fme bebek",
    "\u015Fi\u015Fme kad\u0131n",
    "\u015Fi\u015Fme manken",
    "\u015F\u0131ll\u0131k",
    "\u015F\u0131rf\u0131nt\u0131"
  ]
};

// ../../shared/dictionaries/spanish.json
var spanish_default = {
  words: [
    "asesinato",
    "asno",
    "bastardo",
    "Bollera",
    "Cabron",
    "Cabr\xF3n",
    "Caca",
    "Chupada",
    "Chupapollas",
    "Chupet\xF3n",
    "concha",
    "Concha de tu madre",
    "Co\xF1o",
    "Coprofag\xEDa",
    "Culo",
    "Drogas",
    "Esperma",
    "Fiesta de salchichas",
    "Follador",
    "Follar",
    "Gilipichis",
    "Gilipollas",
    "Hacer una paja",
    "Haciendo el amor",
    "Hero\xEDna",
    "Hija de puta",
    "Hijaputa",
    "Hijo de puta",
    "Hijoputa",
    "Idiota",
    "Imb\xE9cil",
    "infierno",
    "Jilipollas",
    "Kapullo",
    "Lameculos",
    "Maciza",
    "Macizorra",
    "maldito",
    "Mamada",
    "Marica",
    "Maric\xF3n",
    "Mariconazo",
    "martillo",
    "Mierda",
    "Nazi",
    "Orina",
    "Pedo",
    "Pervertido",
    "Pez\xF3n",
    "Pinche",
    "Pis",
    "Prostituta",
    "Puta",
    "Racista",
    "Ramera",
    "S\xE1dico",
    "Semen",
    "Sexo",
    "Sexo oral",
    "Soplagaitas",
    "Soplapollas",
    "Tetas grandes",
    "T\xEDa buena",
    "Travesti",
    "Trio",
    "Verga",
    "vete a la mierda",
    "Vulva"
  ]
};

// ../../shared/dictionaries/swedish.json
var swedish_default = {
  words: [
    "arsle",
    "brutta",
    "discofitta",
    "dra \xE5t helvete",
    "fan",
    "fitta",
    "fittig",
    "f\xF6r helvete",
    "helvete",
    "h\xE5rd",
    "j\xE4vlar",
    "knulla",
    "kuk",
    "kuks\xE5s",
    "k\xF6tthuvud",
    "k\xF6ttnacke",
    "moona",
    "moonade",
    "moonar",
    "moonat",
    "mutta",
    "nigger",
    "neger",
    "olla",
    "pippa",
    "pitt",
    "prutt",
    "p\xF6k",
    "runka",
    "r\xF6v",
    "r\xF6vh\xE5l",
    "r\xF6vknulla",
    "satan",
    "skita",
    "skit ner dig",
    "sk\xE4ggbiff",
    "snedfitta",
    "snefitta",
    "stake",
    "subba",
    "s\xE5s",
    "s\xE4tta p\xE5",
    "tusan"
  ]
};

// ../../shared/dictionaries/thai.json
var thai_default = {
  words: [
    "\u0E01\u0E23\u0E30\u0E14\u0E2D",
    "\u0E01\u0E23\u0E30\u0E40\u0E14\u0E49\u0E32",
    "\u0E01\u0E23\u0E30\u0E2B\u0E23\u0E35\u0E48",
    "\u0E01\u0E30\u0E1B\u0E34",
    "\u0E01\u0E39",
    "\u0E02\u0E35\u0E49",
    "\u0E04\u0E27\u0E22",
    "\u0E08\u0E34\u0E4B\u0E21",
    "\u0E08\u0E39\u0E4B",
    "\u0E40\u0E08\u0E4A\u0E01",
    "\u0E40\u0E08\u0E35\u0E4A\u0E22\u0E27",
    "\u0E14\u0E2D\u0E01\u0E17\u0E2D\u0E07",
    "\u0E15\u0E2D\u0E41\u0E2B\u0E25",
    "\u0E15\u0E39\u0E14",
    "\u0E19\u0E49\u0E4D\u0E32\u0E41\u0E15\u0E01",
    "\u0E21\u0E36\u0E07",
    "\u0E41\u0E21\u0E48\u0E07",
    "\u0E40\u0E22\u0E47\u0E14",
    "\u0E23\u0E39\u0E15\u0E39\u0E14",
    "\u0E25\u0E49\u0E32\u0E07\u0E15\u0E39\u0E49\u0E40\u0E22\u0E47\u0E19",
    "\u0E2A\u0E49\u0E19\u0E15\u0E35\u0E19",
    "\u0E2A\u0E31\u0E14",
    "\u0E40\u0E2A\u0E37\u0E2D\u0E01",
    "\u0E2B\u0E0D\u0E34\u0E07\u0E0A\u0E32\u0E15\u0E34\u0E0A\u0E31\u0E48\u0E27",
    "\u0E2B\u0E25\u0E31\u0E48\u0E07",
    "\u0E2B\u0E48\u0E32",
    "\u0E2B\u0E4D\u0E32",
    "\u0E2B\u0E35",
    "\u0E40\u0E2B\u0E35\u0E49\u0E22",
    "\u0E2D\u0E21\u0E19\u0E01\u0E40\u0E02\u0E32",
    "\u0E44\u0E2D\u0E49\u0E04\u0E27\u0E32\u0E22"
  ]
};

// src/data/dictionary.ts
var dictionary_default = {
  arabic: arabic_default.words,
  chinese: chinese_default.words,
  czech: czech_default.words,
  danish: danish_default.words,
  dutch: dutch_default.words,
  english: english_default.words,
  esperanto: esperanto_default.words,
  finnish: finnish_default.words,
  french: french_default.words,
  german: german_default.words,
  hindi: hindi_default.words,
  hungarian: hungarian_default.words,
  italian: italian_default.words,
  japanese: japanese_default.words,
  korean: korean_default.words,
  norwegian: norwegian_default.words,
  persian: persian_default.words,
  polish: polish_default.words,
  portuguese: portuguese_default.words,
  russian: russian_default.words,
  spanish: spanish_default.words,
  turkish: turkish_default.words,
  swedish: swedish_default.words,
  thai: thai_default.words
};

// src/nlp/contextAnalyzer.ts
var POSITIVE_INDICATORS = /* @__PURE__ */ new Set([
  "amazing",
  "awesome",
  "excellent",
  "fantastic",
  "great",
  "love",
  "wonderful",
  "brilliant",
  "perfect",
  "incredible",
  "outstanding",
  "superb",
  "magnificent",
  "marvelous",
  "spectacular",
  "phenomenal",
  "terrific",
  "fabulous",
  "divine",
  "best",
  "good",
  "nice",
  "cool",
  "sweet",
  "rad",
  "sick",
  "dope",
  "fire",
  "lit",
  "epic",
  "legendary",
  "godlike",
  "insane",
  "crazy",
  "wild",
  "beast",
  "movie",
  "film",
  "show",
  "song",
  "music",
  "game",
  "book",
  "restaurant",
  "food",
  "dish",
  "meal",
  "place",
  "spot",
  "location",
  "experience"
]);
var NEGATIVE_INDICATORS = /* @__PURE__ */ new Set([
  "hate",
  "terrible",
  "awful",
  "horrible",
  "disgusting",
  "pathetic",
  "stupid",
  "idiot",
  "moron",
  "loser",
  "worthless",
  "useless",
  "garbage",
  "trash",
  "suck",
  "sucks",
  "worst",
  "bad",
  "ugly",
  "gross",
  "nasty",
  "annoying",
  "irritating",
  "frustrating",
  "disappointing",
  "lame",
  "weak",
  "fail",
  "you",
  "your",
  "yourself",
  "u",
  "ur",
  "ure",
  "youre"
]);
var GAMING_POSITIVE = /* @__PURE__ */ new Set([
  "player",
  "gamer",
  "team",
  "squad",
  "clan",
  "guild",
  "match",
  "game",
  "round",
  "level",
  "boss",
  "raid",
  "quest",
  "achievement",
  "skill",
  "build",
  "loadout",
  "strategy",
  "tactic",
  "play",
  "move",
  "combo"
]);
var GAMING_ACCEPTABLE_WORDS = /* @__PURE__ */ new Set([
  "kill",
  "killer",
  "killed",
  "killing",
  "shoot",
  "shot",
  "shooting",
  "die",
  "dying",
  "died",
  "dead",
  "death",
  "badass",
  "sick",
  "insane",
  "crazy",
  "mad",
  "beast",
  "savage",
  "suck",
  "sucks",
  "wtf",
  "omg",
  "hell",
  "damn",
  "crap"
]);
var POSITIVE_PHRASES = /* @__PURE__ */ new Map([
  ["the bomb", 0.9],
  // "this movie is the bomb"
  ["da bomb", 0.9],
  // slang for "the best"
  ["bomb.com", 0.9],
  // website reference
  ["bomb diggity", 0.9],
  // slang for excellent
  ["photo bomb", 0.8],
  // photography term
  ["bath bomb", 0.8],
  // cosmetic product
  ["bomb squad", 0.7]
  // could be neutral/positive in gaming
]);
var NEGATIVE_PHRASES = /* @__PURE__ */ new Map([
  ["you are", 0.1],
  // "you are [profanity]"
  ["ur a", 0.1],
  // "ur a [profanity]"
  ["such a", 0.2],
  // "such a [profanity]"
  ["fucking", 0.1],
  // intensifier, usually negative
  ["damn", 0.2]
  // mild profanity, context dependent
]);
var ContextAnalyzer = class {
  constructor(config) {
    this.contextWindow = config.contextWindow;
    this.language = config.language;
    this.domainWhitelists = new Set(
      (config.domainWhitelists || []).map((word) => word.toLowerCase())
    );
  }
  /**
   * Analyzes the context around a profanity match to determine if it should be flagged
   */
  analyzeContext(text, matchWord, matchIndex) {
    const tokens = this.tokenizeWithSpans(text);
    const matchWordIndex = this.findWordIndex(tokens, matchIndex);
    if (matchWordIndex === -1) {
      return {
        contextScore: 0.5,
        reason: "Could not locate match in tokenized text",
        isWhitelisted: false
      };
    }
    const startIndex = Math.max(0, matchWordIndex - this.contextWindow);
    const endIndex = Math.min(tokens.length, matchWordIndex + this.contextWindow + 1);
    const contextWords = tokens.slice(startIndex, endIndex).map((token) => token.word);
    const contextText = contextWords.join(" ").toLowerCase();
    const phraseResult = this.checkPhraseContext(contextText, matchWord);
    if (phraseResult) {
      return phraseResult;
    }
    if (this.isDomainWhitelisted(contextWords, matchWord)) {
      return {
        contextScore: 0.8,
        reason: "Domain-specific whitelist match",
        isWhitelisted: true
      };
    }
    const sentimentScore = this.calculateSentimentScore(contextWords, matchWordIndex - startIndex);
    return {
      contextScore: sentimentScore,
      reason: this.generateReason(sentimentScore, contextWords),
      isWhitelisted: false
    };
  }
  checkPhraseContext(contextText, matchWord) {
    for (const [phrase, score] of POSITIVE_PHRASES.entries()) {
      if (phrase.includes(matchWord) && contextText.includes(phrase)) {
        return {
          contextScore: score,
          reason: `Positive phrase detected: "${phrase}"`,
          isWhitelisted: true
        };
      }
    }
    for (const [phrase, score] of NEGATIVE_PHRASES.entries()) {
      if (contextText.includes(phrase)) {
        return {
          contextScore: score,
          reason: `Negative phrase detected: "${phrase}"`,
          isWhitelisted: false
        };
      }
    }
    return null;
  }
  isDomainWhitelisted(contextWords, matchWord) {
    const normalizedMatchWord = matchWord.toLowerCase();
    for (const word of contextWords) {
      if (this.domainWhitelists.has(word)) {
        return true;
      }
      if (GAMING_POSITIVE.has(word)) {
        if (GAMING_ACCEPTABLE_WORDS.has(normalizedMatchWord)) {
          return true;
        }
      }
    }
    return false;
  }
  generateReason(score, contextWords) {
    const foundPositive = Array.from(new Set(contextWords.filter((word) => POSITIVE_INDICATORS.has(word))));
    const foundNegative = Array.from(new Set(contextWords.filter((word) => NEGATIVE_INDICATORS.has(word))));
    if (score >= 0.7) {
      const details = foundPositive.length > 0 ? ` (found: ${foundPositive.join(", ")})` : "";
      return `Positive context detected${details} - likely not profanity`;
    } else if (score <= 0.3) {
      const details = foundNegative.length > 0 ? ` (found: ${foundNegative.join(", ")})` : "";
      return `Negative context detected${details} - likely profanity`;
    } else {
      return "Neutral context - uncertain classification";
    }
  }
  tokenizeWithSpans(text) {
    const tokens = [];
    const pattern = /\S+/g;
    let match;
    while ((match = pattern.exec(text)) !== null) {
      const raw = match[0];
      const normalized = raw.toLowerCase().replace(/[^\p{L}\p{N}_]/gu, "");
      if (normalized.length > 0) {
        tokens.push({
          word: normalized,
          start: match.index,
          end: match.index + raw.length
        });
      }
    }
    return tokens;
  }
  findWordIndex(tokens, charIndex) {
    if (tokens.length === 0) {
      return -1;
    }
    for (let i = 0; i < tokens.length; i++) {
      const token = tokens[i];
      if (charIndex >= token.start && charIndex < token.end) {
        return i;
      }
      if (charIndex < token.start) {
        return Math.max(0, i - 1);
      }
    }
    return tokens.length - 1;
  }
  calculateSentimentScore(contextWords, matchPosition) {
    let positiveCount = 0;
    let negativeCount = 0;
    const totalWords = contextWords.length;
    for (let i = 0; i < contextWords.length; i++) {
      const word = contextWords[i];
      const distance = Math.abs(i - matchPosition);
      const weight = Math.max(0.1, 1 - distance * 0.2);
      if (POSITIVE_INDICATORS.has(word)) {
        positiveCount += weight;
      } else if (NEGATIVE_INDICATORS.has(word)) {
        negativeCount += weight;
      }
    }
    const totalSentiment = positiveCount + negativeCount;
    if (totalSentiment === 0) {
      return 0.5;
    }
    const rawScore = positiveCount / totalSentiment;
    let adjustedScore = rawScore;
    const confidenceMultiplier = Math.min(1, totalWords / 5);
    adjustedScore = 0.5 + (adjustedScore - 0.5) * confidenceMultiplier;
    const hasPersonalPronouns = contextWords.some(
      (word) => ["you", "your", "u", "ur"].includes(word)
    );
    if (hasPersonalPronouns && rawScore < 0.7) {
      adjustedScore *= 0.7;
    }
    const hasObjectReferences = contextWords.some(
      (word) => ["movie", "song", "game", "book", "show", "this", "that", "it"].includes(word)
    );
    if (hasObjectReferences && rawScore > 0.3) {
      adjustedScore = Math.min(1, adjustedScore * 1.3);
    }
    return Math.max(0, Math.min(1, adjustedScore));
  }
  /**
   * Updates the domain whitelist for this analyzer instance
   */
  updateDomainWhitelist(newWhitelist) {
    this.domainWhitelists = new Set(newWhitelist.map((word) => word.toLowerCase()));
  }
  /**
   * Adds words to the domain whitelist
   */
  addToDomainWhitelist(words) {
    words.forEach((word) => this.domainWhitelists.add(word.toLowerCase()));
  }
};

// src/utils/leetspeak.ts
var BASIC_SUBSTITUTIONS = {
  "0": "o",
  "1": "i",
  "3": "e",
  "4": "a",
  "5": "s",
  "7": "t",
  "8": "b",
  "9": "g"
};
var MODERATE_SUBSTITUTIONS = {
  ...BASIC_SUBSTITUTIONS,
  "@": "a",
  "$": "s",
  "!": "i",
  "(": "c",
  "<": "c",
  "{": "c",
  "[": "c",
  "+": "t",
  "\u20AC": "e",
  "&": "e",
  "#": "h",
  "\xA5": "y",
  "\xA7": "s",
  "\u2020": "t",
  "\xAE": "r",
  "\xA9": "c",
  "\xB2": "2",
  "\xB3": "3"
};
var AGGRESSIVE_MULTI_CHAR = [
  // Letter A patterns
  [/\/\\/g, "a"],
  [/\/-\\/g, "a"],
  [/\^/g, "a"],
  // Letter B patterns
  [/\|3/g, "b"],
  [/13/g, "b"],
  [/ß/g, "b"],
  // Letter D patterns
  [/\|\)/g, "d"],
  [/\|>/g, "d"],
  [/\[\)/g, "d"],
  // Letter F patterns
  [/\|=/g, "f"],
  [/ph/gi, "f"],
  // Letter H patterns
  [/\|-\|/g, "h"],
  [/\}\{/g, "h"],
  // Letter K patterns
  [/\|</g, "k"],
  [/\|\{/g, "k"],
  // Letter L patterns
  [/\|_/g, "l"],
  // Letter M patterns
  [/\/\\\/\\/g, "m"],
  [/\|V\|/g, "m"],
  [/\[V\]/g, "m"],
  // Letter N patterns
  [/\/\\\//g, "n"],
  [/\|\\\|/g, "n"],
  // Letter P patterns
  [/\|\*/g, "p"],
  [/\|o/g, "p"],
  // Letter R patterns
  [/\|2/g, "r"],
  [/\|\?/g, "r"],
  // Letter U patterns
  [/\|_\|/g, "u"],
  [/\\_\\/g, "u"],
  [/\/_\//g, "u"],
  // Letter V patterns
  [/\\\//g, "v"],
  // Letter W patterns
  [/\\\/\\\//g, "w"],
  [/vv/gi, "w"],
  // Letter X patterns
  [/></g, "x"],
  // Letter Y patterns
  [/'\//g, "y"],
  // Letter Z patterns
  [/7_/g, "z"]
];
AGGRESSIVE_MULTI_CHAR.sort((a, b) => b[0].source.length - a[0].source.length);
var AGGRESSIVE_SUBSTITUTIONS = {
  ...MODERATE_SUBSTITUTIONS,
  "|": "i",
  "6": "g",
  "2": "z",
  "%": "z"
};
var AMBIGUOUS_AS_L = { "|": "l", "1": "l", "!": "l" };
var SPACED_CHARS_PATTERN = /\b([a-zA-Z0-9@$!#])\s+([a-zA-Z0-9@$!#])(\s+[a-zA-Z0-9@$!#])+\b/g;
var MEASUREMENT_LIKE = /(?<![A-Za-z])\d+[A-Za-z](?![A-Za-z])/g;
function measurementDigitIndices(text) {
  const keep = /* @__PURE__ */ new Set();
  for (const match of text.matchAll(MEASUREMENT_LIKE)) {
    const start = match.index ?? 0;
    for (let i = start; i < start + match[0].length; i++) {
      if (text[i] >= "0" && text[i] <= "9") keep.add(i);
    }
  }
  return keep;
}
function getLeetspeakTables(level, ambiguousAsL = false) {
  return {
    multi: level === "aggressive" ? AGGRESSIVE_MULTI_CHAR : [],
    single: getSubstitutionMap(level, ambiguousAsL)
  };
}
function normalizeLeetspeak(text, options = {}) {
  const {
    level = "moderate",
    collapseRepeated = true,
    maxRepeated = 2,
    removeSpacedChars = true,
    ambiguousAsL = false
  } = options;
  let normalized = text;
  if (removeSpacedChars) {
    normalized = collapseSpacedCharacters(normalized);
  }
  if (level === "aggressive") {
    for (const [pattern, replacement] of AGGRESSIVE_MULTI_CHAR) {
      normalized = normalized.replace(pattern, replacement);
    }
  }
  const substitutions = getSubstitutionMap(level, ambiguousAsL);
  const keepDigits = measurementDigitIndices(normalized);
  normalized = normalized.split("").map((char, index) => keepDigits.has(index) ? char : substitutions[char] || char).join("");
  if (collapseRepeated) {
    normalized = collapseRepeatedCharacters(normalized, maxRepeated);
  }
  return normalized;
}
function getSubstitutionMap(level, ambiguousAsL = false) {
  if (ambiguousAsL) return { ...getSubstitutionMap(level), ...AMBIGUOUS_AS_L };
  switch (level) {
    case "basic":
      return BASIC_SUBSTITUTIONS;
    case "moderate":
      return MODERATE_SUBSTITUTIONS;
    case "aggressive":
      return AGGRESSIVE_SUBSTITUTIONS;
    default:
      return MODERATE_SUBSTITUTIONS;
  }
}
function collapseSpacedCharacters(text) {
  return text.replace(SPACED_CHARS_PATTERN, (match) => {
    return match.replace(/\s+/g, "");
  });
}
function collapseRepeatedCharacters(text, maxRepeated = 2) {
  const pattern = new RegExp(`(.)\\1{${maxRepeated},}`, "gi");
  return text.replace(pattern, (match, char) => char.repeat(maxRepeated));
}

// src/utils/unicode.ts
var HOMOGLYPHS = {
  // Cyrillic homoglyphs (look like Latin)
  "\u0430": "a",
  // Cyrillic small a
  "\u0410": "A",
  // Cyrillic capital A
  "\u0435": "e",
  // Cyrillic small e
  "\u0415": "E",
  // Cyrillic capital E
  "\u043A": "k",
  // Cyrillic small ka
  "\u041A": "K",
  // Cyrillic capital Ka
  "\u043E": "o",
  // Cyrillic small o
  "\u041E": "O",
  // Cyrillic capital O
  "\u0440": "p",
  // Cyrillic small er
  "\u0420": "P",
  // Cyrillic capital Er
  "\u0441": "c",
  // Cyrillic small es
  "\u0421": "C",
  // Cyrillic capital Es
  "\u0443": "u",
  // Cyrillic small u (map to u, not y)
  "\u0423": "U",
  // Cyrillic capital U
  "\u0445": "x",
  // Cyrillic small ha
  "\u0425": "X",
  // Cyrillic capital Ha
  "\u0456": "i",
  // Cyrillic small i (Ukrainian)
  "\u0406": "I",
  // Cyrillic capital I (Ukrainian)
  "\u0458": "j",
  // Cyrillic small je
  "\u0408": "J",
  // Cyrillic capital Je
  "\u0455": "s",
  // Cyrillic small dze
  "\u0405": "S",
  // Cyrillic capital Dze
  // Currency and special symbols that look like letters
  "\xA2": "c",
  // Cent sign
  "\u0192": "f",
  // Latin small f with hook (florin)
  // Greek homoglyphs
  "\u03B1": "a",
  // Greek small alpha
  "\u0391": "A",
  // Greek capital Alpha
  "\u03B2": "b",
  // Greek small beta (sort of)
  "\u0392": "B",
  // Greek capital Beta
  "\u03B5": "e",
  // Greek small epsilon
  "\u0395": "E",
  // Greek capital Epsilon
  "\u03B7": "n",
  // Greek small eta
  "\u0397": "H",
  // Greek capital Eta
  "\u03B9": "i",
  // Greek small iota
  "\u0399": "I",
  // Greek capital Iota
  "\u03BA": "k",
  // Greek small kappa
  "\u039A": "K",
  // Greek capital Kappa
  "\u03BD": "v",
  // Greek small nu (looks like v)
  "\u039D": "N",
  // Greek capital Nu
  "\u03BF": "o",
  // Greek small omicron
  "\u039F": "O",
  // Greek capital Omicron
  "\u03C1": "p",
  // Greek small rho
  "\u03A1": "P",
  // Greek capital Rho
  "\u03C4": "t",
  // Greek small tau
  "\u03A4": "T",
  // Greek capital Tau
  "\u03C5": "u",
  // Greek small upsilon
  "\u054D": "U",
  // Armenian capital seh (looks like U)
  "\u057D": "u",
  // Armenian small seh (looks like u)
  "\u03A5": "Y",
  // Greek capital Upsilon
  "\u03C7": "x",
  // Greek small chi
  "\u03A7": "X",
  // Greek capital Chi
  // Mathematical symbols
  "\u2102": "C",
  // Double-struck capital C
  "\u210D": "H",
  // Double-struck capital H
  "\u2115": "N",
  // Double-struck capital N
  "\u2119": "P",
  // Double-struck capital P
  "\u211A": "Q",
  // Double-struck capital Q
  "\u211D": "R",
  // Double-struck capital R
  "\u2124": "Z",
  // Double-struck capital Z
  // Subscript/superscript
  "\u1D43": "a",
  "\u1D47": "b",
  "\u1D9C": "c",
  "\u1D48": "d",
  "\u1D49": "e",
  "\u1DA0": "f",
  "\u1D4D": "g",
  "\u02B0": "h",
  "\u2071": "i",
  "\u02B2": "j",
  "\u1D4F": "k",
  "\u02E1": "l",
  "\u1D50": "m",
  "\u207F": "n",
  "\u1D52": "o",
  "\u1D56": "p",
  "\u02B3": "r",
  "\u02E2": "s",
  "\u1D57": "t",
  "\u1D58": "u",
  "\u1D5B": "v",
  "\u02B7": "w",
  "\u02E3": "x",
  "\u02B8": "y",
  "\u1DBB": "z",
  // Small caps
  "\u1D00": "A",
  "\u0299": "B",
  "\u1D04": "C",
  "\u1D05": "D",
  "\u1D07": "E",
  "\uA730": "F",
  "\u0262": "G",
  "\u029C": "H",
  "\u026A": "I",
  "\u1D0A": "J",
  "\u1D0B": "K",
  "\u029F": "L",
  "\u1D0D": "M",
  "\u0274": "N",
  "\u1D0F": "O",
  "\u1D18": "P",
  "\u01EB": "Q",
  "\u0280": "R",
  "\uA731": "S",
  "\u1D1B": "T",
  "\u1D1C": "U",
  "\u1D20": "V",
  "\u1D21": "W",
  "\u028F": "Y",
  "\u1D22": "Z",
  // Other lookalikes
  "\u24D0": "a",
  "\u24D1": "b",
  "\u24D2": "c",
  "\u24D3": "d",
  "\u24D4": "e",
  "\u24D5": "f",
  "\u24D6": "g",
  "\u24D7": "h",
  "\u24D8": "i",
  "\u24D9": "j",
  "\u24DA": "k",
  "\u24DB": "l",
  "\u24DC": "m",
  "\u24DD": "n",
  "\u24DE": "o",
  "\u24DF": "p",
  "\u24E0": "q",
  "\u24E1": "r",
  "\u24E2": "s",
  "\u24E3": "t",
  "\u24E4": "u",
  "\u24E5": "v",
  "\u24E6": "w",
  "\u24E7": "x",
  "\u24E8": "y",
  "\u24E9": "z",
  // Fancy Unicode letters
  "\uFF41": "a",
  "\uFF42": "b",
  "\uFF43": "c",
  "\uFF44": "d",
  "\uFF45": "e",
  "\uFF46": "f",
  "\uFF47": "g",
  "\uFF48": "h",
  "\uFF49": "i",
  "\uFF4A": "j",
  "\uFF4B": "k",
  "\uFF4C": "l",
  "\uFF4D": "m",
  "\uFF4E": "n",
  "\uFF4F": "o",
  "\uFF50": "p",
  "\uFF51": "q",
  "\uFF52": "r",
  "\uFF53": "s",
  "\uFF54": "t",
  "\uFF55": "u",
  "\uFF56": "v",
  "\uFF57": "w",
  "\uFF58": "x",
  "\uFF59": "y",
  "\uFF5A": "z",
  // Mirrored/rotated
  "\u0250": "a",
  "\u0254": "c",
  "\u01DD": "e",
  "\u025F": "j",
  "\u0265": "h",
  "\u026F": "m",
  "\u0279": "r",
  "\u0287": "t",
  "\u028C": "v",
  "\u028D": "w",
  // Common lookalikes
  "\u0142": "l",
  "\u0141": "L",
  "\xF8": "o",
  "\xD8": "O",
  "\u0111": "d",
  "\u0110": "D",
  "\u0127": "h",
  "\u0126": "H",
  "\u0131": "i",
  "\u0130": "I",
  "\u0138": "k",
  "\u0140": "l",
  "\u013F": "L",
  "\u014B": "n",
  "\u014A": "N",
  "\u0153": "oe",
  "\u0152": "OE",
  "\u017F": "s",
  "\u0167": "t",
  "\u0166": "T"
};
var ZERO_WIDTH_CHARS = [
  "\u200B",
  // Zero-width space
  "\u200C",
  // Zero-width non-joiner
  "\u200D",
  // Zero-width joiner
  "\u200E",
  // Left-to-right mark
  "\u200F",
  // Right-to-left mark
  "\u2060",
  // Word joiner
  "\u2061",
  // Function application
  "\u2062",
  // Invisible times
  "\u2063",
  // Invisible separator
  "\u2064",
  // Invisible plus
  "\uFEFF",
  // Byte order mark / zero-width no-break space
  "\xAD",
  // Soft hyphen
  "\u034F",
  // Combining grapheme joiner
  "\u061C",
  // Arabic letter mark
  "\u115F",
  // Hangul choseong filler
  "\u1160",
  // Hangul jungseong filler
  "\u17B4",
  // Khmer vowel inherent Aq
  "\u17B5",
  // Khmer vowel inherent Aa
  "\u180E",
  // Mongolian vowel separator
  "\u3164"
  // Hangul filler
];
var KANA_VOICED_SOUND_MARKS = /* @__PURE__ */ new Set(["\u3099", "\u309A"]);
function normalizeUnicode(text, options = {}) {
  const {
    nfkd = true,
    homoglyphs = true,
    fullWidth = true,
    removeDiacritics = true,
    removeZeroWidth = true
  } = options;
  let normalized = text;
  if (removeZeroWidth) {
    normalized = removeZeroWidthCharacters(normalized);
  }
  if (fullWidth) {
    normalized = convertFullWidth(normalized);
  }
  if (homoglyphs) {
    normalized = convertHomoglyphs(normalized);
  }
  if (nfkd || removeDiacritics) {
    normalized = normalizeNFKD(normalized, removeDiacritics);
  }
  return normalized;
}
function removeZeroWidthCharacters(text) {
  const pattern = new RegExp(`[${ZERO_WIDTH_CHARS.join("")}]`, "g");
  return text.replace(pattern, "");
}
function convertFullWidth(text) {
  return text.replace(/[\uFF01-\uFF5E]/g, (char) => {
    return String.fromCharCode(char.charCodeAt(0) - 65248);
  });
}
function homoglyphToAscii(char) {
  return HOMOGLYPHS[char] ?? char;
}
function convertHomoglyphs(text) {
  return text.split("").map((char) => homoglyphToAscii(char)).join("");
}
function normalizeNFKD(text, removeDiacritics = true) {
  let normalized = text.normalize("NFKD");
  if (removeDiacritics) {
    normalized = normalized.replace(
      /\p{Mn}/gu,
      (mark) => KANA_VOICED_SOUND_MARKS.has(mark) ? mark : ""
    );
    normalized = normalized.normalize("NFC");
  }
  return normalized;
}

// src/filters/normalizeText.ts
function normalizeObfuscated(text) {
  const charMap = { "@": "a", $: "s", "!": "i", "1": "i", "*": "" };
  return text.replace(/([a-zA-Z])\1{1,}/g, "$1$1").replace(/[@$!1*]/g, (m) => charMap[m] || m);
}
function normalizeForMatching(text, aggressive, config) {
  let normalized = text;
  if (config.normalizeUnicode) normalized = normalizeUnicode(normalized);
  if (config.detectLeetspeak) {
    normalized = normalizeLeetspeak(normalized, {
      level: config.leetspeakLevel,
      collapseRepeated: true,
      // Keep double letters like "ss" for the normal check, collapse all when aggressive.
      maxRepeated: aggressive ? 1 : 2,
      removeSpacedChars: true
    });
  }
  if (config.allowObfuscatedMatch && !config.detectLeetspeak) {
    normalized = normalizeObfuscated(normalized);
  }
  return normalized;
}

// src/filters/wordEdges.ts
var KATA = "\\u30a1-\\u30fa\\u30fc-\\u30ff\\u31f0-\\u31ff\\uff66-\\uff9f";
var HAN = /^(?:[\u3005\u3100-\u312f\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]|[\u{20000}-\u{2fa1f}])$/u;
var KATAKANA = new RegExp(`^[${KATA}]$`, "u");
var UNAMBIGUOUS_SINGLE_HAN = new Set(Array.from("\u808F\u5C44\u5C4C\u81A3\u59E6\u59D8"));
var NATIVE = /^[\p{L}\p{N}\p{M}]$/u;
function edgeKind(char, wordLength) {
  if (char.charCodeAt(0) < 128 || !NATIVE.test(char)) return "ascii";
  if (KATAKANA.test(char)) return "kata";
  if (!HAN.test(char)) return "native";
  return wordLength >= 2 || UNAMBIGUOUS_SINGLE_HAN.has(char) ? "free" : "native";
}
function wordEdges(word) {
  const chars = Array.from(word);
  if (chars.length === 0) return { head: "ascii", tail: "ascii" };
  return {
    head: edgeKind(chars[0], chars.length),
    tail: edgeKind(chars[chars.length - 1], chars.length)
  };
}
var HEAD = {
  ascii: "\\b",
  native: "(?<![\\p{L}\\p{N}_])",
  free: "",
  kata: `(?<![${KATA}])`
};
var TAIL = {
  ascii: "\\b",
  native: "(?![\\p{L}\\p{N}_])",
  free: "",
  kata: `(?![${KATA}])`
};
function headSource(kind) {
  return HEAD[kind];
}
function tailSource(kind) {
  return TAIL[kind];
}
function boundedPattern(word) {
  const { head, tail } = wordEdges(word);
  return {
    source: `${HEAD[head]}${word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}${TAIL[tail]}`,
    unicode: head !== "ascii" && head !== "free" || tail !== "ascii" && tail !== "free"
  };
}

// src/filters/wordMatcher.ts
var WordMatcher = class {
  constructor(opts) {
    this.opts = opts;
    this.regexCache = /* @__PURE__ */ new Map();
  }
  get wordBoundaries() {
    return this.opts.wordBoundaries;
  }
  clear() {
    this.regexCache.clear();
  }
  /** Returns a shared global regex for the word with lastIndex reset. */
  getRegex(word) {
    const cached = this.regexCache.get(word);
    if (cached) {
      cached.lastIndex = 0;
      return cached;
    }
    const baseFlags = this.opts.caseSensitive ? "g" : "gi";
    let regex;
    if (this.opts.wordBoundaries) {
      const { source, unicode } = boundedPattern(word);
      regex = new RegExp(source, unicode ? `${baseFlags}u` : baseFlags);
    } else {
      regex = new RegExp(word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), baseFlags);
    }
    this.regexCache.set(word, regex);
    return regex;
  }
  isFuzzyToleranceMatch(word, text) {
    const simplifiedText = text.toLowerCase().replace(/[^a-z]/g, "");
    const simplifiedWord = word.toLowerCase();
    let matchCount = 0;
    let index = 0;
    for (let i = 0; i < simplifiedText.length; i++) {
      if (simplifiedText[i] === simplifiedWord[index]) {
        matchCount++;
        index++;
        if (index === simplifiedWord.length) break;
      }
    }
    return matchCount / simplifiedWord.length >= this.opts.fuzzyToleranceLevel;
  }
  evaluateSeverity(word, text) {
    if (this.getRegex(word).test(text)) {
      return 1 /* EXACT */;
    }
    if (!this.opts.wordBoundaries && this.isFuzzyToleranceMatch(word, text)) {
      return 2 /* FUZZY */;
    }
    return void 0;
  }
};

// src/filters/replacement.ts
function escapeRegExp(value) {
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function escapeCharClass(value) {
  return value.replace(/[\\\][^-]/g, "\\$&");
}
function replaceWords(text, words, replaceWith, wordBoundaries) {
  let processed = text;
  for (const word of new Set(words)) {
    const escaped = escapeRegExp(word);
    let pattern;
    if (wordBoundaries) {
      const { source, unicode } = boundedPattern(word);
      pattern = new RegExp(source, unicode ? "giu" : "gi");
    } else {
      pattern = new RegExp(escaped, "gi");
    }
    processed = processed.replace(pattern, replaceWith);
  }
  return processed;
}
function replaceSpans(text, spans, replaceWith) {
  if (spans.length === 0) return text;
  const sorted = [...spans].sort((a, b) => a[0] - b[0]);
  const merged = [];
  for (const span of sorted) {
    const last = merged[merged.length - 1];
    if (last && span[0] <= last[1]) last[1] = Math.max(last[1], span[1]);
    else merged.push([span[0], span[1]]);
  }
  let out = "";
  let cursor = 0;
  for (const [start, end] of merged) {
    out += text.slice(cursor, start) + replaceWith;
    cursor = end;
  }
  return out + text.slice(cursor);
}

// src/filters/redact.ts
function redactText(text, profaneWords, replaceWith, wordBoundaries, hits, safeSpans) {
  if (hits) {
    return replaceSpans(text, hits.map((h) => [h.start, h.end]), replaceWith);
  }
  if (profaneWords.length === 0) return text;
  const mask = (part) => replaceWords(part, profaneWords, replaceWith, wordBoundaries);
  let out = "";
  let cursor = 0;
  for (const [start, end] of safeSpans) {
    out += mask(text.slice(cursor, start)) + text.slice(start, end);
    cursor = end;
  }
  return out + mask(text.slice(cursor));
}

// src/filters/hitSummary.ts
function uniqueSpans(hits) {
  const seen = /* @__PURE__ */ new Set();
  return hits.filter((h) => {
    const key = `${h.word}|${h.start}|${h.end}`;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}
function summarizeHits(hits) {
  const profaneWords = [];
  const severityMap = {};
  for (const hit of hits) {
    const key = hit.view === "orig" ? hit.text : hit.word;
    if (hit.view === "orig" || !profaneWords.includes(key)) {
      profaneWords.push(key);
      if (severityMap[key] === void 0) severityMap[key] = hit.severity;
    }
  }
  return { profaneWords, severityMap };
}

// src/filters/legacyScan.ts
function legacyIsProfane(ctx, value) {
  const normalizedInput = ctx.normalizeText(value);
  const aggressiveInput = ctx.normalizeText(value, true);
  for (const word of ctx.words.keys()) {
    if (ctx.ignoreWords.has(word.toLowerCase())) continue;
    if (ctx.matcher.evaluateSeverity(word, value) !== void 0) return true;
    if (ctx.matcher.evaluateSeverity(word, normalizedInput) !== void 0) return true;
    if (ctx.matcher.evaluateSeverity(word, aggressiveInput) !== void 0) return true;
  }
  return false;
}
function legacyScan(ctx, text) {
  const originalInput = text.toLowerCase();
  const normalizedInput = ctx.normalizeText(text).toLowerCase();
  const aggressiveInput = ctx.normalizeText(text, true).toLowerCase();
  const profaneWords = [];
  const severityMap = {};
  const record = (word, severity) => {
    if (severityMap[word] === void 0) severityMap[word] = severity;
  };
  for (const dictWord of ctx.words.keys()) {
    if (ctx.ignoreWords.has(dictWord.toLowerCase())) continue;
    let severity = ctx.matcher.evaluateSeverity(dictWord, originalInput);
    if (severity !== void 0) {
      const regex = ctx.matcher.getRegex(dictWord);
      let match;
      while ((match = regex.exec(originalInput)) !== null) {
        profaneWords.push(match[0]);
        record(match[0], severity);
      }
    }
    severity = ctx.matcher.evaluateSeverity(dictWord, normalizedInput);
    if (severity !== void 0) {
      const regex = ctx.matcher.getRegex(dictWord);
      while (regex.exec(normalizedInput) !== null) {
        if (!profaneWords.includes(dictWord)) {
          profaneWords.push(dictWord);
          record(dictWord, severity);
        }
      }
    }
    severity = ctx.matcher.evaluateSeverity(dictWord, aggressiveInput);
    if (severity !== void 0 && !profaneWords.includes(dictWord)) {
      profaneWords.push(dictWord);
      record(dictWord, severity);
    }
  }
  return { profaneWords, severityMap };
}
function legacyContextHits(ctx, text) {
  const views = [
    text.toLowerCase(),
    ctx.normalizeText(text).toLowerCase(),
    ctx.normalizeText(text, true).toLowerCase()
  ];
  const hits = [];
  for (const dictWord of ctx.words.keys()) {
    if (ctx.ignoreWords.has(dictWord.toLowerCase())) continue;
    let found = false;
    for (let v = 0; v < views.length && !found; v++) {
      const severity = ctx.matcher.evaluateSeverity(dictWord, views[v]);
      if (severity === void 0) continue;
      const regex = ctx.matcher.getRegex(dictWord);
      let match;
      while ((match = regex.exec(views[v])) !== null) {
        hits.push({ word: match[0], index: match.index, severity });
        found = true;
      }
      if (!found && v === views.length - 1) hits.push({ word: dictWord, index: 0, severity });
    }
  }
  return hits;
}

// src/filters/pipeline/dictionaryIndex.ts
var NEEDS_NORMALIZING = /[^a-z]|ph|vv|(.)\1\1/;
var LEET_READINGS = {
  "0": "o",
  "1": "il",
  "3": "e",
  "4": "a",
  "5": "s",
  "7": "t",
  "8": "b",
  "9": "g"
};
function squeeze(value) {
  return value.replace(/(.)\1+/gu, "$1");
}
var DictionaryIndex = class {
  constructor(words, options) {
    this.options = options;
    this.exact = /* @__PURE__ */ new Set();
    this.normalized = /* @__PURE__ */ new Set();
    this.byLength = /* @__PURE__ */ new Map();
    this.squeezed = /* @__PURE__ */ new Map();
    this.stretchPatterns = /* @__PURE__ */ new Map();
    let longest = 0;
    for (const raw of words) {
      const word = raw.toLowerCase();
      if (/\s/.test(word)) continue;
      this.exact.add(word);
      longest = Math.max(longest, word.length);
      const norm = this.normalize(word);
      this.normalized.add(norm);
      if (/^\p{L}+$/u.test(word)) {
        const list = this.byLength.get(word.length) ?? [];
        list.push(word);
        this.byLength.set(word.length, list);
      }
      if (/^\p{L}+$/u.test(norm) && norm.length >= 3) {
        const key = squeeze(norm);
        const list = this.squeezed.get(key) ?? [];
        if (!list.includes(norm)) list.push(norm);
        this.squeezed.set(key, list);
      }
    }
    this.maxWordLength = longest;
    this.rankMaskCandidates();
  }
  /**
   * When several words fit a mask, prefer the root other entries build on
   * (fuck over fvck, shit over scat) so reports name the expected word.
   */
  rankMaskCandidates() {
    const roots = /* @__PURE__ */ new Map();
    for (const word of this.exact) {
      for (let len = 3; len < word.length; len++) {
        const prefix = word.slice(0, len);
        roots.set(prefix, (roots.get(prefix) ?? 0) + 1);
      }
    }
    for (const list of this.byLength.values()) {
      list.sort((a, b) => (roots.get(b) ?? 0) - (roots.get(a) ?? 0));
    }
  }
  normalize(value) {
    if (!this.options.leetspeak) return value;
    return normalizeLeetspeak(value, {
      level: this.options.leetspeakLevel,
      maxRepeated: 2,
      removeSpacedChars: false
    }).toLowerCase();
  }
  /** True when the fragment is a dictionary word, directly or after leet decoding. */
  isWord(fragment) {
    const lower = fragment.toLowerCase();
    if (this.exact.has(lower)) return true;
    if (!NEEDS_NORMALIZING.test(lower)) return false;
    return this.normalized.has(this.normalize(lower));
  }
  /**
   * Resolves a token with mask characters (f*ck, sh#t) to a dictionary word of
   * the same length where each mask stands for exactly one letter.
   */
  resolveMasked(token, masks, minVisible) {
    const chars = Array.from(token.toLowerCase());
    const length = chars.length;
    const candidates = this.byLength.get(length);
    if (!candidates) return null;
    let visible = 0;
    let maskCount = 0;
    for (const c of chars) {
      if (masks.has(c)) maskCount++;
      else if (/\p{L}/u.test(c)) visible++;
    }
    if (maskCount === 0 || visible < minVisible) return null;
    if (visible === 1 && !(length >= 4 && !masks.has(chars[0]) && /\p{L}/u.test(chars[0]))) {
      return null;
    }
    if (length <= 3 && visible < 2) return null;
    for (const word of candidates) {
      let ok = true;
      let k = 0;
      for (const w of word) {
        const c = chars[k++];
        if (masks.has(c) || c === w) continue;
        if (LEET_READINGS[c]?.includes(w)) continue;
        ok = false;
        break;
      }
      if (ok) return word;
    }
    return null;
  }
  /**
   * Resolves a stretched token (aaaasshole) to its dictionary word. The token
   * must contain the word's own letter runs, only longer, so "as" never
   * becomes "ass" and legitimate double letters still match.
   */
  resolveStretched(token) {
    const lower = token.toLowerCase();
    const candidates = this.squeezed.get(squeeze(lower));
    if (!candidates) return null;
    for (const word of candidates) {
      if (lower.length <= word.length) continue;
      if (this.stretchPattern(word).test(lower)) return word;
    }
    return null;
  }
  stretchPattern(word) {
    let pattern = this.stretchPatterns.get(word);
    if (!pattern) {
      const runs = word.match(/(.)\1*/gu) ?? [];
      const body = runs.map((run) => {
        const ch = escapeRegExp(Array.from(run)[0]);
        const count = Array.from(run).length;
        return count > 1 ? `${ch}{${count},}` : `${ch}+`;
      }).join("");
      pattern = new RegExp(`^${body}$`, "u");
      this.stretchPatterns.set(word, pattern);
    }
    return pattern;
  }
};

// src/filters/pipeline/mapped.ts
function fromText(text) {
  return { text, starts: null, ends: null };
}
function spanStart(m, index) {
  return m.starts ? m.starts[index] : index;
}
function spanEnd(m, index) {
  return m.ends ? m.ends[index] : index + 1;
}
function toOriginalSpan(m, start, end) {
  if (end <= start) {
    const at = start < m.text.length ? spanStart(m, start) : spanEnd(m, Math.max(0, m.text.length - 1));
    return [at, at];
  }
  return [spanStart(m, start), spanEnd(m, end - 1)];
}
function subsequenceIndices(source, repl) {
  const picked = [];
  let at = 0;
  for (let i = 0; i < repl.length; i++) {
    const found = source.indexOf(repl[i], at);
    if (found < 0) return null;
    picked.push(found);
    at = found + 1;
  }
  return picked;
}
function applyEdits(m, edits) {
  if (edits.length === 0) return m;
  let out = "";
  const starts = [];
  const ends = [];
  let cursor = 0;
  const copy = (from, to) => {
    out += m.text.slice(from, to);
    for (let i = from; i < to; i++) {
      starts.push(spanStart(m, i));
      ends.push(spanEnd(m, i));
    }
  };
  for (const edit of edits) {
    copy(cursor, edit.start);
    const source = m.text.slice(edit.start, edit.end);
    const repl = edit.replacement;
    if (repl.length > 0) {
      const picked = repl.length === source.length ? Array.from({ length: repl.length }, (_, i) => i) : repl.length < source.length ? subsequenceIndices(source, repl) : null;
      if (picked) {
        for (const p of picked) {
          starts.push(spanStart(m, edit.start + p));
          ends.push(spanEnd(m, edit.start + p));
        }
      } else {
        const s = spanStart(m, edit.start);
        const e = spanEnd(m, edit.end - 1);
        for (let i = 0; i < repl.length; i++) {
          starts.push(s);
          ends.push(e);
        }
      }
      out += repl;
    }
    cursor = edit.end;
  }
  copy(cursor, m.text.length);
  return { text: out, starts: Int32Array.from(starts), ends: Int32Array.from(ends) };
}
function replaceRegex(m, pattern, fn) {
  const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g");
  const edits = [];
  let match;
  while ((match = re.exec(m.text)) !== null) {
    if (match[0].length === 0) {
      re.lastIndex++;
      continue;
    }
    const replacement = fn(match);
    if (replacement !== null && replacement !== match[0]) {
      edits.push({ start: match.index, end: match.index + match[0].length, replacement });
    }
  }
  return applyEdits(m, edits);
}
function mapChars(m, fn) {
  const edits = [];
  const text = m.text;
  for (let i = 0; i < text.length; i++) {
    const next = fn(text[i]);
    if (next !== void 0 && next !== text[i]) {
      edits.push({ start: i, end: i + 1, replacement: next });
    }
  }
  return applyEdits(m, edits);
}
function runPipeline(input, steps) {
  let current = input;
  for (const step of steps) current = step(current);
  return current;
}

// src/filters/pipeline/html.ts
var TAG_PATTERN = /<!--[\s\S]{0,2000}?-->|<\/?[a-zA-Z][a-zA-Z0-9-]*(?:\s[^<>]*)?\/?>/g;
var ENTITY_PATTERN = /&(?:#(\d{1,7})|#[xX]([0-9a-fA-F]{1,6})|([a-zA-Z][a-zA-Z0-9]{1,31}));/g;
var BLOCK_TAG = /^<\/?(?:p|div|li|ul|ol|tr|td|th|table|h[1-6]|section|article|blockquote|pre|hr)\b/i;
var NAMED_ENTITIES = {
  amp: "&",
  lt: "<",
  gt: ">",
  quot: '"',
  apos: "'",
  nbsp: " ",
  ensp: " ",
  emsp: " ",
  thinsp: " ",
  shy: "",
  zwnj: "",
  zwj: "",
  lrm: "",
  rlm: "",
  hellip: "...",
  ndash: "-",
  mdash: "-",
  auml: "\xE4",
  ouml: "\xF6",
  uuml: "\xFC",
  Auml: "\xC4",
  Ouml: "\xD6",
  Uuml: "\xDC",
  szlig: "\xDF",
  eacute: "\xE9",
  egrave: "\xE8",
  ecirc: "\xEA",
  aacute: "\xE1",
  agrave: "\xE0",
  acirc: "\xE2",
  iacute: "\xED",
  oacute: "\xF3",
  uacute: "\xFA",
  ntilde: "\xF1",
  ccedil: "\xE7"
};
var SOFT_HYPHEN = "\xAD";
function decodeCodePoint(code) {
  if (!Number.isInteger(code) || code < 1 || code > 1114111) return null;
  if (code >= 55296 && code <= 57343) return null;
  const ch = String.fromCodePoint(code);
  return ch === SOFT_HYPHEN ? "" : ch;
}
function decodeEntity(match) {
  if (match[1] !== void 0) return decodeCodePoint(parseInt(match[1], 10));
  if (match[2] !== void 0) return decodeCodePoint(parseInt(match[2], 16));
  const named = NAMED_ENTITIES[match[3]];
  return named === void 0 ? null : named;
}
function htmlTransformer(options) {
  return (input) => {
    const text = input.text;
    if (text.indexOf("<") < 0 && text.indexOf("&") < 0) return input;
    let m = replaceRegex(input, TAG_PATTERN, (match) => BLOCK_TAG.test(match[0]) ? " " : "");
    for (let pass = 0; pass < options.decodeDepth; pass++) {
      if (m.text.indexOf("&") < 0) break;
      const next = replaceRegex(m, ENTITY_PATTERN, decodeEntity);
      if (next === m) break;
      m = next;
    }
    return m;
  };
}

// src/filters/pipeline/masking.ts
var EDGE_PUNCTUATION = /* @__PURE__ */ new Set(["?", "!"]);
var OPENER_WINDOW = 200;
var EMPHASIS_CHARS = /* @__PURE__ */ new Set(["*", "_"]);
function splitEmphasis(raw) {
  let lead = 0;
  while (lead < raw.length && EMPHASIS_CHARS.has(raw[lead])) lead++;
  if (lead === raw.length) return [raw, "", ""];
  let end = raw.length;
  while (end > lead && EMPHASIS_CHARS.has(raw[end - 1])) end--;
  return [raw.slice(0, lead), raw.slice(lead, end), raw.slice(end)];
}
function isEmphasis(raw, text, tokenStart, masks) {
  const [lead, inner, trail] = splitEmphasis(raw);
  if (Array.from(inner).some((c) => masks.has(c))) return false;
  if (lead && trail) return true;
  if (!trail) return false;
  const window = text.slice(Math.max(0, tokenStart - OPENER_WINDOW), tokenStart);
  const before = window.slice(window.lastIndexOf("\n") + 1);
  return new RegExp(`(?:^|\\s)${escapeRegExp(trail)}\\S`).test(before);
}
function maskingTransformer(index, options) {
  const masks = new Set(Array.from(options.chars));
  const maskClass = escapeCharClass(options.chars);
  const quick = new RegExp(`[${maskClass}]`);
  const token = new RegExp(`[\\p{L}\\p{N}${maskClass}]+`, "gu");
  return (input) => {
    if (!quick.test(input.text)) return input;
    const edits = [];
    token.lastIndex = 0;
    let match;
    while ((match = token.exec(input.text)) !== null) {
      const raw = match[0];
      if (isEmphasis(raw, input.text, match.index, masks)) continue;
      let lo = 0;
      let hi = raw.length;
      while (lo < hi && EDGE_PUNCTUATION.has(raw[lo])) lo++;
      while (hi > lo && EDGE_PUNCTUATION.has(raw[hi - 1])) hi--;
      const core = raw.slice(lo, hi);
      if (core.length < 3) continue;
      const word = index.resolveMasked(core, masks, options.minVisible);
      if (word && word.length === core.length) {
        edits.push({ start: match.index + lo, end: match.index + hi, replacement: word });
      }
    }
    return applyEdits(input, edits);
  };
}

// src/filters/pipeline/normalizers.ts
var ZERO_WIDTH_PATTERN = new RegExp(`[${ZERO_WIDTH_CHARS.join("")}]`, "g");
var NON_ASCII = /[\u0080-\uffff]/;
var unicodeTransformer = (input) => {
  if (!NON_ASCII.test(input.text)) return input;
  let m = replaceRegex(input, ZERO_WIDTH_PATTERN, () => "");
  m = replaceRegex(
    m,
    /[!-~]/g,
    (match) => String.fromCharCode(match[0].charCodeAt(0) - 65248)
  );
  m = mapChars(m, (ch) => HOMOGLYPHS[ch]);
  return decomposeAndStrip(m);
};
function stripMarks(value) {
  return value.normalize("NFKD").replace(/[̀-ͯ]/g, "");
}
function decomposeAndStrip(m) {
  const target = stripMarks(m.text);
  if (target === m.text) return m;
  const edits = [];
  let joined = "";
  let i = 0;
  for (const ch of m.text) {
    const next = ch.length === 1 && ch.charCodeAt(0) < 128 ? ch : stripMarks(ch);
    if (next !== ch) edits.push({ start: i, end: i + ch.length, replacement: next });
    joined += next;
    i += ch.length;
  }
  if (joined === target) return applyEdits(m, edits);
  return applyEdits(m, [{ start: 0, end: m.text.length, replacement: target }]);
}
function leetTransformer(options) {
  const tables = getLeetspeakTables(options.level, options.ambiguousAsL ?? false);
  const repeated = new RegExp(`(.)\\1{${options.maxRepeated},}`, "gi");
  return (input) => {
    let m = replaceRegex(input, SPACED_CHARS_PATTERN, (match) => match[0].replace(/\s+/g, ""));
    for (const [pattern, replacement] of tables.multi) {
      m = replaceRegex(m, pattern, () => replacement);
    }
    const keepDigits = measurementDigitIndices(m.text);
    let at = -1;
    m = mapChars(m, (ch) => keepDigits.has(++at) ? void 0 : tables.single[ch]);
    return replaceRegex(m, repeated, (match) => match[1].repeat(options.maxRepeated));
  };
}
var legacyObfuscationTransformer = (input) => {
  const charMap = { "@": "a", $: "s", "!": "i", "1": "i", "*": "" };
  const m = replaceRegex(input, /([a-zA-Z])\1{1,}/g, (match) => match[1] + match[1]);
  return replaceRegex(m, /[@$!1*]/g, (match) => charMap[match[0]] || match[0]);
};

// src/filters/pipeline/separators.ts
function separatorTransformer(index, options) {
  const sepClass = escapeCharClass(options.chars);
  const chain = new RegExp(
    `[\\p{L}\\p{N}]+(?:[${sepClass} ][\\p{L}\\p{N}]+)+`,
    "gu"
  );
  const quick = new RegExp(
    `[\\p{L}\\p{N}][${sepClass}][\\p{L}\\p{N}]|\\b\\p{L} \\p{L} \\p{L}\\b`,
    "u"
  );
  const sepSet = new Set(Array.from(options.chars));
  return (input) => {
    if (!quick.test(input.text)) return input;
    const edits = [];
    const taken = /* @__PURE__ */ new Set();
    chain.lastIndex = 0;
    let match;
    while ((match = chain.exec(input.text)) !== null) {
      const pieces = splitPieces(match[0], match.index, sepSet);
      if (pieces.length < 2) continue;
      collectJoins(pieces, input.text, index, options, edits, taken);
    }
    edits.sort((a, b) => a.start - b.start);
    return applyEdits(input, edits);
  };
}
function splitPieces(chainText, offset, seps) {
  const pieces = [];
  let start = 0;
  for (let i = 0; i <= chainText.length; i++) {
    const atEnd = i === chainText.length;
    const isSep = !atEnd && (seps.has(chainText[i]) || chainText[i] === " ");
    if (atEnd || isSep) {
      pieces.push({
        text: chainText.slice(start, i),
        start: offset + start,
        end: offset + i,
        sep: atEnd ? "" : chainText[i]
      });
      start = i + 1;
    }
  }
  return pieces;
}
function collectJoins(pieces, source, index, options, edits, taken) {
  let i = 0;
  while (i < pieces.length) {
    let joinedTo = -1;
    let candidate = "";
    for (let j = i; j < pieces.length; j++) {
      candidate += pieces[j].text;
      if (candidate.length > index.maxWordLength) break;
      if (j > i && acceptsJoin(pieces, i, j, candidate, index, options)) joinedTo = j;
    }
    if (joinedTo < 0) {
      i++;
      continue;
    }
    addEdits(pieces, i, joinedTo, source, edits, taken);
    i = joinedTo + 1;
  }
}
function acceptsJoin(pieces, from, to, candidate, index, options) {
  const span = pieces.slice(from, to + 1);
  if (!span.some((p) => p.text.length === 1)) return false;
  if (span.length === 2 && candidate.length < options.minTwoPieceLength) return false;
  const inner = span.slice(0, -1).map((p) => p.sep);
  const spaces = inner.filter((s) => s === " ").length;
  if (spaces > 0) {
    const others = inner.length - spaces;
    const spelledOut = others === 0 && span.length >= 3 && span.every((p) => p.text.length === 1);
    if (!spelledOut && others < (options.allowSpace ? 1 : 2)) return false;
  }
  return index.isWord(candidate) && endsAtWordEnd(pieces, to, index);
}
var SUFFIXES = /* @__PURE__ */ new Set(["s", "es", "ed", "er", "ers", "ing", "y"]);
function endsAtWordEnd(pieces, to, index) {
  const sep = pieces[to - 1].sep;
  let rest = "";
  let k = to + 1;
  while (k < pieces.length && pieces[k].text.length === 1 && pieces[k - 1].sep === sep && rest.length <= index.maxWordLength) {
    rest += pieces[k].text;
    k++;
  }
  if (rest === "") return true;
  return SUFFIXES.has(rest.toLowerCase()) || index.isWord(rest);
}
function addEdits(pieces, from, to, source, edits, taken) {
  const push = (edit) => {
    if (!taken.has(edit.start)) {
      taken.add(edit.start);
      edits.push(edit);
    }
  };
  for (let k = from; k < to; k++) {
    push({ start: pieces[k].end, end: pieces[k + 1].start, replacement: "" });
  }
  const prev = pieces[from].start - 1;
  if (from > 0 && source[prev] === "_") push({ start: prev, end: prev + 1, replacement: " " });
  const next = pieces[to].end;
  if (to + 1 < pieces.length && source[next] === "_") {
    push({ start: next, end: next + 1, replacement: " " });
  }
}

// src/filters/pipeline/stretch.ts
var TOKEN = /[\p{L}\p{N}]+/gu;
var HAS_REPEAT = /(.)\1/u;
function stretchTransformer(index) {
  return (input) => {
    if (!HAS_REPEAT.test(input.text)) return input;
    const edits = [];
    const re = new RegExp(TOKEN.source, TOKEN.flags);
    let match;
    while ((match = re.exec(input.text)) !== null) {
      if (match[0].length < 4) continue;
      const word = index.resolveStretched(match[0]);
      if (word) {
        edits.push({ start: match.index, end: match.index + match[0].length, replacement: word });
      }
    }
    return applyEdits(input, edits);
  };
}

// src/filters/pipeline/views.ts
var LONG_RUN = /(.)\1\1/u;
var AMBIGUOUS_GLYPH = /[\p{L}@$][|1!]+\p{L}/u;
var ViewFactory = class {
  constructor(cfg, index) {
    this.cfg = cfg;
    const obf = cfg.obfuscation;
    const html = obf.html.enabled ? [htmlTransformer(obf.html)] : [];
    const separators = obf.separators.enabled ? [separatorTransformer(index, obf.separators)] : [];
    const masking = obf.masking.enabled ? [maskingTransformer(index, obf.masking)] : [];
    this.prefix = [...html, ...separators, ...masking];
    this.prefixWithoutSeparators = [...html, ...masking];
    this.stretch = obf.stretch ? stretchTransformer(index) : null;
  }
  normalizeSteps(aggressive, ambiguousAsL = false) {
    const steps = [];
    if (this.cfg.normalizeUnicode) steps.push(unicodeTransformer);
    if (this.cfg.detectLeetspeak) {
      steps.push(
        leetTransformer({
          level: this.cfg.leetspeakLevel,
          maxRepeated: aggressive ? 1 : 2,
          ambiguousAsL
        })
      );
    } else if (this.cfg.allowObfuscatedMatch) {
      steps.push(legacyObfuscationTransformer);
    }
    return steps;
  }
  legacyViews(text) {
    const orig = fromText(text);
    return {
      orig,
      norm: runPipeline(orig, this.normalizeSteps(false)),
      aggr: runPipeline(orig, this.normalizeSteps(true))
    };
  }
  /** Extra views, empty when the text carries no obfuscation signal. */
  enhancedViews(text) {
    const orig = fromText(text);
    const prepared = runPipeline(orig, this.prefix);
    const changed = prepared !== orig;
    const longRun = this.stretch !== null && LONG_RUN.test(prepared.text);
    const ambiguous = this.cfg.obfuscation.ambiguousLeet && this.cfg.detectLeetspeak && AMBIGUOUS_GLYPH.test(text);
    if (!changed && !longRun && !ambiguous) return [];
    const views = [];
    const tail = this.stretch ? [this.stretch] : [];
    if (changed || longRun) {
      views.push(runPipeline(prepared, [...this.normalizeSteps(false), ...tail]));
    }
    if (ambiguous) {
      const steps = [...this.normalizeSteps(false, true), ...tail];
      views.push(runPipeline(prepared, steps));
      if (this.cfg.obfuscation.separators.enabled) {
        const unjoined = runPipeline(orig, this.prefixWithoutSeparators);
        if (unjoined.text !== prepared.text) views.push(runPipeline(unjoined, steps));
      }
    }
    return views;
  }
};

// src/filters/builtinWhitelist.ts
var NOT_A_NAME = "(?!(?:You|Your|Big|Small|Little|Huge|Fat|Stupid|Dumb|Such|What|The|This|That|Fucking|Lazy|Ugly|Total|Complete)\\s)";
var BUILTIN_WHITELIST = [
  // Clean words that a leet-decoded or invisible-character-split view turns into "ass".
  { protect: "(?:cl|gl|gr|m|b|br|l|s|p)ass(?:es)?" },
  { protect: "ass(?:ist|istant|istants|istance|ume|umes|umed|ess|essment|assin|assins|assinate|embly|emble|ociate|ociation|ignment|ert|et|ets)" },
  // Idioms and nouns where the dictionary word is not an insult.
  { before: "(?:summa |magna )?", protect: "cum", after: "\\s+laude" },
  { protect: "pussy", after: "\\s+willows?" },
  { before: "(?:great|blue|coal|marsh|willow)\\s+", protect: "tits?" },
  { protect: "bastard", after: "\\s+files?" },
  { protect: "jackass", after: "\\s+penguins?" },
  { protect: "cock(?:ed|ing)?", after: "\\s+the\\s+(?:hammer|gun|pistol)" },
  { protect: "anal", after: "\\s+fissures?" },
  // Place name, also hit by repeated-letter collapsing.
  { protect: "twatt", after: ",?\\s+(?:orkney|shetland)" },
  // Given name: "Moby Dick", "Philip K. Dick", "Dick Van Dyke".
  {
    before: `${NOT_A_NAME}(?:[A-Z][a-z]+|[A-Z]\\.)\\s+`,
    protect: "Dick",
    after: "(?!\\s+(?:Head|Move|Bag|Face)\\b)",
    caseSensitive: true
  },
  { protect: "Dick", after: "\\s+(?!(?:Head|Move|Bag|Face)\\b)[A-Z][a-z]+", caseSensitive: true }
];

// src/filters/whitelist.ts
var ASCII_WORD = /^[A-Za-z0-9_]$/;
function edgeFor(kind, char) {
  return kind === "ascii" && !ASCII_WORD.test(char) ? "native" : kind;
}
var Whitelist = class {
  constructor(userEntries, useBuiltin) {
    const compiled = [];
    if (useBuiltin) {
      for (const entry of BUILTIN_WHITELIST) compiled.push(compileBuiltin(entry));
    }
    for (const raw of userEntries) {
      const entry = raw.trim();
      if (entry.length > 0) compiled.push(compileUser(entry));
    }
    this.compiled = compiled;
  }
  get isEmpty() {
    return this.compiled.length === 0;
  }
  /** Cheap test on plain strings: could any safe span exist at all? */
  mightMatch(texts) {
    for (const { regex } of this.compiled) {
      for (const text of texts) {
        regex.lastIndex = 0;
        if (regex.test(text)) return true;
      }
    }
    return false;
  }
  /** Protected spans of the view, in original-text offsets. */
  intervals(view) {
    const found = [];
    for (const { regex } of this.compiled) {
      regex.lastIndex = 0;
      let match;
      while ((match = regex.exec(view.text)) !== null) {
        const start = match.index + match[1].length;
        const end = start + match[2].length;
        if (end === start) {
          regex.lastIndex++;
          continue;
        }
        found.push(toOriginalSpan(view, start, end));
      }
    }
    return found;
  }
};
function protectedSpans(whitelist, text) {
  const spans = whitelist.intervals(fromText(text)).sort((a, b) => a[0] - b[0]);
  const merged = [];
  for (const span of spans) {
    const last = merged[merged.length - 1];
    if (last && span[0] <= last[1]) last[1] = Math.max(last[1], span[1]);
    else merged.push([span[0], span[1]]);
  }
  return merged;
}
function blankProtected(text, spans) {
  let out = "";
  let cursor = 0;
  for (const [start, end] of spans) {
    out += text.slice(cursor, start) + " ".repeat(end - start);
    cursor = end;
  }
  return out + text.slice(cursor);
}
function compileBuiltin(entry) {
  const body = `(${entry.before ?? ""})(${entry.protect})(${entry.after ?? ""})`;
  return { regex: new RegExp(`\\b${body}\\b`, entry.caseSensitive ? "g" : "gi") };
}
function compileUser(entry) {
  const chars = Array.from(entry);
  const edges = wordEdges(entry);
  const head = edgeFor(edges.head, chars[0]);
  const tail = edgeFor(edges.tail, chars[chars.length - 1]);
  const protect = escapeRegExp(entry).replace(/\s+/g, "\\s+");
  const source = `${headSource(head)}()(${protect})()${tailSource(tail)}`;
  const unicode = [head, tail].some((kind) => kind !== "ascii");
  return { regex: new RegExp(source, unicode ? "giu" : "gi") };
}
function within(hit, span) {
  return hit[0] >= span[0] && hit[1] <= span[1];
}

// src/filters/scanner.ts
var NON_ASCII2 = /[\u0080-\uffff]/;
var ASCII_WORD2 = /^[a-z0-9_]+$/;
var ASCII_TOKEN = /[a-z0-9_]+/g;
var Scanner = class {
  constructor(cfg) {
    this.cfg = cfg;
    this.activeWords = Array.from(cfg.words.keys()).filter(
      (word) => !cfg.ignoreWords.has(word.toLowerCase())
    );
    this.tokenWords = new Set(this.activeWords.filter((word) => ASCII_WORD2.test(word)));
    this.index = new DictionaryIndex(this.activeWords, {
      leetspeak: cfg.detectLeetspeak,
      leetspeakLevel: cfg.leetspeakLevel
    });
    this.views = new ViewFactory(cfg, this.index);
    this.whitelist = new Whitelist(cfg.whitelist, cfg.obfuscation.builtinWhitelist);
    [this.asciiAny, this.nativeAny] = this.buildPrefilters();
  }
  buildPrefilters() {
    const groups = /* @__PURE__ */ new Map();
    for (const word of this.activeWords) {
      const { head, tail } = wordEdges(word);
      const key = `${head}|${tail}`;
      const group = groups.get(key) ?? { head, tail, words: [] };
      group.words.push(word);
      groups.set(key, group);
    }
    const flags = this.cfg.caseSensitive ? "" : "i";
    const ascii = [];
    const native = [];
    for (const { head, tail, words } of groups.values()) {
      const source = `${headSource(head)}(?:${words.map(escapeRegExp).join("|")})${tailSource(tail)}`;
      if (head === "ascii" && tail === "ascii") ascii.push(new RegExp(source, flags));
      else native.push(new RegExp(source, `${flags}u`));
    }
    return [ascii, native];
  }
  /** Dictionary words that occur as whole ASCII tokens, so other words skip their regex. */
  tokensIn(lower) {
    const found = /* @__PURE__ */ new Set();
    for (const token of lower.match(ASCII_TOKEN) ?? []) {
      if (this.tokenWords.has(token)) found.add(token);
    }
    return found;
  }
  hasCandidate(text) {
    if (this.asciiAny.some((regex) => regex.test(text))) return true;
    return this.nativeAny.length > 0 && NON_ASCII2.test(text) && this.nativeAny.some((regex) => regex.test(text));
  }
  /**
   * Boolean check. Clean text exits after one regex test per view; text with a
   * hit also exits early unless a safe word could overlap it.
   */
  isProfane(text) {
    const norm = this.cfg.normalizeText(text);
    const aggr = this.cfg.normalizeText(text, true);
    const enhanced = this.views.enhancedViews(text);
    const candidate = this.hasCandidate(text) || this.hasCandidate(norm) || this.hasCandidate(aggr) || enhanced.some((view) => this.hasCandidate(view.text));
    if (!candidate) return false;
    if (!this.whitelist.mightMatch([text, norm, aggr, ...enhanced.map((v) => v.text)])) return true;
    return this.scan(text).length > 0;
  }
  /**
   * Returns hits in dictionary order, views in order orig, norm, aggr, enhanced.
   * Every path (plain and context-aware) scans the same views.
   */
  scan(text) {
    const enhanced = this.views.enhancedViews(text);
    const quick = [
      text.toLowerCase(),
      this.cfg.normalizeText(text).toLowerCase(),
      this.cfg.normalizeText(text, true).toLowerCase()
    ];
    if (!quick.some((t) => this.hasCandidate(t)) && !enhanced.some((v) => this.hasCandidate(v.text.toLowerCase()))) {
      return [];
    }
    const legacy = this.views.legacyViews(text);
    const views = [];
    const add = (kind, view) => {
      const lowered = view.text.toLowerCase();
      views.push({ kind, view, lower: lowered.length === view.text.length ? lowered : view.text });
    };
    add("orig", legacy.orig);
    add("norm", legacy.norm);
    add("aggr", legacy.aggr);
    for (const view of enhanced) add("enhanced", view);
    const live = views.filter((v) => this.hasCandidate(v.lower)).map((v) => ({ ...v, tokens: this.tokensIn(v.lower) }));
    const hits = [];
    const seen = /* @__PURE__ */ new Set();
    for (const word of this.activeWords) {
      const isToken = this.tokenWords.has(word);
      for (const { kind, view, lower, tokens } of live) {
        if (isToken && !tokens.has(word)) continue;
        const regex = this.cfg.matcher.getRegex(word);
        let match;
        while ((match = regex.exec(lower)) !== null) {
          if (match[0].length === 0) {
            regex.lastIndex++;
            continue;
          }
          const [start, end] = toOriginalSpan(view, match.index, match.index + match[0].length);
          const key = `${word}|${start}|${end}`;
          if (kind === "enhanced" && seen.has(key)) continue;
          seen.add(key);
          hits.push({
            word,
            text: match[0],
            view: kind,
            start,
            end,
            severity: 1 /* EXACT */
          });
        }
      }
    }
    return this.dropWhitelisted(hits, legacy, enhanced);
  }
  dropWhitelisted(hits, legacy, enhanced) {
    if (hits.length === 0 || this.whitelist.isEmpty) return hits;
    const safe = [];
    const sources = [legacy.orig, legacy.norm, legacy.aggr, ...enhanced];
    for (const view of sources) safe.push(...this.whitelist.intervals(view));
    if (safe.length === 0) return hits;
    return hits.filter((hit) => !safe.some((span) => within([hit.start, hit.end], span)));
  }
};

// src/filters/obfuscation.ts
var DEFAULT_LEVEL = "standard";
var DEFAULT_SEPARATORS = ".,/|_-";
var DEFAULT_MASKS = "*#@$%_?!";
function envInt(name, fallback) {
  const raw = typeof process !== "undefined" ? process.env?.[name] : void 0;
  const parsed = raw === void 0 ? NaN : parseInt(raw, 10);
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
}
function object(value) {
  return typeof value === "object" && value !== null ? value : {};
}
function resolveObfuscation(option) {
  const opts = typeof option === "string" ? { level: option } : option ?? {};
  const level = opts.level ?? DEFAULT_LEVEL;
  const on = level !== "off";
  const mid = level === "standard" || level === "strict";
  const strict = level === "strict";
  const flag = (value, byLevel) => value === void 0 ? byLevel : value !== false;
  const html = object(opts.html);
  const sep = object(opts.separators);
  const mask = object(opts.masking);
  return {
    html: {
      enabled: flag(opts.html, on),
      decodeDepth: html.decodeDepth ?? envInt("GLIN_HTML_DECODE_DEPTH", 2)
    },
    separators: {
      enabled: flag(opts.separators, mid),
      chars: sep.chars ?? DEFAULT_SEPARATORS,
      allowSpace: sep.allowSpace ?? strict,
      minTwoPieceLength: sep.minTwoPieceLength ?? envInt("GLIN_SEPARATOR_MIN_JOIN", strict ? 3 : 4)
    },
    masking: {
      enabled: flag(opts.masking, mid),
      chars: mask.chars ?? DEFAULT_MASKS,
      minVisible: mask.minVisible ?? envInt("GLIN_MASK_MIN_VISIBLE", 1)
    },
    stretch: opts.stretch ?? on,
    ambiguousLeet: opts.ambiguousLeet ?? mid,
    builtinWhitelist: opts.builtinWhitelist ?? on
  };
}
function obfuscationEnabled(resolved) {
  return resolved.html.enabled || resolved.separators.enabled || resolved.masking.enabled || resolved.stretch || resolved.ambiguousLeet || resolved.builtinWhitelist;
}

// src/filters/Filter.ts
var _Filter = class _Filter {
  /**
   * Creates a new Filter instance with the specified configuration.
   *
   * @param config - Filter configuration options
   *
   * @example
   * ```typescript
   * // Basic usage
   * const filter = new Filter({ languages: ['english'] });
   *
   * // With leetspeak detection
   * const filter = new Filter({
   *   languages: ['english'],
   *   detectLeetspeak: true,
   *   leetspeakLevel: 'moderate',
   * });
   *
   * // With all advanced features
   * const filter = new Filter({
   *   languages: ['english', 'spanish'],
   *   detectLeetspeak: true,
   *   normalizeUnicode: true,
   *   cacheResults: true,
   *   enableContextAware: true,
   * });
   * ```
   */
  constructor(config) {
    const defaultLanguage = "english";
    this.enableContextAware = config?.enableContextAware ?? false;
    this.contextWindow = config?.contextWindow ?? 3;
    this.confidenceThreshold = config?.confidenceThreshold ?? 0.7;
    this.primaryLanguage = config?.languages?.[0] || defaultLanguage;
    if (this.enableContextAware) {
      this.contextAnalyzer = new ContextAnalyzer({
        contextWindow: this.contextWindow,
        language: this.primaryLanguage,
        domainWhitelists: config?.domainWhitelists?.[this.primaryLanguage] || []
      });
    }
    this.caseSensitive = config?.caseSensitive ?? false;
    this.allowObfuscatedMatch = config?.allowObfuscatedMatch ?? false;
    this.wordBoundaries = config?.wordBoundaries ?? !this.allowObfuscatedMatch;
    this.replaceWith = config?.replaceWith;
    this.severityLevels = config?.severityLevels ?? false;
    this.ignoreWords = new Set(
      config?.ignoreWords?.map((word) => word.toLowerCase()) || []
    );
    this.logProfanity = config?.logProfanity ?? false;
    this.fuzzyToleranceLevel = config?.fuzzyToleranceLevel ?? 0.8;
    this.detectLeetspeak = config?.detectLeetspeak ?? false;
    this.leetspeakLevel = config?.leetspeakLevel ?? "moderate";
    this.normalizeUnicodeEnabled = config?.normalizeUnicode ?? true;
    this.cacheResults = config?.cacheResults ?? false;
    this.maxCacheSize = config?.maxCacheSize ?? 1e3;
    this.cache = /* @__PURE__ */ new Map();
    this.matcher = new WordMatcher({
      caseSensitive: this.caseSensitive,
      wordBoundaries: this.wordBoundaries,
      fuzzyToleranceLevel: this.fuzzyToleranceLevel
    });
    this.obfuscationOption = config?.obfuscation;
    this.whitelistWords = config?.whitelist ?? [];
    let words = [];
    if (config?.allLanguages) {
      for (const lang in dictionary_default) {
        if (Object.prototype.hasOwnProperty.call(dictionary_default, lang)) {
          words = [...words, ...dictionary_default[lang]];
        }
      }
    } else {
      const languages = config?.languages || ["english"];
      const languagesChecks = new Set(languages);
      languagesChecks.forEach((lang) => {
        words = [...words, ...dictionary_default[lang]];
      });
    }
    if (config?.customWords) {
      words = [...words, ...config.customWords];
    }
    if (this.normalizeUnicodeEnabled) {
      words = _Filter.withAccentFoldedAliases(words);
    }
    this.words = new Map(words.map((word) => [word.toLowerCase(), 1]));
    this.scanner = this.createScanner();
    this.legacyWhitelist = !this.scanner && this.whitelistWords.length > 0 ? new Whitelist(this.whitelistWords, false) : void 0;
  }
  static withAccentFoldedAliases(words) {
    const seen = new Set(words.map((word) => word.toLowerCase()));
    const extra = [];
    for (const word of words) {
      const folded = normalizeUnicode(word);
      const key = folded.toLowerCase();
      if (key === word.toLowerCase() || seen.has(key)) continue;
      if (key.replace(/ /g, "").length < _Filter.ACCENT_ALIAS_MIN_LENGTH) continue;
      seen.add(key);
      extra.push(folded);
    }
    return extra.length > 0 ? [...words, ...extra] : words;
  }
  createScanner() {
    const obfuscation = resolveObfuscation(this.obfuscationOption);
    if (!this.wordBoundaries) return void 0;
    if (!obfuscationEnabled(obfuscation) && this.whitelistWords.length === 0) return void 0;
    return new Scanner({
      words: this.words,
      ignoreWords: this.ignoreWords,
      matcher: this.matcher,
      normalizeText: (text, aggressive) => this.normalizeText(text, aggressive),
      detectLeetspeak: this.detectLeetspeak,
      leetspeakLevel: this.leetspeakLevel,
      normalizeUnicode: this.normalizeUnicodeEnabled,
      allowObfuscatedMatch: this.allowObfuscatedMatch,
      obfuscation,
      whitelist: this.whitelistWords,
      caseSensitive: this.caseSensitive
    });
  }
  debugLog(...args) {
    if (this.logProfanity) {
      console.log("[glin-profanity]", ...args);
    }
  }
  /**
   * Normalizes text with every enabled method (Unicode, leetspeak, legacy
   * obfuscation). `aggressive` collapses repeated characters to one.
   */
  normalizeText(text, aggressive = false) {
    return normalizeForMatching(text, aggressive, {
      normalizeUnicode: this.normalizeUnicodeEnabled,
      detectLeetspeak: this.detectLeetspeak,
      leetspeakLevel: this.leetspeakLevel,
      allowObfuscatedMatch: this.allowObfuscatedMatch
    });
  }
  /**
   * Clears the result cache.
   * Useful when dictionary or configuration changes.
   */
  clearCache() {
    this.cache.clear();
    this.matcher.clear();
  }
  /**
   * Gets the current cache size.
   * @returns Number of cached results
   */
  getCacheSize() {
    return this.cache.size;
  }
  /**
   * Exports the current filter configuration as a JSON-serializable object.
   * Useful for saving configuration to files or sharing between environments.
   *
   * @returns The current filter configuration
   *
   * @example
   * ```typescript
   * const filter = new Filter({
   *   languages: ['english', 'spanish'],
   *   detectLeetspeak: true,
   *   leetspeakLevel: 'aggressive',
   * });
   *
   * const config = filter.getConfig();
   * // Save to file: fs.writeFileSync('filter.config.json', JSON.stringify(config));
   *
   * // Later, restore:
   * // const saved = JSON.parse(fs.readFileSync('filter.config.json'));
   * // const restored = new Filter(saved);
   * ```
   */
  getConfig() {
    return {
      languages: [this.primaryLanguage],
      caseSensitive: this.caseSensitive,
      wordBoundaries: this.wordBoundaries,
      replaceWith: this.replaceWith,
      severityLevels: this.severityLevels,
      ignoreWords: Array.from(this.ignoreWords),
      logProfanity: this.logProfanity,
      allowObfuscatedMatch: this.allowObfuscatedMatch,
      fuzzyToleranceLevel: this.fuzzyToleranceLevel,
      enableContextAware: this.enableContextAware,
      contextWindow: this.contextWindow,
      confidenceThreshold: this.confidenceThreshold,
      detectLeetspeak: this.detectLeetspeak,
      leetspeakLevel: this.leetspeakLevel,
      normalizeUnicode: this.normalizeUnicodeEnabled,
      cacheResults: this.cacheResults,
      maxCacheSize: this.maxCacheSize,
      obfuscation: this.obfuscationOption,
      whitelist: this.whitelistWords
    };
  }
  /**
   * Returns the current word dictionary size.
   * Useful for monitoring and debugging.
   *
   * @returns Number of words in the dictionary
   */
  getWordCount() {
    return this.words.size;
  }
  /**
   * Adds a result to the cache, evicting oldest entries if necessary.
   */
  addToCache(key, result) {
    if (!this.cacheResults) return;
    if (this.cache.size >= this.maxCacheSize) {
      const firstKey = this.cache.keys().next().value;
      if (firstKey) {
        this.cache.delete(firstKey);
      }
    }
    this.cache.set(key, result);
  }
  /**
   * Gets a cached result if available.
   */
  getFromCache(key) {
    if (!this.cacheResults) return void 0;
    return this.cache.get(key);
  }
  redact(text, profaneWords, hits, safeSpans = []) {
    if (!this.replaceWith) return text;
    return redactText(text, profaneWords, this.replaceWith, this.wordBoundaries, hits, safeSpans);
  }
  /** Safe spans of the text on the legacy path, and the text with them blanked. */
  legacySafe(text) {
    if (!this.legacyWhitelist) return { spans: [], text };
    const spans = protectedSpans(this.legacyWhitelist, text);
    return { spans, text: spans.length > 0 ? blankProtected(text, spans) : text };
  }
  legacyContext() {
    return {
      words: this.words,
      ignoreWords: this.ignoreWords,
      matcher: this.matcher,
      normalizeText: (text, aggressive) => this.normalizeText(text, aggressive)
    };
  }
  /**
   * Checks if the given text contains profanity.
   *
   * @param value - The text to check
   * @returns True if the text contains profanity
   *
   * @example
   * ```typescript
   * const filter = new Filter({ detectLeetspeak: true });
   *
   * filter.isProfane('hello');     // false
   * filter.isProfane('fuck');      // true
   * filter.isProfane('f4ck');      // true (leetspeak)
   * filter.isProfane('fυck');      // true (Unicode homoglyph)
   * ```
   */
  isProfane(value) {
    if (this.scanner) return this.scanner.isProfane(value);
    return legacyIsProfane(this.legacyContext(), this.legacySafe(value).text);
  }
  matches(word) {
    return this.isProfane(word);
  }
  /**
   * Performs a comprehensive profanity check on the given text.
   *
   * @param text - The text to check for profanity
   * @returns Result object containing detected profanity information
   *
   * @example
   * ```typescript
   * const filter = new Filter({
   *   languages: ['english'],
   *   detectLeetspeak: true,
   *   normalizeUnicode: true,
   * });
   *
   * const result = filter.checkProfanity('This is f4ck!ng bad');
   * console.log(result.containsProfanity);  // true
   * console.log(result.profaneWords);       // ['fuck']
   *
   * // With caching for repeated checks
   * const filter2 = new Filter({ cacheResults: true });
   * filter2.checkProfanity('same text');  // Computed
   * filter2.checkProfanity('same text');  // Retrieved from cache
   * ```
   */
  checkProfanity(text) {
    const cacheKey = text;
    const cachedResult = this.getFromCache(cacheKey);
    if (cachedResult) {
      this.debugLog("Cache hit for:", text.substring(0, 50));
      return cachedResult;
    }
    if (!this.enableContextAware) {
      const hits = this.scanner ? this.scanner.scan(text) : void 0;
      const safe2 = this.legacySafe(text);
      const { profaneWords: profaneWords2, severityMap: severityMap2 } = hits ? summarizeHits(hits) : legacyScan(this.legacyContext(), safe2.text);
      const result2 = {
        containsProfanity: profaneWords2.length > 0,
        profaneWords: Array.from(new Set(profaneWords2)),
        processedText: this.replaceWith ? this.redact(text, profaneWords2, hits, safe2.spans) : void 0,
        severityMap: this.severityLevels && Object.keys(severityMap2).length > 0 ? severityMap2 : void 0
      };
      this.addToCache(cacheKey, result2);
      return result2;
    }
    const originalText = text;
    const profaneWords = [];
    const severityMap = {};
    const matches = [];
    const safe = this.legacySafe(text);
    const legacySafeText = safe.text;
    const scanHits = this.scanner ? uniqueSpans(this.scanner.scan(text)) : void 0;
    const contextHits = scanHits ? scanHits.map((h) => ({
      word: h.view === "orig" ? h.text : h.word,
      index: h.start,
      severity: h.severity,
      scan: h
    })) : legacyContextHits(this.legacyContext(), legacySafeText).map((h) => ({ ...h, scan: void 0 }));
    const keptHits = [];
    for (const hit of contextHits) {
      const matchObj = { word: hit.word, index: hit.index, severity: hit.severity };
      if (this.contextAnalyzer) {
        const contextResult = this.contextAnalyzer.analyzeContext(
          originalText,
          hit.word,
          hit.index
        );
        matchObj.contextScore = contextResult.contextScore;
        matchObj.reason = contextResult.reason;
        matchObj.isWhitelisted = contextResult.isWhitelisted;
        if (contextResult.isWhitelisted || contextResult.contextScore > this.confidenceThreshold) {
          continue;
        }
      }
      profaneWords.push(hit.word);
      if (hit.scan) keptHits.push(hit.scan);
      if (severityMap[hit.word] === void 0) severityMap[hit.word] = hit.severity;
      matches.push(matchObj);
    }
    if (profaneWords.length > 0) {
      this.debugLog("Detected:", profaneWords);
    }
    const processedText = this.redact(text, profaneWords, scanHits ? keptHits : void 0, safe.spans);
    let contextScore;
    if (this.enableContextAware && matches.length > 0) {
      const totalScore = matches.reduce((sum, match) => sum + (match.contextScore || 0.5), 0);
      contextScore = totalScore / matches.length;
    }
    const result = {
      containsProfanity: profaneWords.length > 0,
      profaneWords: Array.from(new Set(profaneWords)),
      processedText: this.replaceWith ? processedText : void 0,
      severityMap: this.severityLevels && Object.keys(severityMap).length > 0 ? severityMap : void 0,
      matches: matches.length > 0 ? matches : void 0,
      contextScore,
      reason: matches.length > 0 ? `Found ${matches.length} potential profanity matches` : "No profanity detected"
    };
    this.addToCache(cacheKey, result);
    return result;
  }
  /**
   * Checks profanity with minimum severity filtering.
   *
   * @param text - The text to check
   * @param minSeverity - Minimum severity level to include in results
   * @returns Object with filtered words and full result
   */
  checkProfanityWithMinSeverity(text, minSeverity = 1 /* EXACT */) {
    const result = this.checkProfanity(text);
    const filteredWords = result.severityMap && result.profaneWords.length > 0 ? result.profaneWords.filter((word) => {
      const severity = result.severityMap?.[word];
      return typeof severity === "number" && severity >= minSeverity;
    }) : [];
    return { filteredWords, result };
  }
};
/**
 * The obfuscation-aware scanner needs word boundaries; fuzzy matching
 * (wordBoundaries off) and `obfuscation: 'off'` keep the legacy scan.
 */
_Filter.ACCENT_ALIAS_MIN_LENGTH = 4;
var Filter = _Filter;

// src/ml/HybridFilter.ts
var HybridFilter = class {
  /**
   * Creates a new HybridFilter instance.
   *
   * @param config - Configuration options
   */
  constructor(config = {}) {
    this.mlDetector = null;
    this.mlInitialized = false;
    const {
      enableML = false,
      mlThreshold = 0.85,
      mlLabels,
      preloadML = false,
      combinationMode = "or",
      borderlineThreshold = 0.5,
      ...filterConfig
    } = config;
    this.config = {
      enableML,
      mlThreshold,
      mlLabels,
      preloadML,
      combinationMode,
      borderlineThreshold
    };
    this.ruleFilter = new Filter(filterConfig);
    if (enableML) {
      this.mlDetector = new ToxicityDetector({
        threshold: mlThreshold,
        labels: mlLabels,
        preloadModel: preloadML
      });
    }
  }
  /**
   * Initializes the hybrid filter, loading the ML model if enabled.
   * Call this before using async methods for best performance.
   *
   * @example
   * ```typescript
   * const filter = new HybridFilter({ enableML: true });
   * await filter.initialize();
   * // Now ready for fast async checks
   * ```
   */
  async initialize() {
    if (this.mlDetector && !this.mlInitialized) {
      await this.mlDetector.loadModel();
      this.mlInitialized = true;
    }
  }
  /**
   * Checks if ML is available and initialized.
   */
  isMLReady() {
    return this.mlDetector?.isModelLoaded() ?? false;
  }
  /**
   * Synchronous profanity check using only rule-based detection.
   * Use this for fast, synchronous checks when ML isn't needed.
   *
   * @param text - Text to check
   * @returns True if profanity detected
   */
  isProfane(text) {
    return this.ruleFilter.isProfane(text);
  }
  /**
   * Synchronous detailed check using only rule-based detection.
   *
   * @param text - Text to check
   * @returns Detailed profanity check result
   */
  checkProfanity(text) {
    return this.ruleFilter.checkProfanity(text);
  }
  /**
   * Async profanity check using both rule-based and ML detection.
   *
   * @param text - Text to check
   * @returns Combined analysis result
   *
   * @example
   * ```typescript
   * const filter = new HybridFilter({
   *   enableML: true,
   *   combinationMode: 'or',
   * });
   * await filter.initialize();
   *
   * const result = await filter.checkProfanityAsync('some text');
   * if (result.isToxic) {
   *   console.log('Reason:', result.reason);
   *   console.log('Confidence:', result.confidence);
   * }
   * ```
   */
  async checkProfanityAsync(text) {
    const ruleResult = this.ruleFilter.checkProfanity(text);
    let mlResult = null;
    if (this.mlDetector) {
      try {
        mlResult = await this.mlDetector.analyze(text);
      } catch (error) {
        console.warn("[glin-profanity] ML analysis failed:", error);
      }
    }
    const { isToxic, confidence, reason } = this.combineResults(
      ruleResult,
      mlResult
    );
    return {
      ruleBasedResult: {
        containsProfanity: ruleResult.containsProfanity,
        profaneWords: ruleResult.profaneWords
      },
      mlResult,
      isToxic,
      confidence,
      reason
    };
  }
  /**
   * Simple async boolean check for toxicity.
   *
   * @param text - Text to check
   * @returns True if toxic
   */
  async isToxicAsync(text) {
    const result = await this.checkProfanityAsync(text);
    return result.isToxic;
  }
  /**
   * Analyzes text with ML only (if available).
   *
   * @param text - Text to analyze
   * @returns ML analysis result or null if ML not available
   */
  async analyzeWithML(text) {
    if (!this.mlDetector) {
      return null;
    }
    return this.mlDetector.analyze(text);
  }
  /**
   * Batch analysis for multiple texts.
   *
   * @param texts - Array of texts to analyze
   * @returns Array of hybrid analysis results
   */
  async checkProfanityBatchAsync(texts) {
    const ruleResults = texts.map((text) => this.ruleFilter.checkProfanity(text));
    let mlResults = null;
    if (this.mlDetector) {
      try {
        mlResults = await this.mlDetector.analyzeBatch(texts);
      } catch (error) {
        console.warn("[glin-profanity] ML batch analysis failed:", error);
      }
    }
    return texts.map((_, i) => {
      const ruleResult = ruleResults[i];
      const mlResult = mlResults?.[i] ?? null;
      const { isToxic, confidence, reason } = this.combineResults(
        ruleResult,
        mlResult
      );
      return {
        ruleBasedResult: {
          containsProfanity: ruleResult.containsProfanity,
          profaneWords: ruleResult.profaneWords
        },
        mlResult,
        isToxic,
        confidence,
        reason
      };
    });
  }
  combineResults(ruleResult, mlResult) {
    const ruleDetected = ruleResult.containsProfanity;
    const mlDetected = mlResult?.isToxic ?? false;
    const mlScore = mlResult?.overallScore ?? 0;
    switch (this.config.combinationMode) {
      case "and":
        if (mlResult === null) {
          return {
            isToxic: ruleDetected,
            confidence: ruleDetected ? 0.7 : 0.9,
            reason: ruleDetected ? `Rule-based detection (ML unavailable): ${ruleResult.profaneWords.join(", ")}` : "No profanity detected (rule-based only)"
          };
        }
        return {
          isToxic: ruleDetected && mlDetected,
          confidence: Math.min(ruleDetected ? 0.9 : 0.5, mlScore),
          reason: ruleDetected && mlDetected ? `Both rule-based and ML detected toxicity: ${ruleResult.profaneWords.join(", ")} (ML: ${mlResult.matchedCategories.join(", ")})` : `Detection disagreement - Rule: ${ruleDetected}, ML: ${mlDetected}`
        };
      case "ml-override":
        if (mlResult === null) {
          return {
            isToxic: ruleDetected,
            confidence: ruleDetected ? 0.7 : 0.8,
            reason: ruleDetected ? `Rule-based detection: ${ruleResult.profaneWords.join(", ")}` : "No profanity detected (rule-based)"
          };
        }
        return {
          isToxic: mlDetected,
          confidence: mlScore,
          reason: mlDetected ? `ML detected toxicity: ${mlResult.matchedCategories.join(", ")}` : "ML analysis: no toxicity detected"
        };
      case "rules-first":
        if (ruleDetected) {
          return {
            isToxic: true,
            confidence: mlResult ? Math.max(0.8, mlScore) : 0.8,
            reason: `Rule-based detection: ${ruleResult.profaneWords.join(", ")}${mlDetected ? ` (confirmed by ML: ${mlResult?.matchedCategories.join(", ")})` : ""}`
          };
        }
        if (mlResult && mlScore >= this.config.borderlineThreshold) {
          return {
            isToxic: mlDetected,
            confidence: mlScore,
            reason: mlDetected ? `ML detected (rules missed): ${mlResult.matchedCategories.join(", ")}` : "Clean text (verified by ML)"
          };
        }
        return {
          isToxic: false,
          confidence: 0.85,
          reason: "No profanity detected (rule-based)"
        };
      case "or":
      default:
        const isToxic = ruleDetected || mlDetected;
        let reason;
        let confidence;
        if (ruleDetected && mlDetected) {
          reason = `Both detected: rules (${ruleResult.profaneWords.join(", ")}), ML (${mlResult?.matchedCategories.join(", ")})`;
          confidence = Math.max(0.95, mlScore);
        } else if (ruleDetected) {
          reason = `Rule-based detection: ${ruleResult.profaneWords.join(", ")}`;
          confidence = 0.85;
        } else if (mlDetected) {
          reason = `ML detected: ${mlResult?.matchedCategories.join(", ")}`;
          confidence = mlScore;
        } else {
          reason = "No toxicity detected";
          confidence = mlResult ? 1 - mlScore : 0.8;
        }
        return { isToxic, confidence, reason };
    }
  }
  /**
   * Gets the underlying rule-based filter.
   */
  getRuleFilter() {
    return this.ruleFilter;
  }
  /**
   * Gets the underlying ML detector (if enabled).
   */
  getMLDetector() {
    return this.mlDetector;
  }
  /**
   * Disposes of resources (ML model).
   */
  dispose() {
    this.mlDetector?.dispose();
  }
};

// src/ml/transformers.ts
var RECOMMENDED_MODELS = {
  /** High accuracy English model (97.5%) - 67M params */
  pardonmyai: "tarekziade/pardonmyai",
  /** Smaller version for constrained environments */
  pardonmyaiTiny: "tarekziade/pardonmyai-tiny",
  /** Multilingual toxicity detection (7 languages) */
  toxicBert: "unitary/toxic-bert",
  /** Offensive speech detector (DeBERTa-based) */
  offensiveSpeech: "KoalaAI/OffensiveSpeechDetector"
};
var MODEL_PROFANE_LABELS = {
  "tarekziade/pardonmyai": "profane",
  "tarekziade/pardonmyai-tiny": "profane",
  "unitary/toxic-bert": "toxic",
  "KoalaAI/OffensiveSpeechDetector": "LABEL_1",
  // Offensive
  default: "LABEL_1"
};
async function getTransformers() {
  try {
    const transformers = await import('@xenova/transformers');
    return transformers;
  } catch {
    throw new Error(
      "Transformers.js is required for ML features. Install it with: npm install @xenova/transformers"
    );
  }
}
async function createMLChecker(config = {}) {
  const {
    model = RECOMMENDED_MODELS.pardonmyai,
    threshold = 0.5,
    profaneLabel = MODEL_PROFANE_LABELS[model] || MODEL_PROFANE_LABELS.default,
    quantized = true,
    device = "cpu"
  } = config;
  const transformers = await getTransformers();
  const classifier = await transformers.pipeline("text-classification", model, {
    quantized,
    device
  });
  return {
    /**
     * Check a single text for profanity
     */
    async check(text) {
      const startTime = Date.now();
      const output = await classifier(text);
      const processingTimeMs = Date.now() - startTime;
      const profaneScore = output.find((o) => o.label === profaneLabel)?.score || 0;
      const containsProfanity = profaneScore >= threshold;
      return {
        containsProfanity,
        confidence: profaneScore,
        rawOutput: output,
        processingTimeMs
      };
    },
    /**
     * Check multiple texts
     */
    async checkBatch(texts) {
      return Promise.all(texts.map((text) => this.check(text)));
    },
    /**
     * Get the profanity score for text (0-1)
     */
    async getScore(text) {
      const result = await this.check(text);
      return result.confidence;
    },
    /**
     * Get current configuration
     */
    getConfig() {
      return { model, threshold, profaneLabel, quantized, device };
    },
    /**
     * Dispose of the model (free memory)
     */
    dispose() {
    }
  };
}
async function createHybridChecker(config = {}) {
  const {
    model = RECOMMENDED_MODELS.pardonmyai,
    threshold = 0.5,
    profaneLabel,
    quantized = true,
    device = "cpu",
    filterConfig = {},
    mlThreshold = 0.3,
    dictionaryWeight = 0.6,
    mlWeight = 0.4
  } = config;
  const filter = new Filter({
    languages: filterConfig.languages || ["english"],
    detectLeetspeak: filterConfig.detectLeetspeak ?? true,
    normalizeUnicode: filterConfig.normalizeUnicode ?? true,
    severityLevels: true,
    cacheResults: true,
    ...filterConfig
  });
  let mlChecker = null;
  async function getMLChecker() {
    if (!mlChecker) {
      mlChecker = await createMLChecker({
        model,
        threshold,
        profaneLabel,
        quantized,
        device
      });
    }
    return mlChecker;
  }
  return {
    /**
     * Check text using hybrid approach
     */
    async check(text) {
      const startTime = Date.now();
      const dictionaryResult = filter.checkProfanity(text);
      if (dictionaryResult.containsProfanity) {
        return {
          containsProfanity: true,
          confidence: 1,
          dictionaryResult,
          usedML: false,
          profaneWords: dictionaryResult.profaneWords,
          processingTimeMs: Date.now() - startTime
        };
      }
      const ml = await getMLChecker();
      const mlResult = await ml.check(text);
      const dictionaryScore = dictionaryResult.containsProfanity ? 1 : 0;
      const combinedScore = dictionaryScore * dictionaryWeight + mlResult.confidence * mlWeight;
      const containsProfanity = combinedScore >= mlThreshold;
      return {
        containsProfanity,
        confidence: combinedScore,
        dictionaryResult,
        mlResult,
        usedML: true,
        profaneWords: dictionaryResult.profaneWords,
        processingTimeMs: Date.now() - startTime
      };
    },
    /**
     * Check multiple texts
     */
    async checkBatch(texts) {
      return Promise.all(texts.map((text) => this.check(text)));
    },
    /**
     * Dictionary-only check (fast, no ML)
     */
    checkFast(text) {
      return filter.checkProfanity(text);
    },
    /**
     * ML-only check (slower, more accurate)
     */
    async checkML(text) {
      const ml = await getMLChecker();
      return ml.check(text);
    },
    /**
     * Get the underlying filter
     */
    getFilter() {
      return filter;
    },
    /**
     * Dispose of resources
     */
    async dispose() {
      if (mlChecker) {
        mlChecker.dispose();
        mlChecker = null;
      }
    }
  };
}
async function isTransformersAvailable() {
  try {
    await getTransformers();
    return true;
  } catch {
    return false;
  }
}
async function preloadModel(model = RECOMMENDED_MODELS.pardonmyai, options = {}) {
  const { quantized = true } = options;
  const transformers = await getTransformers();
  await transformers.pipeline("text-classification", model, {
    quantized
  });
}

exports.HybridFilter = HybridFilter;
exports.RECOMMENDED_MODELS = RECOMMENDED_MODELS;
exports.ToxicityDetector = ToxicityDetector;
exports.createHybridChecker = createHybridChecker;
exports.createMLChecker = createMLChecker;
exports.isTransformersAvailable = isTransformersAvailable;
exports.preloadModel = preloadModel;