@sideid/id-profanity-filter 1.11.12 → 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,6 +3147,10 @@ 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 };
3109
3156
  const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
@@ -3120,30 +3167,43 @@ function findProfanity(text, options = {}) {
3120
3167
  }
3121
3168
  const aliasMap = new Map();
3122
3169
  wordObjects.forEach((wordObj) => {
3123
- if (wordObj.aliases && wordObj.aliases.length > 0) {
3124
- const matchCategory = categories ? categories.includes(wordObj.category) : true;
3125
- const matchRegion = regions ? regions.includes(wordObj.region) : true;
3126
- const matchSeverity = wordObj.severity >= severityThreshold;
3127
- if (matchCategory && matchRegion && matchSeverity) {
3128
- wordObj.aliases.forEach((alias) => {
3129
- aliasMap.set(alias.toLowerCase(), wordObj.word.toLowerCase());
3130
- });
3131
- }
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
+ });
3132
3181
  }
3133
3182
  });
3134
- 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()));
3135
3184
  if (wordsToCheck.length === 0) {
3136
3185
  return [];
3137
3186
  }
3138
3187
  const matches = new Set();
3139
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
+ };
3140
3200
  initializeAhoCorasick(wordsToCheck);
3141
3201
  const basicMatches = globalAhoCorasick.searchUnique(normalizedText);
3142
3202
  for (const match of basicMatches) {
3143
3203
  if (normalizedWhitelist.includes(match.toLowerCase()))
3144
3204
  continue;
3145
3205
  const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
3146
- if (normalizedWhitelist.includes(originalWord))
3206
+ if (!passesFilters(originalWord))
3147
3207
  continue;
3148
3208
  matches.add(originalWord);
3149
3209
  if (!actualMatches.has(originalWord)) {
@@ -3166,7 +3226,7 @@ function findProfanity(text, options = {}) {
3166
3226
  if (normalizedWhitelist.includes(matchedText.toLowerCase()))
3167
3227
  continue;
3168
3228
  const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
3169
- if (normalizedWhitelist.includes(originalWord))
3229
+ if (!passesFilters(originalWord))
3170
3230
  continue;
3171
3231
  matches.add(originalWord);
3172
3232
  if (!actualMatches.has(originalWord)) {
@@ -3191,7 +3251,7 @@ function findProfanity(text, options = {}) {
3191
3251
  if (normalizedWhitelist.includes(matchedText.toLowerCase()))
3192
3252
  continue;
3193
3253
  const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
3194
- if (normalizedWhitelist.includes(originalWord))
3254
+ if (!passesFilters(originalWord))
3195
3255
  continue;
3196
3256
  matches.add(originalWord);
3197
3257
  if (!actualMatches.has(originalWord)) {
@@ -3216,7 +3276,7 @@ function findProfanity(text, options = {}) {
3216
3276
  if (normalizedWhitelist.includes(matchedText.toLowerCase()))
3217
3277
  continue;
3218
3278
  const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
3219
- if (normalizedWhitelist.includes(originalWord))
3279
+ if (!passesFilters(originalWord))
3220
3280
  continue;
3221
3281
  matches.add(originalWord);
3222
3282
  if (!actualMatches.has(originalWord)) {
@@ -3230,11 +3290,10 @@ function findProfanity(text, options = {}) {
3230
3290
  if (useLevenshtein) {
3231
3291
  const possibleProfanity = findProfanityByLevenshteinDistance(text, wordsToCheck, similarityThreshold, maxLevenshteinDistance);
3232
3292
  possibleProfanity.forEach((item) => {
3233
- // Check if the matched word is in whitelist
3234
3293
  if (normalizedWhitelist.includes(item.word.toLowerCase()))
3235
3294
  return;
3236
3295
  const originalWord = aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
3237
- if (normalizedWhitelist.includes(originalWord))
3296
+ if (!passesFilters(originalWord))
3238
3297
  return;
3239
3298
  matches.add(originalWord);
3240
3299
  if (!actualMatches.has(originalWord)) {
@@ -3246,11 +3305,10 @@ function findProfanity(text, options = {}) {
3246
3305
  else {
3247
3306
  const possibleProfanity = findPossibleProfanityBySimiliarity(text, wordsToCheck, similarityThreshold);
3248
3307
  possibleProfanity.forEach((item) => {
3249
- // Check if the matched word is in whitelist
3250
3308
  if (normalizedWhitelist.includes(item.word.toLowerCase()))
3251
3309
  return;
3252
3310
  const originalWord = aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
3253
- if (normalizedWhitelist.includes(originalWord))
3311
+ if (!passesFilters(originalWord))
3254
3312
  return;
3255
3313
  matches.add(originalWord);
3256
3314
  if (!actualMatches.has(originalWord)) {
@@ -3336,8 +3394,6 @@ function calculateSeverity(matchDetails) {
3336
3394
  severitySum += wordSeverity;
3337
3395
  });
3338
3396
  const severityAvg = severitySum / matchDetails.length;
3339
- // Gabungkan jumlah kata dan keparahan rata-rata
3340
- // 70% keparahan kata + 30% faktor jumlah
3341
3397
  return 0.7 * severityAvg + 0.3 * countFactor;
3342
3398
  }
3343
3399