@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.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,6 +3143,10 @@ 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 };
3105
3152
  const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
@@ -3116,30 +3163,43 @@ function findProfanity(text, options = {}) {
3116
3163
  }
3117
3164
  const aliasMap = new Map();
3118
3165
  wordObjects.forEach((wordObj) => {
3119
- if (wordObj.aliases && wordObj.aliases.length > 0) {
3120
- const matchCategory = categories ? categories.includes(wordObj.category) : true;
3121
- const matchRegion = regions ? regions.includes(wordObj.region) : true;
3122
- const matchSeverity = wordObj.severity >= severityThreshold;
3123
- if (matchCategory && matchRegion && matchSeverity) {
3124
- wordObj.aliases.forEach((alias) => {
3125
- aliasMap.set(alias.toLowerCase(), wordObj.word.toLowerCase());
3126
- });
3127
- }
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
+ });
3128
3177
  }
3129
3178
  });
3130
- 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()));
3131
3180
  if (wordsToCheck.length === 0) {
3132
3181
  return [];
3133
3182
  }
3134
3183
  const matches = new Set();
3135
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
+ };
3136
3196
  initializeAhoCorasick(wordsToCheck);
3137
3197
  const basicMatches = globalAhoCorasick.searchUnique(normalizedText);
3138
3198
  for (const match of basicMatches) {
3139
3199
  if (normalizedWhitelist.includes(match.toLowerCase()))
3140
3200
  continue;
3141
3201
  const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
3142
- if (normalizedWhitelist.includes(originalWord))
3202
+ if (!passesFilters(originalWord))
3143
3203
  continue;
3144
3204
  matches.add(originalWord);
3145
3205
  if (!actualMatches.has(originalWord)) {
@@ -3162,7 +3222,7 @@ function findProfanity(text, options = {}) {
3162
3222
  if (normalizedWhitelist.includes(matchedText.toLowerCase()))
3163
3223
  continue;
3164
3224
  const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
3165
- if (normalizedWhitelist.includes(originalWord))
3225
+ if (!passesFilters(originalWord))
3166
3226
  continue;
3167
3227
  matches.add(originalWord);
3168
3228
  if (!actualMatches.has(originalWord)) {
@@ -3187,7 +3247,7 @@ function findProfanity(text, options = {}) {
3187
3247
  if (normalizedWhitelist.includes(matchedText.toLowerCase()))
3188
3248
  continue;
3189
3249
  const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
3190
- if (normalizedWhitelist.includes(originalWord))
3250
+ if (!passesFilters(originalWord))
3191
3251
  continue;
3192
3252
  matches.add(originalWord);
3193
3253
  if (!actualMatches.has(originalWord)) {
@@ -3212,7 +3272,7 @@ function findProfanity(text, options = {}) {
3212
3272
  if (normalizedWhitelist.includes(matchedText.toLowerCase()))
3213
3273
  continue;
3214
3274
  const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
3215
- if (normalizedWhitelist.includes(originalWord))
3275
+ if (!passesFilters(originalWord))
3216
3276
  continue;
3217
3277
  matches.add(originalWord);
3218
3278
  if (!actualMatches.has(originalWord)) {
@@ -3226,11 +3286,10 @@ function findProfanity(text, options = {}) {
3226
3286
  if (useLevenshtein) {
3227
3287
  const possibleProfanity = findProfanityByLevenshteinDistance(text, wordsToCheck, similarityThreshold, maxLevenshteinDistance);
3228
3288
  possibleProfanity.forEach((item) => {
3229
- // Check if the matched word is in whitelist
3230
3289
  if (normalizedWhitelist.includes(item.word.toLowerCase()))
3231
3290
  return;
3232
3291
  const originalWord = aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
3233
- if (normalizedWhitelist.includes(originalWord))
3292
+ if (!passesFilters(originalWord))
3234
3293
  return;
3235
3294
  matches.add(originalWord);
3236
3295
  if (!actualMatches.has(originalWord)) {
@@ -3242,11 +3301,10 @@ function findProfanity(text, options = {}) {
3242
3301
  else {
3243
3302
  const possibleProfanity = findPossibleProfanityBySimiliarity(text, wordsToCheck, similarityThreshold);
3244
3303
  possibleProfanity.forEach((item) => {
3245
- // Check if the matched word is in whitelist
3246
3304
  if (normalizedWhitelist.includes(item.word.toLowerCase()))
3247
3305
  return;
3248
3306
  const originalWord = aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
3249
- if (normalizedWhitelist.includes(originalWord))
3307
+ if (!passesFilters(originalWord))
3250
3308
  return;
3251
3309
  matches.add(originalWord);
3252
3310
  if (!actualMatches.has(originalWord)) {
@@ -3332,8 +3390,6 @@ function calculateSeverity(matchDetails) {
3332
3390
  severitySum += wordSeverity;
3333
3391
  });
3334
3392
  const severityAvg = severitySum / matchDetails.length;
3335
- // Gabungkan jumlah kata dan keparahan rata-rata
3336
- // 70% keparahan kata + 30% faktor jumlah
3337
3393
  return 0.7 * severityAvg + 0.3 * countFactor;
3338
3394
  }
3339
3395