@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 +101 -45
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +101 -45
- package/dist/index.js.map +1 -1
- package/jest.config.mjs +17 -1
- package/package.json +1 -1
- package/src/core/matcher.ts +46 -21
- package/src/utils/regexUtils.ts +80 -30
- package/test/analyzer.test.ts +89 -0
- package/test/filter.test.ts +80 -0
- package/test/jest.setup.ts +9 -0
- package/test/matcher.test.ts +120 -0
- package/test/profanity-filter.test.ts +130 -0
- package/test/utils/ahoCorasick.test.ts +106 -0
- package/test/utils/regexUtils.test.ts +111 -0
- package/test/utils/similarityUtils.test.ts +139 -0
- package/test/utils/stringUtils.test.ts +153 -0
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
|
-
//
|
|
2410
|
-
|
|
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
|
-
//
|
|
2421
|
-
const
|
|
2422
|
-
|
|
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', '
|
|
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
|
-
//
|
|
2443
|
-
|
|
2444
|
-
|
|
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
|
-
|
|
2471
|
+
// Create a character class with all variations
|
|
2472
|
+
result += `[${variations.join('')}]`;
|
|
2450
2473
|
}
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
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
|
|
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
|
-
|
|
2487
|
-
|
|
2488
|
-
const
|
|
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}${
|
|
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
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
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) => !
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|