@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.
- package/package.json +1 -1
- package/src/config/options.ts +178 -0
- package/src/constants/categories/index.ts +31 -31
- package/src/constants/categories/insult.ts +114 -114
- package/src/constants/categories/sexual.ts +99 -100
- package/src/constants/regions/batak.ts +17 -17
- package/src/constants/regions/betawi.ts +39 -39
- package/src/constants/regions/general.ts +111 -111
- package/src/constants/regions/index.ts +62 -62
- package/src/constants/regions/jawa.ts +102 -102
- package/src/constants/regions/sunda.ts +35 -35
- package/src/constants/wordList.ts +125 -125
- package/src/core/analyzer.ts +225 -204
- package/src/core/filter.ts +129 -129
- package/src/core/matcher.ts +259 -267
- package/src/index.ts +188 -133
- package/src/types/index.ts +88 -115
- package/src/utils/regexUtils.ts +203 -0
- package/src/utils/similarityUtils.ts +195 -0
- package/src/utils/stringUtils.ts +213 -0
package/src/core/matcher.ts
CHANGED
|
@@ -1,267 +1,259 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ProfanityWord,
|
|
3
|
-
ProfanityCategory,
|
|
4
|
-
Region,
|
|
5
|
-
FilterOptions,
|
|
6
|
-
} from
|
|
7
|
-
|
|
8
|
-
import { wordObjects, getWordsByFilter } from
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (wordsToCheck.length === 0) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
+
}
|