@sideid/id-profanity-filter 1.12.0 → 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.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  type ProfanityCategory = 'sexual' | 'insult' | 'profanity' | 'slur' | 'drugs' | 'disgusting' | 'blasphemy';
2
- type Region = 'general' | 'jawa' | 'sunda' | 'betawi' | 'batak' | 'minang' | 'bali' | 'madura' | 'bugis' | 'aceh' | 'ambon' | 'papua' | 'manado' | 'banjar' | 'palembang' | 'lampung' | 'ntt' | 'mandailing' | 'ntb';
2
+ type Region = 'general' | 'jawa' | 'sunda' | 'betawi' | 'batak' | 'minang' | 'bali' | 'madura' | 'aceh';
3
3
  interface ProfanityWord {
4
4
  word: string;
5
5
  category: ProfanityCategory;
@@ -62,22 +62,22 @@ declare function findProfanity(text: string, options?: FilterOptions): string[];
62
62
  /**
63
63
  * Mencari kata kotor lengkap dengan metadata
64
64
  *
65
- * @param text Teks yang akan diperika
66
- * @param options Opsi utnuk pencarian kata kotor
65
+ * @param text Teks yang akan diperiksa
66
+ * @param options Opsi untuk pencarian kata kotor
67
67
  * @return Array dari objek kata kotor yang ditemukan
68
68
  */
69
69
  declare function findProfanityWithMetadata(text: string, options?: FilterOptions): ProfanityWord[];
70
70
  /**
71
- * Mencari kategory kata kotor yang ada dalam teks
71
+ * Mencari kategori kata kotor yang ada dalam teks
72
72
  *
73
- * @param matchDetails Hasil pencarian dari fingProfanityWithMetadata()
73
+ * @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
74
74
  * @return Array kategori unik
75
75
  */
76
76
  declare function findCategories(matchDetails: ProfanityWord[]): ProfanityCategory[];
77
77
  /**
78
78
  * Mencari region kata kotor yang ada dalam teks
79
79
  *
80
- * @param matchDetails Hasil pencarian dari fingProfanityWithMetadata()
80
+ * @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
81
81
  * @return Array region unik
82
82
  */
83
83
  declare function findRegions(matchDetails: ProfanityWord[]): Region[];
@@ -748,6 +748,11 @@ declare class IDProfanityFilter {
748
748
  * @param options Opsi baru untuk filter
749
749
  */
750
750
  setOptions(options: Partial<FilterOptions>): void;
751
+ /**
752
+ * Mengatur ulang opsi filter ke default
753
+ * @param options Opsi baru untuk override default
754
+ */
755
+ resetOptions(options?: FilterOptions): void;
751
756
  /**
752
757
  * Menggunakan preset yang telah ditentukan
753
758
  * @param presetName Nama preset yang akan digunakan
package/dist/index.esm.js CHANGED
@@ -4,7 +4,7 @@ const general = [
4
4
  category: 'profanity',
5
5
  region: 'general',
6
6
  severity: 0.7,
7
- aliases: ['anjay', 'anjir', 'anying', 'njing', 'anj', 'anjg', 'ajg'],
7
+ aliases: ['anjay', 'anjir', 'anying', 'njing', 'anj', 'anjg', 'ajg', 'anjic', 'anjink'],
8
8
  description: 'Mengacu pada hewan anjing, digunakan sebagai umpatan',
9
9
  context: 'Umpatan umum untuk menunjukkan kemarahan atau ketidaksetujuan',
10
10
  },
@@ -112,7 +112,7 @@ const general = [
112
112
  category: 'insult',
113
113
  region: 'general',
114
114
  severity: 0.5,
115
- aliases: ['sialn', 'sl'],
115
+ aliases: ['sialn'],
116
116
  description: 'Kata yang mengacu pada orang yang membawa sial',
117
117
  context: 'Hinaan untuk menyebut orang yang dianggap membawa sial',
118
118
  },
@@ -175,7 +175,7 @@ const general = [
175
175
  category: 'sexual',
176
176
  region: 'general',
177
177
  severity: 0.9,
178
- aliases: ['ngew', 'we'],
178
+ aliases: ['ngew'],
179
179
  description: 'Istilah kasar untuk aktivitas seksual',
180
180
  context: 'Kata vulgar yang merujuk pada aktivitas seksual',
181
181
  },
@@ -1607,7 +1607,7 @@ const sexual = [
1607
1607
  category: 'sexual',
1608
1608
  region: 'general',
1609
1609
  severity: 0.9,
1610
- aliases: ['ngew', 'we'],
1610
+ aliases: ['ngew'],
1611
1611
  description: 'Istilah kasar untuk aktivitas seksual',
1612
1612
  context: 'Kata vulgar yang merujuk pada aktivitas seksual',
1613
1613
  },
@@ -2139,16 +2139,7 @@ const wordObjects = [
2139
2139
  ...minang,
2140
2140
  ...bali,
2141
2141
  ...madura,
2142
- // ...bugis,
2143
2142
  ...aceh,
2144
- // ...ambon,
2145
- // ...papua,
2146
- // ...manado,
2147
- // ...banjar,
2148
- // ...palembang,
2149
- // ...lampung,
2150
- // ...ntt,
2151
- // ...ntb,
2152
2143
  ];
2153
2144
  wordObjects.map((item) => item.word);
2154
2145
  /**
@@ -2464,8 +2455,7 @@ function addIndonesianVariations(pattern) {
2464
2455
  const lowerChar = char.toLowerCase();
2465
2456
  const variations = variationMap[lowerChar];
2466
2457
  if (variations && variations.length > 1) {
2467
- // Create a character class with all variations
2468
- result += `[${variations.join('')}]`;
2458
+ result += `(?:${variations.join('|')})`;
2469
2459
  }
2470
2460
  else {
2471
2461
  result += char;
@@ -3101,6 +3091,36 @@ class AhoCorasick {
3101
3091
  const matches = this.search(text);
3102
3092
  return new Set(matches.keys());
3103
3093
  }
3094
+ /**
3095
+ * Mencari semua kemunculan pola beserta posisi indeks awal dan akhirnya
3096
+ * @param text Teks yang akan dicari
3097
+ * @returns Array objek berisi pattern, start, dan end
3098
+ */
3099
+ searchWithPositions(text) {
3100
+ if (!this.built) {
3101
+ this.build();
3102
+ }
3103
+ const results = [];
3104
+ const normalizedText = text.toLowerCase();
3105
+ let node = this.root;
3106
+ for (let i = 0; i < normalizedText.length; i++) {
3107
+ const char = normalizedText[i];
3108
+ while (node !== this.root && !node.children.has(char)) {
3109
+ node = node.fail;
3110
+ }
3111
+ if (node.children.has(char)) {
3112
+ node = node.children.get(char);
3113
+ }
3114
+ for (const match of node.output) {
3115
+ results.push({
3116
+ pattern: match,
3117
+ start: i + 1 - match.length,
3118
+ end: i + 1,
3119
+ });
3120
+ }
3121
+ }
3122
+ return results;
3123
+ }
3104
3124
  /**
3105
3125
  * Mengecek apakah teks mengandung setidaknya satu pola
3106
3126
  * @param text Teks yang akan dicari
@@ -3128,16 +3148,22 @@ class AhoCorasick {
3128
3148
  }
3129
3149
  }
3130
3150
 
3131
- const globalAhoCorasick = new AhoCorasick();
3132
- let ahoCorasickInitialized = false;
3133
- function initializeAhoCorasick(words) {
3134
- if (ahoCorasickInitialized)
3135
- return;
3136
- for (const word of words) {
3137
- globalAhoCorasick.addPattern(word);
3151
+ let defaultAhoCorasick = null;
3152
+ function getDefaultAhoCorasick() {
3153
+ if (!defaultAhoCorasick) {
3154
+ const ac = new AhoCorasick();
3155
+ for (const wordObj of wordObjects) {
3156
+ ac.addPattern(wordObj.word);
3157
+ if (wordObj.aliases) {
3158
+ for (const alias of wordObj.aliases) {
3159
+ ac.addPattern(alias);
3160
+ }
3161
+ }
3162
+ }
3163
+ ac.build();
3164
+ defaultAhoCorasick = ac;
3138
3165
  }
3139
- globalAhoCorasick.build();
3140
- ahoCorasickInitialized = true;
3166
+ return defaultAhoCorasick;
3141
3167
  }
3142
3168
  function getWordMetadata(word) {
3143
3169
  return wordObjects.find((obj) => obj.word.toLowerCase() === word.toLowerCase() ||
@@ -3145,10 +3171,15 @@ function getWordMetadata(word) {
3145
3171
  }
3146
3172
  function findProfanity(text, options = {}) {
3147
3173
  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 };
3174
+ const hasCustomWordList = Boolean(wordList && wordList.length > 0);
3148
3175
  const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
3149
3176
  const normalizedText = normalizeText(text);
3150
- let baseWordsToCheck = wordList.length > 0 ? wordList : [];
3151
- if (baseWordsToCheck.length === 0) {
3177
+ let baseWordsToCheck = [];
3178
+ const aliasMap = new Map();
3179
+ if (hasCustomWordList) {
3180
+ baseWordsToCheck = wordList;
3181
+ }
3182
+ else {
3152
3183
  const filteredWords = wordObjects.filter((word) => {
3153
3184
  const matchCategory = categories ? categories.includes(word.category) : true;
3154
3185
  const matchRegion = regions ? regions.includes(word.region) : true;
@@ -3156,22 +3187,14 @@ function findProfanity(text, options = {}) {
3156
3187
  return matchCategory && matchRegion && matchSeverity;
3157
3188
  });
3158
3189
  baseWordsToCheck = filteredWords.map((word) => word.word);
3190
+ filteredWords.forEach((wordObj) => {
3191
+ if (wordObj.aliases && wordObj.aliases.length > 0) {
3192
+ wordObj.aliases.forEach((alias) => {
3193
+ aliasMap.set(alias.toLowerCase(), wordObj.word.toLowerCase());
3194
+ });
3195
+ }
3196
+ });
3159
3197
  }
3160
- const aliasMap = new Map();
3161
- wordObjects.forEach((wordObj) => {
3162
- const matchCategory = categories ? categories.includes(wordObj.category) : true;
3163
- const matchRegion = regions ? regions.includes(wordObj.region) : true;
3164
- const matchSeverity = wordObj.severity >= severityThreshold;
3165
- if (matchCategory &&
3166
- matchRegion &&
3167
- matchSeverity &&
3168
- wordObj.aliases &&
3169
- wordObj.aliases.length > 0) {
3170
- wordObj.aliases.forEach((alias) => {
3171
- aliasMap.set(alias.toLowerCase(), wordObj.word.toLowerCase());
3172
- });
3173
- }
3174
- });
3175
3198
  const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !normalizedWhitelist.includes(word.toLowerCase()));
3176
3199
  if (wordsToCheck.length === 0) {
3177
3200
  return [];
@@ -3181,6 +3204,8 @@ function findProfanity(text, options = {}) {
3181
3204
  const passesFilters = (word) => {
3182
3205
  if (normalizedWhitelist.includes(word.toLowerCase()))
3183
3206
  return false;
3207
+ if (hasCustomWordList)
3208
+ return true;
3184
3209
  const metadata = getWordMetadata(word);
3185
3210
  if (!metadata)
3186
3211
  return false;
@@ -3189,9 +3214,27 @@ function findProfanity(text, options = {}) {
3189
3214
  const matchSeverity = metadata.severity >= severityThreshold;
3190
3215
  return matchCategory && matchRegion && matchSeverity;
3191
3216
  };
3192
- initializeAhoCorasick(wordsToCheck);
3193
- const basicMatches = globalAhoCorasick.searchUnique(normalizedText);
3194
- for (const match of basicMatches) {
3217
+ let ac;
3218
+ if (hasCustomWordList) {
3219
+ ac = new AhoCorasick();
3220
+ for (const w of wordsToCheck) {
3221
+ ac.addPattern(w);
3222
+ }
3223
+ ac.build();
3224
+ }
3225
+ else {
3226
+ ac = getDefaultAhoCorasick();
3227
+ }
3228
+ const occurrences = ac.searchWithPositions(normalizedText);
3229
+ for (const occ of occurrences) {
3230
+ const { pattern: match, start, end } = occ;
3231
+ if (!checkSubstring) {
3232
+ const isWordStart = start === 0 || !/[a-z0-9_]/i.test(normalizedText[start - 1]);
3233
+ const isWordEnd = end === normalizedText.length || !/[a-z0-9_]/i.test(normalizedText[end]);
3234
+ if (!isWordStart || !isWordEnd) {
3235
+ continue;
3236
+ }
3237
+ }
3195
3238
  if (normalizedWhitelist.includes(match.toLowerCase()))
3196
3239
  continue;
3197
3240
  const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
@@ -3316,8 +3359,8 @@ function findProfanity(text, options = {}) {
3316
3359
  /**
3317
3360
  * Mencari kata kotor lengkap dengan metadata
3318
3361
  *
3319
- * @param text Teks yang akan diperika
3320
- * @param options Opsi utnuk pencarian kata kotor
3362
+ * @param text Teks yang akan diperiksa
3363
+ * @param options Opsi untuk pencarian kata kotor
3321
3364
  * @return Array dari objek kata kotor yang ditemukan
3322
3365
  */
3323
3366
  function findProfanityWithMetadata(text, options = {}) {
@@ -3329,14 +3372,22 @@ function findProfanityWithMetadata(text, options = {}) {
3329
3372
  .map((word) => {
3330
3373
  const wordObject = wordObjects.find((obj) => obj.word.toLowerCase() === word.toLowerCase() ||
3331
3374
  (obj.aliases && obj.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
3332
- return wordObject;
3375
+ if (wordObject) {
3376
+ return wordObject;
3377
+ }
3378
+ return {
3379
+ word,
3380
+ category: 'profanity',
3381
+ region: 'general',
3382
+ severity: 0.5,
3383
+ };
3333
3384
  })
3334
3385
  .filter((word) => word !== undefined);
3335
3386
  }
3336
3387
  /**
3337
- * Mencari kategory kata kotor yang ada dalam teks
3388
+ * Mencari kategori kata kotor yang ada dalam teks
3338
3389
  *
3339
- * @param matchDetails Hasil pencarian dari fingProfanityWithMetadata()
3390
+ * @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
3340
3391
  * @return Array kategori unik
3341
3392
  */
3342
3393
  function findCategories(matchDetails) {
@@ -3349,7 +3400,7 @@ function findCategories(matchDetails) {
3349
3400
  /**
3350
3401
  * Mencari region kata kotor yang ada dalam teks
3351
3402
  *
3352
- * @param matchDetails Hasil pencarian dari fingProfanityWithMetadata()
3403
+ * @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
3353
3404
  * @return Array region unik
3354
3405
  */
3355
3406
  function findRegions(matchDetails) {
@@ -3369,7 +3420,7 @@ function calculateSeverity(matchDetails) {
3369
3420
  if (matchDetails.length === 0) {
3370
3421
  return 0;
3371
3422
  }
3372
- const countFactor = Math.min(matchDetails.length / 10, 1); // Maksimal 10 kata
3423
+ const countFactor = Math.min(matchDetails.length / 10, 1);
3373
3424
  const categoryWeights = {
3374
3425
  sexual: 0.9,
3375
3426
  blasphemy: 0.9,
@@ -3397,21 +3448,9 @@ function calculateSeverity(matchDetails) {
3397
3448
  * @returns FilterResult dengan hasil filter
3398
3449
  */
3399
3450
  function filter(text, options = {}) {
3400
- 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 };
3401
- const matches = findProfanity(text, {
3402
- ...options,
3403
- detectLeetSpeak,
3404
- whitelist,
3405
- checkSubstring,
3406
- indonesianVariation,
3407
- detectSplit,
3408
- detectSimilarity,
3409
- useLevenshtein,
3410
- maxLevenshteinDistance,
3411
- similarityThreshold,
3412
- });
3413
- const actualMatches = findProfanity.lastActualMatches || new Map();
3414
- const matchDetails = findProfanityWithMetadata(text, options);
3451
+ const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
3452
+ const { replaceWith = '*', fullWordCensor = true, detectLeetSpeak = true, whitelist = [], checkSubstring = false, useRandomGrawlix = false, keepFirstAndLast = false, detectSplit = false, } = mergedOptions;
3453
+ const matches = findProfanity(text, mergedOptions);
3415
3454
  if (matches.length === 0) {
3416
3455
  return {
3417
3456
  filtered: text,
@@ -3419,123 +3458,62 @@ function filter(text, options = {}) {
3419
3458
  replacements: [],
3420
3459
  };
3421
3460
  }
3461
+ const actualMatches = findProfanity.lastActualMatches || new Map();
3462
+ const matchDetails = findProfanityWithMetadata(text, mergedOptions);
3463
+ const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
3422
3464
  let filteredText = text;
3423
3465
  const replacements = [];
3424
- matches.forEach((word) => {
3466
+ const getCensoredWord = (originalWord) => {
3467
+ if (useRandomGrawlix) {
3468
+ return makeRandomGrawlixString(originalWord.length);
3469
+ }
3470
+ return censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
3471
+ };
3472
+ const applyReplacement = (pattern, metadata) => {
3473
+ filteredText = filteredText.replace(pattern, (matchedStr) => {
3474
+ if (normalizedWhitelist.includes(matchedStr.toLowerCase())) {
3475
+ return matchedStr;
3476
+ }
3477
+ const censored = getCensoredWord(matchedStr);
3478
+ replacements.push({
3479
+ original: matchedStr,
3480
+ censored,
3481
+ metadata,
3482
+ });
3483
+ return censored;
3484
+ });
3485
+ };
3486
+ for (const word of matches) {
3425
3487
  const metadata = matchDetails.find((m) => m.word.toLowerCase() === word.toLowerCase() ||
3426
3488
  (m.aliases && m.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
3427
3489
  const variants = actualMatches.get(word.toLowerCase()) || [];
3428
- variants.push(word);
3429
- const uniqueVariants = [...new Set(variants)];
3430
- uniqueVariants.forEach((variant) => {
3431
- const regex = new RegExp(`\\b${escapeRegExp(variant)}\\b`, 'gi');
3432
- let match;
3433
- while ((match = regex.exec(filteredText)) !== null) {
3434
- const originalWord = match[0];
3435
- if (whitelist.includes(originalWord.toLowerCase()))
3436
- continue;
3437
- let censoredWord;
3438
- if (useRandomGrawlix) {
3439
- censoredWord = makeRandomGrawlixString(originalWord.length);
3440
- }
3441
- else {
3442
- censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
3443
- }
3444
- replacements.push({
3445
- original: originalWord,
3446
- censored: censoredWord,
3447
- metadata,
3448
- });
3449
- filteredText = filteredText.replace(new RegExp(`\\b${escapeRegExp(originalWord)}\\b`, 'g'), censoredWord);
3450
- }
3451
- });
3452
- if (detectSplit || detectLeetSpeak) {
3453
- if (detectLeetSpeak) {
3454
- const leetRegex = createWordRegex(word, {
3455
- wholeWord: true,
3456
- caseSensitive: false,
3457
- leetSpeak: true,
3458
- detectSplit: false,
3459
- indonesianVariation: false,
3460
- });
3461
- let match;
3462
- while ((match = leetRegex.exec(filteredText)) !== null) {
3463
- const originalWord = match[0];
3464
- if (whitelist.includes(originalWord.toLowerCase()))
3465
- continue;
3466
- let censoredWord;
3467
- if (useRandomGrawlix) {
3468
- censoredWord = makeRandomGrawlixString(originalWord.length);
3469
- }
3470
- else {
3471
- censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
3472
- }
3473
- replacements.push({
3474
- original: originalWord,
3475
- censored: censoredWord,
3476
- metadata,
3477
- });
3478
- filteredText = filteredText.replace(new RegExp(escapeRegExp(originalWord), 'g'), censoredWord);
3479
- }
3480
- }
3481
- if (detectSplit) {
3482
- const splitRegex = createWordRegex(word, {
3483
- wholeWord: false,
3484
- caseSensitive: false,
3485
- leetSpeak: false,
3486
- detectSplit: true,
3487
- indonesianVariation: false,
3488
- });
3489
- let match;
3490
- while ((match = splitRegex.exec(filteredText)) !== null) {
3491
- const originalWord = match[0];
3492
- if (whitelist.includes(originalWord.toLowerCase()))
3493
- continue;
3494
- let censoredWord;
3495
- if (useRandomGrawlix) {
3496
- censoredWord = makeRandomGrawlixString(originalWord.length);
3497
- }
3498
- else {
3499
- censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
3500
- }
3501
- replacements.push({
3502
- original: originalWord,
3503
- censored: censoredWord,
3504
- metadata,
3505
- });
3506
- filteredText = filteredText.replace(new RegExp(escapeRegExp(originalWord), 'g'), censoredWord);
3507
- }
3508
- }
3490
+ const allVariants = [...new Set([...variants, word])].sort((a, b) => b.length - a.length);
3491
+ for (const variant of allVariants) {
3492
+ const boundaryPattern = checkSubstring
3493
+ ? escapeRegExp(variant)
3494
+ : `\\b${escapeRegExp(variant)}\\b`;
3495
+ applyReplacement(new RegExp(boundaryPattern, 'gi'), metadata);
3509
3496
  }
3510
- });
3511
- if (detectSimilarity && useLevenshtein) {
3512
- matches.forEach((word) => {
3513
- const metadata = matchDetails.find((m) => m.word.toLowerCase() === word.toLowerCase() ||
3514
- (m.aliases && m.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
3515
- const variants = actualMatches.get(word.toLowerCase()) || [];
3516
- variants.forEach((variant) => {
3517
- const exactVariantRegex = new RegExp(`\\b${escapeRegExp(variant)}\\b`, 'gi');
3518
- let match;
3519
- while ((match = exactVariantRegex.exec(filteredText)) !== null) {
3520
- const originalWord = match[0];
3521
- if (whitelist.includes(originalWord.toLowerCase()))
3522
- continue;
3523
- let censoredWord;
3524
- if (useRandomGrawlix) {
3525
- censoredWord = makeRandomGrawlixString(originalWord.length);
3526
- }
3527
- else {
3528
- censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
3529
- }
3530
- replacements.push({
3531
- original: originalWord,
3532
- censored: censoredWord,
3533
- metadata,
3534
- });
3535
- filteredText = filteredText.replace(new RegExp(`\\b${escapeRegExp(originalWord)}\\b`, 'g'), censoredWord);
3536
- }
3497
+ if (detectLeetSpeak) {
3498
+ const leetRegex = createWordRegex(word, {
3499
+ wholeWord: !checkSubstring,
3500
+ caseSensitive: false,
3501
+ leetSpeak: true,
3502
+ detectSplit: false,
3503
+ indonesianVariation: false,
3537
3504
  });
3538
- });
3505
+ applyReplacement(leetRegex, metadata);
3506
+ }
3507
+ if (detectSplit) {
3508
+ const splitRegex = createWordRegex(word, {
3509
+ wholeWord: false,
3510
+ caseSensitive: false,
3511
+ leetSpeak: false,
3512
+ detectSplit: true,
3513
+ indonesianVariation: false,
3514
+ });
3515
+ applyReplacement(splitRegex, metadata);
3516
+ }
3539
3517
  }
3540
3518
  return {
3541
3519
  filtered: filteredText,
@@ -3777,6 +3755,13 @@ class IDProfanityFilter {
3777
3755
  ...options,
3778
3756
  };
3779
3757
  }
3758
+ /**
3759
+ * Mengatur ulang opsi filter ke default
3760
+ * @param options Opsi baru untuk override default
3761
+ */
3762
+ resetOptions(options = {}) {
3763
+ this.options = { ...DEFAULT_OPTIONS, ...options };
3764
+ }
3780
3765
  /**
3781
3766
  * Menggunakan preset yang telah ditentukan
3782
3767
  * @param presetName Nama preset yang akan digunakan