@sideid/id-profanity-filter 1.0.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 -2
- 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/tsconfig.json +1 -1
- /package/{jest.config.js → jest.config.mjs} +0 -0
- /package/{rollup.config.js → rollup.config.mjs} +0 -0
package/src/core/analyzer.ts
CHANGED
|
@@ -1,204 +1,225 @@
|
|
|
1
|
-
import {
|
|
2
|
-
FilterOptions,
|
|
3
|
-
AnalysisResult,
|
|
4
|
-
ProfanityWord,
|
|
5
|
-
ProfanityCategory,
|
|
6
|
-
Region,
|
|
7
|
-
} from
|
|
8
|
-
import {
|
|
9
|
-
findProfanity,
|
|
10
|
-
findProfanityWithMetadata,
|
|
11
|
-
findCategories,
|
|
12
|
-
findRegions,
|
|
13
|
-
calculateSeverity,
|
|
14
|
-
} from
|
|
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
|
-
|
|
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
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
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
|
-
return
|
|
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
|
-
|
|
1
|
+
import {
|
|
2
|
+
FilterOptions,
|
|
3
|
+
AnalysisResult,
|
|
4
|
+
ProfanityWord,
|
|
5
|
+
ProfanityCategory,
|
|
6
|
+
Region,
|
|
7
|
+
} from "../types";
|
|
8
|
+
import {
|
|
9
|
+
findProfanity,
|
|
10
|
+
findProfanityWithMetadata,
|
|
11
|
+
findCategories,
|
|
12
|
+
findRegions,
|
|
13
|
+
calculateSeverity,
|
|
14
|
+
} from "./matcher";
|
|
15
|
+
import {
|
|
16
|
+
normalizeText,
|
|
17
|
+
escapeRegExp,
|
|
18
|
+
splitIntoSentences,
|
|
19
|
+
getContextAroundIndex,
|
|
20
|
+
} from "../utils/stringUtils";
|
|
21
|
+
import { findPossibleProfanityBySimiliarity } from "../utils/similarityUtils";
|
|
22
|
+
import { createContextRegex } from "../utils/regexUtils";
|
|
23
|
+
import { DEFAULT_OPTIONS } from "../config/options";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Menganalisis teks untuk kata kotor
|
|
27
|
+
*
|
|
28
|
+
* @param text Teks yang akan dianalisis
|
|
29
|
+
* @param options Opsi untuk analisis
|
|
30
|
+
* @return AnalysisResult dengan hasil analisis
|
|
31
|
+
*/
|
|
32
|
+
export function analyze(
|
|
33
|
+
text: string,
|
|
34
|
+
options: FilterOptions = {},
|
|
35
|
+
): AnalysisResult {
|
|
36
|
+
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
37
|
+
|
|
38
|
+
const matches = findProfanity(text, mergedOptions);
|
|
39
|
+
|
|
40
|
+
if (matches.length === 0) {
|
|
41
|
+
return {
|
|
42
|
+
hasProfanity: false,
|
|
43
|
+
matches: [],
|
|
44
|
+
matchDetails: [],
|
|
45
|
+
categories: [],
|
|
46
|
+
regions: [],
|
|
47
|
+
severityScore: 0,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const matchDetails = findProfanityWithMetadata(text, mergedOptions);
|
|
52
|
+
const categories = findCategories(matchDetails);
|
|
53
|
+
const regions = findRegions(matchDetails);
|
|
54
|
+
const severityScore = calculateSeverity(matchDetails);
|
|
55
|
+
|
|
56
|
+
let similarWords: Array<{
|
|
57
|
+
word: string;
|
|
58
|
+
original: string;
|
|
59
|
+
similarity: number;
|
|
60
|
+
}> = [];
|
|
61
|
+
|
|
62
|
+
if (mergedOptions.detectSimilarity) {
|
|
63
|
+
const wordList = matchDetails.map((word) => word.word);
|
|
64
|
+
similarWords = findPossibleProfanityBySimiliarity(
|
|
65
|
+
text,
|
|
66
|
+
wordList,
|
|
67
|
+
mergedOptions.similarityThreshold || 0.8,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
hasProfanity: true,
|
|
73
|
+
matches,
|
|
74
|
+
matchDetails,
|
|
75
|
+
categories,
|
|
76
|
+
regions,
|
|
77
|
+
severityScore,
|
|
78
|
+
similarWords: mergedOptions.detectSimilarity ? similarWords : undefined,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Menganalisis daftar teks dan ringkasan
|
|
84
|
+
*
|
|
85
|
+
* @param texts Daftar teks yang dianalisis
|
|
86
|
+
* @param options Opsi untuk analisis
|
|
87
|
+
* @return Objek dengan ringkasan analisis
|
|
88
|
+
*/
|
|
89
|
+
export function batchAnalyze(
|
|
90
|
+
texts: string[],
|
|
91
|
+
options: FilterOptions = {},
|
|
92
|
+
): {
|
|
93
|
+
totalTexts: number;
|
|
94
|
+
profaneTexts: number;
|
|
95
|
+
cleanTexts: number;
|
|
96
|
+
averageSeverity: number;
|
|
97
|
+
topCategories: ProfanityCategory[];
|
|
98
|
+
topRegions: Region[];
|
|
99
|
+
mostFrequentWords: Array<{ word: string; count: number }>;
|
|
100
|
+
} {
|
|
101
|
+
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
102
|
+
const results = texts.map((text) => analyze(text, mergedOptions));
|
|
103
|
+
const profaneTexts = results.filter((result) => result.hasProfanity).length;
|
|
104
|
+
const totalSeverity = results.reduce(
|
|
105
|
+
(sum, result) => sum + result.severityScore,
|
|
106
|
+
0,
|
|
107
|
+
);
|
|
108
|
+
const averageSeverity = profaneTexts > 0 ? totalSeverity / profaneTexts : 0;
|
|
109
|
+
const allCategories = results.flatMap((result) => result.categories);
|
|
110
|
+
const categoryCount: Record<string, number> = {};
|
|
111
|
+
|
|
112
|
+
allCategories.forEach((category) => {
|
|
113
|
+
categoryCount[category] = (categoryCount[category] || 0) + 1;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const topCategories = Object.entries(categoryCount)
|
|
117
|
+
.sort((a, b) => b[1] - a[1])
|
|
118
|
+
.map(([category]) => category as ProfanityCategory);
|
|
119
|
+
|
|
120
|
+
const allRegions = results.flatMap((result) => result.regions);
|
|
121
|
+
const regionCount: Record<string, number> = {};
|
|
122
|
+
|
|
123
|
+
allRegions.forEach((region) => {
|
|
124
|
+
regionCount[region] = (regionCount[region] || 0) + 1;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const topRegions = Object.entries(regionCount)
|
|
128
|
+
.sort((a, b) => b[1] - a[1])
|
|
129
|
+
.map(([region]) => region as Region);
|
|
130
|
+
|
|
131
|
+
const allWords = results.flatMap((result) => result.matches);
|
|
132
|
+
const wordCount: Record<string, number> = {};
|
|
133
|
+
|
|
134
|
+
allWords.forEach((word) => {
|
|
135
|
+
wordCount[word] = (wordCount[word] || 0) + 1;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const mostFrequentWords = Object.entries(wordCount)
|
|
139
|
+
.sort((a, b) => b[1] - a[1])
|
|
140
|
+
.slice(0, 10)
|
|
141
|
+
.map(([word, count]) => ({ word, count }));
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
totalTexts: texts.length,
|
|
145
|
+
profaneTexts,
|
|
146
|
+
cleanTexts: texts.length - profaneTexts,
|
|
147
|
+
averageSeverity,
|
|
148
|
+
topCategories,
|
|
149
|
+
topRegions,
|
|
150
|
+
mostFrequentWords,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Menganalisis per-kalimat untuk melokalisasi kata kotor
|
|
156
|
+
*
|
|
157
|
+
* @param text Teks yang akan dianalisis per-kalimat
|
|
158
|
+
* @param options Opsi untuk analisis
|
|
159
|
+
* @returns Array hasil analisis per-kalimat
|
|
160
|
+
*/
|
|
161
|
+
export function analyzeBySentence(
|
|
162
|
+
text: string,
|
|
163
|
+
options: FilterOptions = {},
|
|
164
|
+
): Array<AnalysisResult & { sentence: string }> {
|
|
165
|
+
const sentences = splitIntoSentences(text);
|
|
166
|
+
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
167
|
+
|
|
168
|
+
return sentences.map((sentence) => {
|
|
169
|
+
const result = analyze(sentence, mergedOptions);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
...result,
|
|
173
|
+
sentence,
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Menganalisis teks untuk menemukan kata kotor pada konteks tertentu
|
|
180
|
+
*
|
|
181
|
+
* @param text Teks yang akan dianalisis
|
|
182
|
+
* @param contextWindowSize Ukuran konteks (jumlah kata) di sekitar kata kotor
|
|
183
|
+
* @param options Opsi untuk analisis
|
|
184
|
+
* @returns Konteks di dekat kata kotor
|
|
185
|
+
*/
|
|
186
|
+
export function analyzeWithContext(
|
|
187
|
+
text: string,
|
|
188
|
+
contextWindowSize: number = 5,
|
|
189
|
+
options: FilterOptions = {},
|
|
190
|
+
): Array<{
|
|
191
|
+
word: string;
|
|
192
|
+
context: string;
|
|
193
|
+
position: { start: number; end: number };
|
|
194
|
+
}> {
|
|
195
|
+
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
196
|
+
const matches = findProfanity(text, mergedOptions);
|
|
197
|
+
|
|
198
|
+
if (matches.length === 0) {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const result = [];
|
|
203
|
+
|
|
204
|
+
for (const word of matches) {
|
|
205
|
+
const regex = createContextRegex(word, contextWindowSize);
|
|
206
|
+
|
|
207
|
+
let match;
|
|
208
|
+
while ((match = regex.exec(text)) !== null) {
|
|
209
|
+
const beforeContext = match[1] || "";
|
|
210
|
+
const wordMatch = match[2];
|
|
211
|
+
const afterContext = match[3] || "";
|
|
212
|
+
|
|
213
|
+
result.push({
|
|
214
|
+
word: wordMatch,
|
|
215
|
+
context: beforeContext + wordMatch + afterContext,
|
|
216
|
+
position: {
|
|
217
|
+
start: match.index,
|
|
218
|
+
end: match.index + match[0].length,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return result;
|
|
225
|
+
}
|