@sideid/id-profanity-filter 1.11.13 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CONTRIBUTING.md +91 -129
  2. package/README.md +172 -447
  3. package/dist/index.d.ts +11 -6
  4. package/dist/index.esm.js +161 -180
  5. package/dist/index.esm.js.map +1 -1
  6. package/dist/index.js +161 -180
  7. package/dist/index.js.map +1 -1
  8. package/dist/types/core/matcher.d.ts +5 -5
  9. package/dist/types/index.d.ts +5 -0
  10. package/dist/types/types/index.d.ts +1 -1
  11. package/dist/types/utils/ahoCorasick.d.ts +10 -0
  12. package/eslint.config.mjs +1 -0
  13. package/examples/advanced.ts +4 -1
  14. package/examples/custom-list.ts +3 -3
  15. package/package.json +9 -2
  16. package/rollup.config.mjs +5 -1
  17. package/src/constants/categories/sexual.ts +1 -1
  18. package/src/constants/regions/general.ts +3 -3
  19. package/src/constants/wordList.ts +0 -27
  20. package/src/core/filter.ts +58 -178
  21. package/src/core/matcher.ts +69 -43
  22. package/src/index.ts +8 -0
  23. package/src/types/index.ts +1 -11
  24. package/src/utils/ahoCorasick.ts +37 -0
  25. package/src/utils/regexUtils.ts +1 -2
  26. package/src/utils/similarityUtils.ts +0 -4
  27. package/test/matcher.test.ts +18 -0
  28. package/test/profanity-filter.test.ts +15 -0
  29. package/.eslintrc.js +0 -44
  30. package/src/constants/regions/ambon.ts +0 -0
  31. package/src/constants/regions/banjar.ts +0 -0
  32. package/src/constants/regions/bugis.ts +0 -0
  33. package/src/constants/regions/lampung.ts +0 -0
  34. package/src/constants/regions/manado.ts +0 -0
  35. package/src/constants/regions/ntb.ts +0 -0
  36. package/src/constants/regions/ntt.ts +0 -0
  37. package/src/constants/regions/palembang.ts +0 -0
  38. package/src/constants/regions/papua.ts +0 -0
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ const general = [
8
8
  category: 'profanity',
9
9
  region: 'general',
10
10
  severity: 0.7,
11
- aliases: ['anjay', 'anjir', 'anying', 'njing', 'anj', 'anjg', 'ajg'],
11
+ aliases: ['anjay', 'anjir', 'anying', 'njing', 'anj', 'anjg', 'ajg', 'anjic', 'anjink'],
12
12
  description: 'Mengacu pada hewan anjing, digunakan sebagai umpatan',
13
13
  context: 'Umpatan umum untuk menunjukkan kemarahan atau ketidaksetujuan',
14
14
  },
@@ -116,7 +116,7 @@ const general = [
116
116
  category: 'insult',
117
117
  region: 'general',
118
118
  severity: 0.5,
119
- aliases: ['sialn', 'sl'],
119
+ aliases: ['sialn'],
120
120
  description: 'Kata yang mengacu pada orang yang membawa sial',
121
121
  context: 'Hinaan untuk menyebut orang yang dianggap membawa sial',
122
122
  },
@@ -179,7 +179,7 @@ const general = [
179
179
  category: 'sexual',
180
180
  region: 'general',
181
181
  severity: 0.9,
182
- aliases: ['ngew', 'we'],
182
+ aliases: ['ngew'],
183
183
  description: 'Istilah kasar untuk aktivitas seksual',
184
184
  context: 'Kata vulgar yang merujuk pada aktivitas seksual',
185
185
  },
@@ -1611,7 +1611,7 @@ const sexual = [
1611
1611
  category: 'sexual',
1612
1612
  region: 'general',
1613
1613
  severity: 0.9,
1614
- aliases: ['ngew', 'we'],
1614
+ aliases: ['ngew'],
1615
1615
  description: 'Istilah kasar untuk aktivitas seksual',
1616
1616
  context: 'Kata vulgar yang merujuk pada aktivitas seksual',
1617
1617
  },
@@ -2143,16 +2143,7 @@ const wordObjects = [
2143
2143
  ...minang,
2144
2144
  ...bali,
2145
2145
  ...madura,
2146
- // ...bugis,
2147
2146
  ...aceh,
2148
- // ...ambon,
2149
- // ...papua,
2150
- // ...manado,
2151
- // ...banjar,
2152
- // ...palembang,
2153
- // ...lampung,
2154
- // ...ntt,
2155
- // ...ntb,
2156
2147
  ];
2157
2148
  wordObjects.map((item) => item.word);
2158
2149
  /**
@@ -2468,8 +2459,7 @@ function addIndonesianVariations(pattern) {
2468
2459
  const lowerChar = char.toLowerCase();
2469
2460
  const variations = variationMap[lowerChar];
2470
2461
  if (variations && variations.length > 1) {
2471
- // Create a character class with all variations
2472
- result += `[${variations.join('')}]`;
2462
+ result += `(?:${variations.join('|')})`;
2473
2463
  }
2474
2464
  else {
2475
2465
  result += char;
@@ -2552,19 +2542,15 @@ function levenshteinDistance(str1, str2) {
2552
2542
  const s2 = str2.toLowerCase();
2553
2543
  const len1 = s1.length;
2554
2544
  const len2 = s2.length;
2555
- // Inisialisasi matrix
2556
2545
  const matrix = [];
2557
- // Inisialisasi baris pertama
2558
2546
  for (let i = 0; i <= len2; i++) {
2559
2547
  matrix[0] = matrix[0] || [];
2560
2548
  matrix[0][i] = i;
2561
2549
  }
2562
- // Inisialisasi kolom pertama
2563
2550
  for (let i = 0; i <= len1; i++) {
2564
2551
  matrix[i] = matrix[i] || [];
2565
2552
  matrix[i][0] = i;
2566
2553
  }
2567
- // Isi matrix
2568
2554
  for (let i = 1; i <= len1; i++) {
2569
2555
  for (let j = 1; j <= len2; j++) {
2570
2556
  const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
@@ -3109,6 +3095,36 @@ class AhoCorasick {
3109
3095
  const matches = this.search(text);
3110
3096
  return new Set(matches.keys());
3111
3097
  }
3098
+ /**
3099
+ * Mencari semua kemunculan pola beserta posisi indeks awal dan akhirnya
3100
+ * @param text Teks yang akan dicari
3101
+ * @returns Array objek berisi pattern, start, dan end
3102
+ */
3103
+ searchWithPositions(text) {
3104
+ if (!this.built) {
3105
+ this.build();
3106
+ }
3107
+ const results = [];
3108
+ const normalizedText = text.toLowerCase();
3109
+ let node = this.root;
3110
+ for (let i = 0; i < normalizedText.length; i++) {
3111
+ const char = normalizedText[i];
3112
+ while (node !== this.root && !node.children.has(char)) {
3113
+ node = node.fail;
3114
+ }
3115
+ if (node.children.has(char)) {
3116
+ node = node.children.get(char);
3117
+ }
3118
+ for (const match of node.output) {
3119
+ results.push({
3120
+ pattern: match,
3121
+ start: i + 1 - match.length,
3122
+ end: i + 1,
3123
+ });
3124
+ }
3125
+ }
3126
+ return results;
3127
+ }
3112
3128
  /**
3113
3129
  * Mengecek apakah teks mengandung setidaknya satu pola
3114
3130
  * @param text Teks yang akan dicari
@@ -3136,16 +3152,22 @@ class AhoCorasick {
3136
3152
  }
3137
3153
  }
3138
3154
 
3139
- const globalAhoCorasick = new AhoCorasick();
3140
- let ahoCorasickInitialized = false;
3141
- function initializeAhoCorasick(words) {
3142
- if (ahoCorasickInitialized)
3143
- return;
3144
- for (const word of words) {
3145
- globalAhoCorasick.addPattern(word);
3155
+ let defaultAhoCorasick = null;
3156
+ function getDefaultAhoCorasick() {
3157
+ if (!defaultAhoCorasick) {
3158
+ const ac = new AhoCorasick();
3159
+ for (const wordObj of wordObjects) {
3160
+ ac.addPattern(wordObj.word);
3161
+ if (wordObj.aliases) {
3162
+ for (const alias of wordObj.aliases) {
3163
+ ac.addPattern(alias);
3164
+ }
3165
+ }
3166
+ }
3167
+ ac.build();
3168
+ defaultAhoCorasick = ac;
3146
3169
  }
3147
- globalAhoCorasick.build();
3148
- ahoCorasickInitialized = true;
3170
+ return defaultAhoCorasick;
3149
3171
  }
3150
3172
  function getWordMetadata(word) {
3151
3173
  return wordObjects.find((obj) => obj.word.toLowerCase() === word.toLowerCase() ||
@@ -3153,10 +3175,15 @@ function getWordMetadata(word) {
3153
3175
  }
3154
3176
  function findProfanity(text, options = {}) {
3155
3177
  const { wordList = [], detectLeetSpeak = true, checkSubstring = false, whitelist = [], categories, regions, severityThreshold = 0, indonesianVariation = false, detectSimilarity = false, similarityThreshold = 0.8, detectSplit = false, useLevenshtein = false, maxLevenshteinDistance = 2, } = { ...DEFAULT_OPTIONS, ...options };
3178
+ const hasCustomWordList = Boolean(wordList && wordList.length > 0);
3156
3179
  const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
3157
3180
  const normalizedText = normalizeText(text);
3158
- let baseWordsToCheck = wordList.length > 0 ? wordList : [];
3159
- if (baseWordsToCheck.length === 0) {
3181
+ let baseWordsToCheck = [];
3182
+ const aliasMap = new Map();
3183
+ if (hasCustomWordList) {
3184
+ baseWordsToCheck = wordList;
3185
+ }
3186
+ else {
3160
3187
  const filteredWords = wordObjects.filter((word) => {
3161
3188
  const matchCategory = categories ? categories.includes(word.category) : true;
3162
3189
  const matchRegion = regions ? regions.includes(word.region) : true;
@@ -3164,22 +3191,14 @@ function findProfanity(text, options = {}) {
3164
3191
  return matchCategory && matchRegion && matchSeverity;
3165
3192
  });
3166
3193
  baseWordsToCheck = filteredWords.map((word) => word.word);
3194
+ filteredWords.forEach((wordObj) => {
3195
+ if (wordObj.aliases && wordObj.aliases.length > 0) {
3196
+ wordObj.aliases.forEach((alias) => {
3197
+ aliasMap.set(alias.toLowerCase(), wordObj.word.toLowerCase());
3198
+ });
3199
+ }
3200
+ });
3167
3201
  }
3168
- const aliasMap = new Map();
3169
- wordObjects.forEach((wordObj) => {
3170
- const matchCategory = categories ? categories.includes(wordObj.category) : true;
3171
- const matchRegion = regions ? regions.includes(wordObj.region) : true;
3172
- const matchSeverity = wordObj.severity >= severityThreshold;
3173
- if (matchCategory &&
3174
- matchRegion &&
3175
- matchSeverity &&
3176
- wordObj.aliases &&
3177
- wordObj.aliases.length > 0) {
3178
- wordObj.aliases.forEach((alias) => {
3179
- aliasMap.set(alias.toLowerCase(), wordObj.word.toLowerCase());
3180
- });
3181
- }
3182
- });
3183
3202
  const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !normalizedWhitelist.includes(word.toLowerCase()));
3184
3203
  if (wordsToCheck.length === 0) {
3185
3204
  return [];
@@ -3189,6 +3208,8 @@ function findProfanity(text, options = {}) {
3189
3208
  const passesFilters = (word) => {
3190
3209
  if (normalizedWhitelist.includes(word.toLowerCase()))
3191
3210
  return false;
3211
+ if (hasCustomWordList)
3212
+ return true;
3192
3213
  const metadata = getWordMetadata(word);
3193
3214
  if (!metadata)
3194
3215
  return false;
@@ -3197,9 +3218,27 @@ function findProfanity(text, options = {}) {
3197
3218
  const matchSeverity = metadata.severity >= severityThreshold;
3198
3219
  return matchCategory && matchRegion && matchSeverity;
3199
3220
  };
3200
- initializeAhoCorasick(wordsToCheck);
3201
- const basicMatches = globalAhoCorasick.searchUnique(normalizedText);
3202
- for (const match of basicMatches) {
3221
+ let ac;
3222
+ if (hasCustomWordList) {
3223
+ ac = new AhoCorasick();
3224
+ for (const w of wordsToCheck) {
3225
+ ac.addPattern(w);
3226
+ }
3227
+ ac.build();
3228
+ }
3229
+ else {
3230
+ ac = getDefaultAhoCorasick();
3231
+ }
3232
+ const occurrences = ac.searchWithPositions(normalizedText);
3233
+ for (const occ of occurrences) {
3234
+ const { pattern: match, start, end } = occ;
3235
+ if (!checkSubstring) {
3236
+ const isWordStart = start === 0 || !/[a-z0-9_]/i.test(normalizedText[start - 1]);
3237
+ const isWordEnd = end === normalizedText.length || !/[a-z0-9_]/i.test(normalizedText[end]);
3238
+ if (!isWordStart || !isWordEnd) {
3239
+ continue;
3240
+ }
3241
+ }
3203
3242
  if (normalizedWhitelist.includes(match.toLowerCase()))
3204
3243
  continue;
3205
3244
  const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
@@ -3324,8 +3363,8 @@ function findProfanity(text, options = {}) {
3324
3363
  /**
3325
3364
  * Mencari kata kotor lengkap dengan metadata
3326
3365
  *
3327
- * @param text Teks yang akan diperika
3328
- * @param options Opsi utnuk pencarian kata kotor
3366
+ * @param text Teks yang akan diperiksa
3367
+ * @param options Opsi untuk pencarian kata kotor
3329
3368
  * @return Array dari objek kata kotor yang ditemukan
3330
3369
  */
3331
3370
  function findProfanityWithMetadata(text, options = {}) {
@@ -3337,14 +3376,22 @@ function findProfanityWithMetadata(text, options = {}) {
3337
3376
  .map((word) => {
3338
3377
  const wordObject = wordObjects.find((obj) => obj.word.toLowerCase() === word.toLowerCase() ||
3339
3378
  (obj.aliases && obj.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
3340
- return wordObject;
3379
+ if (wordObject) {
3380
+ return wordObject;
3381
+ }
3382
+ return {
3383
+ word,
3384
+ category: 'profanity',
3385
+ region: 'general',
3386
+ severity: 0.5,
3387
+ };
3341
3388
  })
3342
3389
  .filter((word) => word !== undefined);
3343
3390
  }
3344
3391
  /**
3345
- * Mencari kategory kata kotor yang ada dalam teks
3392
+ * Mencari kategori kata kotor yang ada dalam teks
3346
3393
  *
3347
- * @param matchDetails Hasil pencarian dari fingProfanityWithMetadata()
3394
+ * @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
3348
3395
  * @return Array kategori unik
3349
3396
  */
3350
3397
  function findCategories(matchDetails) {
@@ -3357,7 +3404,7 @@ function findCategories(matchDetails) {
3357
3404
  /**
3358
3405
  * Mencari region kata kotor yang ada dalam teks
3359
3406
  *
3360
- * @param matchDetails Hasil pencarian dari fingProfanityWithMetadata()
3407
+ * @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
3361
3408
  * @return Array region unik
3362
3409
  */
3363
3410
  function findRegions(matchDetails) {
@@ -3377,7 +3424,7 @@ function calculateSeverity(matchDetails) {
3377
3424
  if (matchDetails.length === 0) {
3378
3425
  return 0;
3379
3426
  }
3380
- const countFactor = Math.min(matchDetails.length / 10, 1); // Maksimal 10 kata
3427
+ const countFactor = Math.min(matchDetails.length / 10, 1);
3381
3428
  const categoryWeights = {
3382
3429
  sexual: 0.9,
3383
3430
  blasphemy: 0.9,
@@ -3405,21 +3452,9 @@ function calculateSeverity(matchDetails) {
3405
3452
  * @returns FilterResult dengan hasil filter
3406
3453
  */
3407
3454
  function filter(text, options = {}) {
3408
- const { replaceWith = '*', fullWordCensor = true, detectLeetSpeak = true, whitelist = [], checkSubstring = false, useRandomGrawlix = false, keepFirstAndLast = false, indonesianVariation = false, detectSplit = false, detectSimilarity = false, useLevenshtein = false, maxLevenshteinDistance = 2, similarityThreshold = 0.8, } = { ...DEFAULT_OPTIONS, ...options };
3409
- const matches = findProfanity(text, {
3410
- ...options,
3411
- detectLeetSpeak,
3412
- whitelist,
3413
- checkSubstring,
3414
- indonesianVariation,
3415
- detectSplit,
3416
- detectSimilarity,
3417
- useLevenshtein,
3418
- maxLevenshteinDistance,
3419
- similarityThreshold,
3420
- });
3421
- const actualMatches = findProfanity.lastActualMatches || new Map();
3422
- const matchDetails = findProfanityWithMetadata(text, options);
3455
+ const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
3456
+ const { replaceWith = '*', fullWordCensor = true, detectLeetSpeak = true, whitelist = [], checkSubstring = false, useRandomGrawlix = false, keepFirstAndLast = false, detectSplit = false, } = mergedOptions;
3457
+ const matches = findProfanity(text, mergedOptions);
3423
3458
  if (matches.length === 0) {
3424
3459
  return {
3425
3460
  filtered: text,
@@ -3427,123 +3462,62 @@ function filter(text, options = {}) {
3427
3462
  replacements: [],
3428
3463
  };
3429
3464
  }
3465
+ const actualMatches = findProfanity.lastActualMatches || new Map();
3466
+ const matchDetails = findProfanityWithMetadata(text, mergedOptions);
3467
+ const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
3430
3468
  let filteredText = text;
3431
3469
  const replacements = [];
3432
- matches.forEach((word) => {
3470
+ const getCensoredWord = (originalWord) => {
3471
+ if (useRandomGrawlix) {
3472
+ return makeRandomGrawlixString(originalWord.length);
3473
+ }
3474
+ return censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
3475
+ };
3476
+ const applyReplacement = (pattern, metadata) => {
3477
+ filteredText = filteredText.replace(pattern, (matchedStr) => {
3478
+ if (normalizedWhitelist.includes(matchedStr.toLowerCase())) {
3479
+ return matchedStr;
3480
+ }
3481
+ const censored = getCensoredWord(matchedStr);
3482
+ replacements.push({
3483
+ original: matchedStr,
3484
+ censored,
3485
+ metadata,
3486
+ });
3487
+ return censored;
3488
+ });
3489
+ };
3490
+ for (const word of matches) {
3433
3491
  const metadata = matchDetails.find((m) => m.word.toLowerCase() === word.toLowerCase() ||
3434
3492
  (m.aliases && m.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
3435
3493
  const variants = actualMatches.get(word.toLowerCase()) || [];
3436
- variants.push(word);
3437
- const uniqueVariants = [...new Set(variants)];
3438
- uniqueVariants.forEach((variant) => {
3439
- const regex = new RegExp(`\\b${escapeRegExp(variant)}\\b`, 'gi');
3440
- let match;
3441
- while ((match = regex.exec(filteredText)) !== null) {
3442
- const originalWord = match[0];
3443
- if (whitelist.includes(originalWord.toLowerCase()))
3444
- continue;
3445
- let censoredWord;
3446
- if (useRandomGrawlix) {
3447
- censoredWord = makeRandomGrawlixString(originalWord.length);
3448
- }
3449
- else {
3450
- censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
3451
- }
3452
- replacements.push({
3453
- original: originalWord,
3454
- censored: censoredWord,
3455
- metadata,
3456
- });
3457
- filteredText = filteredText.replace(new RegExp(`\\b${escapeRegExp(originalWord)}\\b`, 'g'), censoredWord);
3458
- }
3459
- });
3460
- if (detectSplit || detectLeetSpeak) {
3461
- if (detectLeetSpeak) {
3462
- const leetRegex = createWordRegex(word, {
3463
- wholeWord: true,
3464
- caseSensitive: false,
3465
- leetSpeak: true,
3466
- detectSplit: false,
3467
- indonesianVariation: false,
3468
- });
3469
- let match;
3470
- while ((match = leetRegex.exec(filteredText)) !== null) {
3471
- const originalWord = match[0];
3472
- if (whitelist.includes(originalWord.toLowerCase()))
3473
- continue;
3474
- let censoredWord;
3475
- if (useRandomGrawlix) {
3476
- censoredWord = makeRandomGrawlixString(originalWord.length);
3477
- }
3478
- else {
3479
- censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
3480
- }
3481
- replacements.push({
3482
- original: originalWord,
3483
- censored: censoredWord,
3484
- metadata,
3485
- });
3486
- filteredText = filteredText.replace(new RegExp(escapeRegExp(originalWord), 'g'), censoredWord);
3487
- }
3488
- }
3489
- if (detectSplit) {
3490
- const splitRegex = createWordRegex(word, {
3491
- wholeWord: false,
3492
- caseSensitive: false,
3493
- leetSpeak: false,
3494
- detectSplit: true,
3495
- indonesianVariation: false,
3496
- });
3497
- let match;
3498
- while ((match = splitRegex.exec(filteredText)) !== null) {
3499
- const originalWord = match[0];
3500
- if (whitelist.includes(originalWord.toLowerCase()))
3501
- continue;
3502
- let censoredWord;
3503
- if (useRandomGrawlix) {
3504
- censoredWord = makeRandomGrawlixString(originalWord.length);
3505
- }
3506
- else {
3507
- censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
3508
- }
3509
- replacements.push({
3510
- original: originalWord,
3511
- censored: censoredWord,
3512
- metadata,
3513
- });
3514
- filteredText = filteredText.replace(new RegExp(escapeRegExp(originalWord), 'g'), censoredWord);
3515
- }
3516
- }
3494
+ const allVariants = [...new Set([...variants, word])].sort((a, b) => b.length - a.length);
3495
+ for (const variant of allVariants) {
3496
+ const boundaryPattern = checkSubstring
3497
+ ? escapeRegExp(variant)
3498
+ : `\\b${escapeRegExp(variant)}\\b`;
3499
+ applyReplacement(new RegExp(boundaryPattern, 'gi'), metadata);
3517
3500
  }
3518
- });
3519
- if (detectSimilarity && useLevenshtein) {
3520
- matches.forEach((word) => {
3521
- const metadata = matchDetails.find((m) => m.word.toLowerCase() === word.toLowerCase() ||
3522
- (m.aliases && m.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
3523
- const variants = actualMatches.get(word.toLowerCase()) || [];
3524
- variants.forEach((variant) => {
3525
- const exactVariantRegex = new RegExp(`\\b${escapeRegExp(variant)}\\b`, 'gi');
3526
- let match;
3527
- while ((match = exactVariantRegex.exec(filteredText)) !== null) {
3528
- const originalWord = match[0];
3529
- if (whitelist.includes(originalWord.toLowerCase()))
3530
- continue;
3531
- let censoredWord;
3532
- if (useRandomGrawlix) {
3533
- censoredWord = makeRandomGrawlixString(originalWord.length);
3534
- }
3535
- else {
3536
- censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
3537
- }
3538
- replacements.push({
3539
- original: originalWord,
3540
- censored: censoredWord,
3541
- metadata,
3542
- });
3543
- filteredText = filteredText.replace(new RegExp(`\\b${escapeRegExp(originalWord)}\\b`, 'g'), censoredWord);
3544
- }
3501
+ if (detectLeetSpeak) {
3502
+ const leetRegex = createWordRegex(word, {
3503
+ wholeWord: !checkSubstring,
3504
+ caseSensitive: false,
3505
+ leetSpeak: true,
3506
+ detectSplit: false,
3507
+ indonesianVariation: false,
3545
3508
  });
3546
- });
3509
+ applyReplacement(leetRegex, metadata);
3510
+ }
3511
+ if (detectSplit) {
3512
+ const splitRegex = createWordRegex(word, {
3513
+ wholeWord: false,
3514
+ caseSensitive: false,
3515
+ leetSpeak: false,
3516
+ detectSplit: true,
3517
+ indonesianVariation: false,
3518
+ });
3519
+ applyReplacement(splitRegex, metadata);
3520
+ }
3547
3521
  }
3548
3522
  return {
3549
3523
  filtered: filteredText,
@@ -3785,6 +3759,13 @@ class IDProfanityFilter {
3785
3759
  ...options,
3786
3760
  };
3787
3761
  }
3762
+ /**
3763
+ * Mengatur ulang opsi filter ke default
3764
+ * @param options Opsi baru untuk override default
3765
+ */
3766
+ resetOptions(options = {}) {
3767
+ this.options = { ...DEFAULT_OPTIONS, ...options };
3768
+ }
3788
3769
  /**
3789
3770
  * Menggunakan preset yang telah ditentukan
3790
3771
  * @param presetName Nama preset yang akan digunakan