@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.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,8 +3143,13 @@ 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 };
|
|
3152
|
+
const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
|
|
3105
3153
|
const normalizedText = normalizeText(text);
|
|
3106
3154
|
let baseWordsToCheck = wordList.length > 0 ? wordList : [];
|
|
3107
3155
|
if (baseWordsToCheck.length === 0) {
|
|
@@ -3115,27 +3163,44 @@ function findProfanity(text, options = {}) {
|
|
|
3115
3163
|
}
|
|
3116
3164
|
const aliasMap = new Map();
|
|
3117
3165
|
wordObjects.forEach((wordObj) => {
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
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
|
+
});
|
|
3127
3177
|
}
|
|
3128
3178
|
});
|
|
3129
|
-
const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !
|
|
3179
|
+
const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !normalizedWhitelist.includes(word.toLowerCase()));
|
|
3130
3180
|
if (wordsToCheck.length === 0) {
|
|
3131
3181
|
return [];
|
|
3132
3182
|
}
|
|
3133
3183
|
const matches = new Set();
|
|
3134
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
|
+
};
|
|
3135
3196
|
initializeAhoCorasick(wordsToCheck);
|
|
3136
3197
|
const basicMatches = globalAhoCorasick.searchUnique(normalizedText);
|
|
3137
3198
|
for (const match of basicMatches) {
|
|
3199
|
+
if (normalizedWhitelist.includes(match.toLowerCase()))
|
|
3200
|
+
continue;
|
|
3138
3201
|
const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
|
|
3202
|
+
if (!passesFilters(originalWord))
|
|
3203
|
+
continue;
|
|
3139
3204
|
matches.add(originalWord);
|
|
3140
3205
|
if (!actualMatches.has(originalWord)) {
|
|
3141
3206
|
actualMatches.set(originalWord, []);
|
|
@@ -3153,7 +3218,12 @@ function findProfanity(text, options = {}) {
|
|
|
3153
3218
|
});
|
|
3154
3219
|
let match;
|
|
3155
3220
|
while ((match = leetRegex.exec(text)) !== null) {
|
|
3221
|
+
const matchedText = match[0];
|
|
3222
|
+
if (normalizedWhitelist.includes(matchedText.toLowerCase()))
|
|
3223
|
+
continue;
|
|
3156
3224
|
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
3225
|
+
if (!passesFilters(originalWord))
|
|
3226
|
+
continue;
|
|
3157
3227
|
matches.add(originalWord);
|
|
3158
3228
|
if (!actualMatches.has(originalWord)) {
|
|
3159
3229
|
actualMatches.set(originalWord, []);
|
|
@@ -3173,7 +3243,12 @@ function findProfanity(text, options = {}) {
|
|
|
3173
3243
|
});
|
|
3174
3244
|
let match;
|
|
3175
3245
|
while ((match = variantRegex.exec(text)) !== null) {
|
|
3246
|
+
const matchedText = match[0];
|
|
3247
|
+
if (normalizedWhitelist.includes(matchedText.toLowerCase()))
|
|
3248
|
+
continue;
|
|
3176
3249
|
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
3250
|
+
if (!passesFilters(originalWord))
|
|
3251
|
+
continue;
|
|
3177
3252
|
matches.add(originalWord);
|
|
3178
3253
|
if (!actualMatches.has(originalWord)) {
|
|
3179
3254
|
actualMatches.set(originalWord, []);
|
|
@@ -3193,7 +3268,12 @@ function findProfanity(text, options = {}) {
|
|
|
3193
3268
|
});
|
|
3194
3269
|
let match;
|
|
3195
3270
|
while ((match = splitRegex.exec(text)) !== null) {
|
|
3271
|
+
const matchedText = match[0];
|
|
3272
|
+
if (normalizedWhitelist.includes(matchedText.toLowerCase()))
|
|
3273
|
+
continue;
|
|
3196
3274
|
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
3275
|
+
if (!passesFilters(originalWord))
|
|
3276
|
+
continue;
|
|
3197
3277
|
matches.add(originalWord);
|
|
3198
3278
|
if (!actualMatches.has(originalWord)) {
|
|
3199
3279
|
actualMatches.set(originalWord, []);
|
|
@@ -3206,7 +3286,11 @@ function findProfanity(text, options = {}) {
|
|
|
3206
3286
|
if (useLevenshtein) {
|
|
3207
3287
|
const possibleProfanity = findProfanityByLevenshteinDistance(text, wordsToCheck, similarityThreshold, maxLevenshteinDistance);
|
|
3208
3288
|
possibleProfanity.forEach((item) => {
|
|
3289
|
+
if (normalizedWhitelist.includes(item.word.toLowerCase()))
|
|
3290
|
+
return;
|
|
3209
3291
|
const originalWord = aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
|
|
3292
|
+
if (!passesFilters(originalWord))
|
|
3293
|
+
return;
|
|
3210
3294
|
matches.add(originalWord);
|
|
3211
3295
|
if (!actualMatches.has(originalWord)) {
|
|
3212
3296
|
actualMatches.set(originalWord, []);
|
|
@@ -3217,8 +3301,12 @@ function findProfanity(text, options = {}) {
|
|
|
3217
3301
|
else {
|
|
3218
3302
|
const possibleProfanity = findPossibleProfanityBySimiliarity(text, wordsToCheck, similarityThreshold);
|
|
3219
3303
|
possibleProfanity.forEach((item) => {
|
|
3220
|
-
|
|
3304
|
+
if (normalizedWhitelist.includes(item.word.toLowerCase()))
|
|
3305
|
+
return;
|
|
3221
3306
|
const originalWord = aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
|
|
3307
|
+
if (!passesFilters(originalWord))
|
|
3308
|
+
return;
|
|
3309
|
+
matches.add(originalWord);
|
|
3222
3310
|
if (!actualMatches.has(originalWord)) {
|
|
3223
3311
|
actualMatches.set(originalWord, []);
|
|
3224
3312
|
}
|
|
@@ -3302,8 +3390,6 @@ function calculateSeverity(matchDetails) {
|
|
|
3302
3390
|
severitySum += wordSeverity;
|
|
3303
3391
|
});
|
|
3304
3392
|
const severityAvg = severitySum / matchDetails.length;
|
|
3305
|
-
// Gabungkan jumlah kata dan keparahan rata-rata
|
|
3306
|
-
// 70% keparahan kata + 30% faktor jumlah
|
|
3307
3393
|
return 0.7 * severityAvg + 0.3 * countFactor;
|
|
3308
3394
|
}
|
|
3309
3395
|
|
|
@@ -3482,6 +3568,9 @@ function isProfane(text, options = {}) {
|
|
|
3482
3568
|
*/
|
|
3483
3569
|
function analyze(text, options = {}) {
|
|
3484
3570
|
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
3571
|
+
if (mergedOptions.whitelist) {
|
|
3572
|
+
mergedOptions.whitelist = mergedOptions.whitelist.map((w) => w.toLowerCase());
|
|
3573
|
+
}
|
|
3485
3574
|
const matches = findProfanity(text, mergedOptions);
|
|
3486
3575
|
if (matches.length === 0) {
|
|
3487
3576
|
return {
|