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