@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.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
type ProfanityCategory = 'sexual' | 'insult' | 'profanity' | 'slur' | 'drugs' | 'disgusting' | 'blasphemy';
|
|
2
|
-
type Region = 'general' | 'jawa' | 'sunda' | 'betawi' | 'batak' | 'minang' | 'bali' | 'madura' | '
|
|
2
|
+
type Region = 'general' | 'jawa' | 'sunda' | 'betawi' | 'batak' | 'minang' | 'bali' | 'madura' | 'aceh';
|
|
3
3
|
interface ProfanityWord {
|
|
4
4
|
word: string;
|
|
5
5
|
category: ProfanityCategory;
|
|
@@ -62,22 +62,22 @@ declare function findProfanity(text: string, options?: FilterOptions): string[];
|
|
|
62
62
|
/**
|
|
63
63
|
* Mencari kata kotor lengkap dengan metadata
|
|
64
64
|
*
|
|
65
|
-
* @param text Teks yang akan
|
|
66
|
-
* @param options Opsi
|
|
65
|
+
* @param text Teks yang akan diperiksa
|
|
66
|
+
* @param options Opsi untuk pencarian kata kotor
|
|
67
67
|
* @return Array dari objek kata kotor yang ditemukan
|
|
68
68
|
*/
|
|
69
69
|
declare function findProfanityWithMetadata(text: string, options?: FilterOptions): ProfanityWord[];
|
|
70
70
|
/**
|
|
71
|
-
* Mencari
|
|
71
|
+
* Mencari kategori kata kotor yang ada dalam teks
|
|
72
72
|
*
|
|
73
|
-
* @param matchDetails Hasil pencarian dari
|
|
73
|
+
* @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
|
|
74
74
|
* @return Array kategori unik
|
|
75
75
|
*/
|
|
76
76
|
declare function findCategories(matchDetails: ProfanityWord[]): ProfanityCategory[];
|
|
77
77
|
/**
|
|
78
78
|
* Mencari region kata kotor yang ada dalam teks
|
|
79
79
|
*
|
|
80
|
-
* @param matchDetails Hasil pencarian dari
|
|
80
|
+
* @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
|
|
81
81
|
* @return Array region unik
|
|
82
82
|
*/
|
|
83
83
|
declare function findRegions(matchDetails: ProfanityWord[]): Region[];
|
|
@@ -748,6 +748,11 @@ declare class IDProfanityFilter {
|
|
|
748
748
|
* @param options Opsi baru untuk filter
|
|
749
749
|
*/
|
|
750
750
|
setOptions(options: Partial<FilterOptions>): void;
|
|
751
|
+
/**
|
|
752
|
+
* Mengatur ulang opsi filter ke default
|
|
753
|
+
* @param options Opsi baru untuk override default
|
|
754
|
+
*/
|
|
755
|
+
resetOptions(options?: FilterOptions): void;
|
|
751
756
|
/**
|
|
752
757
|
* Menggunakan preset yang telah ditentukan
|
|
753
758
|
* @param presetName Nama preset yang akan digunakan
|
package/dist/index.esm.js
CHANGED
|
@@ -4,7 +4,7 @@ const general = [
|
|
|
4
4
|
category: 'profanity',
|
|
5
5
|
region: 'general',
|
|
6
6
|
severity: 0.7,
|
|
7
|
-
aliases: ['anjay', 'anjir', 'anying', 'njing', 'anj', 'anjg', 'ajg'],
|
|
7
|
+
aliases: ['anjay', 'anjir', 'anying', 'njing', 'anj', 'anjg', 'ajg', 'anjic', 'anjink'],
|
|
8
8
|
description: 'Mengacu pada hewan anjing, digunakan sebagai umpatan',
|
|
9
9
|
context: 'Umpatan umum untuk menunjukkan kemarahan atau ketidaksetujuan',
|
|
10
10
|
},
|
|
@@ -112,7 +112,7 @@ const general = [
|
|
|
112
112
|
category: 'insult',
|
|
113
113
|
region: 'general',
|
|
114
114
|
severity: 0.5,
|
|
115
|
-
aliases: ['sialn'
|
|
115
|
+
aliases: ['sialn'],
|
|
116
116
|
description: 'Kata yang mengacu pada orang yang membawa sial',
|
|
117
117
|
context: 'Hinaan untuk menyebut orang yang dianggap membawa sial',
|
|
118
118
|
},
|
|
@@ -175,7 +175,7 @@ const general = [
|
|
|
175
175
|
category: 'sexual',
|
|
176
176
|
region: 'general',
|
|
177
177
|
severity: 0.9,
|
|
178
|
-
aliases: ['ngew'
|
|
178
|
+
aliases: ['ngew'],
|
|
179
179
|
description: 'Istilah kasar untuk aktivitas seksual',
|
|
180
180
|
context: 'Kata vulgar yang merujuk pada aktivitas seksual',
|
|
181
181
|
},
|
|
@@ -1607,7 +1607,7 @@ const sexual = [
|
|
|
1607
1607
|
category: 'sexual',
|
|
1608
1608
|
region: 'general',
|
|
1609
1609
|
severity: 0.9,
|
|
1610
|
-
aliases: ['ngew'
|
|
1610
|
+
aliases: ['ngew'],
|
|
1611
1611
|
description: 'Istilah kasar untuk aktivitas seksual',
|
|
1612
1612
|
context: 'Kata vulgar yang merujuk pada aktivitas seksual',
|
|
1613
1613
|
},
|
|
@@ -2139,16 +2139,7 @@ const wordObjects = [
|
|
|
2139
2139
|
...minang,
|
|
2140
2140
|
...bali,
|
|
2141
2141
|
...madura,
|
|
2142
|
-
// ...bugis,
|
|
2143
2142
|
...aceh,
|
|
2144
|
-
// ...ambon,
|
|
2145
|
-
// ...papua,
|
|
2146
|
-
// ...manado,
|
|
2147
|
-
// ...banjar,
|
|
2148
|
-
// ...palembang,
|
|
2149
|
-
// ...lampung,
|
|
2150
|
-
// ...ntt,
|
|
2151
|
-
// ...ntb,
|
|
2152
2143
|
];
|
|
2153
2144
|
wordObjects.map((item) => item.word);
|
|
2154
2145
|
/**
|
|
@@ -2464,8 +2455,7 @@ function addIndonesianVariations(pattern) {
|
|
|
2464
2455
|
const lowerChar = char.toLowerCase();
|
|
2465
2456
|
const variations = variationMap[lowerChar];
|
|
2466
2457
|
if (variations && variations.length > 1) {
|
|
2467
|
-
|
|
2468
|
-
result += `[${variations.join('')}]`;
|
|
2458
|
+
result += `(?:${variations.join('|')})`;
|
|
2469
2459
|
}
|
|
2470
2460
|
else {
|
|
2471
2461
|
result += char;
|
|
@@ -3101,6 +3091,36 @@ class AhoCorasick {
|
|
|
3101
3091
|
const matches = this.search(text);
|
|
3102
3092
|
return new Set(matches.keys());
|
|
3103
3093
|
}
|
|
3094
|
+
/**
|
|
3095
|
+
* Mencari semua kemunculan pola beserta posisi indeks awal dan akhirnya
|
|
3096
|
+
* @param text Teks yang akan dicari
|
|
3097
|
+
* @returns Array objek berisi pattern, start, dan end
|
|
3098
|
+
*/
|
|
3099
|
+
searchWithPositions(text) {
|
|
3100
|
+
if (!this.built) {
|
|
3101
|
+
this.build();
|
|
3102
|
+
}
|
|
3103
|
+
const results = [];
|
|
3104
|
+
const normalizedText = text.toLowerCase();
|
|
3105
|
+
let node = this.root;
|
|
3106
|
+
for (let i = 0; i < normalizedText.length; i++) {
|
|
3107
|
+
const char = normalizedText[i];
|
|
3108
|
+
while (node !== this.root && !node.children.has(char)) {
|
|
3109
|
+
node = node.fail;
|
|
3110
|
+
}
|
|
3111
|
+
if (node.children.has(char)) {
|
|
3112
|
+
node = node.children.get(char);
|
|
3113
|
+
}
|
|
3114
|
+
for (const match of node.output) {
|
|
3115
|
+
results.push({
|
|
3116
|
+
pattern: match,
|
|
3117
|
+
start: i + 1 - match.length,
|
|
3118
|
+
end: i + 1,
|
|
3119
|
+
});
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
return results;
|
|
3123
|
+
}
|
|
3104
3124
|
/**
|
|
3105
3125
|
* Mengecek apakah teks mengandung setidaknya satu pola
|
|
3106
3126
|
* @param text Teks yang akan dicari
|
|
@@ -3128,16 +3148,22 @@ class AhoCorasick {
|
|
|
3128
3148
|
}
|
|
3129
3149
|
}
|
|
3130
3150
|
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3151
|
+
let defaultAhoCorasick = null;
|
|
3152
|
+
function getDefaultAhoCorasick() {
|
|
3153
|
+
if (!defaultAhoCorasick) {
|
|
3154
|
+
const ac = new AhoCorasick();
|
|
3155
|
+
for (const wordObj of wordObjects) {
|
|
3156
|
+
ac.addPattern(wordObj.word);
|
|
3157
|
+
if (wordObj.aliases) {
|
|
3158
|
+
for (const alias of wordObj.aliases) {
|
|
3159
|
+
ac.addPattern(alias);
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
ac.build();
|
|
3164
|
+
defaultAhoCorasick = ac;
|
|
3138
3165
|
}
|
|
3139
|
-
|
|
3140
|
-
ahoCorasickInitialized = true;
|
|
3166
|
+
return defaultAhoCorasick;
|
|
3141
3167
|
}
|
|
3142
3168
|
function getWordMetadata(word) {
|
|
3143
3169
|
return wordObjects.find((obj) => obj.word.toLowerCase() === word.toLowerCase() ||
|
|
@@ -3145,10 +3171,15 @@ function getWordMetadata(word) {
|
|
|
3145
3171
|
}
|
|
3146
3172
|
function findProfanity(text, options = {}) {
|
|
3147
3173
|
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 };
|
|
3174
|
+
const hasCustomWordList = Boolean(wordList && wordList.length > 0);
|
|
3148
3175
|
const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
|
|
3149
3176
|
const normalizedText = normalizeText(text);
|
|
3150
|
-
let baseWordsToCheck =
|
|
3151
|
-
|
|
3177
|
+
let baseWordsToCheck = [];
|
|
3178
|
+
const aliasMap = new Map();
|
|
3179
|
+
if (hasCustomWordList) {
|
|
3180
|
+
baseWordsToCheck = wordList;
|
|
3181
|
+
}
|
|
3182
|
+
else {
|
|
3152
3183
|
const filteredWords = wordObjects.filter((word) => {
|
|
3153
3184
|
const matchCategory = categories ? categories.includes(word.category) : true;
|
|
3154
3185
|
const matchRegion = regions ? regions.includes(word.region) : true;
|
|
@@ -3156,22 +3187,14 @@ function findProfanity(text, options = {}) {
|
|
|
3156
3187
|
return matchCategory && matchRegion && matchSeverity;
|
|
3157
3188
|
});
|
|
3158
3189
|
baseWordsToCheck = filteredWords.map((word) => word.word);
|
|
3190
|
+
filteredWords.forEach((wordObj) => {
|
|
3191
|
+
if (wordObj.aliases && wordObj.aliases.length > 0) {
|
|
3192
|
+
wordObj.aliases.forEach((alias) => {
|
|
3193
|
+
aliasMap.set(alias.toLowerCase(), wordObj.word.toLowerCase());
|
|
3194
|
+
});
|
|
3195
|
+
}
|
|
3196
|
+
});
|
|
3159
3197
|
}
|
|
3160
|
-
const aliasMap = new Map();
|
|
3161
|
-
wordObjects.forEach((wordObj) => {
|
|
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
|
-
});
|
|
3173
|
-
}
|
|
3174
|
-
});
|
|
3175
3198
|
const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !normalizedWhitelist.includes(word.toLowerCase()));
|
|
3176
3199
|
if (wordsToCheck.length === 0) {
|
|
3177
3200
|
return [];
|
|
@@ -3181,6 +3204,8 @@ function findProfanity(text, options = {}) {
|
|
|
3181
3204
|
const passesFilters = (word) => {
|
|
3182
3205
|
if (normalizedWhitelist.includes(word.toLowerCase()))
|
|
3183
3206
|
return false;
|
|
3207
|
+
if (hasCustomWordList)
|
|
3208
|
+
return true;
|
|
3184
3209
|
const metadata = getWordMetadata(word);
|
|
3185
3210
|
if (!metadata)
|
|
3186
3211
|
return false;
|
|
@@ -3189,9 +3214,27 @@ function findProfanity(text, options = {}) {
|
|
|
3189
3214
|
const matchSeverity = metadata.severity >= severityThreshold;
|
|
3190
3215
|
return matchCategory && matchRegion && matchSeverity;
|
|
3191
3216
|
};
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3217
|
+
let ac;
|
|
3218
|
+
if (hasCustomWordList) {
|
|
3219
|
+
ac = new AhoCorasick();
|
|
3220
|
+
for (const w of wordsToCheck) {
|
|
3221
|
+
ac.addPattern(w);
|
|
3222
|
+
}
|
|
3223
|
+
ac.build();
|
|
3224
|
+
}
|
|
3225
|
+
else {
|
|
3226
|
+
ac = getDefaultAhoCorasick();
|
|
3227
|
+
}
|
|
3228
|
+
const occurrences = ac.searchWithPositions(normalizedText);
|
|
3229
|
+
for (const occ of occurrences) {
|
|
3230
|
+
const { pattern: match, start, end } = occ;
|
|
3231
|
+
if (!checkSubstring) {
|
|
3232
|
+
const isWordStart = start === 0 || !/[a-z0-9_]/i.test(normalizedText[start - 1]);
|
|
3233
|
+
const isWordEnd = end === normalizedText.length || !/[a-z0-9_]/i.test(normalizedText[end]);
|
|
3234
|
+
if (!isWordStart || !isWordEnd) {
|
|
3235
|
+
continue;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3195
3238
|
if (normalizedWhitelist.includes(match.toLowerCase()))
|
|
3196
3239
|
continue;
|
|
3197
3240
|
const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
|
|
@@ -3316,8 +3359,8 @@ function findProfanity(text, options = {}) {
|
|
|
3316
3359
|
/**
|
|
3317
3360
|
* Mencari kata kotor lengkap dengan metadata
|
|
3318
3361
|
*
|
|
3319
|
-
* @param text Teks yang akan
|
|
3320
|
-
* @param options Opsi
|
|
3362
|
+
* @param text Teks yang akan diperiksa
|
|
3363
|
+
* @param options Opsi untuk pencarian kata kotor
|
|
3321
3364
|
* @return Array dari objek kata kotor yang ditemukan
|
|
3322
3365
|
*/
|
|
3323
3366
|
function findProfanityWithMetadata(text, options = {}) {
|
|
@@ -3329,14 +3372,22 @@ function findProfanityWithMetadata(text, options = {}) {
|
|
|
3329
3372
|
.map((word) => {
|
|
3330
3373
|
const wordObject = wordObjects.find((obj) => obj.word.toLowerCase() === word.toLowerCase() ||
|
|
3331
3374
|
(obj.aliases && obj.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
|
|
3332
|
-
|
|
3375
|
+
if (wordObject) {
|
|
3376
|
+
return wordObject;
|
|
3377
|
+
}
|
|
3378
|
+
return {
|
|
3379
|
+
word,
|
|
3380
|
+
category: 'profanity',
|
|
3381
|
+
region: 'general',
|
|
3382
|
+
severity: 0.5,
|
|
3383
|
+
};
|
|
3333
3384
|
})
|
|
3334
3385
|
.filter((word) => word !== undefined);
|
|
3335
3386
|
}
|
|
3336
3387
|
/**
|
|
3337
|
-
* Mencari
|
|
3388
|
+
* Mencari kategori kata kotor yang ada dalam teks
|
|
3338
3389
|
*
|
|
3339
|
-
* @param matchDetails Hasil pencarian dari
|
|
3390
|
+
* @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
|
|
3340
3391
|
* @return Array kategori unik
|
|
3341
3392
|
*/
|
|
3342
3393
|
function findCategories(matchDetails) {
|
|
@@ -3349,7 +3400,7 @@ function findCategories(matchDetails) {
|
|
|
3349
3400
|
/**
|
|
3350
3401
|
* Mencari region kata kotor yang ada dalam teks
|
|
3351
3402
|
*
|
|
3352
|
-
* @param matchDetails Hasil pencarian dari
|
|
3403
|
+
* @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
|
|
3353
3404
|
* @return Array region unik
|
|
3354
3405
|
*/
|
|
3355
3406
|
function findRegions(matchDetails) {
|
|
@@ -3369,7 +3420,7 @@ function calculateSeverity(matchDetails) {
|
|
|
3369
3420
|
if (matchDetails.length === 0) {
|
|
3370
3421
|
return 0;
|
|
3371
3422
|
}
|
|
3372
|
-
const countFactor = Math.min(matchDetails.length / 10, 1);
|
|
3423
|
+
const countFactor = Math.min(matchDetails.length / 10, 1);
|
|
3373
3424
|
const categoryWeights = {
|
|
3374
3425
|
sexual: 0.9,
|
|
3375
3426
|
blasphemy: 0.9,
|
|
@@ -3397,21 +3448,9 @@ function calculateSeverity(matchDetails) {
|
|
|
3397
3448
|
* @returns FilterResult dengan hasil filter
|
|
3398
3449
|
*/
|
|
3399
3450
|
function filter(text, options = {}) {
|
|
3400
|
-
const
|
|
3401
|
-
const
|
|
3402
|
-
|
|
3403
|
-
detectLeetSpeak,
|
|
3404
|
-
whitelist,
|
|
3405
|
-
checkSubstring,
|
|
3406
|
-
indonesianVariation,
|
|
3407
|
-
detectSplit,
|
|
3408
|
-
detectSimilarity,
|
|
3409
|
-
useLevenshtein,
|
|
3410
|
-
maxLevenshteinDistance,
|
|
3411
|
-
similarityThreshold,
|
|
3412
|
-
});
|
|
3413
|
-
const actualMatches = findProfanity.lastActualMatches || new Map();
|
|
3414
|
-
const matchDetails = findProfanityWithMetadata(text, options);
|
|
3451
|
+
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
3452
|
+
const { replaceWith = '*', fullWordCensor = true, detectLeetSpeak = true, whitelist = [], checkSubstring = false, useRandomGrawlix = false, keepFirstAndLast = false, detectSplit = false, } = mergedOptions;
|
|
3453
|
+
const matches = findProfanity(text, mergedOptions);
|
|
3415
3454
|
if (matches.length === 0) {
|
|
3416
3455
|
return {
|
|
3417
3456
|
filtered: text,
|
|
@@ -3419,123 +3458,62 @@ function filter(text, options = {}) {
|
|
|
3419
3458
|
replacements: [],
|
|
3420
3459
|
};
|
|
3421
3460
|
}
|
|
3461
|
+
const actualMatches = findProfanity.lastActualMatches || new Map();
|
|
3462
|
+
const matchDetails = findProfanityWithMetadata(text, mergedOptions);
|
|
3463
|
+
const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
|
|
3422
3464
|
let filteredText = text;
|
|
3423
3465
|
const replacements = [];
|
|
3424
|
-
|
|
3466
|
+
const getCensoredWord = (originalWord) => {
|
|
3467
|
+
if (useRandomGrawlix) {
|
|
3468
|
+
return makeRandomGrawlixString(originalWord.length);
|
|
3469
|
+
}
|
|
3470
|
+
return censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
|
|
3471
|
+
};
|
|
3472
|
+
const applyReplacement = (pattern, metadata) => {
|
|
3473
|
+
filteredText = filteredText.replace(pattern, (matchedStr) => {
|
|
3474
|
+
if (normalizedWhitelist.includes(matchedStr.toLowerCase())) {
|
|
3475
|
+
return matchedStr;
|
|
3476
|
+
}
|
|
3477
|
+
const censored = getCensoredWord(matchedStr);
|
|
3478
|
+
replacements.push({
|
|
3479
|
+
original: matchedStr,
|
|
3480
|
+
censored,
|
|
3481
|
+
metadata,
|
|
3482
|
+
});
|
|
3483
|
+
return censored;
|
|
3484
|
+
});
|
|
3485
|
+
};
|
|
3486
|
+
for (const word of matches) {
|
|
3425
3487
|
const metadata = matchDetails.find((m) => m.word.toLowerCase() === word.toLowerCase() ||
|
|
3426
3488
|
(m.aliases && m.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
|
|
3427
3489
|
const variants = actualMatches.get(word.toLowerCase()) || [];
|
|
3428
|
-
variants.
|
|
3429
|
-
const
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
const originalWord = match[0];
|
|
3435
|
-
if (whitelist.includes(originalWord.toLowerCase()))
|
|
3436
|
-
continue;
|
|
3437
|
-
let censoredWord;
|
|
3438
|
-
if (useRandomGrawlix) {
|
|
3439
|
-
censoredWord = makeRandomGrawlixString(originalWord.length);
|
|
3440
|
-
}
|
|
3441
|
-
else {
|
|
3442
|
-
censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
|
|
3443
|
-
}
|
|
3444
|
-
replacements.push({
|
|
3445
|
-
original: originalWord,
|
|
3446
|
-
censored: censoredWord,
|
|
3447
|
-
metadata,
|
|
3448
|
-
});
|
|
3449
|
-
filteredText = filteredText.replace(new RegExp(`\\b${escapeRegExp(originalWord)}\\b`, 'g'), censoredWord);
|
|
3450
|
-
}
|
|
3451
|
-
});
|
|
3452
|
-
if (detectSplit || detectLeetSpeak) {
|
|
3453
|
-
if (detectLeetSpeak) {
|
|
3454
|
-
const leetRegex = createWordRegex(word, {
|
|
3455
|
-
wholeWord: true,
|
|
3456
|
-
caseSensitive: false,
|
|
3457
|
-
leetSpeak: true,
|
|
3458
|
-
detectSplit: false,
|
|
3459
|
-
indonesianVariation: false,
|
|
3460
|
-
});
|
|
3461
|
-
let match;
|
|
3462
|
-
while ((match = leetRegex.exec(filteredText)) !== null) {
|
|
3463
|
-
const originalWord = match[0];
|
|
3464
|
-
if (whitelist.includes(originalWord.toLowerCase()))
|
|
3465
|
-
continue;
|
|
3466
|
-
let censoredWord;
|
|
3467
|
-
if (useRandomGrawlix) {
|
|
3468
|
-
censoredWord = makeRandomGrawlixString(originalWord.length);
|
|
3469
|
-
}
|
|
3470
|
-
else {
|
|
3471
|
-
censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
|
|
3472
|
-
}
|
|
3473
|
-
replacements.push({
|
|
3474
|
-
original: originalWord,
|
|
3475
|
-
censored: censoredWord,
|
|
3476
|
-
metadata,
|
|
3477
|
-
});
|
|
3478
|
-
filteredText = filteredText.replace(new RegExp(escapeRegExp(originalWord), 'g'), censoredWord);
|
|
3479
|
-
}
|
|
3480
|
-
}
|
|
3481
|
-
if (detectSplit) {
|
|
3482
|
-
const splitRegex = createWordRegex(word, {
|
|
3483
|
-
wholeWord: false,
|
|
3484
|
-
caseSensitive: false,
|
|
3485
|
-
leetSpeak: false,
|
|
3486
|
-
detectSplit: true,
|
|
3487
|
-
indonesianVariation: false,
|
|
3488
|
-
});
|
|
3489
|
-
let match;
|
|
3490
|
-
while ((match = splitRegex.exec(filteredText)) !== null) {
|
|
3491
|
-
const originalWord = match[0];
|
|
3492
|
-
if (whitelist.includes(originalWord.toLowerCase()))
|
|
3493
|
-
continue;
|
|
3494
|
-
let censoredWord;
|
|
3495
|
-
if (useRandomGrawlix) {
|
|
3496
|
-
censoredWord = makeRandomGrawlixString(originalWord.length);
|
|
3497
|
-
}
|
|
3498
|
-
else {
|
|
3499
|
-
censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
|
|
3500
|
-
}
|
|
3501
|
-
replacements.push({
|
|
3502
|
-
original: originalWord,
|
|
3503
|
-
censored: censoredWord,
|
|
3504
|
-
metadata,
|
|
3505
|
-
});
|
|
3506
|
-
filteredText = filteredText.replace(new RegExp(escapeRegExp(originalWord), 'g'), censoredWord);
|
|
3507
|
-
}
|
|
3508
|
-
}
|
|
3490
|
+
const allVariants = [...new Set([...variants, word])].sort((a, b) => b.length - a.length);
|
|
3491
|
+
for (const variant of allVariants) {
|
|
3492
|
+
const boundaryPattern = checkSubstring
|
|
3493
|
+
? escapeRegExp(variant)
|
|
3494
|
+
: `\\b${escapeRegExp(variant)}\\b`;
|
|
3495
|
+
applyReplacement(new RegExp(boundaryPattern, 'gi'), metadata);
|
|
3509
3496
|
}
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
const exactVariantRegex = new RegExp(`\\b${escapeRegExp(variant)}\\b`, 'gi');
|
|
3518
|
-
let match;
|
|
3519
|
-
while ((match = exactVariantRegex.exec(filteredText)) !== null) {
|
|
3520
|
-
const originalWord = match[0];
|
|
3521
|
-
if (whitelist.includes(originalWord.toLowerCase()))
|
|
3522
|
-
continue;
|
|
3523
|
-
let censoredWord;
|
|
3524
|
-
if (useRandomGrawlix) {
|
|
3525
|
-
censoredWord = makeRandomGrawlixString(originalWord.length);
|
|
3526
|
-
}
|
|
3527
|
-
else {
|
|
3528
|
-
censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
|
|
3529
|
-
}
|
|
3530
|
-
replacements.push({
|
|
3531
|
-
original: originalWord,
|
|
3532
|
-
censored: censoredWord,
|
|
3533
|
-
metadata,
|
|
3534
|
-
});
|
|
3535
|
-
filteredText = filteredText.replace(new RegExp(`\\b${escapeRegExp(originalWord)}\\b`, 'g'), censoredWord);
|
|
3536
|
-
}
|
|
3497
|
+
if (detectLeetSpeak) {
|
|
3498
|
+
const leetRegex = createWordRegex(word, {
|
|
3499
|
+
wholeWord: !checkSubstring,
|
|
3500
|
+
caseSensitive: false,
|
|
3501
|
+
leetSpeak: true,
|
|
3502
|
+
detectSplit: false,
|
|
3503
|
+
indonesianVariation: false,
|
|
3537
3504
|
});
|
|
3538
|
-
|
|
3505
|
+
applyReplacement(leetRegex, metadata);
|
|
3506
|
+
}
|
|
3507
|
+
if (detectSplit) {
|
|
3508
|
+
const splitRegex = createWordRegex(word, {
|
|
3509
|
+
wholeWord: false,
|
|
3510
|
+
caseSensitive: false,
|
|
3511
|
+
leetSpeak: false,
|
|
3512
|
+
detectSplit: true,
|
|
3513
|
+
indonesianVariation: false,
|
|
3514
|
+
});
|
|
3515
|
+
applyReplacement(splitRegex, metadata);
|
|
3516
|
+
}
|
|
3539
3517
|
}
|
|
3540
3518
|
return {
|
|
3541
3519
|
filtered: filteredText,
|
|
@@ -3777,6 +3755,13 @@ class IDProfanityFilter {
|
|
|
3777
3755
|
...options,
|
|
3778
3756
|
};
|
|
3779
3757
|
}
|
|
3758
|
+
/**
|
|
3759
|
+
* Mengatur ulang opsi filter ke default
|
|
3760
|
+
* @param options Opsi baru untuk override default
|
|
3761
|
+
*/
|
|
3762
|
+
resetOptions(options = {}) {
|
|
3763
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
3764
|
+
}
|
|
3780
3765
|
/**
|
|
3781
3766
|
* Menggunakan preset yang telah ditentukan
|
|
3782
3767
|
* @param presetName Nama preset yang akan digunakan
|