@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.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;
@@ -2548,19 +2538,15 @@ function levenshteinDistance(str1, str2) {
2548
2538
  const s2 = str2.toLowerCase();
2549
2539
  const len1 = s1.length;
2550
2540
  const len2 = s2.length;
2551
- // Inisialisasi matrix
2552
2541
  const matrix = [];
2553
- // Inisialisasi baris pertama
2554
2542
  for (let i = 0; i <= len2; i++) {
2555
2543
  matrix[0] = matrix[0] || [];
2556
2544
  matrix[0][i] = i;
2557
2545
  }
2558
- // Inisialisasi kolom pertama
2559
2546
  for (let i = 0; i <= len1; i++) {
2560
2547
  matrix[i] = matrix[i] || [];
2561
2548
  matrix[i][0] = i;
2562
2549
  }
2563
- // Isi matrix
2564
2550
  for (let i = 1; i <= len1; i++) {
2565
2551
  for (let j = 1; j <= len2; j++) {
2566
2552
  const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
@@ -3105,6 +3091,36 @@ class AhoCorasick {
3105
3091
  const matches = this.search(text);
3106
3092
  return new Set(matches.keys());
3107
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
+ }
3108
3124
  /**
3109
3125
  * Mengecek apakah teks mengandung setidaknya satu pola
3110
3126
  * @param text Teks yang akan dicari
@@ -3132,16 +3148,22 @@ class AhoCorasick {
3132
3148
  }
3133
3149
  }
3134
3150
 
3135
- const globalAhoCorasick = new AhoCorasick();
3136
- let ahoCorasickInitialized = false;
3137
- function initializeAhoCorasick(words) {
3138
- if (ahoCorasickInitialized)
3139
- return;
3140
- for (const word of words) {
3141
- 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;
3142
3165
  }
3143
- globalAhoCorasick.build();
3144
- ahoCorasickInitialized = true;
3166
+ return defaultAhoCorasick;
3145
3167
  }
3146
3168
  function getWordMetadata(word) {
3147
3169
  return wordObjects.find((obj) => obj.word.toLowerCase() === word.toLowerCase() ||
@@ -3149,10 +3171,15 @@ function getWordMetadata(word) {
3149
3171
  }
3150
3172
  function findProfanity(text, options = {}) {
3151
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);
3152
3175
  const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
3153
3176
  const normalizedText = normalizeText(text);
3154
- let baseWordsToCheck = wordList.length > 0 ? wordList : [];
3155
- if (baseWordsToCheck.length === 0) {
3177
+ let baseWordsToCheck = [];
3178
+ const aliasMap = new Map();
3179
+ if (hasCustomWordList) {
3180
+ baseWordsToCheck = wordList;
3181
+ }
3182
+ else {
3156
3183
  const filteredWords = wordObjects.filter((word) => {
3157
3184
  const matchCategory = categories ? categories.includes(word.category) : true;
3158
3185
  const matchRegion = regions ? regions.includes(word.region) : true;
@@ -3160,22 +3187,14 @@ function findProfanity(text, options = {}) {
3160
3187
  return matchCategory && matchRegion && matchSeverity;
3161
3188
  });
3162
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
+ });
3163
3197
  }
3164
- const aliasMap = new Map();
3165
- wordObjects.forEach((wordObj) => {
3166
- const matchCategory = categories ? categories.includes(wordObj.category) : true;
3167
- const matchRegion = regions ? regions.includes(wordObj.region) : true;
3168
- const matchSeverity = wordObj.severity >= severityThreshold;
3169
- if (matchCategory &&
3170
- matchRegion &&
3171
- matchSeverity &&
3172
- wordObj.aliases &&
3173
- wordObj.aliases.length > 0) {
3174
- wordObj.aliases.forEach((alias) => {
3175
- aliasMap.set(alias.toLowerCase(), wordObj.word.toLowerCase());
3176
- });
3177
- }
3178
- });
3179
3198
  const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !normalizedWhitelist.includes(word.toLowerCase()));
3180
3199
  if (wordsToCheck.length === 0) {
3181
3200
  return [];
@@ -3185,6 +3204,8 @@ function findProfanity(text, options = {}) {
3185
3204
  const passesFilters = (word) => {
3186
3205
  if (normalizedWhitelist.includes(word.toLowerCase()))
3187
3206
  return false;
3207
+ if (hasCustomWordList)
3208
+ return true;
3188
3209
  const metadata = getWordMetadata(word);
3189
3210
  if (!metadata)
3190
3211
  return false;
@@ -3193,9 +3214,27 @@ function findProfanity(text, options = {}) {
3193
3214
  const matchSeverity = metadata.severity >= severityThreshold;
3194
3215
  return matchCategory && matchRegion && matchSeverity;
3195
3216
  };
3196
- initializeAhoCorasick(wordsToCheck);
3197
- const basicMatches = globalAhoCorasick.searchUnique(normalizedText);
3198
- 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
+ }
3199
3238
  if (normalizedWhitelist.includes(match.toLowerCase()))
3200
3239
  continue;
3201
3240
  const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
@@ -3320,8 +3359,8 @@ function findProfanity(text, options = {}) {
3320
3359
  /**
3321
3360
  * Mencari kata kotor lengkap dengan metadata
3322
3361
  *
3323
- * @param text Teks yang akan diperika
3324
- * @param options Opsi utnuk pencarian kata kotor
3362
+ * @param text Teks yang akan diperiksa
3363
+ * @param options Opsi untuk pencarian kata kotor
3325
3364
  * @return Array dari objek kata kotor yang ditemukan
3326
3365
  */
3327
3366
  function findProfanityWithMetadata(text, options = {}) {
@@ -3333,14 +3372,22 @@ function findProfanityWithMetadata(text, options = {}) {
3333
3372
  .map((word) => {
3334
3373
  const wordObject = wordObjects.find((obj) => obj.word.toLowerCase() === word.toLowerCase() ||
3335
3374
  (obj.aliases && obj.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
3336
- return wordObject;
3375
+ if (wordObject) {
3376
+ return wordObject;
3377
+ }
3378
+ return {
3379
+ word,
3380
+ category: 'profanity',
3381
+ region: 'general',
3382
+ severity: 0.5,
3383
+ };
3337
3384
  })
3338
3385
  .filter((word) => word !== undefined);
3339
3386
  }
3340
3387
  /**
3341
- * Mencari kategory kata kotor yang ada dalam teks
3388
+ * Mencari kategori kata kotor yang ada dalam teks
3342
3389
  *
3343
- * @param matchDetails Hasil pencarian dari fingProfanityWithMetadata()
3390
+ * @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
3344
3391
  * @return Array kategori unik
3345
3392
  */
3346
3393
  function findCategories(matchDetails) {
@@ -3353,7 +3400,7 @@ function findCategories(matchDetails) {
3353
3400
  /**
3354
3401
  * Mencari region kata kotor yang ada dalam teks
3355
3402
  *
3356
- * @param matchDetails Hasil pencarian dari fingProfanityWithMetadata()
3403
+ * @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
3357
3404
  * @return Array region unik
3358
3405
  */
3359
3406
  function findRegions(matchDetails) {
@@ -3373,7 +3420,7 @@ function calculateSeverity(matchDetails) {
3373
3420
  if (matchDetails.length === 0) {
3374
3421
  return 0;
3375
3422
  }
3376
- const countFactor = Math.min(matchDetails.length / 10, 1); // Maksimal 10 kata
3423
+ const countFactor = Math.min(matchDetails.length / 10, 1);
3377
3424
  const categoryWeights = {
3378
3425
  sexual: 0.9,
3379
3426
  blasphemy: 0.9,
@@ -3401,21 +3448,9 @@ function calculateSeverity(matchDetails) {
3401
3448
  * @returns FilterResult dengan hasil filter
3402
3449
  */
3403
3450
  function filter(text, options = {}) {
3404
- 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 };
3405
- const matches = findProfanity(text, {
3406
- ...options,
3407
- detectLeetSpeak,
3408
- whitelist,
3409
- checkSubstring,
3410
- indonesianVariation,
3411
- detectSplit,
3412
- detectSimilarity,
3413
- useLevenshtein,
3414
- maxLevenshteinDistance,
3415
- similarityThreshold,
3416
- });
3417
- const actualMatches = findProfanity.lastActualMatches || new Map();
3418
- 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);
3419
3454
  if (matches.length === 0) {
3420
3455
  return {
3421
3456
  filtered: text,
@@ -3423,123 +3458,62 @@ function filter(text, options = {}) {
3423
3458
  replacements: [],
3424
3459
  };
3425
3460
  }
3461
+ const actualMatches = findProfanity.lastActualMatches || new Map();
3462
+ const matchDetails = findProfanityWithMetadata(text, mergedOptions);
3463
+ const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
3426
3464
  let filteredText = text;
3427
3465
  const replacements = [];
3428
- 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) {
3429
3487
  const metadata = matchDetails.find((m) => m.word.toLowerCase() === word.toLowerCase() ||
3430
3488
  (m.aliases && m.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
3431
3489
  const variants = actualMatches.get(word.toLowerCase()) || [];
3432
- variants.push(word);
3433
- const uniqueVariants = [...new Set(variants)];
3434
- uniqueVariants.forEach((variant) => {
3435
- const regex = new RegExp(`\\b${escapeRegExp(variant)}\\b`, 'gi');
3436
- let match;
3437
- while ((match = regex.exec(filteredText)) !== null) {
3438
- const originalWord = match[0];
3439
- if (whitelist.includes(originalWord.toLowerCase()))
3440
- continue;
3441
- let censoredWord;
3442
- if (useRandomGrawlix) {
3443
- censoredWord = makeRandomGrawlixString(originalWord.length);
3444
- }
3445
- else {
3446
- censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
3447
- }
3448
- replacements.push({
3449
- original: originalWord,
3450
- censored: censoredWord,
3451
- metadata,
3452
- });
3453
- filteredText = filteredText.replace(new RegExp(`\\b${escapeRegExp(originalWord)}\\b`, 'g'), censoredWord);
3454
- }
3455
- });
3456
- if (detectSplit || detectLeetSpeak) {
3457
- if (detectLeetSpeak) {
3458
- const leetRegex = createWordRegex(word, {
3459
- wholeWord: true,
3460
- caseSensitive: false,
3461
- leetSpeak: true,
3462
- detectSplit: false,
3463
- indonesianVariation: false,
3464
- });
3465
- let match;
3466
- while ((match = leetRegex.exec(filteredText)) !== null) {
3467
- const originalWord = match[0];
3468
- if (whitelist.includes(originalWord.toLowerCase()))
3469
- continue;
3470
- let censoredWord;
3471
- if (useRandomGrawlix) {
3472
- censoredWord = makeRandomGrawlixString(originalWord.length);
3473
- }
3474
- else {
3475
- censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
3476
- }
3477
- replacements.push({
3478
- original: originalWord,
3479
- censored: censoredWord,
3480
- metadata,
3481
- });
3482
- filteredText = filteredText.replace(new RegExp(escapeRegExp(originalWord), 'g'), censoredWord);
3483
- }
3484
- }
3485
- if (detectSplit) {
3486
- const splitRegex = createWordRegex(word, {
3487
- wholeWord: false,
3488
- caseSensitive: false,
3489
- leetSpeak: false,
3490
- detectSplit: true,
3491
- indonesianVariation: false,
3492
- });
3493
- let match;
3494
- while ((match = splitRegex.exec(filteredText)) !== null) {
3495
- const originalWord = match[0];
3496
- if (whitelist.includes(originalWord.toLowerCase()))
3497
- continue;
3498
- let censoredWord;
3499
- if (useRandomGrawlix) {
3500
- censoredWord = makeRandomGrawlixString(originalWord.length);
3501
- }
3502
- else {
3503
- censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
3504
- }
3505
- replacements.push({
3506
- original: originalWord,
3507
- censored: censoredWord,
3508
- metadata,
3509
- });
3510
- filteredText = filteredText.replace(new RegExp(escapeRegExp(originalWord), 'g'), censoredWord);
3511
- }
3512
- }
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);
3513
3496
  }
3514
- });
3515
- if (detectSimilarity && useLevenshtein) {
3516
- matches.forEach((word) => {
3517
- const metadata = matchDetails.find((m) => m.word.toLowerCase() === word.toLowerCase() ||
3518
- (m.aliases && m.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
3519
- const variants = actualMatches.get(word.toLowerCase()) || [];
3520
- variants.forEach((variant) => {
3521
- const exactVariantRegex = new RegExp(`\\b${escapeRegExp(variant)}\\b`, 'gi');
3522
- let match;
3523
- while ((match = exactVariantRegex.exec(filteredText)) !== null) {
3524
- const originalWord = match[0];
3525
- if (whitelist.includes(originalWord.toLowerCase()))
3526
- continue;
3527
- let censoredWord;
3528
- if (useRandomGrawlix) {
3529
- censoredWord = makeRandomGrawlixString(originalWord.length);
3530
- }
3531
- else {
3532
- censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
3533
- }
3534
- replacements.push({
3535
- original: originalWord,
3536
- censored: censoredWord,
3537
- metadata,
3538
- });
3539
- filteredText = filteredText.replace(new RegExp(`\\b${escapeRegExp(originalWord)}\\b`, 'g'), censoredWord);
3540
- }
3497
+ if (detectLeetSpeak) {
3498
+ const leetRegex = createWordRegex(word, {
3499
+ wholeWord: !checkSubstring,
3500
+ caseSensitive: false,
3501
+ leetSpeak: true,
3502
+ detectSplit: false,
3503
+ indonesianVariation: false,
3541
3504
  });
3542
- });
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
+ }
3543
3517
  }
3544
3518
  return {
3545
3519
  filtered: filteredText,
@@ -3781,6 +3755,13 @@ class IDProfanityFilter {
3781
3755
  ...options,
3782
3756
  };
3783
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
+ }
3784
3765
  /**
3785
3766
  * Menggunakan preset yang telah ditentukan
3786
3767
  * @param presetName Nama preset yang akan digunakan