@sideid/id-profanity-filter 1.12.0 → 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 +160 -454
- package/dist/index.d.ts +11 -6
- package/dist/index.esm.js +161 -176
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +161 -176
- 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/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;
|
|
@@ -3105,6 +3095,36 @@ class AhoCorasick {
|
|
|
3105
3095
|
const matches = this.search(text);
|
|
3106
3096
|
return new Set(matches.keys());
|
|
3107
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
|
+
}
|
|
3108
3128
|
/**
|
|
3109
3129
|
* Mengecek apakah teks mengandung setidaknya satu pola
|
|
3110
3130
|
* @param text Teks yang akan dicari
|
|
@@ -3132,16 +3152,22 @@ class AhoCorasick {
|
|
|
3132
3152
|
}
|
|
3133
3153
|
}
|
|
3134
3154
|
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
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;
|
|
3142
3169
|
}
|
|
3143
|
-
|
|
3144
|
-
ahoCorasickInitialized = true;
|
|
3170
|
+
return defaultAhoCorasick;
|
|
3145
3171
|
}
|
|
3146
3172
|
function getWordMetadata(word) {
|
|
3147
3173
|
return wordObjects.find((obj) => obj.word.toLowerCase() === word.toLowerCase() ||
|
|
@@ -3149,10 +3175,15 @@ function getWordMetadata(word) {
|
|
|
3149
3175
|
}
|
|
3150
3176
|
function findProfanity(text, options = {}) {
|
|
3151
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);
|
|
3152
3179
|
const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
|
|
3153
3180
|
const normalizedText = normalizeText(text);
|
|
3154
|
-
let baseWordsToCheck =
|
|
3155
|
-
|
|
3181
|
+
let baseWordsToCheck = [];
|
|
3182
|
+
const aliasMap = new Map();
|
|
3183
|
+
if (hasCustomWordList) {
|
|
3184
|
+
baseWordsToCheck = wordList;
|
|
3185
|
+
}
|
|
3186
|
+
else {
|
|
3156
3187
|
const filteredWords = wordObjects.filter((word) => {
|
|
3157
3188
|
const matchCategory = categories ? categories.includes(word.category) : true;
|
|
3158
3189
|
const matchRegion = regions ? regions.includes(word.region) : true;
|
|
@@ -3160,22 +3191,14 @@ function findProfanity(text, options = {}) {
|
|
|
3160
3191
|
return matchCategory && matchRegion && matchSeverity;
|
|
3161
3192
|
});
|
|
3162
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
|
+
});
|
|
3163
3201
|
}
|
|
3164
|
-
const aliasMap = new Map();
|
|
3165
|
-
wordObjects.forEach((wordObj) => {
|
|
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
|
-
});
|
|
3177
|
-
}
|
|
3178
|
-
});
|
|
3179
3202
|
const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !normalizedWhitelist.includes(word.toLowerCase()));
|
|
3180
3203
|
if (wordsToCheck.length === 0) {
|
|
3181
3204
|
return [];
|
|
@@ -3185,6 +3208,8 @@ function findProfanity(text, options = {}) {
|
|
|
3185
3208
|
const passesFilters = (word) => {
|
|
3186
3209
|
if (normalizedWhitelist.includes(word.toLowerCase()))
|
|
3187
3210
|
return false;
|
|
3211
|
+
if (hasCustomWordList)
|
|
3212
|
+
return true;
|
|
3188
3213
|
const metadata = getWordMetadata(word);
|
|
3189
3214
|
if (!metadata)
|
|
3190
3215
|
return false;
|
|
@@ -3193,9 +3218,27 @@ function findProfanity(text, options = {}) {
|
|
|
3193
3218
|
const matchSeverity = metadata.severity >= severityThreshold;
|
|
3194
3219
|
return matchCategory && matchRegion && matchSeverity;
|
|
3195
3220
|
};
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
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
|
+
}
|
|
3199
3242
|
if (normalizedWhitelist.includes(match.toLowerCase()))
|
|
3200
3243
|
continue;
|
|
3201
3244
|
const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
|
|
@@ -3320,8 +3363,8 @@ function findProfanity(text, options = {}) {
|
|
|
3320
3363
|
/**
|
|
3321
3364
|
* Mencari kata kotor lengkap dengan metadata
|
|
3322
3365
|
*
|
|
3323
|
-
* @param text Teks yang akan
|
|
3324
|
-
* @param options Opsi
|
|
3366
|
+
* @param text Teks yang akan diperiksa
|
|
3367
|
+
* @param options Opsi untuk pencarian kata kotor
|
|
3325
3368
|
* @return Array dari objek kata kotor yang ditemukan
|
|
3326
3369
|
*/
|
|
3327
3370
|
function findProfanityWithMetadata(text, options = {}) {
|
|
@@ -3333,14 +3376,22 @@ function findProfanityWithMetadata(text, options = {}) {
|
|
|
3333
3376
|
.map((word) => {
|
|
3334
3377
|
const wordObject = wordObjects.find((obj) => obj.word.toLowerCase() === word.toLowerCase() ||
|
|
3335
3378
|
(obj.aliases && obj.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
|
|
3336
|
-
|
|
3379
|
+
if (wordObject) {
|
|
3380
|
+
return wordObject;
|
|
3381
|
+
}
|
|
3382
|
+
return {
|
|
3383
|
+
word,
|
|
3384
|
+
category: 'profanity',
|
|
3385
|
+
region: 'general',
|
|
3386
|
+
severity: 0.5,
|
|
3387
|
+
};
|
|
3337
3388
|
})
|
|
3338
3389
|
.filter((word) => word !== undefined);
|
|
3339
3390
|
}
|
|
3340
3391
|
/**
|
|
3341
|
-
* Mencari
|
|
3392
|
+
* Mencari kategori kata kotor yang ada dalam teks
|
|
3342
3393
|
*
|
|
3343
|
-
* @param matchDetails Hasil pencarian dari
|
|
3394
|
+
* @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
|
|
3344
3395
|
* @return Array kategori unik
|
|
3345
3396
|
*/
|
|
3346
3397
|
function findCategories(matchDetails) {
|
|
@@ -3353,7 +3404,7 @@ function findCategories(matchDetails) {
|
|
|
3353
3404
|
/**
|
|
3354
3405
|
* Mencari region kata kotor yang ada dalam teks
|
|
3355
3406
|
*
|
|
3356
|
-
* @param matchDetails Hasil pencarian dari
|
|
3407
|
+
* @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
|
|
3357
3408
|
* @return Array region unik
|
|
3358
3409
|
*/
|
|
3359
3410
|
function findRegions(matchDetails) {
|
|
@@ -3373,7 +3424,7 @@ function calculateSeverity(matchDetails) {
|
|
|
3373
3424
|
if (matchDetails.length === 0) {
|
|
3374
3425
|
return 0;
|
|
3375
3426
|
}
|
|
3376
|
-
const countFactor = Math.min(matchDetails.length / 10, 1);
|
|
3427
|
+
const countFactor = Math.min(matchDetails.length / 10, 1);
|
|
3377
3428
|
const categoryWeights = {
|
|
3378
3429
|
sexual: 0.9,
|
|
3379
3430
|
blasphemy: 0.9,
|
|
@@ -3401,21 +3452,9 @@ function calculateSeverity(matchDetails) {
|
|
|
3401
3452
|
* @returns FilterResult dengan hasil filter
|
|
3402
3453
|
*/
|
|
3403
3454
|
function filter(text, options = {}) {
|
|
3404
|
-
const
|
|
3405
|
-
const
|
|
3406
|
-
|
|
3407
|
-
detectLeetSpeak,
|
|
3408
|
-
whitelist,
|
|
3409
|
-
checkSubstring,
|
|
3410
|
-
indonesianVariation,
|
|
3411
|
-
detectSplit,
|
|
3412
|
-
detectSimilarity,
|
|
3413
|
-
useLevenshtein,
|
|
3414
|
-
maxLevenshteinDistance,
|
|
3415
|
-
similarityThreshold,
|
|
3416
|
-
});
|
|
3417
|
-
const actualMatches = findProfanity.lastActualMatches || new Map();
|
|
3418
|
-
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);
|
|
3419
3458
|
if (matches.length === 0) {
|
|
3420
3459
|
return {
|
|
3421
3460
|
filtered: text,
|
|
@@ -3423,123 +3462,62 @@ function filter(text, options = {}) {
|
|
|
3423
3462
|
replacements: [],
|
|
3424
3463
|
};
|
|
3425
3464
|
}
|
|
3465
|
+
const actualMatches = findProfanity.lastActualMatches || new Map();
|
|
3466
|
+
const matchDetails = findProfanityWithMetadata(text, mergedOptions);
|
|
3467
|
+
const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
|
|
3426
3468
|
let filteredText = text;
|
|
3427
3469
|
const replacements = [];
|
|
3428
|
-
|
|
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) {
|
|
3429
3491
|
const metadata = matchDetails.find((m) => m.word.toLowerCase() === word.toLowerCase() ||
|
|
3430
3492
|
(m.aliases && m.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
|
|
3431
3493
|
const variants = actualMatches.get(word.toLowerCase()) || [];
|
|
3432
|
-
variants.
|
|
3433
|
-
const
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
const originalWord = match[0];
|
|
3439
|
-
if (whitelist.includes(originalWord.toLowerCase()))
|
|
3440
|
-
continue;
|
|
3441
|
-
let censoredWord;
|
|
3442
|
-
if (useRandomGrawlix) {
|
|
3443
|
-
censoredWord = makeRandomGrawlixString(originalWord.length);
|
|
3444
|
-
}
|
|
3445
|
-
else {
|
|
3446
|
-
censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
|
|
3447
|
-
}
|
|
3448
|
-
replacements.push({
|
|
3449
|
-
original: originalWord,
|
|
3450
|
-
censored: censoredWord,
|
|
3451
|
-
metadata,
|
|
3452
|
-
});
|
|
3453
|
-
filteredText = filteredText.replace(new RegExp(`\\b${escapeRegExp(originalWord)}\\b`, 'g'), censoredWord);
|
|
3454
|
-
}
|
|
3455
|
-
});
|
|
3456
|
-
if (detectSplit || detectLeetSpeak) {
|
|
3457
|
-
if (detectLeetSpeak) {
|
|
3458
|
-
const leetRegex = createWordRegex(word, {
|
|
3459
|
-
wholeWord: true,
|
|
3460
|
-
caseSensitive: false,
|
|
3461
|
-
leetSpeak: true,
|
|
3462
|
-
detectSplit: false,
|
|
3463
|
-
indonesianVariation: false,
|
|
3464
|
-
});
|
|
3465
|
-
let match;
|
|
3466
|
-
while ((match = leetRegex.exec(filteredText)) !== null) {
|
|
3467
|
-
const originalWord = match[0];
|
|
3468
|
-
if (whitelist.includes(originalWord.toLowerCase()))
|
|
3469
|
-
continue;
|
|
3470
|
-
let censoredWord;
|
|
3471
|
-
if (useRandomGrawlix) {
|
|
3472
|
-
censoredWord = makeRandomGrawlixString(originalWord.length);
|
|
3473
|
-
}
|
|
3474
|
-
else {
|
|
3475
|
-
censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
|
|
3476
|
-
}
|
|
3477
|
-
replacements.push({
|
|
3478
|
-
original: originalWord,
|
|
3479
|
-
censored: censoredWord,
|
|
3480
|
-
metadata,
|
|
3481
|
-
});
|
|
3482
|
-
filteredText = filteredText.replace(new RegExp(escapeRegExp(originalWord), 'g'), censoredWord);
|
|
3483
|
-
}
|
|
3484
|
-
}
|
|
3485
|
-
if (detectSplit) {
|
|
3486
|
-
const splitRegex = createWordRegex(word, {
|
|
3487
|
-
wholeWord: false,
|
|
3488
|
-
caseSensitive: false,
|
|
3489
|
-
leetSpeak: false,
|
|
3490
|
-
detectSplit: true,
|
|
3491
|
-
indonesianVariation: false,
|
|
3492
|
-
});
|
|
3493
|
-
let match;
|
|
3494
|
-
while ((match = splitRegex.exec(filteredText)) !== null) {
|
|
3495
|
-
const originalWord = match[0];
|
|
3496
|
-
if (whitelist.includes(originalWord.toLowerCase()))
|
|
3497
|
-
continue;
|
|
3498
|
-
let censoredWord;
|
|
3499
|
-
if (useRandomGrawlix) {
|
|
3500
|
-
censoredWord = makeRandomGrawlixString(originalWord.length);
|
|
3501
|
-
}
|
|
3502
|
-
else {
|
|
3503
|
-
censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
|
|
3504
|
-
}
|
|
3505
|
-
replacements.push({
|
|
3506
|
-
original: originalWord,
|
|
3507
|
-
censored: censoredWord,
|
|
3508
|
-
metadata,
|
|
3509
|
-
});
|
|
3510
|
-
filteredText = filteredText.replace(new RegExp(escapeRegExp(originalWord), 'g'), censoredWord);
|
|
3511
|
-
}
|
|
3512
|
-
}
|
|
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);
|
|
3513
3500
|
}
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
const exactVariantRegex = new RegExp(`\\b${escapeRegExp(variant)}\\b`, 'gi');
|
|
3522
|
-
let match;
|
|
3523
|
-
while ((match = exactVariantRegex.exec(filteredText)) !== null) {
|
|
3524
|
-
const originalWord = match[0];
|
|
3525
|
-
if (whitelist.includes(originalWord.toLowerCase()))
|
|
3526
|
-
continue;
|
|
3527
|
-
let censoredWord;
|
|
3528
|
-
if (useRandomGrawlix) {
|
|
3529
|
-
censoredWord = makeRandomGrawlixString(originalWord.length);
|
|
3530
|
-
}
|
|
3531
|
-
else {
|
|
3532
|
-
censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
|
|
3533
|
-
}
|
|
3534
|
-
replacements.push({
|
|
3535
|
-
original: originalWord,
|
|
3536
|
-
censored: censoredWord,
|
|
3537
|
-
metadata,
|
|
3538
|
-
});
|
|
3539
|
-
filteredText = filteredText.replace(new RegExp(`\\b${escapeRegExp(originalWord)}\\b`, 'g'), censoredWord);
|
|
3540
|
-
}
|
|
3501
|
+
if (detectLeetSpeak) {
|
|
3502
|
+
const leetRegex = createWordRegex(word, {
|
|
3503
|
+
wholeWord: !checkSubstring,
|
|
3504
|
+
caseSensitive: false,
|
|
3505
|
+
leetSpeak: true,
|
|
3506
|
+
detectSplit: false,
|
|
3507
|
+
indonesianVariation: false,
|
|
3541
3508
|
});
|
|
3542
|
-
|
|
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
|
+
}
|
|
3543
3521
|
}
|
|
3544
3522
|
return {
|
|
3545
3523
|
filtered: filteredText,
|
|
@@ -3781,6 +3759,13 @@ class IDProfanityFilter {
|
|
|
3781
3759
|
...options,
|
|
3782
3760
|
};
|
|
3783
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
|
+
}
|
|
3784
3769
|
/**
|
|
3785
3770
|
* Menggunakan preset yang telah ditentukan
|
|
3786
3771
|
* @param presetName Nama preset yang akan digunakan
|