@sideid/id-profanity-filter 1.11.6 → 1.11.8
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.d.ts +2 -2
- package/dist/index.esm.js +1345 -455
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1345 -455
- package/dist/index.js.map +1 -1
- package/dist/types/config/options.d.ts +1 -1
- package/dist/types/constants/categories/blasphemy.d.ts +1 -1
- package/dist/types/constants/categories/disgusting.d.ts +1 -1
- package/dist/types/constants/categories/drugs.d.ts +1 -1
- package/dist/types/constants/categories/insult.d.ts +1 -1
- package/dist/types/constants/categories/profanity.d.ts +1 -1
- package/dist/types/constants/categories/sexual.d.ts +1 -1
- package/dist/types/constants/categories/slur.d.ts +1 -1
- package/dist/types/constants/regions/bali.d.ts +1 -1
- package/dist/types/constants/regions/batak.d.ts +1 -1
- package/dist/types/constants/regions/betawi.d.ts +1 -1
- package/dist/types/constants/regions/general.d.ts +1 -1
- package/dist/types/constants/regions/madura.d.ts +4 -0
- package/dist/types/constants/regions/minang.d.ts +4 -0
- package/dist/types/constants/regions/sunda.d.ts +1 -1
- package/dist/types/constants/wordList.d.ts +10 -1
- package/dist/types/core/analyzer.d.ts +1 -1
- package/dist/types/core/filter.d.ts +1 -1
- package/dist/types/core/matcher.d.ts +1 -1
- package/dist/types/types/index.d.ts +2 -2
- package/dist/types/utils/regexUtils.d.ts +1 -1
- package/package.json +1 -1
- package/src/config/options.ts +25 -30
- package/src/constants/categories/blasphemy.ts +26 -15
- package/src/constants/categories/disgusting.ts +62 -54
- package/src/constants/categories/drugs.ts +56 -47
- package/src/constants/categories/insult.ts +80 -76
- package/src/constants/categories/profanity.ts +98 -92
- package/src/constants/categories/sexual.ts +74 -65
- package/src/constants/categories/slur.ts +73 -66
- package/src/constants/regions/aceh.ts +5 -174
- package/src/constants/regions/bali.ts +4 -137
- package/src/constants/regions/batak.ts +5 -9
- package/src/constants/regions/betawi.ts +19 -22
- package/src/constants/regions/general.ts +140 -144
- package/src/constants/regions/jawa.ts +11 -29
- package/src/constants/regions/madura.ts +57 -0
- package/src/constants/regions/minang.ts +53 -0
- package/src/constants/regions/sunda.ts +343 -19
- package/src/constants/wordList.ts +31 -34
- package/src/core/analyzer.ts +15 -26
- package/src/core/filter.ts +23 -39
- package/src/core/matcher.ts +25 -49
- package/src/index.ts +5 -16
- package/src/types/index.ts +26 -25
- package/src/utils/ahoCorasick.ts +1 -1
- package/src/utils/regexUtils.ts +37 -43
- package/src/utils/similarityUtils.ts +13 -28
- package/src/utils/stringUtils.ts +30 -38
- /package/{prettierrc → .prettierrc} +0 -0
package/src/core/filter.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { FilterOptions, FilterResult, ProfanityWord } from
|
|
2
|
-
import { findProfanity, findProfanityWithMetadata } from
|
|
3
|
-
import { censorWord, escapeRegExp } from
|
|
4
|
-
import { createWordRegex } from
|
|
5
|
-
import { DEFAULT_OPTIONS, makeRandomGrawlixString } from
|
|
1
|
+
import { FilterOptions, FilterResult, ProfanityWord } from '../types';
|
|
2
|
+
import { findProfanity, findProfanityWithMetadata } from './matcher';
|
|
3
|
+
import { censorWord, escapeRegExp } from '../utils/stringUtils';
|
|
4
|
+
import { createWordRegex } from '../utils/regexUtils';
|
|
5
|
+
import { DEFAULT_OPTIONS, makeRandomGrawlixString } from '../config/options';
|
|
6
6
|
|
|
7
7
|
interface FindProfanityFunction {
|
|
8
8
|
(text: string, options?: FilterOptions): string[];
|
|
@@ -16,12 +16,9 @@ interface FindProfanityFunction {
|
|
|
16
16
|
* @param options Opsi untuk filter
|
|
17
17
|
* @returns FilterResult dengan hasil filter
|
|
18
18
|
*/
|
|
19
|
-
export function filter(
|
|
20
|
-
text: string,
|
|
21
|
-
options: FilterOptions = {},
|
|
22
|
-
): FilterResult {
|
|
19
|
+
export function filter(text: string, options: FilterOptions = {}): FilterResult {
|
|
23
20
|
const {
|
|
24
|
-
replaceWith =
|
|
21
|
+
replaceWith = '*',
|
|
25
22
|
fullWordCensor = true,
|
|
26
23
|
detectLeetSpeak = true,
|
|
27
24
|
whitelist = [],
|
|
@@ -74,10 +71,7 @@ export function filter(
|
|
|
74
71
|
const metadata = matchDetails.find(
|
|
75
72
|
(m) =>
|
|
76
73
|
m.word.toLowerCase() === word.toLowerCase() ||
|
|
77
|
-
(m.aliases &&
|
|
78
|
-
m.aliases.some(
|
|
79
|
-
(alias) => alias.toLowerCase() === word.toLowerCase(),
|
|
80
|
-
)),
|
|
74
|
+
(m.aliases && m.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase()))
|
|
81
75
|
);
|
|
82
76
|
|
|
83
77
|
const variants = actualMatches.get(word.toLowerCase()) || [];
|
|
@@ -86,7 +80,7 @@ export function filter(
|
|
|
86
80
|
const uniqueVariants = [...new Set(variants)];
|
|
87
81
|
|
|
88
82
|
uniqueVariants.forEach((variant) => {
|
|
89
|
-
const regex = new RegExp(`\\b${escapeRegExp(variant)}\\b`,
|
|
83
|
+
const regex = new RegExp(`\\b${escapeRegExp(variant)}\\b`, 'gi');
|
|
90
84
|
|
|
91
85
|
let match;
|
|
92
86
|
while ((match = regex.exec(filteredText)) !== null) {
|
|
@@ -98,11 +92,7 @@ export function filter(
|
|
|
98
92
|
if (useRandomGrawlix) {
|
|
99
93
|
censoredWord = makeRandomGrawlixString(originalWord.length);
|
|
100
94
|
} else {
|
|
101
|
-
censoredWord = censorWord(
|
|
102
|
-
originalWord,
|
|
103
|
-
replaceWith,
|
|
104
|
-
!fullWordCensor && keepFirstAndLast,
|
|
105
|
-
);
|
|
95
|
+
censoredWord = censorWord(originalWord, replaceWith, !fullWordCensor && keepFirstAndLast);
|
|
106
96
|
}
|
|
107
97
|
|
|
108
98
|
replacements.push({
|
|
@@ -112,8 +102,8 @@ export function filter(
|
|
|
112
102
|
});
|
|
113
103
|
|
|
114
104
|
filteredText = filteredText.replace(
|
|
115
|
-
new RegExp(`\\b${escapeRegExp(originalWord)}\\b`,
|
|
116
|
-
censoredWord
|
|
105
|
+
new RegExp(`\\b${escapeRegExp(originalWord)}\\b`, 'g'),
|
|
106
|
+
censoredWord
|
|
117
107
|
);
|
|
118
108
|
}
|
|
119
109
|
});
|
|
@@ -141,7 +131,7 @@ export function filter(
|
|
|
141
131
|
censoredWord = censorWord(
|
|
142
132
|
originalWord,
|
|
143
133
|
replaceWith,
|
|
144
|
-
!fullWordCensor && keepFirstAndLast
|
|
134
|
+
!fullWordCensor && keepFirstAndLast
|
|
145
135
|
);
|
|
146
136
|
}
|
|
147
137
|
|
|
@@ -152,8 +142,8 @@ export function filter(
|
|
|
152
142
|
});
|
|
153
143
|
|
|
154
144
|
filteredText = filteredText.replace(
|
|
155
|
-
new RegExp(escapeRegExp(originalWord),
|
|
156
|
-
censoredWord
|
|
145
|
+
new RegExp(escapeRegExp(originalWord), 'g'),
|
|
146
|
+
censoredWord
|
|
157
147
|
);
|
|
158
148
|
}
|
|
159
149
|
}
|
|
@@ -180,7 +170,7 @@ export function filter(
|
|
|
180
170
|
censoredWord = censorWord(
|
|
181
171
|
originalWord,
|
|
182
172
|
replaceWith,
|
|
183
|
-
!fullWordCensor && keepFirstAndLast
|
|
173
|
+
!fullWordCensor && keepFirstAndLast
|
|
184
174
|
);
|
|
185
175
|
}
|
|
186
176
|
|
|
@@ -191,8 +181,8 @@ export function filter(
|
|
|
191
181
|
});
|
|
192
182
|
|
|
193
183
|
filteredText = filteredText.replace(
|
|
194
|
-
new RegExp(escapeRegExp(originalWord),
|
|
195
|
-
censoredWord
|
|
184
|
+
new RegExp(escapeRegExp(originalWord), 'g'),
|
|
185
|
+
censoredWord
|
|
196
186
|
);
|
|
197
187
|
}
|
|
198
188
|
}
|
|
@@ -204,19 +194,13 @@ export function filter(
|
|
|
204
194
|
const metadata = matchDetails.find(
|
|
205
195
|
(m) =>
|
|
206
196
|
m.word.toLowerCase() === word.toLowerCase() ||
|
|
207
|
-
(m.aliases &&
|
|
208
|
-
m.aliases.some(
|
|
209
|
-
(alias) => alias.toLowerCase() === word.toLowerCase(),
|
|
210
|
-
)),
|
|
197
|
+
(m.aliases && m.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase()))
|
|
211
198
|
);
|
|
212
199
|
|
|
213
200
|
const variants = actualMatches.get(word.toLowerCase()) || [];
|
|
214
201
|
|
|
215
202
|
variants.forEach((variant) => {
|
|
216
|
-
const exactVariantRegex = new RegExp(
|
|
217
|
-
`\\b${escapeRegExp(variant)}\\b`,
|
|
218
|
-
"gi",
|
|
219
|
-
);
|
|
203
|
+
const exactVariantRegex = new RegExp(`\\b${escapeRegExp(variant)}\\b`, 'gi');
|
|
220
204
|
|
|
221
205
|
let match;
|
|
222
206
|
while ((match = exactVariantRegex.exec(filteredText)) !== null) {
|
|
@@ -231,7 +215,7 @@ export function filter(
|
|
|
231
215
|
censoredWord = censorWord(
|
|
232
216
|
originalWord,
|
|
233
217
|
replaceWith,
|
|
234
|
-
!fullWordCensor && keepFirstAndLast
|
|
218
|
+
!fullWordCensor && keepFirstAndLast
|
|
235
219
|
);
|
|
236
220
|
}
|
|
237
221
|
|
|
@@ -242,8 +226,8 @@ export function filter(
|
|
|
242
226
|
});
|
|
243
227
|
|
|
244
228
|
filteredText = filteredText.replace(
|
|
245
|
-
new RegExp(`\\b${escapeRegExp(originalWord)}\\b`,
|
|
246
|
-
censoredWord
|
|
229
|
+
new RegExp(`\\b${escapeRegExp(originalWord)}\\b`, 'g'),
|
|
230
|
+
censoredWord
|
|
247
231
|
);
|
|
248
232
|
}
|
|
249
233
|
});
|
package/src/core/matcher.ts
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from "../types";
|
|
7
|
-
|
|
8
|
-
import { wordObjects } from "../constants/wordList";
|
|
9
|
-
import { normalizeText } from "../utils/stringUtils";
|
|
10
|
-
import { createWordRegex } from "../utils/regexUtils";
|
|
1
|
+
import { ProfanityWord, ProfanityCategory, Region, FilterOptions } from '../types';
|
|
2
|
+
|
|
3
|
+
import { wordObjects } from '../constants/wordList';
|
|
4
|
+
import { normalizeText } from '../utils/stringUtils';
|
|
5
|
+
import { createWordRegex } from '../utils/regexUtils';
|
|
11
6
|
import {
|
|
12
7
|
findPossibleProfanityBySimiliarity,
|
|
13
8
|
findProfanityByLevenshteinDistance,
|
|
14
|
-
} from
|
|
15
|
-
import { DEFAULT_OPTIONS } from
|
|
16
|
-
import { AhoCorasick } from
|
|
9
|
+
} from '../utils/similarityUtils';
|
|
10
|
+
import { DEFAULT_OPTIONS } from '../config/options';
|
|
11
|
+
import { AhoCorasick } from '../utils/ahoCorasick';
|
|
17
12
|
|
|
18
13
|
const globalAhoCorasick = new AhoCorasick();
|
|
19
14
|
let ahoCorasickInitialized = false;
|
|
@@ -34,10 +29,7 @@ interface FindProfanityFunction {
|
|
|
34
29
|
lastActualMatches?: Map<string, string[]>;
|
|
35
30
|
}
|
|
36
31
|
|
|
37
|
-
export function findProfanity(
|
|
38
|
-
text: string,
|
|
39
|
-
options: FilterOptions = {},
|
|
40
|
-
): string[] {
|
|
32
|
+
export function findProfanity(text: string, options: FilterOptions = {}): string[] {
|
|
41
33
|
const {
|
|
42
34
|
wordList = [],
|
|
43
35
|
detectLeetSpeak = true,
|
|
@@ -60,9 +52,7 @@ export function findProfanity(
|
|
|
60
52
|
|
|
61
53
|
if (baseWordsToCheck.length === 0) {
|
|
62
54
|
const filteredWords = wordObjects.filter((word) => {
|
|
63
|
-
const matchCategory = categories
|
|
64
|
-
? categories.includes(word.category)
|
|
65
|
-
: true;
|
|
55
|
+
const matchCategory = categories ? categories.includes(word.category) : true;
|
|
66
56
|
const matchRegion = regions ? regions.includes(word.region) : true;
|
|
67
57
|
const matchSeverity = word.severity >= severityThreshold;
|
|
68
58
|
return matchCategory && matchRegion && matchSeverity;
|
|
@@ -74,9 +64,7 @@ export function findProfanity(
|
|
|
74
64
|
const aliasMap = new Map<string, string>();
|
|
75
65
|
wordObjects.forEach((wordObj) => {
|
|
76
66
|
if (wordObj.aliases && wordObj.aliases.length > 0) {
|
|
77
|
-
const matchCategory = categories
|
|
78
|
-
? categories.includes(wordObj.category)
|
|
79
|
-
: true;
|
|
67
|
+
const matchCategory = categories ? categories.includes(wordObj.category) : true;
|
|
80
68
|
const matchRegion = regions ? regions.includes(wordObj.region) : true;
|
|
81
69
|
const matchSeverity = wordObj.severity >= severityThreshold;
|
|
82
70
|
|
|
@@ -88,10 +76,9 @@ export function findProfanity(
|
|
|
88
76
|
}
|
|
89
77
|
});
|
|
90
78
|
|
|
91
|
-
const wordsToCheck = [
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
].filter((word) => !whitelist.includes(word.toLowerCase()));
|
|
79
|
+
const wordsToCheck = [...baseWordsToCheck, ...Array.from(aliasMap.keys())].filter(
|
|
80
|
+
(word) => !whitelist.includes(word.toLowerCase())
|
|
81
|
+
);
|
|
95
82
|
|
|
96
83
|
if (wordsToCheck.length === 0) {
|
|
97
84
|
return [];
|
|
@@ -104,8 +91,7 @@ export function findProfanity(
|
|
|
104
91
|
|
|
105
92
|
const basicMatches = globalAhoCorasick.searchUnique(normalizedText);
|
|
106
93
|
for (const match of basicMatches) {
|
|
107
|
-
const originalWord =
|
|
108
|
-
aliasMap.get(match.toLowerCase()) || match.toLowerCase();
|
|
94
|
+
const originalWord = aliasMap.get(match.toLowerCase()) || match.toLowerCase();
|
|
109
95
|
matches.add(originalWord);
|
|
110
96
|
|
|
111
97
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -126,8 +112,7 @@ export function findProfanity(
|
|
|
126
112
|
|
|
127
113
|
let match;
|
|
128
114
|
while ((match = leetRegex.exec(text)) !== null) {
|
|
129
|
-
const originalWord =
|
|
130
|
-
aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
115
|
+
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
131
116
|
matches.add(originalWord);
|
|
132
117
|
|
|
133
118
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -150,8 +135,7 @@ export function findProfanity(
|
|
|
150
135
|
|
|
151
136
|
let match;
|
|
152
137
|
while ((match = variantRegex.exec(text)) !== null) {
|
|
153
|
-
const originalWord =
|
|
154
|
-
aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
138
|
+
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
155
139
|
matches.add(originalWord);
|
|
156
140
|
|
|
157
141
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -174,8 +158,7 @@ export function findProfanity(
|
|
|
174
158
|
|
|
175
159
|
let match;
|
|
176
160
|
while ((match = splitRegex.exec(text)) !== null) {
|
|
177
|
-
const originalWord =
|
|
178
|
-
aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
161
|
+
const originalWord = aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
179
162
|
matches.add(originalWord);
|
|
180
163
|
|
|
181
164
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -192,13 +175,12 @@ export function findProfanity(
|
|
|
192
175
|
text,
|
|
193
176
|
wordsToCheck,
|
|
194
177
|
similarityThreshold,
|
|
195
|
-
maxLevenshteinDistance
|
|
178
|
+
maxLevenshteinDistance
|
|
196
179
|
);
|
|
197
180
|
|
|
198
181
|
possibleProfanity.forEach((item) => {
|
|
199
182
|
const originalWord =
|
|
200
|
-
aliasMap.get(item.original.toLowerCase()) ||
|
|
201
|
-
item.original.toLowerCase();
|
|
183
|
+
aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
|
|
202
184
|
matches.add(originalWord);
|
|
203
185
|
|
|
204
186
|
if (!actualMatches.has(originalWord)) {
|
|
@@ -210,15 +192,14 @@ export function findProfanity(
|
|
|
210
192
|
const possibleProfanity = findPossibleProfanityBySimiliarity(
|
|
211
193
|
text,
|
|
212
194
|
wordsToCheck,
|
|
213
|
-
similarityThreshold
|
|
195
|
+
similarityThreshold
|
|
214
196
|
);
|
|
215
197
|
|
|
216
198
|
possibleProfanity.forEach((item) => {
|
|
217
199
|
matches.add(item.original.toLowerCase());
|
|
218
200
|
|
|
219
201
|
const originalWord =
|
|
220
|
-
aliasMap.get(item.original.toLowerCase()) ||
|
|
221
|
-
item.original.toLowerCase();
|
|
202
|
+
aliasMap.get(item.original.toLowerCase()) || item.original.toLowerCase();
|
|
222
203
|
if (!actualMatches.has(originalWord)) {
|
|
223
204
|
actualMatches.set(originalWord, []);
|
|
224
205
|
}
|
|
@@ -241,7 +222,7 @@ export function findProfanity(
|
|
|
241
222
|
*/
|
|
242
223
|
export function findProfanityWithMetadata(
|
|
243
224
|
text: string,
|
|
244
|
-
options: FilterOptions = {}
|
|
225
|
+
options: FilterOptions = {}
|
|
245
226
|
): ProfanityWord[] {
|
|
246
227
|
const matches = findProfanity(text, options);
|
|
247
228
|
if (matches.length === 0) {
|
|
@@ -253,10 +234,7 @@ export function findProfanityWithMetadata(
|
|
|
253
234
|
const wordObject = wordObjects.find(
|
|
254
235
|
(obj) =>
|
|
255
236
|
obj.word.toLowerCase() === word.toLowerCase() ||
|
|
256
|
-
(obj.aliases &&
|
|
257
|
-
obj.aliases.some(
|
|
258
|
-
(alias) => alias.toLowerCase() === word.toLowerCase(),
|
|
259
|
-
)),
|
|
237
|
+
(obj.aliases && obj.aliases.some((alias) => alias.toLowerCase() === word.toLowerCase()))
|
|
260
238
|
);
|
|
261
239
|
|
|
262
240
|
return wordObject;
|
|
@@ -270,9 +248,7 @@ export function findProfanityWithMetadata(
|
|
|
270
248
|
* @param matchDetails Hasil pencarian dari fingProfanityWithMetadata()
|
|
271
249
|
* @return Array kategori unik
|
|
272
250
|
*/
|
|
273
|
-
export function findCategories(
|
|
274
|
-
matchDetails: ProfanityWord[],
|
|
275
|
-
): ProfanityCategory[] {
|
|
251
|
+
export function findCategories(matchDetails: ProfanityWord[]): ProfanityCategory[] {
|
|
276
252
|
const categories = new Set<ProfanityCategory>();
|
|
277
253
|
|
|
278
254
|
matchDetails.forEach((word) => {
|
package/src/index.ts
CHANGED
|
@@ -8,12 +8,7 @@ export * from './utils/similarityUtils';
|
|
|
8
8
|
export * from './config/options';
|
|
9
9
|
|
|
10
10
|
import { filter, isProfane } from './core/filter';
|
|
11
|
-
import {
|
|
12
|
-
analyze,
|
|
13
|
-
batchAnalyze,
|
|
14
|
-
analyzeBySentence,
|
|
15
|
-
analyzeWithContext,
|
|
16
|
-
} from './core/analyzer';
|
|
11
|
+
import { analyze, batchAnalyze, analyzeBySentence, analyzeWithContext } from './core/analyzer';
|
|
17
12
|
import { FilterOptions, FilterResult, AnalysisResult } from './types';
|
|
18
13
|
import {
|
|
19
14
|
DEFAULT_OPTIONS,
|
|
@@ -105,10 +100,7 @@ export class IDProfanityFilter {
|
|
|
105
100
|
* @param presetName Nama preset yang akan digunakan
|
|
106
101
|
* @param additionalOptions Opsi tambahan untuk override
|
|
107
102
|
*/
|
|
108
|
-
usePreset(
|
|
109
|
-
presetName: string,
|
|
110
|
-
additionalOptions: Partial<FilterOptions> = {},
|
|
111
|
-
) {
|
|
103
|
+
usePreset(presetName: string, additionalOptions: Partial<FilterOptions> = {}) {
|
|
112
104
|
this.options = getPresetOptions(presetName, additionalOptions);
|
|
113
105
|
}
|
|
114
106
|
|
|
@@ -125,10 +117,7 @@ export class IDProfanityFilter {
|
|
|
125
117
|
* @param word Kata yang akan diabaikan
|
|
126
118
|
*/
|
|
127
119
|
addToWhitelist(word: string) {
|
|
128
|
-
this.options.whitelist = [
|
|
129
|
-
...(this.options.whitelist || []),
|
|
130
|
-
word.toLowerCase(),
|
|
131
|
-
];
|
|
120
|
+
this.options.whitelist = [...(this.options.whitelist || []), word.toLowerCase()];
|
|
132
121
|
}
|
|
133
122
|
|
|
134
123
|
/**
|
|
@@ -139,7 +128,7 @@ export class IDProfanityFilter {
|
|
|
139
128
|
if (!this.options.whitelist) return;
|
|
140
129
|
|
|
141
130
|
this.options.whitelist = this.options.whitelist.filter(
|
|
142
|
-
(w) => w.toLowerCase() !== word.toLowerCase()
|
|
131
|
+
(w) => w.toLowerCase() !== word.toLowerCase()
|
|
143
132
|
);
|
|
144
133
|
}
|
|
145
134
|
|
|
@@ -166,7 +155,7 @@ export class IDProfanityFilter {
|
|
|
166
155
|
enableSimilarityDetection(
|
|
167
156
|
threshold: number = 0.8,
|
|
168
157
|
useLevenshtein: boolean = false,
|
|
169
|
-
maxLevenshteinDistance: number = 2
|
|
158
|
+
maxLevenshteinDistance: number = 2
|
|
170
159
|
) {
|
|
171
160
|
this.options.detectSimilarity = true;
|
|
172
161
|
this.options.similarityThreshold = threshold;
|
package/src/types/index.ts
CHANGED
|
@@ -1,31 +1,32 @@
|
|
|
1
1
|
export type ProfanityCategory =
|
|
2
|
-
|
|
|
3
|
-
|
|
|
4
|
-
|
|
|
5
|
-
|
|
|
6
|
-
|
|
|
7
|
-
|
|
|
8
|
-
|
|
|
2
|
+
| 'sexual'
|
|
3
|
+
| 'insult'
|
|
4
|
+
| 'profanity'
|
|
5
|
+
| 'slur'
|
|
6
|
+
| 'drugs'
|
|
7
|
+
| 'disgusting'
|
|
8
|
+
| 'blasphemy';
|
|
9
9
|
|
|
10
10
|
export type Region =
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
11
|
+
| 'general'
|
|
12
|
+
| 'jawa'
|
|
13
|
+
| 'sunda'
|
|
14
|
+
| 'betawi'
|
|
15
|
+
| 'batak'
|
|
16
|
+
| 'minang'
|
|
17
|
+
| 'bali'
|
|
18
|
+
| 'madura'
|
|
19
|
+
| 'bugis'
|
|
20
|
+
| 'aceh'
|
|
21
|
+
| 'ambon'
|
|
22
|
+
| 'papua'
|
|
23
|
+
| 'manado'
|
|
24
|
+
| 'banjar'
|
|
25
|
+
| 'palembang'
|
|
26
|
+
| 'lampung'
|
|
27
|
+
| 'ntt'
|
|
28
|
+
| 'mandailing'
|
|
29
|
+
| 'ntb';
|
|
29
30
|
|
|
30
31
|
export interface ProfanityWord {
|
|
31
32
|
word: string;
|
package/src/utils/ahoCorasick.ts
CHANGED
|
@@ -30,7 +30,7 @@ export class AhoCorasick {
|
|
|
30
30
|
*/
|
|
31
31
|
addPattern(pattern: string): void {
|
|
32
32
|
if (this.built) {
|
|
33
|
-
throw new Error(
|
|
33
|
+
throw new Error('Cannot add patterns after the automaton is built');
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
let node = this.root;
|
package/src/utils/regexUtils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { escapeRegExp } from
|
|
2
|
-
import { RegexOptions } from
|
|
1
|
+
import { escapeRegExp } from './stringUtils';
|
|
2
|
+
import { RegexOptions } from '../types';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Membuat pola regex untuk mencocokkan kata
|
|
@@ -8,10 +8,7 @@ import { RegexOptions } from "../types";
|
|
|
8
8
|
* @param options Opsi untuk pembuatan regex
|
|
9
9
|
* @returns Objek RegExp
|
|
10
10
|
*/
|
|
11
|
-
export function createWordRegex(
|
|
12
|
-
word: string,
|
|
13
|
-
options: RegexOptions = {},
|
|
14
|
-
): RegExp {
|
|
11
|
+
export function createWordRegex(word: string, options: RegexOptions = {}): RegExp {
|
|
15
12
|
const {
|
|
16
13
|
wholeWord = true,
|
|
17
14
|
caseSensitive = false,
|
|
@@ -44,7 +41,7 @@ export function createWordRegex(
|
|
|
44
41
|
}
|
|
45
42
|
|
|
46
43
|
// Buat regex dengan flag case-insensitive jika diminta
|
|
47
|
-
return new RegExp(pattern, caseSensitive ?
|
|
44
|
+
return new RegExp(pattern, caseSensitive ? 'g' : 'gi');
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
/**
|
|
@@ -59,32 +56,32 @@ export function createWordRegex(
|
|
|
59
56
|
*/
|
|
60
57
|
export function addLeetSpeakVariations(pattern: string): string {
|
|
61
58
|
const leetMap: Record<string, string[]> = {
|
|
62
|
-
a: [
|
|
63
|
-
b: [
|
|
64
|
-
c: [
|
|
65
|
-
e: [
|
|
66
|
-
g: [
|
|
67
|
-
i: [
|
|
68
|
-
l: [
|
|
69
|
-
o: [
|
|
70
|
-
s: [
|
|
71
|
-
t: [
|
|
72
|
-
z: [
|
|
59
|
+
a: ['a', '4', '@'],
|
|
60
|
+
b: ['b', '8', '6'],
|
|
61
|
+
c: ['c', '(', '{', '<'],
|
|
62
|
+
e: ['e', '3'],
|
|
63
|
+
g: ['g', '6', '9'],
|
|
64
|
+
i: ['i', '1', '!', '|'],
|
|
65
|
+
l: ['l', '1', '|'],
|
|
66
|
+
o: ['o', '0'],
|
|
67
|
+
s: ['s', '5', '$'],
|
|
68
|
+
t: ['t', '7', '+'],
|
|
69
|
+
z: ['z', '2'],
|
|
73
70
|
};
|
|
74
71
|
|
|
75
72
|
return pattern
|
|
76
|
-
.split(
|
|
73
|
+
.split('')
|
|
77
74
|
.map((char) => {
|
|
78
75
|
const lowerChar = char.toLowerCase();
|
|
79
76
|
const variations = leetMap[lowerChar];
|
|
80
77
|
|
|
81
78
|
if (variations && variations.length > 1) {
|
|
82
|
-
return `[${variations.join(
|
|
79
|
+
return `[${variations.join('')}]`;
|
|
83
80
|
}
|
|
84
81
|
|
|
85
82
|
return char;
|
|
86
83
|
})
|
|
87
|
-
.join(
|
|
84
|
+
.join('');
|
|
88
85
|
}
|
|
89
86
|
|
|
90
87
|
/**
|
|
@@ -95,7 +92,7 @@ export function addLeetSpeakVariations(pattern: string): string {
|
|
|
95
92
|
*/
|
|
96
93
|
export function addSplitVariations(pattern: string): string {
|
|
97
94
|
// Tambahkan kemungkinan spasi atau karakter penghubung di antara setiap huruf
|
|
98
|
-
return pattern.split(
|
|
95
|
+
return pattern.split('').join('[\\s\\-._*+]?');
|
|
99
96
|
}
|
|
100
97
|
|
|
101
98
|
/**
|
|
@@ -109,7 +106,7 @@ export function createEvasionRegex(word: string): RegExp {
|
|
|
109
106
|
// Tambahkan kemungkinan spasi atau karakter penghubung di antara setiap huruf
|
|
110
107
|
const pattern = addSplitVariations(escapeRegExp(word));
|
|
111
108
|
|
|
112
|
-
return new RegExp(pattern,
|
|
109
|
+
return new RegExp(pattern, 'gi');
|
|
113
110
|
}
|
|
114
111
|
|
|
115
112
|
/**
|
|
@@ -121,30 +118,30 @@ export function createEvasionRegex(word: string): RegExp {
|
|
|
121
118
|
export function addIndonesianVariations(pattern: string): string {
|
|
122
119
|
// Variasi ejaan dalam Bahasa Indonesia
|
|
123
120
|
const variationMap: Record<string, string[]> = {
|
|
124
|
-
c: [
|
|
125
|
-
k: [
|
|
126
|
-
j: [
|
|
127
|
-
y: [
|
|
128
|
-
u: [
|
|
129
|
-
f: [
|
|
130
|
-
z: [
|
|
131
|
-
x: [
|
|
121
|
+
c: ['c', 'k'], // contoh: becok/bekok
|
|
122
|
+
k: ['k', 'c', 'q'], // contoh: kacau/qacau
|
|
123
|
+
j: ['j', 'dj'], // contoh: jualan/djualan (ejaan lama)
|
|
124
|
+
y: ['y', 'j'], // contoh: ya/ja
|
|
125
|
+
u: ['u', 'oe'], // contoh: untuk/oentoek (ejaan lama)
|
|
126
|
+
f: ['f', 'p', 'v'], // contoh: kafir/kapir
|
|
127
|
+
z: ['z', 'j', 's'], // contoh: zaman/jaman
|
|
128
|
+
x: ['x', 'ks'], // contoh: taxi/taksi
|
|
132
129
|
};
|
|
133
130
|
|
|
134
131
|
// Ganti tiap karakter dengan variasinya
|
|
135
132
|
return pattern
|
|
136
|
-
.split(
|
|
133
|
+
.split('')
|
|
137
134
|
.map((char) => {
|
|
138
135
|
const lowerChar = char.toLowerCase();
|
|
139
136
|
const variations = variationMap[lowerChar];
|
|
140
137
|
|
|
141
138
|
if (variations && variations.length > 1) {
|
|
142
|
-
return `[${variations.join(
|
|
139
|
+
return `[${variations.join('')}]`;
|
|
143
140
|
}
|
|
144
141
|
|
|
145
142
|
return char;
|
|
146
143
|
})
|
|
147
|
-
.join(
|
|
144
|
+
.join('');
|
|
148
145
|
}
|
|
149
146
|
|
|
150
147
|
/**
|
|
@@ -155,7 +152,7 @@ export function addIndonesianVariations(pattern: string): string {
|
|
|
155
152
|
*/
|
|
156
153
|
export function createIndonesianVariationRegex(word: string): RegExp {
|
|
157
154
|
const pattern = addIndonesianVariations(escapeRegExp(word));
|
|
158
|
-
return new RegExp(`\\b${pattern}\\b`,
|
|
155
|
+
return new RegExp(`\\b${pattern}\\b`, 'gi');
|
|
159
156
|
}
|
|
160
157
|
|
|
161
158
|
/**
|
|
@@ -165,16 +162,13 @@ export function createIndonesianVariationRegex(word: string): RegExp {
|
|
|
165
162
|
* @param contextSize Jumlah kata konteks sebelum dan sesudah
|
|
166
163
|
* @returns Objek RegExp
|
|
167
164
|
*/
|
|
168
|
-
export function createContextRegex(
|
|
169
|
-
word: string,
|
|
170
|
-
contextSize: number = 3,
|
|
171
|
-
): RegExp {
|
|
165
|
+
export function createContextRegex(word: string, contextSize: number = 3): RegExp {
|
|
172
166
|
const wordPattern = escapeRegExp(word);
|
|
173
167
|
|
|
174
168
|
// Membuat pola yang menangkap beberapa kata sebelum dan setelah kata target
|
|
175
169
|
const pattern = `((?:\\S+\\s+){0,${contextSize}})(\\b${wordPattern}\\b)((?:\\s+\\S+){0,${contextSize}})`;
|
|
176
170
|
|
|
177
|
-
return new RegExp(pattern,
|
|
171
|
+
return new RegExp(pattern, 'gi');
|
|
178
172
|
}
|
|
179
173
|
|
|
180
174
|
/**
|
|
@@ -186,8 +180,8 @@ export function createContextRegex(
|
|
|
186
180
|
export function createWordFormRegex(word: string): RegExp {
|
|
187
181
|
// Implementasi sederhana untuk mencocokkan berbagai imbuhan
|
|
188
182
|
// Ini bisa dikembangkan lebih lanjut untuk mencocokkan bentukan kata yang lebih kompleks
|
|
189
|
-
const prefixes = [
|
|
190
|
-
const suffixes = [
|
|
183
|
+
const prefixes = ['', 'me', 'pe', 'ber', 'di', 'ter', 'se'];
|
|
184
|
+
const suffixes = ['', 'kan', 'an', 'i', 'nya'];
|
|
191
185
|
|
|
192
186
|
const patterns = [];
|
|
193
187
|
|
|
@@ -198,5 +192,5 @@ export function createWordFormRegex(word: string): RegExp {
|
|
|
198
192
|
}
|
|
199
193
|
}
|
|
200
194
|
|
|
201
|
-
return new RegExp(patterns.join(
|
|
195
|
+
return new RegExp(patterns.join('|'), 'gi');
|
|
202
196
|
}
|