@sideid/id-profanity-filter 1.11.12 → 1.11.13
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/dist/index.esm.js +101 -45
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +101 -45
- package/dist/index.js.map +1 -1
- package/jest.config.mjs +17 -1
- package/package.json +1 -1
- package/src/core/matcher.ts +46 -21
- package/src/utils/regexUtils.ts +80 -30
- package/test/analyzer.test.ts +89 -0
- package/test/filter.test.ts +80 -0
- package/test/jest.setup.ts +9 -0
- package/test/matcher.test.ts +120 -0
- package/test/profanity-filter.test.ts +130 -0
- package/test/utils/ahoCorasick.test.ts +106 -0
- package/test/utils/regexUtils.test.ts +111 -0
- package/test/utils/similarityUtils.test.ts +139 -0
- package/test/utils/stringUtils.test.ts +153 -0
package/src/core/matcher.ts
CHANGED
|
@@ -29,6 +29,14 @@ interface FindProfanityFunction {
|
|
|
29
29
|
lastActualMatches?: Map<string, string[]>;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function getWordMetadata(word: string): ProfanityWord | undefined {
|
|
33
|
+
return wordObjects.find(
|
|
34
|
+
(obj) =>
|
|
35
|
+
obj.word.toLowerCase() === word.toLowerCase() ||
|
|
36
|
+
(obj.aliases && obj.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase()))
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
export function findProfanity(text: string, options: FilterOptions = {}): string[] {
|
|
33
41
|
const {
|
|
34
42
|
wordList = [],
|
|
@@ -65,21 +73,25 @@ export function findProfanity(text: string, options: FilterOptions = {}): string
|
|
|
65
73
|
|
|
66
74
|
const aliasMap = new Map<string, string>();
|
|
67
75
|
wordObjects.forEach((wordObj) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
const matchCategory = categories ? categories.includes(wordObj.category) : true;
|
|
77
|
+
const matchRegion = regions ? regions.includes(wordObj.region) : true;
|
|
78
|
+
const matchSeverity = wordObj.severity >= severityThreshold;
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
matchCategory &&
|
|
82
|
+
matchRegion &&
|
|
83
|
+
matchSeverity &&
|
|
84
|
+
wordObj.aliases &&
|
|
85
|
+
wordObj.aliases.length > 0
|
|
86
|
+
) {
|
|
87
|
+
wordObj.aliases.forEach((alias) => {
|
|
88
|
+
aliasMap.set(alias.toLowerCase(), wordObj.word.toLowerCase());
|
|
89
|
+
});
|
|
78
90
|
}
|
|
79
91
|
});
|
|
80
92
|
|
|
81
93
|
const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter(
|
|
82
|
-
(word) => !
|
|
94
|
+
(word) => !normalizedWhitelist.includes(word.toLowerCase())
|
|
83
95
|
);
|
|
84
96
|
|
|
85
97
|
if (wordsToCheck.length === 0) {
|
|
@@ -89,6 +101,19 @@ export function findProfanity(text: string, options: FilterOptions = {}): string
|
|
|
89
101
|
const matches = new Set<string>();
|
|
90
102
|
const actualMatches = new Map<string, string[]>();
|
|
91
103
|
|
|
104
|
+
const passesFilters = (word: string): boolean => {
|
|
105
|
+
if (normalizedWhitelist.includes(word.toLowerCase())) return false;
|
|
106
|
+
|
|
107
|
+
const metadata = getWordMetadata(word);
|
|
108
|
+
if (!metadata) return false;
|
|
109
|
+
|
|
110
|
+
const matchCategory = categories ? categories.includes(metadata.category) : true;
|
|
111
|
+
const matchRegion = regions ? regions.includes(metadata.region) : true;
|
|
112
|
+
const matchSeverity = metadata.severity >= severityThreshold;
|
|
113
|
+
|
|
114
|
+
return matchCategory && matchRegion && matchSeverity;
|
|
115
|
+
};
|
|
116
|
+
|
|
92
117
|
initializeAhoCorasick(wordsToCheck);
|
|
93
118
|
|
|
94
119
|
const basicMatches = globalAhoCorasick.searchUnique(normalizedText);
|
|
@@ -96,7 +121,8 @@ export function findProfanity(text: string, options: FilterOptions = {}): string
|
|
|
96
121
|
if (normalizedWhitelist.includes(match.toLowerCase())) continue;
|
|
97
122
|
|
|
98
123
|
const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
|
|
99
|
-
|
|
124
|
+
|
|
125
|
+
if (!passesFilters(originalWord)) continue;
|
|
100
126
|
|
|
101
127
|
matches.add(originalWord);
|
|
102
128
|
|
|
@@ -122,7 +148,8 @@ export function findProfanity(text: string, options: FilterOptions = {}): string
|
|
|
122
148
|
if (normalizedWhitelist.includes(matchedText.toLowerCase())) continue;
|
|
123
149
|
|
|
124
150
|
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
125
|
-
|
|
151
|
+
|
|
152
|
+
if (!passesFilters(originalWord)) continue;
|
|
126
153
|
|
|
127
154
|
matches.add(originalWord);
|
|
128
155
|
|
|
@@ -150,7 +177,8 @@ export function findProfanity(text: string, options: FilterOptions = {}): string
|
|
|
150
177
|
if (normalizedWhitelist.includes(matchedText.toLowerCase())) continue;
|
|
151
178
|
|
|
152
179
|
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
153
|
-
|
|
180
|
+
|
|
181
|
+
if (!passesFilters(originalWord)) continue;
|
|
154
182
|
|
|
155
183
|
matches.add(originalWord);
|
|
156
184
|
|
|
@@ -178,7 +206,8 @@ export function findProfanity(text: string, options: FilterOptions = {}): string
|
|
|
178
206
|
if (normalizedWhitelist.includes(matchedText.toLowerCase())) continue;
|
|
179
207
|
|
|
180
208
|
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
181
|
-
|
|
209
|
+
|
|
210
|
+
if (!passesFilters(originalWord)) continue;
|
|
182
211
|
|
|
183
212
|
matches.add(originalWord);
|
|
184
213
|
|
|
@@ -200,12 +229,11 @@ export function findProfanity(text: string, options: FilterOptions = {}): string
|
|
|
200
229
|
);
|
|
201
230
|
|
|
202
231
|
possibleProfanity.forEach((item) => {
|
|
203
|
-
// Check if the matched word is in whitelist
|
|
204
232
|
if (normalizedWhitelist.includes(item.word.toLowerCase())) return;
|
|
205
233
|
|
|
206
234
|
const originalWord =
|
|
207
235
|
aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
|
|
208
|
-
if (
|
|
236
|
+
if (!passesFilters(originalWord)) return;
|
|
209
237
|
|
|
210
238
|
matches.add(originalWord);
|
|
211
239
|
|
|
@@ -222,12 +250,11 @@ export function findProfanity(text: string, options: FilterOptions = {}): string
|
|
|
222
250
|
);
|
|
223
251
|
|
|
224
252
|
possibleProfanity.forEach((item) => {
|
|
225
|
-
// Check if the matched word is in whitelist
|
|
226
253
|
if (normalizedWhitelist.includes(item.word.toLowerCase())) return;
|
|
227
254
|
|
|
228
255
|
const originalWord =
|
|
229
256
|
aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
|
|
230
|
-
if (
|
|
257
|
+
if (!passesFilters(originalWord)) return;
|
|
231
258
|
|
|
232
259
|
matches.add(originalWord);
|
|
233
260
|
|
|
@@ -337,7 +364,5 @@ export function calculateSeverity(matchDetails: ProfanityWord[]): number {
|
|
|
337
364
|
|
|
338
365
|
const severityAvg = severitySum / matchDetails.length;
|
|
339
366
|
|
|
340
|
-
// Gabungkan jumlah kata dan keparahan rata-rata
|
|
341
|
-
// 70% keparahan kata + 30% faktor jumlah
|
|
342
367
|
return 0.7 * severityAvg + 0.3 * countFactor;
|
|
343
368
|
}
|
package/src/utils/regexUtils.ts
CHANGED
|
@@ -58,14 +58,14 @@ export function addLeetSpeakVariations(pattern: string): string {
|
|
|
58
58
|
const leetMap: Record<string, string[]> = {
|
|
59
59
|
a: ['a', '4', '@'],
|
|
60
60
|
b: ['b', '8', '6'],
|
|
61
|
-
c: ['c', '(', '{', '<'],
|
|
61
|
+
c: ['c', '\\(', '\\{', '<'],
|
|
62
62
|
e: ['e', '3'],
|
|
63
63
|
g: ['g', '6', '9'],
|
|
64
|
-
i: ['i', '1', '!', '
|
|
65
|
-
l: ['l', '1', '
|
|
64
|
+
i: ['i', '1', '!', '\\|'],
|
|
65
|
+
l: ['l', '1', '\\|'],
|
|
66
66
|
o: ['o', '0'],
|
|
67
|
-
s: ['s', '5', '
|
|
68
|
-
t: ['t', '7', '
|
|
67
|
+
s: ['s', '5', '\\$'],
|
|
68
|
+
t: ['t', '7', '\\+'],
|
|
69
69
|
z: ['z', '2'],
|
|
70
70
|
};
|
|
71
71
|
|
|
@@ -91,8 +91,23 @@ export function addLeetSpeakVariations(pattern: string): string {
|
|
|
91
91
|
* @returns Pola regex dengan kemungkinan split
|
|
92
92
|
*/
|
|
93
93
|
export function addSplitVariations(pattern: string): string {
|
|
94
|
-
//
|
|
95
|
-
|
|
94
|
+
// Instead of joining character by character with a separator pattern,
|
|
95
|
+
// we'll create a simpler version that matches the pattern with optional separators
|
|
96
|
+
|
|
97
|
+
// Convert each character to a pattern that allows optional separators before it
|
|
98
|
+
// except for the first character
|
|
99
|
+
let result = '';
|
|
100
|
+
const chars = pattern.split('');
|
|
101
|
+
|
|
102
|
+
for (let i = 0; i < chars.length; i++) {
|
|
103
|
+
if (i > 0) {
|
|
104
|
+
// Add optional separator before each character except the first
|
|
105
|
+
result += '[\\s\\-._*+]?';
|
|
106
|
+
}
|
|
107
|
+
result += chars[i];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return result;
|
|
96
111
|
}
|
|
97
112
|
|
|
98
113
|
/**
|
|
@@ -103,10 +118,24 @@ export function addSplitVariations(pattern: string): string {
|
|
|
103
118
|
* @returns Objek RegExp
|
|
104
119
|
*/
|
|
105
120
|
export function createEvasionRegex(word: string): RegExp {
|
|
106
|
-
//
|
|
107
|
-
const
|
|
121
|
+
// Escape karakter khusus regex
|
|
122
|
+
const escaped = escapeRegExp(word);
|
|
108
123
|
|
|
109
|
-
|
|
124
|
+
// Create a regex pattern that allows any separator between characters
|
|
125
|
+
let result = '';
|
|
126
|
+
const chars = escaped.split('');
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < chars.length; i++) {
|
|
129
|
+
// Add the character
|
|
130
|
+
result += chars[i];
|
|
131
|
+
|
|
132
|
+
// Add optional separator after each character except the last
|
|
133
|
+
if (i < chars.length - 1) {
|
|
134
|
+
result += '[\\s\\-._*+]?';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return new RegExp(result, 'gi');
|
|
110
139
|
}
|
|
111
140
|
|
|
112
141
|
/**
|
|
@@ -120,7 +149,7 @@ export function addIndonesianVariations(pattern: string): string {
|
|
|
120
149
|
const variationMap: Record<string, string[]> = {
|
|
121
150
|
c: ['c', 'k'], // contoh: becok/bekok
|
|
122
151
|
k: ['k', 'c', 'q'], // contoh: kacau/qacau
|
|
123
|
-
j: ['j', '
|
|
152
|
+
j: ['j', 'd'], // contoh: jualan/dualan (ejaan lama)
|
|
124
153
|
y: ['y', 'j'], // contoh: ya/ja
|
|
125
154
|
u: ['u', 'oe'], // contoh: untuk/oentoek (ejaan lama)
|
|
126
155
|
f: ['f', 'p', 'v'], // contoh: kafir/kapir
|
|
@@ -128,20 +157,21 @@ export function addIndonesianVariations(pattern: string): string {
|
|
|
128
157
|
x: ['x', 'ks'], // contoh: taxi/taksi
|
|
129
158
|
};
|
|
130
159
|
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
160
|
+
// Go through each character in the pattern and replace with variations
|
|
161
|
+
let result = '';
|
|
162
|
+
for (const char of pattern) {
|
|
163
|
+
const lowerChar = char.toLowerCase();
|
|
164
|
+
const variations = variationMap[lowerChar];
|
|
165
|
+
|
|
166
|
+
if (variations && variations.length > 1) {
|
|
167
|
+
// Create a character class with all variations
|
|
168
|
+
result += `[${variations.join('')}]`;
|
|
169
|
+
} else {
|
|
170
|
+
result += char;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
141
173
|
|
|
142
|
-
|
|
143
|
-
})
|
|
144
|
-
.join('');
|
|
174
|
+
return result;
|
|
145
175
|
}
|
|
146
176
|
|
|
147
177
|
/**
|
|
@@ -151,7 +181,8 @@ export function addIndonesianVariations(pattern: string): string {
|
|
|
151
181
|
* @returns Objek RegExp
|
|
152
182
|
*/
|
|
153
183
|
export function createIndonesianVariationRegex(word: string): RegExp {
|
|
154
|
-
const
|
|
184
|
+
const escapedWord = escapeRegExp(word);
|
|
185
|
+
const pattern = addIndonesianVariations(escapedWord);
|
|
155
186
|
return new RegExp(`\\b${pattern}\\b`, 'gi');
|
|
156
187
|
}
|
|
157
188
|
|
|
@@ -179,18 +210,37 @@ export function createContextRegex(word: string, contextSize: number = 3): RegEx
|
|
|
179
210
|
*/
|
|
180
211
|
export function createWordFormRegex(word: string): RegExp {
|
|
181
212
|
// Implementasi sederhana untuk mencocokkan berbagai imbuhan
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
213
|
+
const escapedWord = escapeRegExp(word);
|
|
214
|
+
|
|
215
|
+
// Common Indonesian prefixes and suffixes
|
|
216
|
+
const prefixes = [
|
|
217
|
+
'',
|
|
218
|
+
'me',
|
|
219
|
+
'pe',
|
|
220
|
+
'ber',
|
|
221
|
+
'di',
|
|
222
|
+
'ter',
|
|
223
|
+
'se',
|
|
224
|
+
'ke',
|
|
225
|
+
'mem',
|
|
226
|
+
'pem',
|
|
227
|
+
'bel',
|
|
228
|
+
'peng',
|
|
229
|
+
'meng',
|
|
230
|
+
];
|
|
231
|
+
const suffixes = ['', 'kan', 'an', 'i', 'nya', 'lah', 'kah'];
|
|
185
232
|
|
|
186
233
|
const patterns = [];
|
|
187
234
|
|
|
188
|
-
// Kombinasikan prefix dan suffix
|
|
189
235
|
for (const prefix of prefixes) {
|
|
190
236
|
for (const suffix of suffixes) {
|
|
191
|
-
patterns.push(`\\b${prefix}${
|
|
237
|
+
patterns.push(`\\b${prefix}${escapedWord}${suffix}\\b`);
|
|
192
238
|
}
|
|
193
239
|
}
|
|
194
240
|
|
|
241
|
+
if (/^[aiueo]/.test(word)) {
|
|
242
|
+
patterns.push(`\\bng${escapedWord}\\b`);
|
|
243
|
+
}
|
|
244
|
+
|
|
195
245
|
return new RegExp(patterns.join('|'), 'gi');
|
|
196
246
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { analyze, batchAnalyze, analyzeBySentence, analyzeWithContext } from '../src/core/analyzer';
|
|
2
|
+
|
|
3
|
+
describe('Analyzer Core', () => {
|
|
4
|
+
describe('analyze function', () => {
|
|
5
|
+
test('should correctly analyze clean text', () => {
|
|
6
|
+
const result = analyze('Ini adalah kalimat yang sopan');
|
|
7
|
+
expect(result.hasProfanity).toBe(false);
|
|
8
|
+
expect(result.matches).toEqual([]);
|
|
9
|
+
expect(result.severityScore).toBe(0);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('should correctly analyze text with profanity', () => {
|
|
13
|
+
const result = analyze('Dasar anjing!');
|
|
14
|
+
expect(result.hasProfanity).toBe(true);
|
|
15
|
+
expect(result.matches).toContain('anjing');
|
|
16
|
+
expect(result.severityScore).toBeGreaterThan(0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('should correctly analyze text with multiple profanities', () => {
|
|
20
|
+
const result = analyze('Dasar anjing dan babi!');
|
|
21
|
+
expect(result.hasProfanity).toBe(true);
|
|
22
|
+
expect(result.matches).toContain('anjing');
|
|
23
|
+
expect(result.matches).toContain('babi');
|
|
24
|
+
expect(result.matches.length).toBe(2);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should provide region information', () => {
|
|
28
|
+
// Assuming "anjing" is categorized as 'general' region
|
|
29
|
+
const result = analyze('anjing');
|
|
30
|
+
expect(result.regions).toContain('general');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('should provide category information', () => {
|
|
34
|
+
// Assuming "anjing" is categorized as 'profanity'
|
|
35
|
+
const result = analyze('anjing');
|
|
36
|
+
expect(result.categories).toContain('profanity');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('should detect similar words when option enabled', () => {
|
|
40
|
+
const result = analyze('anjiing', { detectSimilarity: true });
|
|
41
|
+
expect(result.hasProfanity).toBe(true);
|
|
42
|
+
expect(result.similarWords).toBeDefined();
|
|
43
|
+
expect(result.similarWords!.length).toBeGreaterThan(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should respect whitelist', () => {
|
|
47
|
+
const result = analyze('anjing', { whitelist: ['anjing'] });
|
|
48
|
+
expect(result.hasProfanity).toBe(false);
|
|
49
|
+
expect(result.matches).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('batchAnalyze function', () => {
|
|
54
|
+
test('should analyze multiple texts', () => {
|
|
55
|
+
const texts = ['Ini kalimat bersih', 'Dasar anjing kamu!', 'Babi itu jorok'];
|
|
56
|
+
|
|
57
|
+
const result = batchAnalyze(texts);
|
|
58
|
+
expect(result.totalTexts).toBe(3);
|
|
59
|
+
expect(result.profaneTexts).toBe(2);
|
|
60
|
+
expect(result.cleanTexts).toBe(1);
|
|
61
|
+
expect(result.mostFrequentWords.length).toBeGreaterThan(0);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('analyzeBySentence function', () => {
|
|
66
|
+
test('should analyze text by sentence', () => {
|
|
67
|
+
const text =
|
|
68
|
+
'Kalimat pertama bersih. Kalimat kedua anjing kotor. Kalimat ketiga juga bersih.';
|
|
69
|
+
const results = analyzeBySentence(text);
|
|
70
|
+
|
|
71
|
+
expect(results.length).toBe(3);
|
|
72
|
+
expect(results[0].hasProfanity).toBe(false);
|
|
73
|
+
expect(results[1].hasProfanity).toBe(true);
|
|
74
|
+
expect(results[1].matches).toContain('anjing');
|
|
75
|
+
expect(results[2].hasProfanity).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('analyzeWithContext function', () => {
|
|
80
|
+
test('should provide context around profanity', () => {
|
|
81
|
+
const text = 'Saya sangat kesal dengan perilaku anjing tersebut karena sangat mengganggu';
|
|
82
|
+
const results = analyzeWithContext(text, 3);
|
|
83
|
+
|
|
84
|
+
expect(results.length).toBe(1);
|
|
85
|
+
expect(results[0].word).toBe('anjing');
|
|
86
|
+
expect(results[0].context).toContain('perilaku anjing tersebut');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { filter, isProfane } from '../src/core/filter';
|
|
2
|
+
|
|
3
|
+
describe('Filter Core', () => {
|
|
4
|
+
describe('filter function', () => {
|
|
5
|
+
test('should filter profanity with default settings', () => {
|
|
6
|
+
const result = filter('Dasar anjing kamu!');
|
|
7
|
+
expect(result.filtered).toBe('Dasar ****** kamu!');
|
|
8
|
+
expect(result.censored).toBe(1);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('should not filter clean text', () => {
|
|
12
|
+
const cleanText = 'Ini adalah kalimat yang sopan';
|
|
13
|
+
const result = filter(cleanText);
|
|
14
|
+
expect(result.filtered).toBe(cleanText);
|
|
15
|
+
expect(result.censored).toBe(0);
|
|
16
|
+
expect(result.replacements).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('should filter multiple profanities', () => {
|
|
20
|
+
const text = 'Dasar anjing dan babi!';
|
|
21
|
+
const result = filter(text);
|
|
22
|
+
expect(result.filtered).not.toBe(text);
|
|
23
|
+
expect(result.censored).toBe(2);
|
|
24
|
+
expect(result.replacements.length).toBe(2);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should respect custom replacement character', () => {
|
|
28
|
+
const result = filter('Dasar anjing!', { replaceWith: '#' });
|
|
29
|
+
expect(result.filtered).toBe('Dasar ######!');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('should handle keepFirstAndLast option', () => {
|
|
33
|
+
const result = filter('anjing', {
|
|
34
|
+
keepFirstAndLast: true,
|
|
35
|
+
fullWordCensor: false,
|
|
36
|
+
});
|
|
37
|
+
expect(result.filtered).toBe('a****g');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// test('should handle random grawlix option', () => {
|
|
41
|
+
// const result = filter('anjing', { useRandomGrawlix: true });
|
|
42
|
+
// expect(result.filtered).not.toBe('anjing');
|
|
43
|
+
// expect(result.filtered.length).toBe(6);
|
|
44
|
+
// });
|
|
45
|
+
|
|
46
|
+
test('should use whitelist', () => {
|
|
47
|
+
const text = 'Anjing adalah binatang peliharaan yang setia';
|
|
48
|
+
|
|
49
|
+
// Without whitelist
|
|
50
|
+
const withoutWhitelist = filter(text);
|
|
51
|
+
expect(withoutWhitelist.censored).toBe(1);
|
|
52
|
+
|
|
53
|
+
// With whitelist
|
|
54
|
+
const withWhitelist = filter(text, { whitelist: ['anjing'] });
|
|
55
|
+
expect(withWhitelist.censored).toBe(0);
|
|
56
|
+
expect(withWhitelist.filtered).toBe(text);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('isProfane function', () => {
|
|
61
|
+
test('should detect profanity', () => {
|
|
62
|
+
expect(isProfane('anjing')).toBe(true);
|
|
63
|
+
expect(isProfane('babi')).toBe(true);
|
|
64
|
+
expect(isProfane('kucing')).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('should respect whitelist', () => {
|
|
68
|
+
expect(isProfane('anjing', { whitelist: ['anjing'] })).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('should detect based on threshold', () => {
|
|
72
|
+
// Assuming "kontol" has a higher severity than "babi"
|
|
73
|
+
expect(isProfane('kontol', { severityThreshold: 0.8 })).toBe(true);
|
|
74
|
+
|
|
75
|
+
// This might fail if the severity isn't configured as expected
|
|
76
|
+
// Just an example of how the test would work
|
|
77
|
+
expect(isProfane('babi', { severityThreshold: 0.7 })).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Any global setup code can go here
|
|
2
|
+
|
|
3
|
+
// For example, if you need to mock some global objects:
|
|
4
|
+
// global.fetch = jest.fn();
|
|
5
|
+
|
|
6
|
+
// Or silence console logs during tests
|
|
7
|
+
// global.console.log = jest.fn();
|
|
8
|
+
// global.console.warn = jest.fn();
|
|
9
|
+
// global.console.error = jest.fn();
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findProfanity,
|
|
3
|
+
findProfanityWithMetadata,
|
|
4
|
+
findCategories,
|
|
5
|
+
findRegions,
|
|
6
|
+
calculateSeverity,
|
|
7
|
+
} from '../src/core/matcher';
|
|
8
|
+
|
|
9
|
+
describe('Matcher Core', () => {
|
|
10
|
+
describe('findProfanity function', () => {
|
|
11
|
+
test('should find profanity in text', () => {
|
|
12
|
+
const results = findProfanity('Dasar anjing!');
|
|
13
|
+
expect(results).toContain('anjing');
|
|
14
|
+
expect(results.length).toBe(1);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('should find multiple profanities', () => {
|
|
18
|
+
const results = findProfanity('Dasar anjing dan babi!');
|
|
19
|
+
expect(results).toContain('anjing');
|
|
20
|
+
expect(results).toContain('babi');
|
|
21
|
+
expect(results.length).toBe(2);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('should detect leetspeak variations', () => {
|
|
25
|
+
const results = findProfanity('Dasar 4nj1ng!', { detectLeetSpeak: true });
|
|
26
|
+
expect(results).toContain('anjing');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('should detect Indonesian variations', () => {
|
|
30
|
+
const results = findProfanity('Kamu itu anjic!', { indonesianVariation: true });
|
|
31
|
+
expect(results).toContain('anjing');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('should detect split words', () => {
|
|
35
|
+
const results = findProfanity('a-n-j-i-n-g', { detectSplit: true });
|
|
36
|
+
expect(results).toContain('anjing');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('should respect whitelist', () => {
|
|
40
|
+
const results = findProfanity('anjing', { whitelist: ['anjing'] });
|
|
41
|
+
expect(results).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should filter by category', () => {
|
|
45
|
+
// Assuming "kontol" is sexual and "anjing" is profanity
|
|
46
|
+
const resultsWithSexual = findProfanity('memek dan anjing', {
|
|
47
|
+
categories: ['sexual'],
|
|
48
|
+
});
|
|
49
|
+
expect(resultsWithSexual).toContain('memek');
|
|
50
|
+
expect(resultsWithSexual).not.toContain('anjing');
|
|
51
|
+
|
|
52
|
+
const resultsWithProfanity = findProfanity('kontol dan anjing', {
|
|
53
|
+
categories: ['profanity'],
|
|
54
|
+
});
|
|
55
|
+
expect(resultsWithProfanity).toContain('anjing');
|
|
56
|
+
expect(resultsWithProfanity).not.toContain('kontol');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('should filter by region', () => {
|
|
60
|
+
// Assuming "jancok" is from jawa region and "anjing" is general
|
|
61
|
+
const resultsWithJawa = findProfanity('jancok dan anjing', { regions: ['jawa'] });
|
|
62
|
+
expect(resultsWithJawa).toContain('jancok');
|
|
63
|
+
expect(resultsWithJawa).not.toContain('anjing');
|
|
64
|
+
|
|
65
|
+
const resultsWithGeneral = findProfanity('jancok dan anjing', { regions: ['general'] });
|
|
66
|
+
expect(resultsWithGeneral).toContain('anjing');
|
|
67
|
+
expect(resultsWithGeneral).not.toContain('jancok');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('findProfanityWithMetadata function', () => {
|
|
72
|
+
test('should return metadata for profanity', () => {
|
|
73
|
+
const results = findProfanityWithMetadata('anjing');
|
|
74
|
+
expect(results.length).toBe(1);
|
|
75
|
+
expect(results[0].word).toBe('anjing');
|
|
76
|
+
expect(results[0].category).toBeDefined();
|
|
77
|
+
expect(results[0].region).toBeDefined();
|
|
78
|
+
expect(results[0].severity).toBeDefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('findCategories function', () => {
|
|
83
|
+
test('should extract unique categories', () => {
|
|
84
|
+
const metadata = findProfanityWithMetadata('anjing dan kontol');
|
|
85
|
+
const categories = findCategories(metadata);
|
|
86
|
+
|
|
87
|
+
// Assuming "anjing" is profanity and "kontol" is sexual
|
|
88
|
+
expect(categories.includes('profanity')).toBe(true);
|
|
89
|
+
expect(categories.includes('sexual')).toBe(true);
|
|
90
|
+
expect(categories.length).toBe(2); // Should be unique
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('findRegions function', () => {
|
|
95
|
+
test('should extract unique regions', () => {
|
|
96
|
+
const metadata = findProfanityWithMetadata('anjing dan jancok');
|
|
97
|
+
const regions = findRegions(metadata);
|
|
98
|
+
|
|
99
|
+
// Assuming "anjing" is general and "jancok" is jawa
|
|
100
|
+
expect(regions.includes('general')).toBe(true);
|
|
101
|
+
expect(regions.includes('jawa')).toBe(true);
|
|
102
|
+
expect(regions.length).toBe(2); // Should be unique
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('calculateSeverity function', () => {
|
|
107
|
+
test('should calculate severity score', () => {
|
|
108
|
+
const metadata = findProfanityWithMetadata('anjing');
|
|
109
|
+
const severity = calculateSeverity(metadata);
|
|
110
|
+
|
|
111
|
+
expect(severity).toBeGreaterThanOrEqual(0);
|
|
112
|
+
expect(severity).toBeLessThanOrEqual(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('should return 0 for empty metadata', () => {
|
|
116
|
+
const severity = calculateSeverity([]);
|
|
117
|
+
expect(severity).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|