@sideid/id-profanity-filter 1.11.12 → 1.12.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.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
 
@@ -2509,19 +2552,15 @@ function levenshteinDistance(str1, str2) {
2509
2552
  const s2 = str2.toLowerCase();
2510
2553
  const len1 = s1.length;
2511
2554
  const len2 = s2.length;
2512
- // Inisialisasi matrix
2513
2555
  const matrix = [];
2514
- // Inisialisasi baris pertama
2515
2556
  for (let i = 0; i <= len2; i++) {
2516
2557
  matrix[0] = matrix[0] || [];
2517
2558
  matrix[0][i] = i;
2518
2559
  }
2519
- // Inisialisasi kolom pertama
2520
2560
  for (let i = 0; i <= len1; i++) {
2521
2561
  matrix[i] = matrix[i] || [];
2522
2562
  matrix[i][0] = i;
2523
2563
  }
2524
- // Isi matrix
2525
2564
  for (let i = 1; i <= len1; i++) {
2526
2565
  for (let j = 1; j <= len2; j++) {
2527
2566
  const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
@@ -3104,6 +3143,10 @@ function initializeAhoCorasick(words) {
3104
3143
  globalAhoCorasick.build();
3105
3144
  ahoCorasickInitialized = true;
3106
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
+ }
3107
3150
  function findProfanity(text, options = {}) {
3108
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 };
3109
3152
  const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
@@ -3120,30 +3163,43 @@ function findProfanity(text, options = {}) {
3120
3163
  }
3121
3164
  const aliasMap = new Map();
3122
3165
  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
- }
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
+ });
3132
3177
  }
3133
3178
  });
3134
- 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()));
3135
3180
  if (wordsToCheck.length === 0) {
3136
3181
  return [];
3137
3182
  }
3138
3183
  const matches = new Set();
3139
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
+ };
3140
3196
  initializeAhoCorasick(wordsToCheck);
3141
3197
  const basicMatches = globalAhoCorasick.searchUnique(normalizedText);
3142
3198
  for (const match of basicMatches) {
3143
3199
  if (normalizedWhitelist.includes(match.toLowerCase()))
3144
3200
  continue;
3145
3201
  const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
3146
- if (normalizedWhitelist.includes(originalWord))
3202
+ if (!passesFilters(originalWord))
3147
3203
  continue;
3148
3204
  matches.add(originalWord);
3149
3205
  if (!actualMatches.has(originalWord)) {
@@ -3166,7 +3222,7 @@ function findProfanity(text, options = {}) {
3166
3222
  if (normalizedWhitelist.includes(matchedText.toLowerCase()))
3167
3223
  continue;
3168
3224
  const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
3169
- if (normalizedWhitelist.includes(originalWord))
3225
+ if (!passesFilters(originalWord))
3170
3226
  continue;
3171
3227
  matches.add(originalWord);
3172
3228
  if (!actualMatches.has(originalWord)) {
@@ -3191,7 +3247,7 @@ function findProfanity(text, options = {}) {
3191
3247
  if (normalizedWhitelist.includes(matchedText.toLowerCase()))
3192
3248
  continue;
3193
3249
  const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
3194
- if (normalizedWhitelist.includes(originalWord))
3250
+ if (!passesFilters(originalWord))
3195
3251
  continue;
3196
3252
  matches.add(originalWord);
3197
3253
  if (!actualMatches.has(originalWord)) {
@@ -3216,7 +3272,7 @@ function findProfanity(text, options = {}) {
3216
3272
  if (normalizedWhitelist.includes(matchedText.toLowerCase()))
3217
3273
  continue;
3218
3274
  const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
3219
- if (normalizedWhitelist.includes(originalWord))
3275
+ if (!passesFilters(originalWord))
3220
3276
  continue;
3221
3277
  matches.add(originalWord);
3222
3278
  if (!actualMatches.has(originalWord)) {
@@ -3230,11 +3286,10 @@ function findProfanity(text, options = {}) {
3230
3286
  if (useLevenshtein) {
3231
3287
  const possibleProfanity = findProfanityByLevenshteinDistance(text, wordsToCheck, similarityThreshold, maxLevenshteinDistance);
3232
3288
  possibleProfanity.forEach((item) => {
3233
- // Check if the matched word is in whitelist
3234
3289
  if (normalizedWhitelist.includes(item.word.toLowerCase()))
3235
3290
  return;
3236
3291
  const originalWord = aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
3237
- if (normalizedWhitelist.includes(originalWord))
3292
+ if (!passesFilters(originalWord))
3238
3293
  return;
3239
3294
  matches.add(originalWord);
3240
3295
  if (!actualMatches.has(originalWord)) {
@@ -3246,11 +3301,10 @@ function findProfanity(text, options = {}) {
3246
3301
  else {
3247
3302
  const possibleProfanity = findPossibleProfanityBySimiliarity(text, wordsToCheck, similarityThreshold);
3248
3303
  possibleProfanity.forEach((item) => {
3249
- // Check if the matched word is in whitelist
3250
3304
  if (normalizedWhitelist.includes(item.word.toLowerCase()))
3251
3305
  return;
3252
3306
  const originalWord = aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
3253
- if (normalizedWhitelist.includes(originalWord))
3307
+ if (!passesFilters(originalWord))
3254
3308
  return;
3255
3309
  matches.add(originalWord);
3256
3310
  if (!actualMatches.has(originalWord)) {
@@ -3336,8 +3390,6 @@ function calculateSeverity(matchDetails) {
3336
3390
  severitySum += wordSeverity;
3337
3391
  });
3338
3392
  const severityAvg = severitySum / matchDetails.length;
3339
- // Gabungkan jumlah kata dan keparahan rata-rata
3340
- // 70% keparahan kata + 30% faktor jumlah
3341
3393
  return 0.7 * severityAvg + 0.3 * countFactor;
3342
3394
  }
3343
3395