@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/README.md
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img alt="SideID - Profanity Filter" src="https://socialify.git.ci/SideeID/id-profanity-filter/image?custom_description=Library+JavaScript%2FTypeScript+untuk+mendeteksi%2C+menyensor%2C+dan+menganalisis+kata-kata+kotor+dalam+Indonesia+dan+daerah.&description=1&font=Inter&forks=1&language=1&name=1&owner=1&pattern=Circuit+Board&stargazers=1&theme=Auto">
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<div align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@sideid/id-profanity-filter">
|
|
9
|
+
<img src="https://img.shields.io/npm/v/@sideid/id-profanity-filter.svg" alt="NPM Version">
|
|
10
|
+
</a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/@sideid/id-profanity-filter">
|
|
12
|
+
<img src="https://img.shields.io/npm/dt/@sideid/id-profanity-filter?label=npm&color=%23CB3837" alt="NPM Downloads">
|
|
13
|
+
</a>
|
|
14
|
+
<a href="https://github.com/SideeID/id-profanity-filter">
|
|
15
|
+
<img src="https://img.shields.io/github/languages/code-size/SideeID/id-profanity-filter" alt="GitHub Code Size">
|
|
16
|
+
</a>
|
|
17
|
+
<a href="https://github.com/SideeID/id-profanity-filter">
|
|
18
|
+
<img src="https://img.shields.io/github/license/SideeID/id-profanity-filter" alt="GitHub License">
|
|
19
|
+
</a>
|
|
20
|
+
<a href="https://github.com/SideeID/id-profanity-filter">
|
|
21
|
+
<img src="https://img.shields.io/github/stars/SideeID/id-profanity-filter" alt="GitHub Stars">
|
|
22
|
+
</a>
|
|
23
|
+
<a href="https://github.com/SideeID/id-profanity-filter">
|
|
24
|
+
<img src="https://img.shields.io/github/forks/SideeID/id-profanity-filter" alt="GitHub Forks">
|
|
25
|
+
</a>
|
|
26
|
+
</div>
|
|
8
27
|
|
|
9
28
|
## Fitur Utama
|
|
10
29
|
|
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
|
|
|
@@ -2505,19 +2548,15 @@ function levenshteinDistance(str1, str2) {
|
|
|
2505
2548
|
const s2 = str2.toLowerCase();
|
|
2506
2549
|
const len1 = s1.length;
|
|
2507
2550
|
const len2 = s2.length;
|
|
2508
|
-
// Inisialisasi matrix
|
|
2509
2551
|
const matrix = [];
|
|
2510
|
-
// Inisialisasi baris pertama
|
|
2511
2552
|
for (let i = 0; i <= len2; i++) {
|
|
2512
2553
|
matrix[0] = matrix[0] || [];
|
|
2513
2554
|
matrix[0][i] = i;
|
|
2514
2555
|
}
|
|
2515
|
-
// Inisialisasi kolom pertama
|
|
2516
2556
|
for (let i = 0; i <= len1; i++) {
|
|
2517
2557
|
matrix[i] = matrix[i] || [];
|
|
2518
2558
|
matrix[i][0] = i;
|
|
2519
2559
|
}
|
|
2520
|
-
// Isi matrix
|
|
2521
2560
|
for (let i = 1; i <= len1; i++) {
|
|
2522
2561
|
for (let j = 1; j <= len2; j++) {
|
|
2523
2562
|
const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
|
|
@@ -3100,6 +3139,10 @@ function initializeAhoCorasick(words) {
|
|
|
3100
3139
|
globalAhoCorasick.build();
|
|
3101
3140
|
ahoCorasickInitialized = true;
|
|
3102
3141
|
}
|
|
3142
|
+
function getWordMetadata(word) {
|
|
3143
|
+
return wordObjects.find((obj) => obj.word.toLowerCase() === word.toLowerCase() ||
|
|
3144
|
+
(obj.aliases && obj.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
|
|
3145
|
+
}
|
|
3103
3146
|
function findProfanity(text, options = {}) {
|
|
3104
3147
|
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
3148
|
const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
|
|
@@ -3116,30 +3159,43 @@ function findProfanity(text, options = {}) {
|
|
|
3116
3159
|
}
|
|
3117
3160
|
const aliasMap = new Map();
|
|
3118
3161
|
wordObjects.forEach((wordObj) => {
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3162
|
+
const matchCategory = categories ? categories.includes(wordObj.category) : true;
|
|
3163
|
+
const matchRegion = regions ? regions.includes(wordObj.region) : true;
|
|
3164
|
+
const matchSeverity = wordObj.severity >= severityThreshold;
|
|
3165
|
+
if (matchCategory &&
|
|
3166
|
+
matchRegion &&
|
|
3167
|
+
matchSeverity &&
|
|
3168
|
+
wordObj.aliases &&
|
|
3169
|
+
wordObj.aliases.length > 0) {
|
|
3170
|
+
wordObj.aliases.forEach((alias) => {
|
|
3171
|
+
aliasMap.set(alias.toLowerCase(), wordObj.word.toLowerCase());
|
|
3172
|
+
});
|
|
3128
3173
|
}
|
|
3129
3174
|
});
|
|
3130
|
-
const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !
|
|
3175
|
+
const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !normalizedWhitelist.includes(word.toLowerCase()));
|
|
3131
3176
|
if (wordsToCheck.length === 0) {
|
|
3132
3177
|
return [];
|
|
3133
3178
|
}
|
|
3134
3179
|
const matches = new Set();
|
|
3135
3180
|
const actualMatches = new Map();
|
|
3181
|
+
const passesFilters = (word) => {
|
|
3182
|
+
if (normalizedWhitelist.includes(word.toLowerCase()))
|
|
3183
|
+
return false;
|
|
3184
|
+
const metadata = getWordMetadata(word);
|
|
3185
|
+
if (!metadata)
|
|
3186
|
+
return false;
|
|
3187
|
+
const matchCategory = categories ? categories.includes(metadata.category) : true;
|
|
3188
|
+
const matchRegion = regions ? regions.includes(metadata.region) : true;
|
|
3189
|
+
const matchSeverity = metadata.severity >= severityThreshold;
|
|
3190
|
+
return matchCategory && matchRegion && matchSeverity;
|
|
3191
|
+
};
|
|
3136
3192
|
initializeAhoCorasick(wordsToCheck);
|
|
3137
3193
|
const basicMatches = globalAhoCorasick.searchUnique(normalizedText);
|
|
3138
3194
|
for (const match of basicMatches) {
|
|
3139
3195
|
if (normalizedWhitelist.includes(match.toLowerCase()))
|
|
3140
3196
|
continue;
|
|
3141
3197
|
const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
|
|
3142
|
-
if (
|
|
3198
|
+
if (!passesFilters(originalWord))
|
|
3143
3199
|
continue;
|
|
3144
3200
|
matches.add(originalWord);
|
|
3145
3201
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -3162,7 +3218,7 @@ function findProfanity(text, options = {}) {
|
|
|
3162
3218
|
if (normalizedWhitelist.includes(matchedText.toLowerCase()))
|
|
3163
3219
|
continue;
|
|
3164
3220
|
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
3165
|
-
if (
|
|
3221
|
+
if (!passesFilters(originalWord))
|
|
3166
3222
|
continue;
|
|
3167
3223
|
matches.add(originalWord);
|
|
3168
3224
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -3187,7 +3243,7 @@ function findProfanity(text, options = {}) {
|
|
|
3187
3243
|
if (normalizedWhitelist.includes(matchedText.toLowerCase()))
|
|
3188
3244
|
continue;
|
|
3189
3245
|
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
3190
|
-
if (
|
|
3246
|
+
if (!passesFilters(originalWord))
|
|
3191
3247
|
continue;
|
|
3192
3248
|
matches.add(originalWord);
|
|
3193
3249
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -3212,7 +3268,7 @@ function findProfanity(text, options = {}) {
|
|
|
3212
3268
|
if (normalizedWhitelist.includes(matchedText.toLowerCase()))
|
|
3213
3269
|
continue;
|
|
3214
3270
|
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
3215
|
-
if (
|
|
3271
|
+
if (!passesFilters(originalWord))
|
|
3216
3272
|
continue;
|
|
3217
3273
|
matches.add(originalWord);
|
|
3218
3274
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -3226,11 +3282,10 @@ function findProfanity(text, options = {}) {
|
|
|
3226
3282
|
if (useLevenshtein) {
|
|
3227
3283
|
const possibleProfanity = findProfanityByLevenshteinDistance(text, wordsToCheck, similarityThreshold, maxLevenshteinDistance);
|
|
3228
3284
|
possibleProfanity.forEach((item) => {
|
|
3229
|
-
// Check if the matched word is in whitelist
|
|
3230
3285
|
if (normalizedWhitelist.includes(item.word.toLowerCase()))
|
|
3231
3286
|
return;
|
|
3232
3287
|
const originalWord = aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
|
|
3233
|
-
if (
|
|
3288
|
+
if (!passesFilters(originalWord))
|
|
3234
3289
|
return;
|
|
3235
3290
|
matches.add(originalWord);
|
|
3236
3291
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -3242,11 +3297,10 @@ function findProfanity(text, options = {}) {
|
|
|
3242
3297
|
else {
|
|
3243
3298
|
const possibleProfanity = findPossibleProfanityBySimiliarity(text, wordsToCheck, similarityThreshold);
|
|
3244
3299
|
possibleProfanity.forEach((item) => {
|
|
3245
|
-
// Check if the matched word is in whitelist
|
|
3246
3300
|
if (normalizedWhitelist.includes(item.word.toLowerCase()))
|
|
3247
3301
|
return;
|
|
3248
3302
|
const originalWord = aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
|
|
3249
|
-
if (
|
|
3303
|
+
if (!passesFilters(originalWord))
|
|
3250
3304
|
return;
|
|
3251
3305
|
matches.add(originalWord);
|
|
3252
3306
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -3332,8 +3386,6 @@ function calculateSeverity(matchDetails) {
|
|
|
3332
3386
|
severitySum += wordSeverity;
|
|
3333
3387
|
});
|
|
3334
3388
|
const severityAvg = severitySum / matchDetails.length;
|
|
3335
|
-
// Gabungkan jumlah kata dan keparahan rata-rata
|
|
3336
|
-
// 70% keparahan kata + 30% faktor jumlah
|
|
3337
3389
|
return 0.7 * severityAvg + 0.3 * countFactor;
|
|
3338
3390
|
}
|
|
3339
3391
|
|