@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/README.md CHANGED
@@ -1,10 +1,29 @@
1
- # ID-Profanity-Filter
2
-
3
- ![License](https://img.shields.io/npm/l/@sideid/id-profanity-filter)
4
- ![Version](https://img.shields.io/npm/v/@sideid/id-profanity-filter)
5
- ![Downloads](https://img.shields.io/npm/dt/@sideid/id-profanity-filter)
6
-
7
- Library JavaScript/TypeScript untuk mendeteksi, menyensor, dan menganalisis kata-kata kotor dalam Bahasa Indonesia dan bahasa daerah.
1
+ <div align="center">
2
+ <img alt="SideID - Profanity Filter" src="https://socialify.git.ci/SideeID/id-profanity-filter/image?custom_description=Library+JavaScript%2FTypeScript+untuk+mendeteksi%2C+menyensor%2C+dan+menganalisis+kata-kata+kotor+dalam+Indonesia+dan+daerah.&description=1&font=Inter&forks=1&language=1&name=1&owner=1&pattern=Circuit+Board&stargazers=1&theme=Auto">
3
+ </div>
4
+
5
+ ---
6
+
7
+ <div align="center">
8
+ <a href="https://www.npmjs.com/package/@sideid/id-profanity-filter">
9
+ <img src="https://img.shields.io/npm/v/@sideid/id-profanity-filter.svg" alt="NPM Version">
10
+ </a>
11
+ <a href="https://www.npmjs.com/package/@sideid/id-profanity-filter">
12
+ <img src="https://img.shields.io/npm/dt/@sideid/id-profanity-filter?label=npm&color=%23CB3837" alt="NPM Downloads">
13
+ </a>
14
+ <a href="https://github.com/SideeID/id-profanity-filter">
15
+ <img src="https://img.shields.io/github/languages/code-size/SideeID/id-profanity-filter" alt="GitHub Code Size">
16
+ </a>
17
+ <a href="https://github.com/SideeID/id-profanity-filter">
18
+ <img src="https://img.shields.io/github/license/SideeID/id-profanity-filter" alt="GitHub License">
19
+ </a>
20
+ <a href="https://github.com/SideeID/id-profanity-filter">
21
+ <img src="https://img.shields.io/github/stars/SideeID/id-profanity-filter" alt="GitHub Stars">
22
+ </a>
23
+ <a href="https://github.com/SideeID/id-profanity-filter">
24
+ <img src="https://img.shields.io/github/forks/SideeID/id-profanity-filter" alt="GitHub Forks">
25
+ </a>
26
+ </div>
8
27
 
9
28
  ## Fitur Utama
10
29
 
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
 
@@ -2505,19 +2548,15 @@ function levenshteinDistance(str1, str2) {
2505
2548
  const s2 = str2.toLowerCase();
2506
2549
  const len1 = s1.length;
2507
2550
  const len2 = s2.length;
2508
- // Inisialisasi matrix
2509
2551
  const matrix = [];
2510
- // Inisialisasi baris pertama
2511
2552
  for (let i = 0; i <= len2; i++) {
2512
2553
  matrix[0] = matrix[0] || [];
2513
2554
  matrix[0][i] = i;
2514
2555
  }
2515
- // Inisialisasi kolom pertama
2516
2556
  for (let i = 0; i <= len1; i++) {
2517
2557
  matrix[i] = matrix[i] || [];
2518
2558
  matrix[i][0] = i;
2519
2559
  }
2520
- // Isi matrix
2521
2560
  for (let i = 1; i <= len1; i++) {
2522
2561
  for (let j = 1; j <= len2; j++) {
2523
2562
  const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
@@ -3100,6 +3139,10 @@ function initializeAhoCorasick(words) {
3100
3139
  globalAhoCorasick.build();
3101
3140
  ahoCorasickInitialized = true;
3102
3141
  }
3142
+ function getWordMetadata(word) {
3143
+ return wordObjects.find((obj) => obj.word.toLowerCase() === word.toLowerCase() ||
3144
+ (obj.aliases && obj.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
3145
+ }
3103
3146
  function findProfanity(text, options = {}) {
3104
3147
  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
3148
  const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
@@ -3116,30 +3159,43 @@ function findProfanity(text, options = {}) {
3116
3159
  }
3117
3160
  const aliasMap = new Map();
3118
3161
  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
- }
3162
+ const matchCategory = categories ? categories.includes(wordObj.category) : true;
3163
+ const matchRegion = regions ? regions.includes(wordObj.region) : true;
3164
+ const matchSeverity = wordObj.severity >= severityThreshold;
3165
+ if (matchCategory &&
3166
+ matchRegion &&
3167
+ matchSeverity &&
3168
+ wordObj.aliases &&
3169
+ wordObj.aliases.length > 0) {
3170
+ wordObj.aliases.forEach((alias) => {
3171
+ aliasMap.set(alias.toLowerCase(), wordObj.word.toLowerCase());
3172
+ });
3128
3173
  }
3129
3174
  });
3130
- const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !whitelist.includes(word.toLowerCase()));
3175
+ const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !normalizedWhitelist.includes(word.toLowerCase()));
3131
3176
  if (wordsToCheck.length === 0) {
3132
3177
  return [];
3133
3178
  }
3134
3179
  const matches = new Set();
3135
3180
  const actualMatches = new Map();
3181
+ const passesFilters = (word) => {
3182
+ if (normalizedWhitelist.includes(word.toLowerCase()))
3183
+ return false;
3184
+ const metadata = getWordMetadata(word);
3185
+ if (!metadata)
3186
+ return false;
3187
+ const matchCategory = categories ? categories.includes(metadata.category) : true;
3188
+ const matchRegion = regions ? regions.includes(metadata.region) : true;
3189
+ const matchSeverity = metadata.severity >= severityThreshold;
3190
+ return matchCategory && matchRegion && matchSeverity;
3191
+ };
3136
3192
  initializeAhoCorasick(wordsToCheck);
3137
3193
  const basicMatches = globalAhoCorasick.searchUnique(normalizedText);
3138
3194
  for (const match of basicMatches) {
3139
3195
  if (normalizedWhitelist.includes(match.toLowerCase()))
3140
3196
  continue;
3141
3197
  const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
3142
- if (normalizedWhitelist.includes(originalWord))
3198
+ if (!passesFilters(originalWord))
3143
3199
  continue;
3144
3200
  matches.add(originalWord);
3145
3201
  if (!actualMatches.has(originalWord)) {
@@ -3162,7 +3218,7 @@ function findProfanity(text, options = {}) {
3162
3218
  if (normalizedWhitelist.includes(matchedText.toLowerCase()))
3163
3219
  continue;
3164
3220
  const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
3165
- if (normalizedWhitelist.includes(originalWord))
3221
+ if (!passesFilters(originalWord))
3166
3222
  continue;
3167
3223
  matches.add(originalWord);
3168
3224
  if (!actualMatches.has(originalWord)) {
@@ -3187,7 +3243,7 @@ function findProfanity(text, options = {}) {
3187
3243
  if (normalizedWhitelist.includes(matchedText.toLowerCase()))
3188
3244
  continue;
3189
3245
  const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
3190
- if (normalizedWhitelist.includes(originalWord))
3246
+ if (!passesFilters(originalWord))
3191
3247
  continue;
3192
3248
  matches.add(originalWord);
3193
3249
  if (!actualMatches.has(originalWord)) {
@@ -3212,7 +3268,7 @@ function findProfanity(text, options = {}) {
3212
3268
  if (normalizedWhitelist.includes(matchedText.toLowerCase()))
3213
3269
  continue;
3214
3270
  const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
3215
- if (normalizedWhitelist.includes(originalWord))
3271
+ if (!passesFilters(originalWord))
3216
3272
  continue;
3217
3273
  matches.add(originalWord);
3218
3274
  if (!actualMatches.has(originalWord)) {
@@ -3226,11 +3282,10 @@ function findProfanity(text, options = {}) {
3226
3282
  if (useLevenshtein) {
3227
3283
  const possibleProfanity = findProfanityByLevenshteinDistance(text, wordsToCheck, similarityThreshold, maxLevenshteinDistance);
3228
3284
  possibleProfanity.forEach((item) => {
3229
- // Check if the matched word is in whitelist
3230
3285
  if (normalizedWhitelist.includes(item.word.toLowerCase()))
3231
3286
  return;
3232
3287
  const originalWord = aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
3233
- if (normalizedWhitelist.includes(originalWord))
3288
+ if (!passesFilters(originalWord))
3234
3289
  return;
3235
3290
  matches.add(originalWord);
3236
3291
  if (!actualMatches.has(originalWord)) {
@@ -3242,11 +3297,10 @@ function findProfanity(text, options = {}) {
3242
3297
  else {
3243
3298
  const possibleProfanity = findPossibleProfanityBySimiliarity(text, wordsToCheck, similarityThreshold);
3244
3299
  possibleProfanity.forEach((item) => {
3245
- // Check if the matched word is in whitelist
3246
3300
  if (normalizedWhitelist.includes(item.word.toLowerCase()))
3247
3301
  return;
3248
3302
  const originalWord = aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
3249
- if (normalizedWhitelist.includes(originalWord))
3303
+ if (!passesFilters(originalWord))
3250
3304
  return;
3251
3305
  matches.add(originalWord);
3252
3306
  if (!actualMatches.has(originalWord)) {
@@ -3332,8 +3386,6 @@ function calculateSeverity(matchDetails) {
3332
3386
  severitySum += wordSeverity;
3333
3387
  });
3334
3388
  const severityAvg = severitySum / matchDetails.length;
3335
- // Gabungkan jumlah kata dan keparahan rata-rata
3336
- // 70% keparahan kata + 30% faktor jumlah
3337
3389
  return 0.7 * severityAvg + 0.3 * countFactor;
3338
3390
  }
3339
3391