@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.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
|
-
//
|
|
2406
|
-
|
|
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
|
-
//
|
|
2417
|
-
const
|
|
2418
|
-
|
|
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', '
|
|
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
|
-
//
|
|
2439
|
-
|
|
2440
|
-
|
|
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
|
-
|
|
2467
|
+
// Create a character class with all variations
|
|
2468
|
+
result += `[${variations.join('')}]`;
|
|
2446
2469
|
}
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
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
|
|
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
|
-
|
|
2483
|
-
|
|
2484
|
-
const
|
|
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}${
|
|
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
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
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) => !
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|