@sideid/id-profanity-filter 1.11.11 → 1.11.13

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.esm.js CHANGED
@@ -2373,14 +2373,14 @@ function addLeetSpeakVariations(pattern) {
2373
2373
  const leetMap = {
2374
2374
  a: ['a', '4', '@'],
2375
2375
  b: ['b', '8', '6'],
2376
- c: ['c', '(', '{', '<'],
2376
+ c: ['c', '\\(', '\\{', '<'],
2377
2377
  e: ['e', '3'],
2378
2378
  g: ['g', '6', '9'],
2379
- i: ['i', '1', '!', '|'],
2380
- l: ['l', '1', '|'],
2379
+ i: ['i', '1', '!', '\\|'],
2380
+ l: ['l', '1', '\\|'],
2381
2381
  o: ['o', '0'],
2382
- s: ['s', '5', '$'],
2383
- t: ['t', '7', '+'],
2382
+ s: ['s', '5', '\\$'],
2383
+ t: ['t', '7', '\\+'],
2384
2384
  z: ['z', '2'],
2385
2385
  };
2386
2386
  return pattern
@@ -2402,8 +2402,20 @@ function addLeetSpeakVariations(pattern) {
2402
2402
  * @returns Pola regex dengan kemungkinan split
2403
2403
  */
2404
2404
  function addSplitVariations(pattern) {
2405
- // Tambahkan kemungkinan spasi atau karakter penghubung di antara setiap huruf
2406
- return pattern.split('').join('[\\s\\-._*+]?');
2405
+ // Instead of joining character by character with a separator pattern,
2406
+ // we'll create a simpler version that matches the pattern with optional separators
2407
+ // Convert each character to a pattern that allows optional separators before it
2408
+ // except for the first character
2409
+ let result = '';
2410
+ const chars = pattern.split('');
2411
+ for (let i = 0; i < chars.length; i++) {
2412
+ if (i > 0) {
2413
+ // Add optional separator before each character except the first
2414
+ result += '[\\s\\-._*+]?';
2415
+ }
2416
+ result += chars[i];
2417
+ }
2418
+ return result;
2407
2419
  }
2408
2420
  /**
2409
2421
  * Membuat regex untuk mencari kata dengan variasi spasi dan karakter penghubung
@@ -2413,9 +2425,20 @@ function addSplitVariations(pattern) {
2413
2425
  * @returns Objek RegExp
2414
2426
  */
2415
2427
  function createEvasionRegex(word) {
2416
- // Tambahkan kemungkinan spasi atau karakter penghubung di antara setiap huruf
2417
- const pattern = addSplitVariations(escapeRegExp(word));
2418
- return new RegExp(pattern, 'gi');
2428
+ // Escape karakter khusus regex
2429
+ const escaped = escapeRegExp(word);
2430
+ // Create a regex pattern that allows any separator between characters
2431
+ let result = '';
2432
+ const chars = escaped.split('');
2433
+ for (let i = 0; i < chars.length; i++) {
2434
+ // Add the character
2435
+ result += chars[i];
2436
+ // Add optional separator after each character except the last
2437
+ if (i < chars.length - 1) {
2438
+ result += '[\\s\\-._*+]?';
2439
+ }
2440
+ }
2441
+ return new RegExp(result, 'gi');
2419
2442
  }
2420
2443
  /**
2421
2444
  * Menambahkan variasi ejaan Bahasa Indonesia
@@ -2428,25 +2451,27 @@ function addIndonesianVariations(pattern) {
2428
2451
  const variationMap = {
2429
2452
  c: ['c', 'k'], // contoh: becok/bekok
2430
2453
  k: ['k', 'c', 'q'], // contoh: kacau/qacau
2431
- j: ['j', 'dj'], // contoh: jualan/djualan (ejaan lama)
2454
+ j: ['j', 'd'], // contoh: jualan/dualan (ejaan lama)
2432
2455
  y: ['y', 'j'], // contoh: ya/ja
2433
2456
  u: ['u', 'oe'], // contoh: untuk/oentoek (ejaan lama)
2434
2457
  f: ['f', 'p', 'v'], // contoh: kafir/kapir
2435
2458
  z: ['z', 'j', 's'], // contoh: zaman/jaman
2436
2459
  x: ['x', 'ks'], // contoh: taxi/taksi
2437
2460
  };
2438
- // Ganti tiap karakter dengan variasinya
2439
- return pattern
2440
- .split('')
2441
- .map((char) => {
2461
+ // Go through each character in the pattern and replace with variations
2462
+ let result = '';
2463
+ for (const char of pattern) {
2442
2464
  const lowerChar = char.toLowerCase();
2443
2465
  const variations = variationMap[lowerChar];
2444
2466
  if (variations && variations.length > 1) {
2445
- return `[${variations.join('')}]`;
2467
+ // Create a character class with all variations
2468
+ result += `[${variations.join('')}]`;
2446
2469
  }
2447
- return char;
2448
- })
2449
- .join('');
2470
+ else {
2471
+ result += char;
2472
+ }
2473
+ }
2474
+ return result;
2450
2475
  }
2451
2476
  /**
2452
2477
  * Membuat regex untuk mencocokkan kata dengan mempertimbangkan variasi ejaan Bahasa Indonesia
@@ -2455,7 +2480,8 @@ function addIndonesianVariations(pattern) {
2455
2480
  * @returns Objek RegExp
2456
2481
  */
2457
2482
  function createIndonesianVariationRegex(word) {
2458
- const pattern = addIndonesianVariations(escapeRegExp(word));
2483
+ const escapedWord = escapeRegExp(word);
2484
+ const pattern = addIndonesianVariations(escapedWord);
2459
2485
  return new RegExp(`\\b${pattern}\\b`, 'gi');
2460
2486
  }
2461
2487
  /**
@@ -2479,16 +2505,33 @@ function createContextRegex(word, contextSize = 3) {
2479
2505
  */
2480
2506
  function createWordFormRegex(word) {
2481
2507
  // Implementasi sederhana untuk mencocokkan berbagai imbuhan
2482
- // Ini bisa dikembangkan lebih lanjut untuk mencocokkan bentukan kata yang lebih kompleks
2483
- const prefixes = ['', 'me', 'pe', 'ber', 'di', 'ter', 'se'];
2484
- const suffixes = ['', 'kan', 'an', 'i', 'nya'];
2508
+ const escapedWord = escapeRegExp(word);
2509
+ // Common Indonesian prefixes and suffixes
2510
+ const prefixes = [
2511
+ '',
2512
+ 'me',
2513
+ 'pe',
2514
+ 'ber',
2515
+ 'di',
2516
+ 'ter',
2517
+ 'se',
2518
+ 'ke',
2519
+ 'mem',
2520
+ 'pem',
2521
+ 'bel',
2522
+ 'peng',
2523
+ 'meng',
2524
+ ];
2525
+ const suffixes = ['', 'kan', 'an', 'i', 'nya', 'lah', 'kah'];
2485
2526
  const patterns = [];
2486
- // Kombinasikan prefix dan suffix
2487
2527
  for (const prefix of prefixes) {
2488
2528
  for (const suffix of suffixes) {
2489
- patterns.push(`\\b${prefix}${escapeRegExp(word)}${suffix}\\b`);
2529
+ patterns.push(`\\b${prefix}${escapedWord}${suffix}\\b`);
2490
2530
  }
2491
2531
  }
2532
+ if (/^[aiueo]/.test(word)) {
2533
+ patterns.push(`\\bng${escapedWord}\\b`);
2534
+ }
2492
2535
  return new RegExp(patterns.join('|'), 'gi');
2493
2536
  }
2494
2537
 
@@ -3100,8 +3143,13 @@ function initializeAhoCorasick(words) {
3100
3143
  globalAhoCorasick.build();
3101
3144
  ahoCorasickInitialized = true;
3102
3145
  }
3146
+ function getWordMetadata(word) {
3147
+ return wordObjects.find((obj) => obj.word.toLowerCase() === word.toLowerCase() ||
3148
+ (obj.aliases && obj.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
3149
+ }
3103
3150
  function findProfanity(text, options = {}) {
3104
3151
  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 };
3152
+ const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
3105
3153
  const normalizedText = normalizeText(text);
3106
3154
  let baseWordsToCheck = wordList.length > 0 ? wordList : [];
3107
3155
  if (baseWordsToCheck.length === 0) {
@@ -3115,27 +3163,44 @@ function findProfanity(text, options = {}) {
3115
3163
  }
3116
3164
  const aliasMap = new Map();
3117
3165
  wordObjects.forEach((wordObj) => {
3118
- if (wordObj.aliases && wordObj.aliases.length > 0) {
3119
- const matchCategory = categories ? categories.includes(wordObj.category) : true;
3120
- const matchRegion = regions ? regions.includes(wordObj.region) : true;
3121
- const matchSeverity = wordObj.severity >= severityThreshold;
3122
- if (matchCategory && matchRegion && matchSeverity) {
3123
- wordObj.aliases.forEach((alias) => {
3124
- aliasMap.set(alias.toLowerCase(), wordObj.word.toLowerCase());
3125
- });
3126
- }
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
+ });
3127
3177
  }
3128
3178
  });
3129
- const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !whitelist.includes(word.toLowerCase()));
3179
+ const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !normalizedWhitelist.includes(word.toLowerCase()));
3130
3180
  if (wordsToCheck.length === 0) {
3131
3181
  return [];
3132
3182
  }
3133
3183
  const matches = new Set();
3134
3184
  const actualMatches = new Map();
3185
+ const passesFilters = (word) => {
3186
+ if (normalizedWhitelist.includes(word.toLowerCase()))
3187
+ return false;
3188
+ const metadata = getWordMetadata(word);
3189
+ if (!metadata)
3190
+ return false;
3191
+ const matchCategory = categories ? categories.includes(metadata.category) : true;
3192
+ const matchRegion = regions ? regions.includes(metadata.region) : true;
3193
+ const matchSeverity = metadata.severity >= severityThreshold;
3194
+ return matchCategory && matchRegion && matchSeverity;
3195
+ };
3135
3196
  initializeAhoCorasick(wordsToCheck);
3136
3197
  const basicMatches = globalAhoCorasick.searchUnique(normalizedText);
3137
3198
  for (const match of basicMatches) {
3199
+ if (normalizedWhitelist.includes(match.toLowerCase()))
3200
+ continue;
3138
3201
  const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
3202
+ if (!passesFilters(originalWord))
3203
+ continue;
3139
3204
  matches.add(originalWord);
3140
3205
  if (!actualMatches.has(originalWord)) {
3141
3206
  actualMatches.set(originalWord, []);
@@ -3153,7 +3218,12 @@ function findProfanity(text, options = {}) {
3153
3218
  });
3154
3219
  let match;
3155
3220
  while ((match = leetRegex.exec(text)) !== null) {
3221
+ const matchedText = match[0];
3222
+ if (normalizedWhitelist.includes(matchedText.toLowerCase()))
3223
+ continue;
3156
3224
  const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
3225
+ if (!passesFilters(originalWord))
3226
+ continue;
3157
3227
  matches.add(originalWord);
3158
3228
  if (!actualMatches.has(originalWord)) {
3159
3229
  actualMatches.set(originalWord, []);
@@ -3173,7 +3243,12 @@ function findProfanity(text, options = {}) {
3173
3243
  });
3174
3244
  let match;
3175
3245
  while ((match = variantRegex.exec(text)) !== null) {
3246
+ const matchedText = match[0];
3247
+ if (normalizedWhitelist.includes(matchedText.toLowerCase()))
3248
+ continue;
3176
3249
  const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
3250
+ if (!passesFilters(originalWord))
3251
+ continue;
3177
3252
  matches.add(originalWord);
3178
3253
  if (!actualMatches.has(originalWord)) {
3179
3254
  actualMatches.set(originalWord, []);
@@ -3193,7 +3268,12 @@ function findProfanity(text, options = {}) {
3193
3268
  });
3194
3269
  let match;
3195
3270
  while ((match = splitRegex.exec(text)) !== null) {
3271
+ const matchedText = match[0];
3272
+ if (normalizedWhitelist.includes(matchedText.toLowerCase()))
3273
+ continue;
3196
3274
  const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
3275
+ if (!passesFilters(originalWord))
3276
+ continue;
3197
3277
  matches.add(originalWord);
3198
3278
  if (!actualMatches.has(originalWord)) {
3199
3279
  actualMatches.set(originalWord, []);
@@ -3206,7 +3286,11 @@ function findProfanity(text, options = {}) {
3206
3286
  if (useLevenshtein) {
3207
3287
  const possibleProfanity = findProfanityByLevenshteinDistance(text, wordsToCheck, similarityThreshold, maxLevenshteinDistance);
3208
3288
  possibleProfanity.forEach((item) => {
3289
+ if (normalizedWhitelist.includes(item.word.toLowerCase()))
3290
+ return;
3209
3291
  const originalWord = aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
3292
+ if (!passesFilters(originalWord))
3293
+ return;
3210
3294
  matches.add(originalWord);
3211
3295
  if (!actualMatches.has(originalWord)) {
3212
3296
  actualMatches.set(originalWord, []);
@@ -3217,8 +3301,12 @@ function findProfanity(text, options = {}) {
3217
3301
  else {
3218
3302
  const possibleProfanity = findPossibleProfanityBySimiliarity(text, wordsToCheck, similarityThreshold);
3219
3303
  possibleProfanity.forEach((item) => {
3220
- matches.add(item.original.toLowerCase());
3304
+ if (normalizedWhitelist.includes(item.word.toLowerCase()))
3305
+ return;
3221
3306
  const originalWord = aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
3307
+ if (!passesFilters(originalWord))
3308
+ return;
3309
+ matches.add(originalWord);
3222
3310
  if (!actualMatches.has(originalWord)) {
3223
3311
  actualMatches.set(originalWord, []);
3224
3312
  }
@@ -3302,8 +3390,6 @@ function calculateSeverity(matchDetails) {
3302
3390
  severitySum += wordSeverity;
3303
3391
  });
3304
3392
  const severityAvg = severitySum / matchDetails.length;
3305
- // Gabungkan jumlah kata dan keparahan rata-rata
3306
- // 70% keparahan kata + 30% faktor jumlah
3307
3393
  return 0.7 * severityAvg + 0.3 * countFactor;
3308
3394
  }
3309
3395
 
@@ -3482,6 +3568,9 @@ function isProfane(text, options = {}) {
3482
3568
  */
3483
3569
  function analyze(text, options = {}) {
3484
3570
  const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
3571
+ if (mergedOptions.whitelist) {
3572
+ mergedOptions.whitelist = mergedOptions.whitelist.map((w) => w.toLowerCase());
3573
+ }
3485
3574
  const matches = findProfanity(text, mergedOptions);
3486
3575
  if (matches.length === 0) {
3487
3576
  return {