@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.
@@ -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
- if (wordObj.aliases && wordObj.aliases.length > 0) {
69
- const matchCategory = categories ? categories.includes(wordObj.category) : true;
70
- const matchRegion = regions ? regions.includes(wordObj.region) : true;
71
- const matchSeverity = wordObj.severity >= severityThreshold;
72
-
73
- if (matchCategory && matchRegion && matchSeverity) {
74
- wordObj.aliases.forEach((alias) => {
75
- aliasMap.set(alias.toLowerCase(), wordObj.word.toLowerCase());
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) => !whitelist.includes(word.toLowerCase())
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
- if (normalizedWhitelist.includes(originalWord)) continue;
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
- if (normalizedWhitelist.includes(originalWord)) continue;
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
- if (normalizedWhitelist.includes(originalWord)) continue;
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
- if (normalizedWhitelist.includes(originalWord)) continue;
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 (normalizedWhitelist.includes(originalWord)) return;
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 (normalizedWhitelist.includes(originalWord)) return;
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
  }
@@ -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
- // Tambahkan kemungkinan spasi atau karakter penghubung di antara setiap huruf
95
- return pattern.split('').join('[\\s\\-._*+]?');
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
- // Tambahkan kemungkinan spasi atau karakter penghubung di antara setiap huruf
107
- const pattern = addSplitVariations(escapeRegExp(word));
121
+ // Escape karakter khusus regex
122
+ const escaped = escapeRegExp(word);
108
123
 
109
- return new RegExp(pattern, 'gi');
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', 'dj'], // contoh: jualan/djualan (ejaan lama)
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
- // Ganti tiap karakter dengan variasinya
132
- return pattern
133
- .split('')
134
- .map((char) => {
135
- const lowerChar = char.toLowerCase();
136
- const variations = variationMap[lowerChar];
137
-
138
- if (variations && variations.length > 1) {
139
- return `[${variations.join('')}]`;
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
- return char;
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 pattern = addIndonesianVariations(escapeRegExp(word));
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
- // Ini bisa dikembangkan lebih lanjut untuk mencocokkan bentukan kata yang lebih kompleks
183
- const prefixes = ['', 'me', 'pe', 'ber', 'di', 'ter', 'se'];
184
- const suffixes = ['', 'kan', 'an', 'i', 'nya'];
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}${escapeRegExp(word)}${suffix}\\b`);
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
+ });