@sideid/id-profanity-filter 1.11.11 → 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 +127 -38
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +127 -38
- package/dist/index.js.map +1 -1
- package/jest.config.mjs +17 -1
- package/package.json +1 -1
- package/src/core/analyzer.ts +4 -0
- package/src/core/matcher.ts +70 -14
- 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 = [],
|
|
@@ -46,6 +54,8 @@ export function findProfanity(text: string, options: FilterOptions = {}): string
|
|
|
46
54
|
maxLevenshteinDistance = 2,
|
|
47
55
|
} = { ...DEFAULT_OPTIONS, ...options };
|
|
48
56
|
|
|
57
|
+
const normalizedWhitelist = whitelist.map((w) => w.toLowerCase());
|
|
58
|
+
|
|
49
59
|
const normalizedText = normalizeText(text);
|
|
50
60
|
|
|
51
61
|
let baseWordsToCheck: string[] = wordList.length > 0 ? wordList : [];
|
|
@@ -63,21 +73,25 @@ export function findProfanity(text: string, options: FilterOptions = {}): string
|
|
|
63
73
|
|
|
64
74
|
const aliasMap = new Map<string, string>();
|
|
65
75
|
wordObjects.forEach((wordObj) => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
});
|
|
76
90
|
}
|
|
77
91
|
});
|
|
78
92
|
|
|
79
93
|
const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter(
|
|
80
|
-
(word) => !
|
|
94
|
+
(word) => !normalizedWhitelist.includes(word.toLowerCase())
|
|
81
95
|
);
|
|
82
96
|
|
|
83
97
|
if (wordsToCheck.length === 0) {
|
|
@@ -87,11 +101,29 @@ export function findProfanity(text: string, options: FilterOptions = {}): string
|
|
|
87
101
|
const matches = new Set<string>();
|
|
88
102
|
const actualMatches = new Map<string, string[]>();
|
|
89
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
|
+
|
|
90
117
|
initializeAhoCorasick(wordsToCheck);
|
|
91
118
|
|
|
92
119
|
const basicMatches = globalAhoCorasick.searchUnique(normalizedText);
|
|
93
120
|
for (const match of basicMatches) {
|
|
121
|
+
if (normalizedWhitelist.includes(match.toLowerCase())) continue;
|
|
122
|
+
|
|
94
123
|
const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
|
|
124
|
+
|
|
125
|
+
if (!passesFilters(originalWord)) continue;
|
|
126
|
+
|
|
95
127
|
matches.add(originalWord);
|
|
96
128
|
|
|
97
129
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -112,7 +144,13 @@ export function findProfanity(text: string, options: FilterOptions = {}): string
|
|
|
112
144
|
|
|
113
145
|
let match;
|
|
114
146
|
while ((match = leetRegex.exec(text)) !== null) {
|
|
147
|
+
const matchedText = match[0];
|
|
148
|
+
if (normalizedWhitelist.includes(matchedText.toLowerCase())) continue;
|
|
149
|
+
|
|
115
150
|
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
151
|
+
|
|
152
|
+
if (!passesFilters(originalWord)) continue;
|
|
153
|
+
|
|
116
154
|
matches.add(originalWord);
|
|
117
155
|
|
|
118
156
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -135,7 +173,13 @@ export function findProfanity(text: string, options: FilterOptions = {}): string
|
|
|
135
173
|
|
|
136
174
|
let match;
|
|
137
175
|
while ((match = variantRegex.exec(text)) !== null) {
|
|
176
|
+
const matchedText = match[0];
|
|
177
|
+
if (normalizedWhitelist.includes(matchedText.toLowerCase())) continue;
|
|
178
|
+
|
|
138
179
|
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
180
|
+
|
|
181
|
+
if (!passesFilters(originalWord)) continue;
|
|
182
|
+
|
|
139
183
|
matches.add(originalWord);
|
|
140
184
|
|
|
141
185
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -158,7 +202,13 @@ export function findProfanity(text: string, options: FilterOptions = {}): string
|
|
|
158
202
|
|
|
159
203
|
let match;
|
|
160
204
|
while ((match = splitRegex.exec(text)) !== null) {
|
|
205
|
+
const matchedText = match[0];
|
|
206
|
+
if (normalizedWhitelist.includes(matchedText.toLowerCase())) continue;
|
|
207
|
+
|
|
161
208
|
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
209
|
+
|
|
210
|
+
if (!passesFilters(originalWord)) continue;
|
|
211
|
+
|
|
162
212
|
matches.add(originalWord);
|
|
163
213
|
|
|
164
214
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -179,8 +229,12 @@ export function findProfanity(text: string, options: FilterOptions = {}): string
|
|
|
179
229
|
);
|
|
180
230
|
|
|
181
231
|
possibleProfanity.forEach((item) => {
|
|
232
|
+
if (normalizedWhitelist.includes(item.word.toLowerCase())) return;
|
|
233
|
+
|
|
182
234
|
const originalWord =
|
|
183
235
|
aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
|
|
236
|
+
if (!passesFilters(originalWord)) return;
|
|
237
|
+
|
|
184
238
|
matches.add(originalWord);
|
|
185
239
|
|
|
186
240
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -196,10 +250,14 @@ export function findProfanity(text: string, options: FilterOptions = {}): string
|
|
|
196
250
|
);
|
|
197
251
|
|
|
198
252
|
possibleProfanity.forEach((item) => {
|
|
199
|
-
|
|
253
|
+
if (normalizedWhitelist.includes(item.word.toLowerCase())) return;
|
|
200
254
|
|
|
201
255
|
const originalWord =
|
|
202
256
|
aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
|
|
257
|
+
if (!passesFilters(originalWord)) return;
|
|
258
|
+
|
|
259
|
+
matches.add(originalWord);
|
|
260
|
+
|
|
203
261
|
if (!actualMatches.has(originalWord)) {
|
|
204
262
|
actualMatches.set(originalWord, []);
|
|
205
263
|
}
|
|
@@ -306,7 +364,5 @@ export function calculateSeverity(matchDetails: ProfanityWord[]): number {
|
|
|
306
364
|
|
|
307
365
|
const severityAvg = severitySum / matchDetails.length;
|
|
308
366
|
|
|
309
|
-
// Gabungkan jumlah kata dan keparahan rata-rata
|
|
310
|
-
// 70% keparahan kata + 30% faktor jumlah
|
|
311
367
|
return 0.7 * severityAvg + 0.3 * countFactor;
|
|
312
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
|
+
});
|