@sideid/id-profanity-filter 1.11.13 → 1.13.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/CONTRIBUTING.md +91 -129
- package/README.md +172 -447
- package/dist/index.d.ts +11 -6
- package/dist/index.esm.js +161 -180
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +161 -180
- package/dist/index.js.map +1 -1
- package/dist/types/core/matcher.d.ts +5 -5
- package/dist/types/index.d.ts +5 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/utils/ahoCorasick.d.ts +10 -0
- package/eslint.config.mjs +1 -0
- package/examples/advanced.ts +4 -1
- package/examples/custom-list.ts +3 -3
- package/package.json +9 -2
- package/rollup.config.mjs +5 -1
- package/src/constants/categories/sexual.ts +1 -1
- package/src/constants/regions/general.ts +3 -3
- package/src/constants/wordList.ts +0 -27
- package/src/core/filter.ts +58 -178
- package/src/core/matcher.ts +69 -43
- package/src/index.ts +8 -0
- package/src/types/index.ts +1 -11
- package/src/utils/ahoCorasick.ts +37 -0
- package/src/utils/regexUtils.ts +1 -2
- package/src/utils/similarityUtils.ts +0 -4
- package/test/matcher.test.ts +18 -0
- package/test/profanity-filter.test.ts +15 -0
- package/.eslintrc.js +0 -44
- package/src/constants/regions/ambon.ts +0 -0
- package/src/constants/regions/banjar.ts +0 -0
- package/src/constants/regions/bugis.ts +0 -0
- package/src/constants/regions/lampung.ts +0 -0
- package/src/constants/regions/manado.ts +0 -0
- package/src/constants/regions/ntb.ts +0 -0
- package/src/constants/regions/ntt.ts +0 -0
- package/src/constants/regions/palembang.ts +0 -0
- package/src/constants/regions/papua.ts +0 -0
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ const general = [
|
|
|
8
8
|
category: 'profanity',
|
|
9
9
|
region: 'general',
|
|
10
10
|
severity: 0.7,
|
|
11
|
-
aliases: ['anjay', 'anjir', 'anying', 'njing', 'anj', 'anjg', 'ajg'],
|
|
11
|
+
aliases: ['anjay', 'anjir', 'anying', 'njing', 'anj', 'anjg', 'ajg', 'anjic', 'anjink'],
|
|
12
12
|
description: 'Mengacu pada hewan anjing, digunakan sebagai umpatan',
|
|
13
13
|
context: 'Umpatan umum untuk menunjukkan kemarahan atau ketidaksetujuan',
|
|
14
14
|
},
|
|
@@ -116,7 +116,7 @@ const general = [
|
|
|
116
116
|
category: 'insult',
|
|
117
117
|
region: 'general',
|
|
118
118
|
severity: 0.5,
|
|
119
|
-
aliases: ['sialn'
|
|
119
|
+
aliases: ['sialn'],
|
|
120
120
|
description: 'Kata yang mengacu pada orang yang membawa sial',
|
|
121
121
|
context: 'Hinaan untuk menyebut orang yang dianggap membawa sial',
|
|
122
122
|
},
|
|
@@ -179,7 +179,7 @@ const general = [
|
|
|
179
179
|
category: 'sexual',
|
|
180
180
|
region: 'general',
|
|
181
181
|
severity: 0.9,
|
|
182
|
-
aliases: ['ngew'
|
|
182
|
+
aliases: ['ngew'],
|
|
183
183
|
description: 'Istilah kasar untuk aktivitas seksual',
|
|
184
184
|
context: 'Kata vulgar yang merujuk pada aktivitas seksual',
|
|
185
185
|
},
|
|
@@ -1611,7 +1611,7 @@ const sexual = [
|
|
|
1611
1611
|
category: 'sexual',
|
|
1612
1612
|
region: 'general',
|
|
1613
1613
|
severity: 0.9,
|
|
1614
|
-
aliases: ['ngew'
|
|
1614
|
+
aliases: ['ngew'],
|
|
1615
1615
|
description: 'Istilah kasar untuk aktivitas seksual',
|
|
1616
1616
|
context: 'Kata vulgar yang merujuk pada aktivitas seksual',
|
|
1617
1617
|
},
|
|
@@ -2143,16 +2143,7 @@ const wordObjects = [
|
|
|
2143
2143
|
...minang,
|
|
2144
2144
|
...bali,
|
|
2145
2145
|
...madura,
|
|
2146
|
-
// ...bugis,
|
|
2147
2146
|
...aceh,
|
|
2148
|
-
// ...ambon,
|
|
2149
|
-
// ...papua,
|
|
2150
|
-
// ...manado,
|
|
2151
|
-
// ...banjar,
|
|
2152
|
-
// ...palembang,
|
|
2153
|
-
// ...lampung,
|
|
2154
|
-
// ...ntt,
|
|
2155
|
-
// ...ntb,
|
|
2156
2147
|
];
|
|
2157
2148
|
wordObjects.map((item) => item.word);
|
|
2158
2149
|
/**
|
|
@@ -2468,8 +2459,7 @@ function addIndonesianVariations(pattern) {
|
|
|
2468
2459
|
const lowerChar = char.toLowerCase();
|
|
2469
2460
|
const variations = variationMap[lowerChar];
|
|
2470
2461
|
if (variations && variations.length > 1) {
|
|
2471
|
-
|
|
2472
|
-
result += `[${variations.join('')}]`;
|
|
2462
|
+
result += `(?:${variations.join('|')})`;
|
|
2473
2463
|
}
|
|
2474
2464
|
else {
|
|
2475
2465
|
result += char;
|
|
@@ -2552,19 +2542,15 @@ function levenshteinDistance(str1, str2) {
|
|
|
2552
2542
|
const s2 = str2.toLowerCase();
|
|
2553
2543
|
const len1 = s1.length;
|
|
2554
2544
|
const len2 = s2.length;
|
|
2555
|
-
// Inisialisasi matrix
|
|
2556
2545
|
const matrix = [];
|
|
2557
|
-
// Inisialisasi baris pertama
|
|
2558
2546
|
for (let i = 0; i <= len2; i++) {
|
|
2559
2547
|
matrix[0] = matrix[0] || [];
|
|
2560
2548
|
matrix[0][i] = i;
|
|
2561
2549
|
}
|
|
2562
|
-
// Inisialisasi kolom pertama
|
|
2563
2550
|
for (let i = 0; i <= len1; i++) {
|
|
2564
2551
|
matrix[i] = matrix[i] || [];
|
|
2565
2552
|
matrix[i][0] = i;
|
|
2566
2553
|
}
|
|
2567
|
-
// Isi matrix
|
|
2568
2554
|
for (let i = 1; i <= len1; i++) {
|
|
2569
2555
|
for (let j = 1; j <= len2; j++) {
|
|
2570
2556
|
const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
|
|
@@ -3109,6 +3095,36 @@ class AhoCorasick {
|
|
|
3109
3095
|
const matches = this.search(text);
|
|
3110
3096
|
return new Set(matches.keys());
|
|
3111
3097
|
}
|
|
3098
|
+
/**
|
|
3099
|
+
* Mencari semua kemunculan pola beserta posisi indeks awal dan akhirnya
|
|
3100
|
+
* @param text Teks yang akan dicari
|
|
3101
|
+
* @returns Array objek berisi pattern, start, dan end
|
|
3102
|
+
*/
|
|
3103
|
+
searchWithPositions(text) {
|
|
3104
|
+
if (!this.built) {
|
|
3105
|
+
this.build();
|
|
3106
|
+
}
|
|
3107
|
+
const results = [];
|
|
3108
|
+
const normalizedText = text.toLowerCase();
|
|
3109
|
+
let node = this.root;
|
|
3110
|
+
for (let i = 0; i < normalizedText.length; i++) {
|
|
3111
|
+
const char = normalizedText[i];
|
|
3112
|
+
while (node !== this.root && !node.children.has(char)) {
|
|
3113
|
+
node = node.fail;
|
|
3114
|
+
}
|
|
3115
|
+
if (node.children.has(char)) {
|
|
3116
|
+
node = node.children.get(char);
|
|
3117
|
+
}
|
|
3118
|
+
for (const match of node.output) {
|
|
3119
|
+
results.push({
|
|
3120
|
+
pattern: match,
|
|
3121
|
+
start: i + 1 - match.length,
|
|
3122
|
+
end: i + 1,
|
|
3123
|
+
});
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
return results;
|
|
3127
|
+
}
|
|
3112
3128
|
/**
|
|
3113
3129
|
* Mengecek apakah teks mengandung setidaknya satu pola
|
|
3114
3130
|
* @param text Teks yang akan dicari
|
|
@@ -3136,16 +3152,22 @@ class AhoCorasick {
|
|
|
3136
3152
|
}
|
|
3137
3153
|
}
|
|
3138
3154
|
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3155
|
+
let defaultAhoCorasick = null;
|
|
3156
|
+
function getDefaultAhoCorasick() {
|
|
3157
|
+
if (!defaultAhoCorasick) {
|
|
3158
|
+
const ac = new AhoCorasick();
|
|
3159
|
+
for (const wordObj of wordObjects) {
|
|
3160
|
+
ac.addPattern(wordObj.word);
|
|
3161
|
+
if (wordObj.aliases) {
|
|
3162
|
+
for (const alias of wordObj.aliases) {
|
|
3163
|
+
ac.addPattern(alias);
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
ac.build();
|
|
3168
|
+
defaultAhoCorasick = ac;
|
|
3146
3169
|
}
|
|
3147
|
-
|
|
3148
|
-
ahoCorasickInitialized = true;
|
|
3170
|
+
return defaultAhoCorasick;
|
|
3149
3171
|
}
|
|
3150
3172
|
function getWordMetadata(word) {
|
|
3151
3173
|
return wordObjects.find((obj) => obj.word.toLowerCase() === word.toLowerCase() ||
|
|
@@ -3153,10 +3175,15 @@ function getWordMetadata(word) {
|
|
|
3153
3175
|
}
|
|
3154
3176
|
function findProfanity(text, options = {}) {
|
|
3155
3177
|
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 };
|
|
3178
|
+
const hasCustomWordList = Boolean(wordList && wordList.length > 0);
|
|
3156
3179
|
const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
|
|
3157
3180
|
const normalizedText = normalizeText(text);
|
|
3158
|
-
let baseWordsToCheck =
|
|
3159
|
-
|
|
3181
|
+
let baseWordsToCheck = [];
|
|
3182
|
+
const aliasMap = new Map();
|
|
3183
|
+
if (hasCustomWordList) {
|
|
3184
|
+
baseWordsToCheck = wordList;
|
|
3185
|
+
}
|
|
3186
|
+
else {
|
|
3160
3187
|
const filteredWords = wordObjects.filter((word) => {
|
|
3161
3188
|
const matchCategory = categories ? categories.includes(word.category) : true;
|
|
3162
3189
|
const matchRegion = regions ? regions.includes(word.region) : true;
|
|
@@ -3164,22 +3191,14 @@ function findProfanity(text, options = {}) {
|
|
|
3164
3191
|
return matchCategory && matchRegion && matchSeverity;
|
|
3165
3192
|
});
|
|
3166
3193
|
baseWordsToCheck = filteredWords.map((word) => word.word);
|
|
3194
|
+
filteredWords.forEach((wordObj) => {
|
|
3195
|
+
if (wordObj.aliases && wordObj.aliases.length > 0) {
|
|
3196
|
+
wordObj.aliases.forEach((alias) => {
|
|
3197
|
+
aliasMap.set(alias.toLowerCase(), wordObj.word.toLowerCase());
|
|
3198
|
+
});
|
|
3199
|
+
}
|
|
3200
|
+
});
|
|
3167
3201
|
}
|
|
3168
|
-
const aliasMap = new Map();
|
|
3169
|
-
wordObjects.forEach((wordObj) => {
|
|
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
|
-
});
|
|
3181
|
-
}
|
|
3182
|
-
});
|
|
3183
3202
|
const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !normalizedWhitelist.includes(word.toLowerCase()));
|
|
3184
3203
|
if (wordsToCheck.length === 0) {
|
|
3185
3204
|
return [];
|
|
@@ -3189,6 +3208,8 @@ function findProfanity(text, options = {}) {
|
|
|
3189
3208
|
const passesFilters = (word) => {
|
|
3190
3209
|
if (normalizedWhitelist.includes(word.toLowerCase()))
|
|
3191
3210
|
return false;
|
|
3211
|
+
if (hasCustomWordList)
|
|
3212
|
+
return true;
|
|
3192
3213
|
const metadata = getWordMetadata(word);
|
|
3193
3214
|
if (!metadata)
|
|
3194
3215
|
return false;
|
|
@@ -3197,9 +3218,27 @@ function findProfanity(text, options = {}) {
|
|
|
3197
3218
|
const matchSeverity = metadata.severity >= severityThreshold;
|
|
3198
3219
|
return matchCategory && matchRegion && matchSeverity;
|
|
3199
3220
|
};
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3221
|
+
let ac;
|
|
3222
|
+
if (hasCustomWordList) {
|
|
3223
|
+
ac = new AhoCorasick();
|
|
3224
|
+
for (const w of wordsToCheck) {
|
|
3225
|
+
ac.addPattern(w);
|
|
3226
|
+
}
|
|
3227
|
+
ac.build();
|
|
3228
|
+
}
|
|
3229
|
+
else {
|
|
3230
|
+
ac = getDefaultAhoCorasick();
|
|
3231
|
+
}
|
|
3232
|
+
const occurrences = ac.searchWithPositions(normalizedText);
|
|
3233
|
+
for (const occ of occurrences) {
|
|
3234
|
+
const { pattern: match, start, end } = occ;
|
|
3235
|
+
if (!checkSubstring) {
|
|
3236
|
+
const isWordStart = start === 0 || !/[a-z0-9_]/i.test(normalizedText[start - 1]);
|
|
3237
|
+
const isWordEnd = end === normalizedText.length || !/[a-z0-9_]/i.test(normalizedText[end]);
|
|
3238
|
+
if (!isWordStart || !isWordEnd) {
|
|
3239
|
+
continue;
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3203
3242
|
if (normalizedWhitelist.includes(match.toLowerCase()))
|
|
3204
3243
|
continue;
|
|
3205
3244
|
const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
|
|
@@ -3324,8 +3363,8 @@ function findProfanity(text, options = {}) {
|
|
|
3324
3363
|
/**
|
|
3325
3364
|
* Mencari kata kotor lengkap dengan metadata
|
|
3326
3365
|
*
|
|
3327
|
-
* @param text Teks yang akan
|
|
3328
|
-
* @param options Opsi
|
|
3366
|
+
* @param text Teks yang akan diperiksa
|
|
3367
|
+
* @param options Opsi untuk pencarian kata kotor
|
|
3329
3368
|
* @return Array dari objek kata kotor yang ditemukan
|
|
3330
3369
|
*/
|
|
3331
3370
|
function findProfanityWithMetadata(text, options = {}) {
|
|
@@ -3337,14 +3376,22 @@ function findProfanityWithMetadata(text, options = {}) {
|
|
|
3337
3376
|
.map((word) => {
|
|
3338
3377
|
const wordObject = wordObjects.find((obj) => obj.word.toLowerCase() === word.toLowerCase() ||
|
|
3339
3378
|
(obj.aliases && obj.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
|
|
3340
|
-
|
|
3379
|
+
if (wordObject) {
|
|
3380
|
+
return wordObject;
|
|
3381
|
+
}
|
|
3382
|
+
return {
|
|
3383
|
+
word,
|
|
3384
|
+
category: 'profanity',
|
|
3385
|
+
region: 'general',
|
|
3386
|
+
severity: 0.5,
|
|
3387
|
+
};
|
|
3341
3388
|
})
|
|
3342
3389
|
.filter((word) => word !== undefined);
|
|
3343
3390
|
}
|
|
3344
3391
|
/**
|
|
3345
|
-
* Mencari
|
|
3392
|
+
* Mencari kategori kata kotor yang ada dalam teks
|
|
3346
3393
|
*
|
|
3347
|
-
* @param matchDetails Hasil pencarian dari
|
|
3394
|
+
* @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
|
|
3348
3395
|
* @return Array kategori unik
|
|
3349
3396
|
*/
|
|
3350
3397
|
function findCategories(matchDetails) {
|
|
@@ -3357,7 +3404,7 @@ function findCategories(matchDetails) {
|
|
|
3357
3404
|
/**
|
|
3358
3405
|
* Mencari region kata kotor yang ada dalam teks
|
|
3359
3406
|
*
|
|
3360
|
-
* @param matchDetails Hasil pencarian dari
|
|
3407
|
+
* @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
|
|
3361
3408
|
* @return Array region unik
|
|
3362
3409
|
*/
|
|
3363
3410
|
function findRegions(matchDetails) {
|
|
@@ -3377,7 +3424,7 @@ function calculateSeverity(matchDetails) {
|
|
|
3377
3424
|
if (matchDetails.length === 0) {
|
|
3378
3425
|
return 0;
|
|
3379
3426
|
}
|
|
3380
|
-
const countFactor = Math.min(matchDetails.length / 10, 1);
|
|
3427
|
+
const countFactor = Math.min(matchDetails.length / 10, 1);
|
|
3381
3428
|
const categoryWeights = {
|
|
3382
3429
|
sexual: 0.9,
|
|
3383
3430
|
blasphemy: 0.9,
|
|
@@ -3405,21 +3452,9 @@ function calculateSeverity(matchDetails) {
|
|
|
3405
3452
|
* @returns FilterResult dengan hasil filter
|
|
3406
3453
|
*/
|
|
3407
3454
|
function filter(text, options = {}) {
|
|
3408
|
-
const
|
|
3409
|
-
const
|
|
3410
|
-
|
|
3411
|
-
detectLeetSpeak,
|
|
3412
|
-
whitelist,
|
|
3413
|
-
checkSubstring,
|
|
3414
|
-
indonesianVariation,
|
|
3415
|
-
detectSplit,
|
|
3416
|
-
detectSimilarity,
|
|
3417
|
-
useLevenshtein,
|
|
3418
|
-
maxLevenshteinDistance,
|
|
3419
|
-
similarityThreshold,
|
|
3420
|
-
});
|
|
3421
|
-
const actualMatches = findProfanity.lastActualMatches || new Map();
|
|
3422
|
-
const matchDetails = findProfanityWithMetadata(text, options);
|
|
3455
|
+
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
3456
|
+
const { replaceWith = '*', fullWordCensor = true, detectLeetSpeak = true, whitelist = [], checkSubstring = false, useRandomGrawlix = false, keepFirstAndLast = false, detectSplit = false, } = mergedOptions;
|
|
3457
|
+
const matches = findProfanity(text, mergedOptions);
|
|
3423
3458
|
if (matches.length === 0) {
|
|
3424
3459
|
return {
|
|
3425
3460
|
filtered: text,
|
|
@@ -3427,123 +3462,62 @@ function filter(text, options = {}) {
|
|
|
3427
3462
|
replacements: [],
|
|
3428
3463
|
};
|
|
3429
3464
|
}
|
|
3465
|
+
const actualMatches = findProfanity.lastActualMatches || new Map();
|
|
3466
|
+
const matchDetails = findProfanityWithMetadata(text, mergedOptions);
|
|
3467
|
+
const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
|
|
3430
3468
|
let filteredText = text;
|
|
3431
3469
|
const replacements = [];
|
|
3432
|
-
|
|
3470
|
+
const getCensoredWord = (originalWord) => {
|
|
3471
|
+
if (useRandomGrawlix) {
|
|
3472
|
+
return makeRandomGrawlixString(originalWord.length);
|
|
3473
|
+
}
|
|
3474
|
+
return censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
|
|
3475
|
+
};
|
|
3476
|
+
const applyReplacement = (pattern, metadata) => {
|
|
3477
|
+
filteredText = filteredText.replace(pattern, (matchedStr) => {
|
|
3478
|
+
if (normalizedWhitelist.includes(matchedStr.toLowerCase())) {
|
|
3479
|
+
return matchedStr;
|
|
3480
|
+
}
|
|
3481
|
+
const censored = getCensoredWord(matchedStr);
|
|
3482
|
+
replacements.push({
|
|
3483
|
+
original: matchedStr,
|
|
3484
|
+
censored,
|
|
3485
|
+
metadata,
|
|
3486
|
+
});
|
|
3487
|
+
return censored;
|
|
3488
|
+
});
|
|
3489
|
+
};
|
|
3490
|
+
for (const word of matches) {
|
|
3433
3491
|
const metadata = matchDetails.find((m) => m.word.toLowerCase() === word.toLowerCase() ||
|
|
3434
3492
|
(m.aliases && m.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
|
|
3435
3493
|
const variants = actualMatches.get(word.toLowerCase()) || [];
|
|
3436
|
-
variants.
|
|
3437
|
-
const
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
const originalWord = match[0];
|
|
3443
|
-
if (whitelist.includes(originalWord.toLowerCase()))
|
|
3444
|
-
continue;
|
|
3445
|
-
let censoredWord;
|
|
3446
|
-
if (useRandomGrawlix) {
|
|
3447
|
-
censoredWord = makeRandomGrawlixString(originalWord.length);
|
|
3448
|
-
}
|
|
3449
|
-
else {
|
|
3450
|
-
censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
|
|
3451
|
-
}
|
|
3452
|
-
replacements.push({
|
|
3453
|
-
original: originalWord,
|
|
3454
|
-
censored: censoredWord,
|
|
3455
|
-
metadata,
|
|
3456
|
-
});
|
|
3457
|
-
filteredText = filteredText.replace(new RegExp(`\\b${escapeRegExp(originalWord)}\\b`, 'g'), censoredWord);
|
|
3458
|
-
}
|
|
3459
|
-
});
|
|
3460
|
-
if (detectSplit || detectLeetSpeak) {
|
|
3461
|
-
if (detectLeetSpeak) {
|
|
3462
|
-
const leetRegex = createWordRegex(word, {
|
|
3463
|
-
wholeWord: true,
|
|
3464
|
-
caseSensitive: false,
|
|
3465
|
-
leetSpeak: true,
|
|
3466
|
-
detectSplit: false,
|
|
3467
|
-
indonesianVariation: false,
|
|
3468
|
-
});
|
|
3469
|
-
let match;
|
|
3470
|
-
while ((match = leetRegex.exec(filteredText)) !== null) {
|
|
3471
|
-
const originalWord = match[0];
|
|
3472
|
-
if (whitelist.includes(originalWord.toLowerCase()))
|
|
3473
|
-
continue;
|
|
3474
|
-
let censoredWord;
|
|
3475
|
-
if (useRandomGrawlix) {
|
|
3476
|
-
censoredWord = makeRandomGrawlixString(originalWord.length);
|
|
3477
|
-
}
|
|
3478
|
-
else {
|
|
3479
|
-
censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
|
|
3480
|
-
}
|
|
3481
|
-
replacements.push({
|
|
3482
|
-
original: originalWord,
|
|
3483
|
-
censored: censoredWord,
|
|
3484
|
-
metadata,
|
|
3485
|
-
});
|
|
3486
|
-
filteredText = filteredText.replace(new RegExp(escapeRegExp(originalWord), 'g'), censoredWord);
|
|
3487
|
-
}
|
|
3488
|
-
}
|
|
3489
|
-
if (detectSplit) {
|
|
3490
|
-
const splitRegex = createWordRegex(word, {
|
|
3491
|
-
wholeWord: false,
|
|
3492
|
-
caseSensitive: false,
|
|
3493
|
-
leetSpeak: false,
|
|
3494
|
-
detectSplit: true,
|
|
3495
|
-
indonesianVariation: false,
|
|
3496
|
-
});
|
|
3497
|
-
let match;
|
|
3498
|
-
while ((match = splitRegex.exec(filteredText)) !== null) {
|
|
3499
|
-
const originalWord = match[0];
|
|
3500
|
-
if (whitelist.includes(originalWord.toLowerCase()))
|
|
3501
|
-
continue;
|
|
3502
|
-
let censoredWord;
|
|
3503
|
-
if (useRandomGrawlix) {
|
|
3504
|
-
censoredWord = makeRandomGrawlixString(originalWord.length);
|
|
3505
|
-
}
|
|
3506
|
-
else {
|
|
3507
|
-
censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
|
|
3508
|
-
}
|
|
3509
|
-
replacements.push({
|
|
3510
|
-
original: originalWord,
|
|
3511
|
-
censored: censoredWord,
|
|
3512
|
-
metadata,
|
|
3513
|
-
});
|
|
3514
|
-
filteredText = filteredText.replace(new RegExp(escapeRegExp(originalWord), 'g'), censoredWord);
|
|
3515
|
-
}
|
|
3516
|
-
}
|
|
3494
|
+
const allVariants = [...new Set([...variants, word])].sort((a, b) => b.length - a.length);
|
|
3495
|
+
for (const variant of allVariants) {
|
|
3496
|
+
const boundaryPattern = checkSubstring
|
|
3497
|
+
? escapeRegExp(variant)
|
|
3498
|
+
: `\\b${escapeRegExp(variant)}\\b`;
|
|
3499
|
+
applyReplacement(new RegExp(boundaryPattern, 'gi'), metadata);
|
|
3517
3500
|
}
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
const exactVariantRegex = new RegExp(`\\b${escapeRegExp(variant)}\\b`, 'gi');
|
|
3526
|
-
let match;
|
|
3527
|
-
while ((match = exactVariantRegex.exec(filteredText)) !== null) {
|
|
3528
|
-
const originalWord = match[0];
|
|
3529
|
-
if (whitelist.includes(originalWord.toLowerCase()))
|
|
3530
|
-
continue;
|
|
3531
|
-
let censoredWord;
|
|
3532
|
-
if (useRandomGrawlix) {
|
|
3533
|
-
censoredWord = makeRandomGrawlixString(originalWord.length);
|
|
3534
|
-
}
|
|
3535
|
-
else {
|
|
3536
|
-
censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
|
|
3537
|
-
}
|
|
3538
|
-
replacements.push({
|
|
3539
|
-
original: originalWord,
|
|
3540
|
-
censored: censoredWord,
|
|
3541
|
-
metadata,
|
|
3542
|
-
});
|
|
3543
|
-
filteredText = filteredText.replace(new RegExp(`\\b${escapeRegExp(originalWord)}\\b`, 'g'), censoredWord);
|
|
3544
|
-
}
|
|
3501
|
+
if (detectLeetSpeak) {
|
|
3502
|
+
const leetRegex = createWordRegex(word, {
|
|
3503
|
+
wholeWord: !checkSubstring,
|
|
3504
|
+
caseSensitive: false,
|
|
3505
|
+
leetSpeak: true,
|
|
3506
|
+
detectSplit: false,
|
|
3507
|
+
indonesianVariation: false,
|
|
3545
3508
|
});
|
|
3546
|
-
|
|
3509
|
+
applyReplacement(leetRegex, metadata);
|
|
3510
|
+
}
|
|
3511
|
+
if (detectSplit) {
|
|
3512
|
+
const splitRegex = createWordRegex(word, {
|
|
3513
|
+
wholeWord: false,
|
|
3514
|
+
caseSensitive: false,
|
|
3515
|
+
leetSpeak: false,
|
|
3516
|
+
detectSplit: true,
|
|
3517
|
+
indonesianVariation: false,
|
|
3518
|
+
});
|
|
3519
|
+
applyReplacement(splitRegex, metadata);
|
|
3520
|
+
}
|
|
3547
3521
|
}
|
|
3548
3522
|
return {
|
|
3549
3523
|
filtered: filteredText,
|
|
@@ -3785,6 +3759,13 @@ class IDProfanityFilter {
|
|
|
3785
3759
|
...options,
|
|
3786
3760
|
};
|
|
3787
3761
|
}
|
|
3762
|
+
/**
|
|
3763
|
+
* Mengatur ulang opsi filter ke default
|
|
3764
|
+
* @param options Opsi baru untuk override default
|
|
3765
|
+
*/
|
|
3766
|
+
resetOptions(options = {}) {
|
|
3767
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
3768
|
+
}
|
|
3788
3769
|
/**
|
|
3789
3770
|
* Menggunakan preset yang telah ditentukan
|
|
3790
3771
|
* @param presetName Nama preset yang akan digunakan
|