@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 +26 -7
- package/dist/index.esm.js +101 -49
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +101 -49
- 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/src/utils/similarityUtils.ts +0 -4
- 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
|
|
|
@@ -2509,19 +2552,15 @@ function levenshteinDistance(str1, str2) {
|
|
|
2509
2552
|
const s2 = str2.toLowerCase();
|
|
2510
2553
|
const len1 = s1.length;
|
|
2511
2554
|
const len2 = s2.length;
|
|
2512
|
-
// Inisialisasi matrix
|
|
2513
2555
|
const matrix = [];
|
|
2514
|
-
// Inisialisasi baris pertama
|
|
2515
2556
|
for (let i = 0; i <= len2; i++) {
|
|
2516
2557
|
matrix[0] = matrix[0] || [];
|
|
2517
2558
|
matrix[0][i] = i;
|
|
2518
2559
|
}
|
|
2519
|
-
// Inisialisasi kolom pertama
|
|
2520
2560
|
for (let i = 0; i <= len1; i++) {
|
|
2521
2561
|
matrix[i] = matrix[i] || [];
|
|
2522
2562
|
matrix[i][0] = i;
|
|
2523
2563
|
}
|
|
2524
|
-
// Isi matrix
|
|
2525
2564
|
for (let i = 1; i <= len1; i++) {
|
|
2526
2565
|
for (let j = 1; j <= len2; j++) {
|
|
2527
2566
|
const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
|
|
@@ -3104,6 +3143,10 @@ function initializeAhoCorasick(words) {
|
|
|
3104
3143
|
globalAhoCorasick.build();
|
|
3105
3144
|
ahoCorasickInitialized = true;
|
|
3106
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
|
+
}
|
|
3107
3150
|
function findProfanity(text, options = {}) {
|
|
3108
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 };
|
|
3109
3152
|
const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
|
|
@@ -3120,30 +3163,43 @@ function findProfanity(text, options = {}) {
|
|
|
3120
3163
|
}
|
|
3121
3164
|
const aliasMap = new Map();
|
|
3122
3165
|
wordObjects.forEach((wordObj) => {
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
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
|
+
});
|
|
3132
3177
|
}
|
|
3133
3178
|
});
|
|
3134
|
-
const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !
|
|
3179
|
+
const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !normalizedWhitelist.includes(word.toLowerCase()));
|
|
3135
3180
|
if (wordsToCheck.length === 0) {
|
|
3136
3181
|
return [];
|
|
3137
3182
|
}
|
|
3138
3183
|
const matches = new Set();
|
|
3139
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
|
+
};
|
|
3140
3196
|
initializeAhoCorasick(wordsToCheck);
|
|
3141
3197
|
const basicMatches = globalAhoCorasick.searchUnique(normalizedText);
|
|
3142
3198
|
for (const match of basicMatches) {
|
|
3143
3199
|
if (normalizedWhitelist.includes(match.toLowerCase()))
|
|
3144
3200
|
continue;
|
|
3145
3201
|
const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
|
|
3146
|
-
if (
|
|
3202
|
+
if (!passesFilters(originalWord))
|
|
3147
3203
|
continue;
|
|
3148
3204
|
matches.add(originalWord);
|
|
3149
3205
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -3166,7 +3222,7 @@ function findProfanity(text, options = {}) {
|
|
|
3166
3222
|
if (normalizedWhitelist.includes(matchedText.toLowerCase()))
|
|
3167
3223
|
continue;
|
|
3168
3224
|
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
3169
|
-
if (
|
|
3225
|
+
if (!passesFilters(originalWord))
|
|
3170
3226
|
continue;
|
|
3171
3227
|
matches.add(originalWord);
|
|
3172
3228
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -3191,7 +3247,7 @@ function findProfanity(text, options = {}) {
|
|
|
3191
3247
|
if (normalizedWhitelist.includes(matchedText.toLowerCase()))
|
|
3192
3248
|
continue;
|
|
3193
3249
|
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
3194
|
-
if (
|
|
3250
|
+
if (!passesFilters(originalWord))
|
|
3195
3251
|
continue;
|
|
3196
3252
|
matches.add(originalWord);
|
|
3197
3253
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -3216,7 +3272,7 @@ function findProfanity(text, options = {}) {
|
|
|
3216
3272
|
if (normalizedWhitelist.includes(matchedText.toLowerCase()))
|
|
3217
3273
|
continue;
|
|
3218
3274
|
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
3219
|
-
if (
|
|
3275
|
+
if (!passesFilters(originalWord))
|
|
3220
3276
|
continue;
|
|
3221
3277
|
matches.add(originalWord);
|
|
3222
3278
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -3230,11 +3286,10 @@ function findProfanity(text, options = {}) {
|
|
|
3230
3286
|
if (useLevenshtein) {
|
|
3231
3287
|
const possibleProfanity = findProfanityByLevenshteinDistance(text, wordsToCheck, similarityThreshold, maxLevenshteinDistance);
|
|
3232
3288
|
possibleProfanity.forEach((item) => {
|
|
3233
|
-
// Check if the matched word is in whitelist
|
|
3234
3289
|
if (normalizedWhitelist.includes(item.word.toLowerCase()))
|
|
3235
3290
|
return;
|
|
3236
3291
|
const originalWord = aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
|
|
3237
|
-
if (
|
|
3292
|
+
if (!passesFilters(originalWord))
|
|
3238
3293
|
return;
|
|
3239
3294
|
matches.add(originalWord);
|
|
3240
3295
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -3246,11 +3301,10 @@ function findProfanity(text, options = {}) {
|
|
|
3246
3301
|
else {
|
|
3247
3302
|
const possibleProfanity = findPossibleProfanityBySimiliarity(text, wordsToCheck, similarityThreshold);
|
|
3248
3303
|
possibleProfanity.forEach((item) => {
|
|
3249
|
-
// Check if the matched word is in whitelist
|
|
3250
3304
|
if (normalizedWhitelist.includes(item.word.toLowerCase()))
|
|
3251
3305
|
return;
|
|
3252
3306
|
const originalWord = aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
|
|
3253
|
-
if (
|
|
3307
|
+
if (!passesFilters(originalWord))
|
|
3254
3308
|
return;
|
|
3255
3309
|
matches.add(originalWord);
|
|
3256
3310
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -3336,8 +3390,6 @@ function calculateSeverity(matchDetails) {
|
|
|
3336
3390
|
severitySum += wordSeverity;
|
|
3337
3391
|
});
|
|
3338
3392
|
const severityAvg = severitySum / matchDetails.length;
|
|
3339
|
-
// Gabungkan jumlah kata dan keparahan rata-rata
|
|
3340
|
-
// 70% keparahan kata + 30% faktor jumlah
|
|
3341
3393
|
return 0.7 * severityAvg + 0.3 * countFactor;
|
|
3342
3394
|
}
|
|
3343
3395
|
|