@sideid/id-profanity-filter 1.11.11 → 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 +127 -38
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +127 -38
- package/dist/index.js.map +1 -1
- package/jest.config.mjs +17 -1
- package/package.json +1 -1
- package/src/core/analyzer.ts +4 -0
- package/src/core/matcher.ts +70 -14
- 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,8 +3147,13 @@ 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 };
|
|
3156
|
+
const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
|
|
3109
3157
|
const normalizedText = normalizeText(text);
|
|
3110
3158
|
let baseWordsToCheck = wordList.length > 0 ? wordList : [];
|
|
3111
3159
|
if (baseWordsToCheck.length === 0) {
|
|
@@ -3119,27 +3167,44 @@ function findProfanity(text, options = {}) {
|
|
|
3119
3167
|
}
|
|
3120
3168
|
const aliasMap = new Map();
|
|
3121
3169
|
wordObjects.forEach((wordObj) => {
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
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
|
+
});
|
|
3131
3181
|
}
|
|
3132
3182
|
});
|
|
3133
|
-
const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !
|
|
3183
|
+
const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !normalizedWhitelist.includes(word.toLowerCase()));
|
|
3134
3184
|
if (wordsToCheck.length === 0) {
|
|
3135
3185
|
return [];
|
|
3136
3186
|
}
|
|
3137
3187
|
const matches = new Set();
|
|
3138
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
|
+
};
|
|
3139
3200
|
initializeAhoCorasick(wordsToCheck);
|
|
3140
3201
|
const basicMatches = globalAhoCorasick.searchUnique(normalizedText);
|
|
3141
3202
|
for (const match of basicMatches) {
|
|
3203
|
+
if (normalizedWhitelist.includes(match.toLowerCase()))
|
|
3204
|
+
continue;
|
|
3142
3205
|
const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
|
|
3206
|
+
if (!passesFilters(originalWord))
|
|
3207
|
+
continue;
|
|
3143
3208
|
matches.add(originalWord);
|
|
3144
3209
|
if (!actualMatches.has(originalWord)) {
|
|
3145
3210
|
actualMatches.set(originalWord, []);
|
|
@@ -3157,7 +3222,12 @@ function findProfanity(text, options = {}) {
|
|
|
3157
3222
|
});
|
|
3158
3223
|
let match;
|
|
3159
3224
|
while ((match = leetRegex.exec(text)) !== null) {
|
|
3225
|
+
const matchedText = match[0];
|
|
3226
|
+
if (normalizedWhitelist.includes(matchedText.toLowerCase()))
|
|
3227
|
+
continue;
|
|
3160
3228
|
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
3229
|
+
if (!passesFilters(originalWord))
|
|
3230
|
+
continue;
|
|
3161
3231
|
matches.add(originalWord);
|
|
3162
3232
|
if (!actualMatches.has(originalWord)) {
|
|
3163
3233
|
actualMatches.set(originalWord, []);
|
|
@@ -3177,7 +3247,12 @@ function findProfanity(text, options = {}) {
|
|
|
3177
3247
|
});
|
|
3178
3248
|
let match;
|
|
3179
3249
|
while ((match = variantRegex.exec(text)) !== null) {
|
|
3250
|
+
const matchedText = match[0];
|
|
3251
|
+
if (normalizedWhitelist.includes(matchedText.toLowerCase()))
|
|
3252
|
+
continue;
|
|
3180
3253
|
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
3254
|
+
if (!passesFilters(originalWord))
|
|
3255
|
+
continue;
|
|
3181
3256
|
matches.add(originalWord);
|
|
3182
3257
|
if (!actualMatches.has(originalWord)) {
|
|
3183
3258
|
actualMatches.set(originalWord, []);
|
|
@@ -3197,7 +3272,12 @@ function findProfanity(text, options = {}) {
|
|
|
3197
3272
|
});
|
|
3198
3273
|
let match;
|
|
3199
3274
|
while ((match = splitRegex.exec(text)) !== null) {
|
|
3275
|
+
const matchedText = match[0];
|
|
3276
|
+
if (normalizedWhitelist.includes(matchedText.toLowerCase()))
|
|
3277
|
+
continue;
|
|
3200
3278
|
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
3279
|
+
if (!passesFilters(originalWord))
|
|
3280
|
+
continue;
|
|
3201
3281
|
matches.add(originalWord);
|
|
3202
3282
|
if (!actualMatches.has(originalWord)) {
|
|
3203
3283
|
actualMatches.set(originalWord, []);
|
|
@@ -3210,7 +3290,11 @@ function findProfanity(text, options = {}) {
|
|
|
3210
3290
|
if (useLevenshtein) {
|
|
3211
3291
|
const possibleProfanity = findProfanityByLevenshteinDistance(text, wordsToCheck, similarityThreshold, maxLevenshteinDistance);
|
|
3212
3292
|
possibleProfanity.forEach((item) => {
|
|
3293
|
+
if (normalizedWhitelist.includes(item.word.toLowerCase()))
|
|
3294
|
+
return;
|
|
3213
3295
|
const originalWord = aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
|
|
3296
|
+
if (!passesFilters(originalWord))
|
|
3297
|
+
return;
|
|
3214
3298
|
matches.add(originalWord);
|
|
3215
3299
|
if (!actualMatches.has(originalWord)) {
|
|
3216
3300
|
actualMatches.set(originalWord, []);
|
|
@@ -3221,8 +3305,12 @@ function findProfanity(text, options = {}) {
|
|
|
3221
3305
|
else {
|
|
3222
3306
|
const possibleProfanity = findPossibleProfanityBySimiliarity(text, wordsToCheck, similarityThreshold);
|
|
3223
3307
|
possibleProfanity.forEach((item) => {
|
|
3224
|
-
|
|
3308
|
+
if (normalizedWhitelist.includes(item.word.toLowerCase()))
|
|
3309
|
+
return;
|
|
3225
3310
|
const originalWord = aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
|
|
3311
|
+
if (!passesFilters(originalWord))
|
|
3312
|
+
return;
|
|
3313
|
+
matches.add(originalWord);
|
|
3226
3314
|
if (!actualMatches.has(originalWord)) {
|
|
3227
3315
|
actualMatches.set(originalWord, []);
|
|
3228
3316
|
}
|
|
@@ -3306,8 +3394,6 @@ function calculateSeverity(matchDetails) {
|
|
|
3306
3394
|
severitySum += wordSeverity;
|
|
3307
3395
|
});
|
|
3308
3396
|
const severityAvg = severitySum / matchDetails.length;
|
|
3309
|
-
// Gabungkan jumlah kata dan keparahan rata-rata
|
|
3310
|
-
// 70% keparahan kata + 30% faktor jumlah
|
|
3311
3397
|
return 0.7 * severityAvg + 0.3 * countFactor;
|
|
3312
3398
|
}
|
|
3313
3399
|
|
|
@@ -3486,6 +3572,9 @@ function isProfane(text, options = {}) {
|
|
|
3486
3572
|
*/
|
|
3487
3573
|
function analyze(text, options = {}) {
|
|
3488
3574
|
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
3575
|
+
if (mergedOptions.whitelist) {
|
|
3576
|
+
mergedOptions.whitelist = mergedOptions.whitelist.map((w) => w.toLowerCase());
|
|
3577
|
+
}
|
|
3489
3578
|
const matches = findProfanity(text, mergedOptions);
|
|
3490
3579
|
if (matches.length === 0) {
|
|
3491
3580
|
return {
|