@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.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;
|
|
@@ -2548,19 +2538,15 @@ function levenshteinDistance(str1, str2) {
|
|
|
2548
2538
|
const s2 = str2.toLowerCase();
|
|
2549
2539
|
const len1 = s1.length;
|
|
2550
2540
|
const len2 = s2.length;
|
|
2551
|
-
// Inisialisasi matrix
|
|
2552
2541
|
const matrix = [];
|
|
2553
|
-
// Inisialisasi baris pertama
|
|
2554
2542
|
for (let i = 0; i <= len2; i++) {
|
|
2555
2543
|
matrix[0] = matrix[0] || [];
|
|
2556
2544
|
matrix[0][i] = i;
|
|
2557
2545
|
}
|
|
2558
|
-
// Inisialisasi kolom pertama
|
|
2559
2546
|
for (let i = 0; i <= len1; i++) {
|
|
2560
2547
|
matrix[i] = matrix[i] || [];
|
|
2561
2548
|
matrix[i][0] = i;
|
|
2562
2549
|
}
|
|
2563
|
-
// Isi matrix
|
|
2564
2550
|
for (let i = 1; i <= len1; i++) {
|
|
2565
2551
|
for (let j = 1; j <= len2; j++) {
|
|
2566
2552
|
const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
|
|
@@ -3105,6 +3091,36 @@ class AhoCorasick {
|
|
|
3105
3091
|
const matches = this.search(text);
|
|
3106
3092
|
return new Set(matches.keys());
|
|
3107
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
|
+
}
|
|
3108
3124
|
/**
|
|
3109
3125
|
* Mengecek apakah teks mengandung setidaknya satu pola
|
|
3110
3126
|
* @param text Teks yang akan dicari
|
|
@@ -3132,16 +3148,22 @@ class AhoCorasick {
|
|
|
3132
3148
|
}
|
|
3133
3149
|
}
|
|
3134
3150
|
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
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;
|
|
3142
3165
|
}
|
|
3143
|
-
|
|
3144
|
-
ahoCorasickInitialized = true;
|
|
3166
|
+
return defaultAhoCorasick;
|
|
3145
3167
|
}
|
|
3146
3168
|
function getWordMetadata(word) {
|
|
3147
3169
|
return wordObjects.find((obj) => obj.word.toLowerCase() === word.toLowerCase() ||
|
|
@@ -3149,10 +3171,15 @@ function getWordMetadata(word) {
|
|
|
3149
3171
|
}
|
|
3150
3172
|
function findProfanity(text, options = {}) {
|
|
3151
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);
|
|
3152
3175
|
const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
|
|
3153
3176
|
const normalizedText = normalizeText(text);
|
|
3154
|
-
let baseWordsToCheck =
|
|
3155
|
-
|
|
3177
|
+
let baseWordsToCheck = [];
|
|
3178
|
+
const aliasMap = new Map();
|
|
3179
|
+
if (hasCustomWordList) {
|
|
3180
|
+
baseWordsToCheck = wordList;
|
|
3181
|
+
}
|
|
3182
|
+
else {
|
|
3156
3183
|
const filteredWords = wordObjects.filter((word) => {
|
|
3157
3184
|
const matchCategory = categories ? categories.includes(word.category) : true;
|
|
3158
3185
|
const matchRegion = regions ? regions.includes(word.region) : true;
|
|
@@ -3160,22 +3187,14 @@ function findProfanity(text, options = {}) {
|
|
|
3160
3187
|
return matchCategory && matchRegion && matchSeverity;
|
|
3161
3188
|
});
|
|
3162
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
|
+
});
|
|
3163
3197
|
}
|
|
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
3198
|
const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter((word) => !normalizedWhitelist.includes(word.toLowerCase()));
|
|
3180
3199
|
if (wordsToCheck.length === 0) {
|
|
3181
3200
|
return [];
|
|
@@ -3185,6 +3204,8 @@ function findProfanity(text, options = {}) {
|
|
|
3185
3204
|
const passesFilters = (word) => {
|
|
3186
3205
|
if (normalizedWhitelist.includes(word.toLowerCase()))
|
|
3187
3206
|
return false;
|
|
3207
|
+
if (hasCustomWordList)
|
|
3208
|
+
return true;
|
|
3188
3209
|
const metadata = getWordMetadata(word);
|
|
3189
3210
|
if (!metadata)
|
|
3190
3211
|
return false;
|
|
@@ -3193,9 +3214,27 @@ function findProfanity(text, options = {}) {
|
|
|
3193
3214
|
const matchSeverity = metadata.severity >= severityThreshold;
|
|
3194
3215
|
return matchCategory && matchRegion && matchSeverity;
|
|
3195
3216
|
};
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
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
|
+
}
|
|
3199
3238
|
if (normalizedWhitelist.includes(match.toLowerCase()))
|
|
3200
3239
|
continue;
|
|
3201
3240
|
const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
|
|
@@ -3320,8 +3359,8 @@ function findProfanity(text, options = {}) {
|
|
|
3320
3359
|
/**
|
|
3321
3360
|
* Mencari kata kotor lengkap dengan metadata
|
|
3322
3361
|
*
|
|
3323
|
-
* @param text Teks yang akan
|
|
3324
|
-
* @param options Opsi
|
|
3362
|
+
* @param text Teks yang akan diperiksa
|
|
3363
|
+
* @param options Opsi untuk pencarian kata kotor
|
|
3325
3364
|
* @return Array dari objek kata kotor yang ditemukan
|
|
3326
3365
|
*/
|
|
3327
3366
|
function findProfanityWithMetadata(text, options = {}) {
|
|
@@ -3333,14 +3372,22 @@ function findProfanityWithMetadata(text, options = {}) {
|
|
|
3333
3372
|
.map((word) => {
|
|
3334
3373
|
const wordObject = wordObjects.find((obj) => obj.word.toLowerCase() === word.toLowerCase() ||
|
|
3335
3374
|
(obj.aliases && obj.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
|
|
3336
|
-
|
|
3375
|
+
if (wordObject) {
|
|
3376
|
+
return wordObject;
|
|
3377
|
+
}
|
|
3378
|
+
return {
|
|
3379
|
+
word,
|
|
3380
|
+
category: 'profanity',
|
|
3381
|
+
region: 'general',
|
|
3382
|
+
severity: 0.5,
|
|
3383
|
+
};
|
|
3337
3384
|
})
|
|
3338
3385
|
.filter((word) => word !== undefined);
|
|
3339
3386
|
}
|
|
3340
3387
|
/**
|
|
3341
|
-
* Mencari
|
|
3388
|
+
* Mencari kategori kata kotor yang ada dalam teks
|
|
3342
3389
|
*
|
|
3343
|
-
* @param matchDetails Hasil pencarian dari
|
|
3390
|
+
* @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
|
|
3344
3391
|
* @return Array kategori unik
|
|
3345
3392
|
*/
|
|
3346
3393
|
function findCategories(matchDetails) {
|
|
@@ -3353,7 +3400,7 @@ function findCategories(matchDetails) {
|
|
|
3353
3400
|
/**
|
|
3354
3401
|
* Mencari region kata kotor yang ada dalam teks
|
|
3355
3402
|
*
|
|
3356
|
-
* @param matchDetails Hasil pencarian dari
|
|
3403
|
+
* @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
|
|
3357
3404
|
* @return Array region unik
|
|
3358
3405
|
*/
|
|
3359
3406
|
function findRegions(matchDetails) {
|
|
@@ -3373,7 +3420,7 @@ function calculateSeverity(matchDetails) {
|
|
|
3373
3420
|
if (matchDetails.length === 0) {
|
|
3374
3421
|
return 0;
|
|
3375
3422
|
}
|
|
3376
|
-
const countFactor = Math.min(matchDetails.length / 10, 1);
|
|
3423
|
+
const countFactor = Math.min(matchDetails.length / 10, 1);
|
|
3377
3424
|
const categoryWeights = {
|
|
3378
3425
|
sexual: 0.9,
|
|
3379
3426
|
blasphemy: 0.9,
|
|
@@ -3401,21 +3448,9 @@ function calculateSeverity(matchDetails) {
|
|
|
3401
3448
|
* @returns FilterResult dengan hasil filter
|
|
3402
3449
|
*/
|
|
3403
3450
|
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);
|
|
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);
|
|
3419
3454
|
if (matches.length === 0) {
|
|
3420
3455
|
return {
|
|
3421
3456
|
filtered: text,
|
|
@@ -3423,123 +3458,62 @@ function filter(text, options = {}) {
|
|
|
3423
3458
|
replacements: [],
|
|
3424
3459
|
};
|
|
3425
3460
|
}
|
|
3461
|
+
const actualMatches = findProfanity.lastActualMatches || new Map();
|
|
3462
|
+
const matchDetails = findProfanityWithMetadata(text, mergedOptions);
|
|
3463
|
+
const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
|
|
3426
3464
|
let filteredText = text;
|
|
3427
3465
|
const replacements = [];
|
|
3428
|
-
|
|
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) {
|
|
3429
3487
|
const metadata = matchDetails.find((m) => m.word.toLowerCase() === word.toLowerCase() ||
|
|
3430
3488
|
(m.aliases && m.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase())));
|
|
3431
3489
|
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
|
-
}
|
|
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);
|
|
3513
3496
|
}
|
|
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
|
-
}
|
|
3497
|
+
if (detectLeetSpeak) {
|
|
3498
|
+
const leetRegex = createWordRegex(word, {
|
|
3499
|
+
wholeWord: !checkSubstring,
|
|
3500
|
+
caseSensitive: false,
|
|
3501
|
+
leetSpeak: true,
|
|
3502
|
+
detectSplit: false,
|
|
3503
|
+
indonesianVariation: false,
|
|
3541
3504
|
});
|
|
3542
|
-
|
|
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
|
+
}
|
|
3543
3517
|
}
|
|
3544
3518
|
return {
|
|
3545
3519
|
filtered: filteredText,
|
|
@@ -3781,6 +3755,13 @@ class IDProfanityFilter {
|
|
|
3781
3755
|
...options,
|
|
3782
3756
|
};
|
|
3783
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
|
+
}
|
|
3784
3765
|
/**
|
|
3785
3766
|
* Menggunakan preset yang telah ditentukan
|
|
3786
3767
|
* @param presetName Nama preset yang akan digunakan
|