@sideid/id-profanity-filter 1.1.0 → 1.9.4

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.
@@ -1,267 +1,259 @@
1
- import {
2
- ProfanityWord,
3
- ProfanityCategory,
4
- Region,
5
- FilterOptions,
6
- } from '../types';
7
-
8
- import { wordObjects, getWordsByFilter } from '../constants/wordList';
9
-
10
- export function findProfanity(
11
- text: string,
12
- options: FilterOptions = {},
13
- ): string[] {
14
- const {
15
- wordList = [],
16
- detectLeetSpeak = true,
17
- checkSubstring = false,
18
- whitelist = [],
19
- categories,
20
- regions,
21
- severityThreshold = 0,
22
- } = options;
23
-
24
- const normalizedText = normalizeText(text);
25
-
26
- let wordsToCheck: string[] = wordList;
27
-
28
- if (wordsToCheck.length === 0) {
29
- if (categories || regions || severityThreshold > 0) {
30
- wordsToCheck = wordObjects
31
- .filter((word) => {
32
- const matchCategory = categories
33
- ? categories.includes(word.category)
34
- : true;
35
- const matchRegion = regions ? regions.includes(word.region) : true;
36
- const matchSeverity = word.severity >= severityThreshold;
37
- return matchCategory && matchRegion && matchSeverity;
38
- })
39
- .map((word) => word.word);
40
- } else {
41
- wordsToCheck = wordObjects.map((word) => word.word);
42
- }
43
- }
44
-
45
- wordsToCheck = wordsToCheck.filter(
46
- (word) => !whitelist.includes(word.toLocaleLowerCase()),
47
- );
48
-
49
- if (wordsToCheck.length === 0) {
50
- return [];
51
- }
52
-
53
- const matches = new Set<string>();
54
-
55
- wordsToCheck.forEach((word) => {
56
- const pattern = checkSubstring ? word : `\\b${escapeRegExp(word)}\\b`;
57
-
58
- const regex = new RegExp(pattern, 'gi');
59
-
60
- let match;
61
- while ((match = regex.exec(normalizedText)) !== null) {
62
- matches.add(match[0].toLowerCase());
63
- }
64
- });
65
-
66
- // jika detectLeetSpeak diaktifkan, cari variasi leet speak
67
- if (detectLeetSpeak) {
68
- wordsToCheck.forEach((word) => {
69
- // Cek leet speak
70
- const leetPattern = createLeetSpeakPattern(word);
71
- const leetRegex = new RegExp(
72
- checkSubstring ? leetPattern : `\\b${leetPattern}\\b`,
73
- 'gi',
74
- );
75
-
76
- let match;
77
- while ((match = leetRegex.exec(text)) !== null) {
78
- matches.add(word.toLowerCase());
79
- }
80
-
81
- // cek karakter yang dipisah
82
- const evasionPattern = createEvasionPattern(word);
83
- const evasionRegex = new RegExp(evasionPattern, 'gi');
84
- while ((match = evasionRegex.exec(text)) !== null) {
85
- matches.add(word.toLowerCase());
86
- }
87
- });
88
- }
89
-
90
- return Array.from(matches);
91
- }
92
-
93
- /**
94
- * Menormalisasi teks untuk perbandingan
95
- *
96
- * @param text Teks untuk dinormalisasi
97
- * @returns Teks yang dinormalisasi
98
- */
99
- function normalizeText(text: string): string {
100
- return text
101
- .toLowerCase()
102
- .normalize('NFD') // Normalisasi Unicode
103
- .replace(/[\u0300-\u036f]/g, '') // Hapus diacritic marks
104
- .trim();
105
- }
106
-
107
- /**
108
- * Escape karakter khusus dalam regex
109
- *
110
- * @param string String untuk di-escape
111
- * @returns String yang sudah di-escape
112
- */
113
- function escapeRegExp(string: string): string {
114
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
115
- }
116
-
117
- /**
118
- * Pattern untuk leet speak
119
- *
120
- * @param word Kata untuk dibuat pattern leet speak
121
- * @return Pattern regex untuk leet speak
122
- */
123
- function createLeetSpeakPattern(word: string): string {
124
- const leetMap: Record<string, string[]> = {
125
- a: ['a', '4', '@'],
126
- b: ['b', '8', '6'],
127
- c: ['c', '<', '(', '{'],
128
- e: ['e', '3'],
129
- g: ['g', '9'],
130
- i: ['i', '1', '!'],
131
- l: ['l', '1', '|'],
132
- o: ['o', '0'],
133
- s: ['s', '5', '$'],
134
- t: ['t', '7', '+'],
135
- z: ['z', '2'],
136
- };
137
-
138
- return word
139
- .split('')
140
- .map((char) => {
141
- const lowerChar = char.toLowerCase();
142
- const replacements = leetMap[lowerChar];
143
-
144
- if (replacements && replacements.length > 0) {
145
- return `[${replacements.join('')}]`;
146
- } else {
147
- return escapeRegExp(char);
148
- }
149
- })
150
- .join('');
151
- }
152
-
153
- /**
154
- * Pattern untuk deteksi upaya menghindari filter
155
- *
156
- * @param word Kata untuk dibuat pattern evasion
157
- * @return Pattern regex untuk evasion
158
- */
159
- function createEvasionPattern(word: string): string {
160
- return word
161
- .split('')
162
- .map((char) => escapeRegExp(char))
163
- .join('[\\s\\-._*+]?');
164
- }
165
-
166
- /**
167
- * Mencari kata kotor lengkap dengan metadata
168
- *
169
- * @param text Teks yang akan diperika
170
- * @param options Opsi utnuk pencarian kata kotor
171
- * @return Array dari objek kata kotor yang ditemukan
172
- */
173
- export function findProfanityWithMetadata(
174
- text: string,
175
- options: FilterOptions = {},
176
- ): ProfanityWord[] {
177
- const matches = findProfanity(text, options);
178
- if (matches.length === 0) {
179
- return [];
180
- }
181
-
182
- return matches
183
- .map((word) => {
184
- const wordObject = wordObjects.find(
185
- (obj) =>
186
- obj.word.toLowerCase() === word.toLowerCase() ||
187
- (obj.aliases &&
188
- obj.aliases.some(
189
- (alias) => alias.toLowerCase() === word.toLowerCase(),
190
- )),
191
- );
192
-
193
- return wordObject;
194
- })
195
- .filter((word): word is ProfanityWord => word !== undefined);
196
- }
197
-
198
- /**
199
- * Mencari kategory kata kotor yang ada dalam teks
200
- *
201
- * @param text matchDetails Hasil pencarian dari fingProfanityWithMetadata()
202
- * @param options Opsi untuk pencarian kategori
203
- */
204
- export function findCategories(
205
- matchDetails: ProfanityWord[],
206
- ): ProfanityCategory[] {
207
- const categories = new Set<ProfanityCategory>();
208
-
209
- matchDetails.forEach((word) => {
210
- categories.add(word.category);
211
- });
212
-
213
- return Array.from(categories);
214
- }
215
-
216
- /**
217
- * Mencari region kata kotor yang ada dalam teks
218
- *
219
- * @param text matchDetails Hasil pencarian dari fingProfanityWithMetadata()
220
- * @param options Opsi untuk pencarian region
221
- */
222
- export function findRegions(matchDetails: ProfanityWord[]): Region[] {
223
- const regions = new Set<Region>();
224
-
225
- matchDetails.forEach((word) => {
226
- regions.add(word.region);
227
- });
228
-
229
- return Array.from(regions);
230
- }
231
-
232
- /**
233
- * Menghitung tingkat keparahan kata kotor yang ditemukan
234
- *
235
- * @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
236
- * @return Skor keparahan dari 0-1
237
- */
238
- export function calculateSeverity(matchDetails: ProfanityWord[]): number {
239
- if (matchDetails.length === 0) {
240
- return 0;
241
- }
242
-
243
- const countFactor = Math.min(matchDetails.length / 10, 1); // Maksimal 10 kata
244
-
245
- const categoryWeights: Record<ProfanityCategory, number> = {
246
- sexual: 0.9,
247
- blasphemy: 0.9,
248
- slur: 0.8,
249
- profanity: 0.7,
250
- insult: 0.6,
251
- drugs: 0.5,
252
- disgusting: 0.5,
253
- };
254
-
255
- let severitySum = 0;
256
- matchDetails.forEach((word) => {
257
- const categoryWeight = categoryWeights[word.category] || 0.5;
258
- const wordSeverity = word.severity * categoryWeight;
259
- severitySum += wordSeverity;
260
- });
261
-
262
- const severityAvg = severitySum / matchDetails.length;
263
-
264
- // Gabungkan jumlah kata dan keparahan rata-rata
265
- // 70% keparahan kata + 30% faktor jumlah
266
- return 0.7 * severityAvg + 0.3 * countFactor;
267
- }
1
+ import {
2
+ ProfanityWord,
3
+ ProfanityCategory,
4
+ Region,
5
+ FilterOptions,
6
+ } from "../types";
7
+
8
+ import { wordObjects, getWordsByFilter } from "../constants/wordList";
9
+ import {
10
+ normalizeText,
11
+ escapeRegExp,
12
+ containsAnyWord,
13
+ detectSplitWords,
14
+ } from "../utils/stringUtils";
15
+ import {
16
+ createWordRegex,
17
+ addLeetSpeakVariations,
18
+ addIndonesianVariations,
19
+ addSplitVariations,
20
+ } from "../utils/regexUtils";
21
+ import {
22
+ findPossibleProfanityBySimiliarity,
23
+ stringSimilarity,
24
+ } from "../utils/similarityUtils";
25
+ import { DEFAULT_OPTIONS } from "../config/options";
26
+
27
+ export function findProfanity(
28
+ text: string,
29
+ options: FilterOptions = {},
30
+ ): string[] {
31
+ const {
32
+ wordList = [],
33
+ detectLeetSpeak = true,
34
+ checkSubstring = false,
35
+ whitelist = [],
36
+ categories,
37
+ regions,
38
+ severityThreshold = 0,
39
+ indonesianVariation = false,
40
+ detectSimilarity = false,
41
+ similarityThreshold = 0.8,
42
+ detectSplit = false,
43
+ } = { ...DEFAULT_OPTIONS, ...options };
44
+
45
+ const normalizedText = normalizeText(text);
46
+
47
+ let wordsToCheck: string[] = wordList.length > 0 ? wordList : [];
48
+
49
+ if (wordsToCheck.length === 0) {
50
+ if (categories || regions || severityThreshold > 0) {
51
+ wordsToCheck = wordObjects
52
+ .filter((word) => {
53
+ const matchCategory = categories
54
+ ? categories.includes(word.category)
55
+ : true;
56
+ const matchRegion = regions ? regions.includes(word.region) : true;
57
+ const matchSeverity = word.severity >= severityThreshold;
58
+ return matchCategory && matchRegion && matchSeverity;
59
+ })
60
+ .map((word) => word.word);
61
+ } else {
62
+ wordsToCheck = wordObjects.map((word) => word.word);
63
+ }
64
+ }
65
+
66
+ wordsToCheck = wordsToCheck.filter(
67
+ (word) => !whitelist.includes(word.toLocaleLowerCase()),
68
+ );
69
+
70
+ if (wordsToCheck.length === 0) {
71
+ return [];
72
+ }
73
+
74
+ const matches = new Set<string>();
75
+
76
+ wordsToCheck.forEach((word) => {
77
+ const regex = createWordRegex(word, {
78
+ wholeWord: !checkSubstring,
79
+ caseSensitive: false,
80
+ leetSpeak: false,
81
+ detectSplit: false,
82
+ indonesianVariation: false,
83
+ });
84
+
85
+ let match;
86
+ while ((match = regex.exec(normalizedText)) !== null) {
87
+ matches.add(word.toLowerCase());
88
+ }
89
+ });
90
+
91
+ if (detectLeetSpeak) {
92
+ wordsToCheck.forEach((word) => {
93
+ const leetRegex = createWordRegex(word, {
94
+ wholeWord: !checkSubstring,
95
+ caseSensitive: false,
96
+ leetSpeak: true,
97
+ detectSplit: false,
98
+ indonesianVariation: false,
99
+ });
100
+
101
+ let match;
102
+ while ((match = leetRegex.exec(text)) !== null) {
103
+ matches.add(word.toLowerCase());
104
+ }
105
+ });
106
+ }
107
+
108
+ if (indonesianVariation) {
109
+ wordsToCheck.forEach((word) => {
110
+ const variantRegex = createWordRegex(word, {
111
+ wholeWord: !checkSubstring,
112
+ caseSensitive: false,
113
+ leetSpeak: false,
114
+ detectSplit: false,
115
+ indonesianVariation: true,
116
+ });
117
+
118
+ let match;
119
+ while ((match = variantRegex.exec(text)) !== null) {
120
+ matches.add(word.toLowerCase());
121
+ }
122
+ });
123
+ }
124
+
125
+ if (detectSplit) {
126
+ if (detectSplitWords(text, wordsToCheck)) {
127
+ wordsToCheck.forEach((word) => {
128
+ const splitRegex = createWordRegex(word, {
129
+ wholeWord: false,
130
+ caseSensitive: false,
131
+ leetSpeak: false,
132
+ detectSplit: true,
133
+ indonesianVariation: false,
134
+ });
135
+
136
+ if (splitRegex.test(text)) {
137
+ matches.add(word.toLowerCase());
138
+ }
139
+ });
140
+ }
141
+ }
142
+
143
+ if (detectSimilarity) {
144
+ const possibleProfanity = findPossibleProfanityBySimiliarity(
145
+ text,
146
+ wordsToCheck,
147
+ similarityThreshold,
148
+ );
149
+
150
+ possibleProfanity.forEach((item) => {
151
+ matches.add(item.original.toLowerCase());
152
+ });
153
+ }
154
+
155
+ return Array.from(matches);
156
+ }
157
+
158
+ /**
159
+ * Mencari kata kotor lengkap dengan metadata
160
+ *
161
+ * @param text Teks yang akan diperika
162
+ * @param options Opsi utnuk pencarian kata kotor
163
+ * @return Array dari objek kata kotor yang ditemukan
164
+ */
165
+ export function findProfanityWithMetadata(
166
+ text: string,
167
+ options: FilterOptions = {},
168
+ ): ProfanityWord[] {
169
+ const matches = findProfanity(text, options);
170
+ if (matches.length === 0) {
171
+ return [];
172
+ }
173
+
174
+ return matches
175
+ .map((word) => {
176
+ const wordObject = wordObjects.find(
177
+ (obj) =>
178
+ obj.word.toLowerCase() === word.toLowerCase() ||
179
+ (obj.aliases &&
180
+ obj.aliases.some(
181
+ (alias) => alias.toLowerCase() === word.toLowerCase(),
182
+ )),
183
+ );
184
+
185
+ return wordObject;
186
+ })
187
+ .filter((word): word is ProfanityWord => word !== undefined);
188
+ }
189
+
190
+ /**
191
+ * Mencari kategory kata kotor yang ada dalam teks
192
+ *
193
+ * @param matchDetails Hasil pencarian dari fingProfanityWithMetadata()
194
+ * @return Array kategori unik
195
+ */
196
+ export function findCategories(
197
+ matchDetails: ProfanityWord[],
198
+ ): ProfanityCategory[] {
199
+ const categories = new Set<ProfanityCategory>();
200
+
201
+ matchDetails.forEach((word) => {
202
+ categories.add(word.category);
203
+ });
204
+
205
+ return Array.from(categories);
206
+ }
207
+
208
+ /**
209
+ * Mencari region kata kotor yang ada dalam teks
210
+ *
211
+ * @param matchDetails Hasil pencarian dari fingProfanityWithMetadata()
212
+ * @return Array region unik
213
+ */
214
+ export function findRegions(matchDetails: ProfanityWord[]): Region[] {
215
+ const regions = new Set<Region>();
216
+
217
+ matchDetails.forEach((word) => {
218
+ regions.add(word.region);
219
+ });
220
+
221
+ return Array.from(regions);
222
+ }
223
+
224
+ /**
225
+ * Menghitung tingkat keparahan kata kotor yang ditemukan
226
+ *
227
+ * @param matchDetails Hasil pencarian dari findProfanityWithMetadata()
228
+ * @return Skor keparahan dari 0-1
229
+ */
230
+ export function calculateSeverity(matchDetails: ProfanityWord[]): number {
231
+ if (matchDetails.length === 0) {
232
+ return 0;
233
+ }
234
+
235
+ const countFactor = Math.min(matchDetails.length / 10, 1); // Maksimal 10 kata
236
+
237
+ const categoryWeights: Record<ProfanityCategory, number> = {
238
+ sexual: 0.9,
239
+ blasphemy: 0.9,
240
+ slur: 0.8,
241
+ profanity: 0.7,
242
+ insult: 0.6,
243
+ drugs: 0.5,
244
+ disgusting: 0.5,
245
+ };
246
+
247
+ let severitySum = 0;
248
+ matchDetails.forEach((word) => {
249
+ const categoryWeight = categoryWeights[word.category] || 0.5;
250
+ const wordSeverity = word.severity * categoryWeight;
251
+ severitySum += wordSeverity;
252
+ });
253
+
254
+ const severityAvg = severitySum / matchDetails.length;
255
+
256
+ // Gabungkan jumlah kata dan keparahan rata-rata
257
+ // 70% keparahan kata + 30% faktor jumlah
258
+ return 0.7 * severityAvg + 0.3 * countFactor;
259
+ }