@sideid/id-profanity-filter 1.11.7 → 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.
Files changed (53) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.esm.js +1004 -438
  3. package/dist/index.esm.js.map +1 -1
  4. package/dist/index.js +1004 -438
  5. package/dist/index.js.map +1 -1
  6. package/dist/types/config/options.d.ts +1 -1
  7. package/dist/types/constants/categories/blasphemy.d.ts +1 -1
  8. package/dist/types/constants/categories/disgusting.d.ts +1 -1
  9. package/dist/types/constants/categories/drugs.d.ts +1 -1
  10. package/dist/types/constants/categories/insult.d.ts +1 -1
  11. package/dist/types/constants/categories/profanity.d.ts +1 -1
  12. package/dist/types/constants/categories/sexual.d.ts +1 -1
  13. package/dist/types/constants/categories/slur.d.ts +1 -1
  14. package/dist/types/constants/regions/bali.d.ts +1 -1
  15. package/dist/types/constants/regions/batak.d.ts +1 -1
  16. package/dist/types/constants/regions/betawi.d.ts +1 -1
  17. package/dist/types/constants/regions/general.d.ts +1 -1
  18. package/dist/types/constants/regions/minang.d.ts +4 -0
  19. package/dist/types/constants/wordList.d.ts +10 -1
  20. package/dist/types/core/analyzer.d.ts +1 -1
  21. package/dist/types/core/filter.d.ts +1 -1
  22. package/dist/types/core/matcher.d.ts +1 -1
  23. package/dist/types/types/index.d.ts +2 -2
  24. package/dist/types/utils/regexUtils.d.ts +1 -1
  25. package/package.json +1 -1
  26. package/src/config/options.ts +25 -30
  27. package/src/constants/categories/blasphemy.ts +26 -15
  28. package/src/constants/categories/disgusting.ts +62 -54
  29. package/src/constants/categories/drugs.ts +56 -47
  30. package/src/constants/categories/insult.ts +80 -76
  31. package/src/constants/categories/profanity.ts +98 -92
  32. package/src/constants/categories/sexual.ts +74 -65
  33. package/src/constants/categories/slur.ts +73 -66
  34. package/src/constants/regions/aceh.ts +5 -9
  35. package/src/constants/regions/bali.ts +4 -7
  36. package/src/constants/regions/batak.ts +5 -9
  37. package/src/constants/regions/betawi.ts +19 -22
  38. package/src/constants/regions/general.ts +140 -144
  39. package/src/constants/regions/jawa.ts +11 -29
  40. package/src/constants/regions/madura.ts +2 -4
  41. package/src/constants/regions/minang.ts +53 -0
  42. package/src/constants/regions/sunda.ts +2 -4
  43. package/src/constants/wordList.ts +31 -34
  44. package/src/core/analyzer.ts +15 -26
  45. package/src/core/filter.ts +23 -39
  46. package/src/core/matcher.ts +25 -49
  47. package/src/index.ts +5 -16
  48. package/src/types/index.ts +26 -25
  49. package/src/utils/ahoCorasick.ts +1 -1
  50. package/src/utils/regexUtils.ts +37 -43
  51. package/src/utils/similarityUtils.ts +13 -28
  52. package/src/utils/stringUtils.ts +30 -38
  53. /package/{prettierrc → .prettierrc} +0 -0
@@ -1,5 +1,5 @@
1
- import { escapeRegExp } from "./stringUtils";
2
- import { RegexOptions } from "../types";
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 ? "g" : "gi");
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: ["a", "4", "@"],
63
- b: ["b", "8", "6"],
64
- c: ["c", "(", "{", "<"],
65
- e: ["e", "3"],
66
- g: ["g", "6", "9"],
67
- i: ["i", "1", "!", "|"],
68
- l: ["l", "1", "|"],
69
- o: ["o", "0"],
70
- s: ["s", "5", "$"],
71
- t: ["t", "7", "+"],
72
- z: ["z", "2"],
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("").join("[\\s\\-._*+]?");
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, "gi");
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: ["c", "k"], // contoh: becok/bekok
125
- k: ["k", "c", "q"], // contoh: kacau/qacau
126
- j: ["j", "dj"], // contoh: jualan/djualan (ejaan lama)
127
- y: ["y", "j"], // contoh: ya/ja
128
- u: ["u", "oe"], // contoh: untuk/oentoek (ejaan lama)
129
- f: ["f", "p", "v"], // contoh: kafir/kapir
130
- z: ["z", "j", "s"], // contoh: zaman/jaman
131
- x: ["x", "ks"], // contoh: taxi/taksi
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`, "gi");
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, "gi");
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 = ["", "me", "pe", "ber", "di", "ter", "se"];
190
- const suffixes = ["", "kan", "an", "i", "nya"];
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("|"), "gi");
195
+ return new RegExp(patterns.join('|'), 'gi');
202
196
  }
@@ -36,7 +36,7 @@ export function levenshteinDistance(str1: string, str2: string): number {
36
36
  matrix[i][j] = Math.min(
37
37
  matrix[i - 1][j] + 1, // deletion
38
38
  matrix[i][j - 1] + 1, // insertion
39
- matrix[i - 1][j - 1] + cost, // substitution
39
+ matrix[i - 1][j - 1] + cost // substitution
40
40
  );
41
41
  }
42
42
  }
@@ -72,7 +72,7 @@ export function stringSimilarity(str1: string, str2: string): number {
72
72
  export function findMostSimilar(
73
73
  target: string,
74
74
  candidates: string[],
75
- threshold: number = 0.7,
75
+ threshold: number = 0.7
76
76
  ): string | null {
77
77
  if (!candidates.length) return null;
78
78
 
@@ -104,7 +104,7 @@ export function findMostSimilarWithLevenshtein(
104
104
  target: string,
105
105
  candidates: string[],
106
106
  threshold: number = 0.7,
107
- maxDistance: number = 3,
107
+ maxDistance: number = 3
108
108
  ): string | null {
109
109
  if (!candidates.length) return null;
110
110
 
@@ -145,7 +145,7 @@ export function findMostSimilarWithLevenshtein(
145
145
  export function isPossibleProfanityVariation(
146
146
  input: string,
147
147
  profanityWords: string[],
148
- threshold: number = 0.75,
148
+ threshold: number = 0.75
149
149
  ): [boolean, string | null] {
150
150
  if (!input || !profanityWords.length) return [false, null];
151
151
 
@@ -167,10 +167,7 @@ export function isPossibleProfanityVariation(
167
167
  * @param threshold Batas minimum kesamaan (default: 0.8)
168
168
  * @returns Array kluster kata yang mirip
169
169
  */
170
- export function clusterSimilarWords(
171
- words: string[],
172
- threshold: number = 0.8,
173
- ): string[][] {
170
+ export function clusterSimilarWords(words: string[], threshold: number = 0.8): string[][] {
174
171
  const clusters: string[][] = [];
175
172
  const processed: Set<string> = new Set();
176
173
 
@@ -217,10 +214,9 @@ export function clusterSimilarWords(
217
214
  export function findPossibleProfanityBySimiliarity(
218
215
  text: string,
219
216
  profanityWords: string[],
220
- threshold: number = 0.8,
217
+ threshold: number = 0.8
221
218
  ): Array<{ word: string; original: string; similarity: number }> {
222
- const result: Array<{ word: string; original: string; similarity: number }> =
223
- [];
219
+ const result: Array<{ word: string; original: string; similarity: number }> = [];
224
220
 
225
221
  // Optimasi dengan bikin map kata-kata kotor berdasarkan huruf pertama
226
222
  const profanityMap = new Map<string, string[]>();
@@ -262,7 +258,7 @@ export function findPossibleProfanityBySimiliarity(
262
258
 
263
259
  // Filter kandidat berdasarkan perbedaan panjang sebelum ngitung kemiripannya
264
260
  const lengthFilteredCandidates = allCandidates.filter(
265
- (candidate) => Math.abs(candidate.length - word.length) <= 2,
261
+ (candidate) => Math.abs(candidate.length - word.length) <= 2
266
262
  );
267
263
 
268
264
  // Cari yang paling cocok
@@ -275,10 +271,7 @@ export function findPossibleProfanityBySimiliarity(
275
271
  for (const profanity of lengthFilteredCandidates) {
276
272
  const similarity = stringSimilarity(word, profanity);
277
273
 
278
- if (
279
- similarity >= threshold &&
280
- (!bestMatch || similarity > bestMatch.similarity)
281
- ) {
274
+ if (similarity >= threshold && (!bestMatch || similarity > bestMatch.similarity)) {
282
275
  bestMatch = {
283
276
  word,
284
277
  original: profanity,
@@ -308,7 +301,7 @@ export function findProfanityByLevenshteinDistance(
308
301
  text: string,
309
302
  profanityWords: string[],
310
303
  threshold: number = 0.8,
311
- maxDistance: number = 2,
304
+ maxDistance: number = 2
312
305
  ): Array<{
313
306
  word: string;
314
307
  original: string;
@@ -361,13 +354,9 @@ export function findProfanityByLevenshteinDistance(
361
354
  const distance = levenshteinDistance(word, profanity);
362
355
 
363
356
  if (distance <= maxDistance) {
364
- const similarity =
365
- 1 - distance / Math.max(word.length, profanity.length);
357
+ const similarity = 1 - distance / Math.max(word.length, profanity.length);
366
358
 
367
- if (
368
- similarity >= threshold &&
369
- (!bestMatch || similarity > bestMatch.similarity)
370
- ) {
359
+ if (similarity >= threshold && (!bestMatch || similarity > bestMatch.similarity)) {
371
360
  bestMatch = {
372
361
  word,
373
362
  original: profanity,
@@ -395,11 +384,7 @@ export function findProfanityByLevenshteinDistance(
395
384
  * Helper function to efficiently check if character counts between two strings
396
385
  * are similar enough to warrant a full Levenshtein calculation
397
386
  */
398
- function isCharacterCountSimilar(
399
- str1: string,
400
- str2: string,
401
- maxDifference: number,
402
- ): boolean {
387
+ function isCharacterCountSimilar(str1: string, str2: string, maxDifference: number): boolean {
403
388
  const charCount1: Record<string, number> = {};
404
389
  const charCount2: Record<string, number> = {};
405
390
 
@@ -8,8 +8,8 @@
8
8
  */
9
9
  export function censorWord(
10
10
  word: string,
11
- replaceChar: string = "*",
12
- keepFirstAndLast: boolean = false,
11
+ replaceChar: string = '*',
12
+ keepFirstAndLast: boolean = false
13
13
  ): string {
14
14
  if (word.length <= 2) {
15
15
  return replaceChar.repeat(word.length);
@@ -31,9 +31,9 @@ export function censorWord(
31
31
  export function normalizeText(text: string): string {
32
32
  return text
33
33
  .toLowerCase()
34
- .normalize("NFD") // Normalisasi Unicode
35
- .replace(/[\u0300-\u036f]/g, "") // Hapus diacritic marks
36
- .replace(/[^\w\s]/g, "") // Hapus karakter non-alphanumeric
34
+ .normalize('NFD') // Normalisasi Unicode
35
+ .replace(/[\u0300-\u036f]/g, '') // Hapus diacritic marks
36
+ .replace(/[^\w\s]/g, '') // Hapus karakter non-alphanumeric
37
37
  .trim(); // Hapus whitespace di awal dan akhir
38
38
  }
39
39
 
@@ -48,7 +48,7 @@ export function normalizeText(text: string): string {
48
48
  export function containsAnyWord(
49
49
  text: string,
50
50
  wordList: string[],
51
- checkSubstring: boolean = false,
51
+ checkSubstring: boolean = false
52
52
  ): boolean {
53
53
  const normalizedText = normalizeText(text);
54
54
 
@@ -56,9 +56,7 @@ export function containsAnyWord(
56
56
  const normalizedWord = normalizeText(word);
57
57
  return checkSubstring
58
58
  ? normalizedText.includes(normalizedWord)
59
- : new RegExp(`\\b${escapeRegExp(normalizedWord)}\\b`, "i").test(
60
- normalizedText,
61
- );
59
+ : new RegExp(`\\b${escapeRegExp(normalizedWord)}\\b`, 'i').test(normalizedText);
62
60
  });
63
61
  }
64
62
 
@@ -78,7 +76,7 @@ export function containsEuphemism(text: string, wordList: string[]): boolean {
78
76
  const lastChar = word[word.length - 1];
79
77
  const pattern = new RegExp(
80
78
  `\\b${escapeRegExp(firstChar)}[*@#\\-_.!?\\s]{${word.length - 2}}${escapeRegExp(lastChar)}\\b`,
81
- "i",
79
+ 'i'
82
80
  );
83
81
 
84
82
  return pattern.test(text);
@@ -94,13 +92,13 @@ export function containsEuphemism(text: string, wordList: string[]): boolean {
94
92
  * @returns Boolean apakah teks mengandung upaya menghindari filter
95
93
  */
96
94
  export function detectSplitWords(text: string, wordList: string[]): boolean {
97
- const compressedText = text.replace(/[\s\-_.!?*]/g, "").toLowerCase();
95
+ const compressedText = text.replace(/[\s\-_.!?*]/g, '').toLowerCase();
98
96
 
99
97
  return wordList.some((word) => compressedText.includes(normalizeText(word)));
100
98
  }
101
99
 
102
100
  export function escapeRegExp(string: string): string {
103
- return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
101
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
104
102
  }
105
103
 
106
104
  /**
@@ -117,9 +115,9 @@ export function maskText(
117
115
  text: string,
118
116
  visibleStart: number = 1,
119
117
  visibleEnd: number = 1,
120
- maskChar: string = "*",
118
+ maskChar: string = '*'
121
119
  ): string {
122
- if (!text) return "";
120
+ if (!text) return '';
123
121
  if (text.length <= visibleStart + visibleEnd) return text;
124
122
 
125
123
  const start = text.substring(0, visibleStart);
@@ -138,26 +136,26 @@ export function maskText(
138
136
  */
139
137
  export function toLeetSpeak(text: string): string {
140
138
  const leetMap: Record<string, string[]> = {
141
- a: ["4", "@"],
142
- b: ["8", "6"],
143
- c: ["<", "(", "{"],
144
- e: ["3"],
145
- g: ["9"],
146
- i: ["1", "!"],
147
- l: ["1", "|"],
148
- o: ["0"],
149
- s: ["5", "$"],
150
- t: ["7", "+"],
151
- z: ["2"],
139
+ a: ['4', '@'],
140
+ b: ['8', '6'],
141
+ c: ['<', '(', '{'],
142
+ e: ['3'],
143
+ g: ['9'],
144
+ i: ['1', '!'],
145
+ l: ['1', '|'],
146
+ o: ['0'],
147
+ s: ['5', '$'],
148
+ t: ['7', '+'],
149
+ z: ['2'],
152
150
  };
153
151
 
154
152
  return text
155
- .split("")
153
+ .split('')
156
154
  .map((char) => {
157
155
  const lowerChar = char.toLowerCase();
158
156
  return leetMap[lowerChar] || char;
159
157
  })
160
- .join("");
158
+ .join('');
161
159
  }
162
160
 
163
161
  /**
@@ -168,9 +166,7 @@ export function toLeetSpeak(text: string): string {
168
166
  */
169
167
  export function splitIntoSentences(text: string): string[] {
170
168
  // split berdasarkan titik, seru, taya yagn diikuti spasi atau akhir string
171
- return text
172
- .split(/(?<=[.!?])\s+|(?<=[.!?])$/)
173
- .filter((sentence) => sentence.trim().length > 0);
169
+ return text.split(/(?<=[.!?])\s+|(?<=[.!?])$/).filter((sentence) => sentence.trim().length > 0);
174
170
  }
175
171
 
176
172
  /**
@@ -181,12 +177,8 @@ export function splitIntoSentences(text: string): string[] {
181
177
  * @param windowSize jumlah kata di sekitar indeks
182
178
  * @return Kata-kata di sekitar indeks
183
179
  */
184
- export function getContextAroundIndex(
185
- text: string,
186
- index: number,
187
- windowSize: number = 5,
188
- ): string {
189
- if (!text || index < 0 || index >= text.length) return "";
180
+ export function getContextAroundIndex(text: string, index: number, windowSize: number = 5): string {
181
+ if (!text || index < 0 || index >= text.length) return '';
190
182
 
191
183
  const words = text.split(/\s+/);
192
184
 
@@ -203,11 +195,11 @@ export function getContextAroundIndex(
203
195
  currentPosition += wordLength + 1;
204
196
  }
205
197
 
206
- if (targetWordIndex === -1) return "";
198
+ if (targetWordIndex === -1) return '';
207
199
 
208
200
  // Ambil kata-kata di sekitar
209
201
  const startIndex = Math.max(0, targetWordIndex - windowSize);
210
202
  const endIndex = Math.min(words.length, targetWordIndex + windowSize + 1);
211
203
 
212
- return words.slice(startIndex, endIndex).join(" ");
204
+ return words.slice(startIndex, endIndex).join(' ');
213
205
  }
File without changes