@sideid/id-profanity-filter 1.9.6 → 1.10.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/.eslintrc.js +30 -2
- package/README.md +45 -3
- package/dist/config/options.d.ts +24 -0
- package/dist/constants/wordList.d.ts +1 -1
- package/dist/core/analyzer.d.ts +1 -1
- package/dist/core/filter.d.ts +1 -1
- package/dist/core/matcher.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +411 -77
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +412 -76
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/utils/similarityUtils.d.ts +25 -0
- package/eslint.config.mjs +40 -0
- package/package.json +2 -2
- package/src/config/options.ts +2 -0
- package/src/constants/regions/general.ts +102 -2
- package/src/constants/regions/jawa.ts +10 -0
- package/src/constants/wordList.ts +21 -14
- package/src/core/analyzer.ts +8 -14
- package/src/core/filter.ts +182 -41
- package/src/core/matcher.ts +131 -61
- package/src/index.ts +34 -15
- package/src/types/index.ts +4 -2
- package/src/utils/regexUtils.ts +0 -1
- package/src/utils/similarityUtils.ts +97 -2
- package/test.js +184 -0
- package/src/constants/categories/index.ts +0 -31
- package/src/constants/regions/index.ts +0 -62
package/src/core/filter.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { FilterOptions, FilterResult, ProfanityWord } from
|
|
2
|
-
import { findProfanity, findProfanityWithMetadata } from
|
|
3
|
-
import { censorWord, escapeRegExp
|
|
4
|
-
import { createWordRegex } from
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
+
|
|
7
|
+
interface FindProfanityFunction {
|
|
8
|
+
(text: string, options?: FilterOptions): string[];
|
|
9
|
+
lastActualMatches?: Map<string, string[]>;
|
|
10
|
+
}
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Menyensor kata kotor dalam teks
|
|
@@ -20,7 +21,7 @@ export function filter(
|
|
|
20
21
|
options: FilterOptions = {},
|
|
21
22
|
): FilterResult {
|
|
22
23
|
const {
|
|
23
|
-
replaceWith =
|
|
24
|
+
replaceWith = '*',
|
|
24
25
|
fullWordCensor = true,
|
|
25
26
|
detectLeetSpeak = true,
|
|
26
27
|
whitelist = [],
|
|
@@ -28,6 +29,11 @@ export function filter(
|
|
|
28
29
|
useRandomGrawlix = false,
|
|
29
30
|
keepFirstAndLast = false,
|
|
30
31
|
indonesianVariation = false,
|
|
32
|
+
detectSplit = false,
|
|
33
|
+
detectSimilarity = false,
|
|
34
|
+
useLevenshtein = false,
|
|
35
|
+
maxLevenshteinDistance = 2,
|
|
36
|
+
similarityThreshold = 0.8,
|
|
31
37
|
} = { ...DEFAULT_OPTIONS, ...options };
|
|
32
38
|
|
|
33
39
|
const matches = findProfanity(text, {
|
|
@@ -36,8 +42,16 @@ export function filter(
|
|
|
36
42
|
whitelist,
|
|
37
43
|
checkSubstring,
|
|
38
44
|
indonesianVariation,
|
|
45
|
+
detectSplit,
|
|
46
|
+
detectSimilarity,
|
|
47
|
+
useLevenshtein,
|
|
48
|
+
maxLevenshteinDistance,
|
|
49
|
+
similarityThreshold,
|
|
39
50
|
});
|
|
40
51
|
|
|
52
|
+
const actualMatches: Map<string, string[]> =
|
|
53
|
+
(findProfanity as FindProfanityFunction).lastActualMatches || new Map();
|
|
54
|
+
|
|
41
55
|
const matchDetails = findProfanityWithMetadata(text, options);
|
|
42
56
|
|
|
43
57
|
if (matches.length === 0) {
|
|
@@ -66,49 +80,176 @@ export function filter(
|
|
|
66
80
|
)),
|
|
67
81
|
);
|
|
68
82
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
detectSplit: false,
|
|
74
|
-
indonesianVariation: false,
|
|
75
|
-
});
|
|
83
|
+
const variants = actualMatches.get(word.toLowerCase()) || [];
|
|
84
|
+
variants.push(word);
|
|
85
|
+
|
|
86
|
+
const uniqueVariants = [...new Set(variants)];
|
|
76
87
|
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
uniqueVariants.forEach((variant) => {
|
|
89
|
+
const regex = new RegExp(`\\b${escapeRegExp(variant)}\\b`, 'gi');
|
|
79
90
|
|
|
80
|
-
|
|
91
|
+
let match;
|
|
92
|
+
while ((match = regex.exec(filteredText)) !== null) {
|
|
93
|
+
const originalWord = match[0];
|
|
81
94
|
|
|
82
|
-
|
|
83
|
-
const originalWord = match[0];
|
|
95
|
+
if (whitelist.includes(originalWord.toLowerCase())) continue;
|
|
84
96
|
|
|
85
|
-
|
|
97
|
+
let censoredWord;
|
|
98
|
+
if (useRandomGrawlix) {
|
|
99
|
+
censoredWord = makeRandomGrawlixString(originalWord.length);
|
|
100
|
+
} else {
|
|
101
|
+
censoredWord = censorWord(
|
|
102
|
+
originalWord,
|
|
103
|
+
replaceWith,
|
|
104
|
+
!fullWordCensor && keepFirstAndLast,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
86
107
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
108
|
+
replacements.push({
|
|
109
|
+
original: originalWord,
|
|
110
|
+
censored: censoredWord,
|
|
111
|
+
metadata,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
filteredText = filteredText.replace(
|
|
115
|
+
new RegExp(`\\b${escapeRegExp(originalWord)}\\b`, 'g'),
|
|
116
|
+
censoredWord,
|
|
95
117
|
);
|
|
96
118
|
}
|
|
119
|
+
});
|
|
97
120
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
121
|
+
if (detectSplit || detectLeetSpeak) {
|
|
122
|
+
if (detectLeetSpeak) {
|
|
123
|
+
const leetRegex = createWordRegex(word, {
|
|
124
|
+
wholeWord: true,
|
|
125
|
+
caseSensitive: false,
|
|
126
|
+
leetSpeak: true,
|
|
127
|
+
detectSplit: false,
|
|
128
|
+
indonesianVariation: false,
|
|
129
|
+
});
|
|
103
130
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
131
|
+
let match;
|
|
132
|
+
while ((match = leetRegex.exec(filteredText)) !== null) {
|
|
133
|
+
const originalWord = match[0];
|
|
134
|
+
|
|
135
|
+
if (whitelist.includes(originalWord.toLowerCase())) continue;
|
|
136
|
+
|
|
137
|
+
let censoredWord;
|
|
138
|
+
if (useRandomGrawlix) {
|
|
139
|
+
censoredWord = makeRandomGrawlixString(originalWord.length);
|
|
140
|
+
} else {
|
|
141
|
+
censoredWord = censorWord(
|
|
142
|
+
originalWord,
|
|
143
|
+
replaceWith,
|
|
144
|
+
!fullWordCensor && keepFirstAndLast,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
replacements.push({
|
|
149
|
+
original: originalWord,
|
|
150
|
+
censored: censoredWord,
|
|
151
|
+
metadata,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
filteredText = filteredText.replace(
|
|
155
|
+
new RegExp(escapeRegExp(originalWord), 'g'),
|
|
156
|
+
censoredWord,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (detectSplit) {
|
|
162
|
+
const splitRegex = createWordRegex(word, {
|
|
163
|
+
wholeWord: false,
|
|
164
|
+
caseSensitive: false,
|
|
165
|
+
leetSpeak: false,
|
|
166
|
+
detectSplit: true,
|
|
167
|
+
indonesianVariation: false,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
let match;
|
|
171
|
+
while ((match = splitRegex.exec(filteredText)) !== null) {
|
|
172
|
+
const originalWord = match[0];
|
|
173
|
+
|
|
174
|
+
if (whitelist.includes(originalWord.toLowerCase())) continue;
|
|
175
|
+
|
|
176
|
+
let censoredWord;
|
|
177
|
+
if (useRandomGrawlix) {
|
|
178
|
+
censoredWord = makeRandomGrawlixString(originalWord.length);
|
|
179
|
+
} else {
|
|
180
|
+
censoredWord = censorWord(
|
|
181
|
+
originalWord,
|
|
182
|
+
replaceWith,
|
|
183
|
+
!fullWordCensor && keepFirstAndLast,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
replacements.push({
|
|
188
|
+
original: originalWord,
|
|
189
|
+
censored: censoredWord,
|
|
190
|
+
metadata,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
filteredText = filteredText.replace(
|
|
194
|
+
new RegExp(escapeRegExp(originalWord), 'g'),
|
|
195
|
+
censoredWord,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
109
199
|
}
|
|
110
200
|
});
|
|
111
201
|
|
|
202
|
+
if (detectSimilarity && useLevenshtein) {
|
|
203
|
+
matches.forEach((word) => {
|
|
204
|
+
const metadata = matchDetails.find(
|
|
205
|
+
(m) =>
|
|
206
|
+
m.word.toLowerCase() === word.toLowerCase() ||
|
|
207
|
+
(m.aliases &&
|
|
208
|
+
m.aliases.some(
|
|
209
|
+
(alias) => alias.toLowerCase() === word.toLowerCase(),
|
|
210
|
+
)),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const variants = actualMatches.get(word.toLowerCase()) || [];
|
|
214
|
+
|
|
215
|
+
variants.forEach((variant) => {
|
|
216
|
+
const exactVariantRegex = new RegExp(
|
|
217
|
+
`\\b${escapeRegExp(variant)}\\b`,
|
|
218
|
+
'gi',
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
let match;
|
|
222
|
+
while ((match = exactVariantRegex.exec(filteredText)) !== null) {
|
|
223
|
+
const originalWord = match[0];
|
|
224
|
+
|
|
225
|
+
if (whitelist.includes(originalWord.toLowerCase())) continue;
|
|
226
|
+
|
|
227
|
+
let censoredWord;
|
|
228
|
+
if (useRandomGrawlix) {
|
|
229
|
+
censoredWord = makeRandomGrawlixString(originalWord.length);
|
|
230
|
+
} else {
|
|
231
|
+
censoredWord = censorWord(
|
|
232
|
+
originalWord,
|
|
233
|
+
replaceWith,
|
|
234
|
+
!fullWordCensor && keepFirstAndLast,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
replacements.push({
|
|
239
|
+
original: originalWord,
|
|
240
|
+
censored: censoredWord,
|
|
241
|
+
metadata,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
filteredText = filteredText.replace(
|
|
245
|
+
new RegExp(`\\b${escapeRegExp(originalWord)}\\b`, 'g'),
|
|
246
|
+
censoredWord,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
112
253
|
return {
|
|
113
254
|
filtered: filteredText,
|
|
114
255
|
censored: replacements.length,
|
package/src/core/matcher.ts
CHANGED
|
@@ -3,26 +3,21 @@ import {
|
|
|
3
3
|
ProfanityCategory,
|
|
4
4
|
Region,
|
|
5
5
|
FilterOptions,
|
|
6
|
-
} from
|
|
6
|
+
} from '../types';
|
|
7
7
|
|
|
8
|
-
import { wordObjects
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
escapeRegExp,
|
|
12
|
-
containsAnyWord,
|
|
13
|
-
detectSplitWords,
|
|
14
|
-
} from "../utils/stringUtils";
|
|
15
|
-
import {
|
|
16
|
-
createWordRegex,
|
|
17
|
-
addLeetSpeakVariations,
|
|
18
|
-
addIndonesianVariations,
|
|
19
|
-
addSplitVariations,
|
|
20
|
-
} from "../utils/regexUtils";
|
|
8
|
+
import { wordObjects } from '../constants/wordList';
|
|
9
|
+
import { normalizeText } from '../utils/stringUtils';
|
|
10
|
+
import { createWordRegex } from '../utils/regexUtils';
|
|
21
11
|
import {
|
|
22
12
|
findPossibleProfanityBySimiliarity,
|
|
23
|
-
|
|
24
|
-
} from
|
|
25
|
-
import { DEFAULT_OPTIONS } from
|
|
13
|
+
findProfanityByLevenshteinDistance,
|
|
14
|
+
} from '../utils/similarityUtils';
|
|
15
|
+
import { DEFAULT_OPTIONS } from '../config/options';
|
|
16
|
+
|
|
17
|
+
interface FindProfanityFunction {
|
|
18
|
+
(text: string, options?: FilterOptions): string[];
|
|
19
|
+
lastActualMatches?: Map<string, string[]>;
|
|
20
|
+
}
|
|
26
21
|
|
|
27
22
|
export function findProfanity(
|
|
28
23
|
text: string,
|
|
@@ -40,38 +35,55 @@ export function findProfanity(
|
|
|
40
35
|
detectSimilarity = false,
|
|
41
36
|
similarityThreshold = 0.8,
|
|
42
37
|
detectSplit = false,
|
|
38
|
+
useLevenshtein = false,
|
|
39
|
+
maxLevenshteinDistance = 2,
|
|
43
40
|
} = { ...DEFAULT_OPTIONS, ...options };
|
|
44
41
|
|
|
45
42
|
const normalizedText = normalizeText(text);
|
|
46
43
|
|
|
47
|
-
let
|
|
44
|
+
let baseWordsToCheck: string[] = wordList.length > 0 ? wordList : [];
|
|
48
45
|
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
.map((word) => word.word);
|
|
61
|
-
} else {
|
|
62
|
-
wordsToCheck = wordObjects.map((word) => word.word);
|
|
63
|
-
}
|
|
46
|
+
if (baseWordsToCheck.length === 0) {
|
|
47
|
+
const filteredWords = wordObjects.filter((word) => {
|
|
48
|
+
const matchCategory = categories
|
|
49
|
+
? categories.includes(word.category)
|
|
50
|
+
: true;
|
|
51
|
+
const matchRegion = regions ? regions.includes(word.region) : true;
|
|
52
|
+
const matchSeverity = word.severity >= severityThreshold;
|
|
53
|
+
return matchCategory && matchRegion && matchSeverity;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
baseWordsToCheck = filteredWords.map((word) => word.word);
|
|
64
57
|
}
|
|
65
58
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
const aliasMap = new Map<string, string>();
|
|
60
|
+
wordObjects.forEach((wordObj) => {
|
|
61
|
+
if (wordObj.aliases && wordObj.aliases.length > 0) {
|
|
62
|
+
const matchCategory = categories
|
|
63
|
+
? categories.includes(wordObj.category)
|
|
64
|
+
: true;
|
|
65
|
+
const matchRegion = regions ? regions.includes(wordObj.region) : true;
|
|
66
|
+
const matchSeverity = wordObj.severity >= severityThreshold;
|
|
67
|
+
|
|
68
|
+
if (matchCategory && matchRegion && matchSeverity) {
|
|
69
|
+
wordObj.aliases.forEach((alias) => {
|
|
70
|
+
aliasMap.set(alias.toLowerCase(), wordObj.word.toLowerCase());
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const wordsToCheck = [
|
|
77
|
+
...baseWordsToCheck,
|
|
78
|
+
...Array.from(aliasMap.keys()),
|
|
79
|
+
].filter((word) => !whitelist.includes(word.toLowerCase()));
|
|
69
80
|
|
|
70
81
|
if (wordsToCheck.length === 0) {
|
|
71
82
|
return [];
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
const matches = new Set<string>();
|
|
86
|
+
const actualMatches = new Map<string, string[]>();
|
|
75
87
|
|
|
76
88
|
wordsToCheck.forEach((word) => {
|
|
77
89
|
const regex = createWordRegex(word, {
|
|
@@ -84,7 +96,14 @@ export function findProfanity(
|
|
|
84
96
|
|
|
85
97
|
let match;
|
|
86
98
|
while ((match = regex.exec(normalizedText)) !== null) {
|
|
87
|
-
|
|
99
|
+
const originalWord =
|
|
100
|
+
aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
101
|
+
matches.add(originalWord);
|
|
102
|
+
|
|
103
|
+
if (!actualMatches.has(originalWord)) {
|
|
104
|
+
actualMatches.set(originalWord, []);
|
|
105
|
+
}
|
|
106
|
+
actualMatches.get(originalWord)?.push(match[0]);
|
|
88
107
|
}
|
|
89
108
|
});
|
|
90
109
|
|
|
@@ -100,7 +119,14 @@ export function findProfanity(
|
|
|
100
119
|
|
|
101
120
|
let match;
|
|
102
121
|
while ((match = leetRegex.exec(text)) !== null) {
|
|
103
|
-
|
|
122
|
+
const originalWord =
|
|
123
|
+
aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
124
|
+
matches.add(originalWord);
|
|
125
|
+
|
|
126
|
+
if (!actualMatches.has(originalWord)) {
|
|
127
|
+
actualMatches.set(originalWord, []);
|
|
128
|
+
}
|
|
129
|
+
actualMatches.get(originalWord)?.push(match[0]);
|
|
104
130
|
}
|
|
105
131
|
});
|
|
106
132
|
}
|
|
@@ -117,41 +143,85 @@ export function findProfanity(
|
|
|
117
143
|
|
|
118
144
|
let match;
|
|
119
145
|
while ((match = variantRegex.exec(text)) !== null) {
|
|
120
|
-
|
|
146
|
+
const originalWord =
|
|
147
|
+
aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
148
|
+
matches.add(originalWord);
|
|
149
|
+
|
|
150
|
+
if (!actualMatches.has(originalWord)) {
|
|
151
|
+
actualMatches.set(originalWord, []);
|
|
152
|
+
}
|
|
153
|
+
actualMatches.get(originalWord)?.push(match[0]);
|
|
121
154
|
}
|
|
122
155
|
});
|
|
123
156
|
}
|
|
124
157
|
|
|
125
158
|
if (detectSplit) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
});
|
|
159
|
+
wordsToCheck.forEach((word) => {
|
|
160
|
+
const splitRegex = createWordRegex(word, {
|
|
161
|
+
wholeWord: false,
|
|
162
|
+
caseSensitive: false,
|
|
163
|
+
leetSpeak: false,
|
|
164
|
+
detectSplit: true,
|
|
165
|
+
indonesianVariation: false,
|
|
166
|
+
});
|
|
135
167
|
|
|
136
|
-
|
|
137
|
-
|
|
168
|
+
let match;
|
|
169
|
+
while ((match = splitRegex.exec(text)) !== null) {
|
|
170
|
+
const originalWord =
|
|
171
|
+
aliasMap.get(word.toLowerCase()) || word.toLowerCase();
|
|
172
|
+
matches.add(originalWord);
|
|
173
|
+
|
|
174
|
+
if (!actualMatches.has(originalWord)) {
|
|
175
|
+
actualMatches.set(originalWord, []);
|
|
138
176
|
}
|
|
139
|
-
|
|
140
|
-
|
|
177
|
+
actualMatches.get(originalWord)?.push(match[0]);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
141
180
|
}
|
|
142
181
|
|
|
143
182
|
if (detectSimilarity) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
183
|
+
if (useLevenshtein) {
|
|
184
|
+
const possibleProfanity = findProfanityByLevenshteinDistance(
|
|
185
|
+
text,
|
|
186
|
+
wordsToCheck,
|
|
187
|
+
similarityThreshold,
|
|
188
|
+
maxLevenshteinDistance,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
possibleProfanity.forEach((item) => {
|
|
192
|
+
const originalWord =
|
|
193
|
+
aliasMap.get(item.original.toLowerCase()) ||
|
|
194
|
+
item.original.toLowerCase();
|
|
195
|
+
matches.add(originalWord);
|
|
196
|
+
|
|
197
|
+
if (!actualMatches.has(originalWord)) {
|
|
198
|
+
actualMatches.set(originalWord, []);
|
|
199
|
+
}
|
|
200
|
+
actualMatches.get(originalWord)?.push(item.word);
|
|
201
|
+
});
|
|
202
|
+
} else {
|
|
203
|
+
const possibleProfanity = findPossibleProfanityBySimiliarity(
|
|
204
|
+
text,
|
|
205
|
+
wordsToCheck,
|
|
206
|
+
similarityThreshold,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
possibleProfanity.forEach((item) => {
|
|
210
|
+
matches.add(item.original.toLowerCase());
|
|
211
|
+
|
|
212
|
+
const originalWord =
|
|
213
|
+
aliasMap.get(item.original.toLowerCase()) ||
|
|
214
|
+
item.original.toLowerCase();
|
|
215
|
+
if (!actualMatches.has(originalWord)) {
|
|
216
|
+
actualMatches.set(originalWord, []);
|
|
217
|
+
}
|
|
218
|
+
actualMatches.get(originalWord)?.push(item.word);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
153
221
|
}
|
|
154
222
|
|
|
223
|
+
(findProfanity as FindProfanityFunction).lastActualMatches = actualMatches;
|
|
224
|
+
|
|
155
225
|
return Array.from(matches);
|
|
156
226
|
}
|
|
157
227
|
|
package/src/index.ts
CHANGED
|
@@ -1,28 +1,27 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
5
|
-
export * from
|
|
6
|
-
export * from
|
|
7
|
-
export * from
|
|
8
|
-
export * from
|
|
9
|
-
|
|
10
|
-
import { filter, isProfane } from
|
|
1
|
+
export * from './types';
|
|
2
|
+
export * from './core/matcher';
|
|
3
|
+
export * from './core/filter';
|
|
4
|
+
export * from './core/analyzer';
|
|
5
|
+
export * from './utils/stringUtils';
|
|
6
|
+
export * from './utils/regexUtils';
|
|
7
|
+
export * from './utils/similarityUtils';
|
|
8
|
+
export * from './config/options';
|
|
9
|
+
|
|
10
|
+
import { filter, isProfane } from './core/filter';
|
|
11
11
|
import {
|
|
12
12
|
analyze,
|
|
13
13
|
batchAnalyze,
|
|
14
14
|
analyzeBySentence,
|
|
15
15
|
analyzeWithContext,
|
|
16
|
-
} from
|
|
17
|
-
import { FilterOptions, FilterResult, AnalysisResult } from
|
|
16
|
+
} from './core/analyzer';
|
|
17
|
+
import { FilterOptions, FilterResult, AnalysisResult } from './types';
|
|
18
18
|
import {
|
|
19
19
|
DEFAULT_OPTIONS,
|
|
20
20
|
FILTER_PRESETS,
|
|
21
21
|
CATEGORY_PRESETS,
|
|
22
22
|
REGION_PRESETS,
|
|
23
23
|
getPresetOptions,
|
|
24
|
-
|
|
25
|
-
} from "./config/options";
|
|
24
|
+
} from './config/options';
|
|
26
25
|
|
|
27
26
|
export class IDProfanityFilter {
|
|
28
27
|
private options: FilterOptions;
|
|
@@ -161,10 +160,30 @@ export class IDProfanityFilter {
|
|
|
161
160
|
/**
|
|
162
161
|
* Mengaktifkan deteksi berdasarkan kesamaan
|
|
163
162
|
* @param threshold Threshold kesamaan (0-1)
|
|
163
|
+
* @param useLevenshtein Gunakan algoritma Levenshtein untuk deteksi
|
|
164
|
+
* @param maxLevenshteinDistance Jarak maksimal Levenshtein (default: 2)
|
|
164
165
|
*/
|
|
165
|
-
enableSimilarityDetection(
|
|
166
|
+
enableSimilarityDetection(
|
|
167
|
+
threshold: number = 0.8,
|
|
168
|
+
useLevenshtein: boolean = false,
|
|
169
|
+
maxLevenshteinDistance: number = 2,
|
|
170
|
+
) {
|
|
171
|
+
this.options.detectSimilarity = true;
|
|
172
|
+
this.options.similarityThreshold = threshold;
|
|
173
|
+
this.options.useLevenshtein = useLevenshtein;
|
|
174
|
+
this.options.maxLevenshteinDistance = maxLevenshteinDistance;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Mengaktifkan deteksi berbasis Levenshtein distance
|
|
179
|
+
* @param threshold Threshold kesamaan (0-1)
|
|
180
|
+
* @param maxDistance Jarak maksimal Levenshtein (default: 2)
|
|
181
|
+
*/
|
|
182
|
+
enableLevenshteinDetection(threshold: number = 0.8, maxDistance: number = 2) {
|
|
166
183
|
this.options.detectSimilarity = true;
|
|
184
|
+
this.options.useLevenshtein = true;
|
|
167
185
|
this.options.similarityThreshold = threshold;
|
|
186
|
+
this.options.maxLevenshteinDistance = maxDistance;
|
|
168
187
|
}
|
|
169
188
|
}
|
|
170
189
|
|
package/src/types/index.ts
CHANGED
|
@@ -50,9 +50,11 @@ export interface FilterOptions {
|
|
|
50
50
|
useRandomGrawlix?: boolean;
|
|
51
51
|
keepFirstAndLast?: boolean;
|
|
52
52
|
indonesianVariation?: boolean;
|
|
53
|
-
detectSimilarity?: boolean;
|
|
54
|
-
similarityThreshold?: number;
|
|
53
|
+
detectSimilarity?: boolean; // Enable/disable Levenshtein distance matching
|
|
54
|
+
similarityThreshold?: number; // Threshold for Levenshtein distance similarity (0-1)
|
|
55
55
|
detectSplit?: boolean;
|
|
56
|
+
useLevenshtein?: boolean; // New option specifically for Levenshtein algorithm
|
|
57
|
+
maxLevenshteinDistance?: number; // Maximum allowed Levenshtein distance
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
export interface FilterResult {
|