censorship-id 1.1.0 → 2.0.0
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/README.md +168 -82
- package/dist/index.d.mts +130 -44
- package/dist/index.d.ts +130 -44
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,82 +1,168 @@
|
|
|
1
|
-
# censorship-id
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/censorship-id)
|
|
4
|
-
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
|
|
6
|
-
A robust and
|
|
7
|
-
|
|
8
|
-
## Features
|
|
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
|
-
|
|
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
|
-
|
|
1
|
+
# censorship-id
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/censorship-id)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
A robust, feature-rich, and lightweight Indonesian profanity filter. Fully redesigned for v2.0.0, it supports structured metadata detection, severity levels, categories, whitelisting, leetspeak evasion handling, and custom dictionary management.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Multi-purpose API**: Censor text, detect profane words with metadata (severity/category), or perform simple boolean checks.
|
|
11
|
+
- **Smart Mode**: Handles advanced evasion techniques (leetspeak, repeated characters, punctuation-in-word).
|
|
12
|
+
- **Categorization & Severity**: Dictionary entries are classified into categories (`vulgar`, `slur`, `sexual`, `offensive`, `slang`) and severity levels (`low`, `medium`, `high`).
|
|
13
|
+
- **Whitelisting**: Easily allow specific words to bypass filtering.
|
|
14
|
+
- **Dictionary Management**: Dynamically add or remove words globally at runtime.
|
|
15
|
+
- **Lightweight**: Zero production dependencies. Built for CJS and ESM with full TypeScript support.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install censorship-id
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Reorganized Structure
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
censorship-id/
|
|
27
|
+
├── src/
|
|
28
|
+
│ ├── index.js # Core filter implementation (detect, censorId, isProfane)
|
|
29
|
+
│ ├── dictionary.js # Structured Indonesian profanity database
|
|
30
|
+
│ └── index.d.ts # TypeScript declarations
|
|
31
|
+
├── test/
|
|
32
|
+
│ └── index.test.js # Jest unit tests
|
|
33
|
+
├── dist/ # Build outputs (CJS, ESM, Types)
|
|
34
|
+
└── package.json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## API Reference
|
|
40
|
+
|
|
41
|
+
### 1. `censorId(text, options)`
|
|
42
|
+
|
|
43
|
+
Replaces profane words with a mask character.
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
const { censorId } = require("censorship-id");
|
|
47
|
+
|
|
48
|
+
// Basic
|
|
49
|
+
censorId("anjing kau!");
|
|
50
|
+
// => "****** kau!"
|
|
51
|
+
|
|
52
|
+
// Keep first and last letter
|
|
53
|
+
censorId("anjing kau!", { keepFirstAndLast: true });
|
|
54
|
+
// => "a****g kau!"
|
|
55
|
+
|
|
56
|
+
// Custom mask character
|
|
57
|
+
censorId("anjing kau!", { mask: "#" });
|
|
58
|
+
// => "###### kau!"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Options:**
|
|
62
|
+
|
|
63
|
+
- `mask` (string, default: `*`): Mask character.
|
|
64
|
+
- `keepFirstAndLast` (boolean, default: `false`): Keep first and last letters visible.
|
|
65
|
+
- `allowedWords` (string[]): Words to bypass filtering.
|
|
66
|
+
- `customWords` (string[] | object[]): Additional words to censor.
|
|
67
|
+
- `smartMode` (boolean, default: `false`): Handle obfuscation (e.g., `4njing`, `baaaaabiii`).
|
|
68
|
+
- `minSeverity` (`'low' | 'medium' | 'high'`): Minimum severity level to censor.
|
|
69
|
+
- `categories` (string[]): Only censor specific categories (e.g., `['slur', 'sexual']`).
|
|
70
|
+
|
|
71
|
+
### 2. `detect(text, options)`
|
|
72
|
+
|
|
73
|
+
Scans text and returns structured metadata about all matches.
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
const { detect } = require("censorship-id");
|
|
77
|
+
|
|
78
|
+
const results = detect("dasar anjing kau!");
|
|
79
|
+
console.log(results);
|
|
80
|
+
/*
|
|
81
|
+
Output:
|
|
82
|
+
[
|
|
83
|
+
{
|
|
84
|
+
match: "anjing",
|
|
85
|
+
word: "anjing",
|
|
86
|
+
index: 6,
|
|
87
|
+
length: 6,
|
|
88
|
+
severity: "high",
|
|
89
|
+
category: "offensive"
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
// Using smartMode
|
|
95
|
+
detect("dasar 4njing!", { smartMode: true });
|
|
96
|
+
/*
|
|
97
|
+
Output:
|
|
98
|
+
[
|
|
99
|
+
{
|
|
100
|
+
match: "4njing",
|
|
101
|
+
word: "anjing",
|
|
102
|
+
index: 6,
|
|
103
|
+
length: 6,
|
|
104
|
+
severity: "high",
|
|
105
|
+
category: "offensive"
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
*/
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Options:** Same as `censorId` (minus `mask` and `keepFirstAndLast`) plus:
|
|
112
|
+
|
|
113
|
+
- `unique` (boolean, default: `false`): If `true`, only returns the first occurrence of each unique word.
|
|
114
|
+
|
|
115
|
+
### 3. `isProfane(text, options)`
|
|
116
|
+
|
|
117
|
+
Returns a boolean indicating if the text contains any profanity.
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
120
|
+
const { isProfane } = require("censorship-id");
|
|
121
|
+
|
|
122
|
+
isProfane("Halo apa kabar?"); // => false
|
|
123
|
+
isProfane("Dasar anjing!"); // => true
|
|
124
|
+
isProfane("Dasar anjing!", { minSeverity: "high" }); // => true
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 4. Dictionary Management
|
|
128
|
+
|
|
129
|
+
Modify the default database at runtime.
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
const { addWords, removeWords, getDictionary } = require("censorship-id");
|
|
133
|
+
|
|
134
|
+
// Add string words (assigns medium severity & custom category)
|
|
135
|
+
addWords(["perkosa", "bodoh"]);
|
|
136
|
+
|
|
137
|
+
// Add words with metadata
|
|
138
|
+
addWords([{ word: "jelek", severity: "low", category: "offensive" }]);
|
|
139
|
+
|
|
140
|
+
// Remove words
|
|
141
|
+
removeWords(["jelek", "bodoh"]);
|
|
142
|
+
|
|
143
|
+
// Retrieve copy of active dictionary
|
|
144
|
+
const activeDictionary = getDictionary();
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Severity & Categories
|
|
150
|
+
|
|
151
|
+
The package classifies Indonesian words into the following categories and levels:
|
|
152
|
+
|
|
153
|
+
| Category | Level | Description | Examples |
|
|
154
|
+
| ----------- | ------ | ---------------------------------------- | --------------------------------------- |
|
|
155
|
+
| `sexual` | High | Highly vulgar/anatomical terms | `kontol`, `memek`, `ngentot`, `pepek` |
|
|
156
|
+
| `slur` | High | Offensive slurs targeting identity | `banci`, `bencong`, `lonte`, `perek` |
|
|
157
|
+
| `offensive` | High | Harsh expressions, insults | `bangsat`, `bajingan`, `anjing`, `babi` |
|
|
158
|
+
| `vulgar` | Medium | Inappropriate bodily or vulgar slang | `berak`, `tai`, `sempak` |
|
|
159
|
+
| `offensive` | Medium | Insulting terms for intellect | `bego`, `goblok`, `tolol`, `dungu` |
|
|
160
|
+
| `slang` | Medium | Inappropriate regional slang/swear words | `anjir`, `cuk`, `dancuk`, `jancuk` |
|
|
161
|
+
| `offensive` | Low | Mild insults or slang | `buaya`, `najis`, `ndeso`, `kere` |
|
|
162
|
+
| `vulgar` | Low | Mild sexual references, innuendo | `bokep`, `sange`, `lendir` |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT © [rheatkhs](https://github.com/rheatkhs)
|
package/dist/index.d.mts
CHANGED
|
@@ -1,44 +1,130 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*/
|
|
4
|
-
interface
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
1
|
+
/**
|
|
2
|
+
* A single match result from detect().
|
|
3
|
+
*/
|
|
4
|
+
interface Match {
|
|
5
|
+
/** The matched text from the input (may differ from word in smartMode) */
|
|
6
|
+
match: string;
|
|
7
|
+
/** The dictionary word (normalized) */
|
|
8
|
+
word: string;
|
|
9
|
+
/** The index in the input string where the match starts */
|
|
10
|
+
index: number;
|
|
11
|
+
/** The length of the matched text */
|
|
12
|
+
length: number;
|
|
13
|
+
/** The severity level of the matched word */
|
|
14
|
+
severity: string;
|
|
15
|
+
/** The category of the matched word */
|
|
16
|
+
category: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A dictionary entry.
|
|
21
|
+
*/
|
|
22
|
+
interface DictionaryEntry {
|
|
23
|
+
word: string;
|
|
24
|
+
severity: string;
|
|
25
|
+
category: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Options for the censorship filter.
|
|
30
|
+
*/
|
|
31
|
+
interface CensorIdOptions {
|
|
32
|
+
/**
|
|
33
|
+
* The character used to mask the words.
|
|
34
|
+
* @default '*'
|
|
35
|
+
*/
|
|
36
|
+
mask?: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* An array of additional words to censor.
|
|
40
|
+
*/
|
|
41
|
+
customWords?: string[];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* An array of words to exclude from censoring (whitelist).
|
|
45
|
+
*/
|
|
46
|
+
allowedWords?: string[];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* If true, keeps the first and last letters visible (e.g., "a****g").
|
|
50
|
+
* @default false
|
|
51
|
+
*/
|
|
52
|
+
keepFirstAndLast?: boolean;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* If true, handles leetspeak (e.g., "4njing") and repeated characters (e.g., "baaaaaaabiii").
|
|
56
|
+
* @default false
|
|
57
|
+
*/
|
|
58
|
+
smartMode?: boolean;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Minimum severity level to include. Words below this severity are ignored.
|
|
62
|
+
* Severity order: low < medium < high.
|
|
63
|
+
*/
|
|
64
|
+
minSeverity?: "low" | "medium" | "high";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Only include words in these categories (e.g., ['offensive', 'slur']).
|
|
68
|
+
*/
|
|
69
|
+
categories?: string[];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* If true, only return the first occurrence of each unique word.
|
|
73
|
+
* Only used by detect().
|
|
74
|
+
* @default false
|
|
75
|
+
*/
|
|
76
|
+
unique?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Censors Indonesian profanity and inappropriate words from the given text.
|
|
81
|
+
*
|
|
82
|
+
* @param text The input text to be filtered.
|
|
83
|
+
* @param options Configuration for filtering behavior.
|
|
84
|
+
* @returns The censored text.
|
|
85
|
+
*/
|
|
86
|
+
declare function censorId(text: string, options?: CensorIdOptions): string;
|
|
87
|
+
declare function censorId<T>(text: T, options?: CensorIdOptions): T;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Detects profanity in text and returns structured results.
|
|
91
|
+
*
|
|
92
|
+
* @param text The input text to inspect.
|
|
93
|
+
* @param options Detection options (same as CensorIdOptions, minus mask/keepFirstAndLast).
|
|
94
|
+
* @returns An array of match objects with metadata.
|
|
95
|
+
*/
|
|
96
|
+
declare function detect(text: string, options?: CensorIdOptions): Match[];
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Checks if the text contains profanity.
|
|
100
|
+
*
|
|
101
|
+
* @param text The input text to check.
|
|
102
|
+
* @param options Detection options.
|
|
103
|
+
* @returns True if profanity is found.
|
|
104
|
+
*/
|
|
105
|
+
declare function isProfane(text: string, options?: CensorIdOptions): boolean;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Add words to the default dictionary.
|
|
109
|
+
* Accepts strings or objects with word/severity/category.
|
|
110
|
+
*/
|
|
111
|
+
declare function addWords(
|
|
112
|
+
words: (string | { word: string; severity?: string; category?: string })[],
|
|
113
|
+
): void;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Remove words from the default dictionary.
|
|
117
|
+
*/
|
|
118
|
+
declare function removeWords(words: string[]): void;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get a copy of the current default dictionary.
|
|
122
|
+
*/
|
|
123
|
+
declare function getDictionary(): DictionaryEntry[];
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The default dictionary of Indonesian profanity used by the filter.
|
|
127
|
+
*/
|
|
128
|
+
declare const dictionary: DictionaryEntry[];
|
|
129
|
+
|
|
130
|
+
export { type CensorIdOptions, type DictionaryEntry, type Match, addWords, censorId, detect, dictionary, getDictionary, isProfane, removeWords };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,44 +1,130 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*/
|
|
4
|
-
interface
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
1
|
+
/**
|
|
2
|
+
* A single match result from detect().
|
|
3
|
+
*/
|
|
4
|
+
interface Match {
|
|
5
|
+
/** The matched text from the input (may differ from word in smartMode) */
|
|
6
|
+
match: string;
|
|
7
|
+
/** The dictionary word (normalized) */
|
|
8
|
+
word: string;
|
|
9
|
+
/** The index in the input string where the match starts */
|
|
10
|
+
index: number;
|
|
11
|
+
/** The length of the matched text */
|
|
12
|
+
length: number;
|
|
13
|
+
/** The severity level of the matched word */
|
|
14
|
+
severity: string;
|
|
15
|
+
/** The category of the matched word */
|
|
16
|
+
category: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A dictionary entry.
|
|
21
|
+
*/
|
|
22
|
+
interface DictionaryEntry {
|
|
23
|
+
word: string;
|
|
24
|
+
severity: string;
|
|
25
|
+
category: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Options for the censorship filter.
|
|
30
|
+
*/
|
|
31
|
+
interface CensorIdOptions {
|
|
32
|
+
/**
|
|
33
|
+
* The character used to mask the words.
|
|
34
|
+
* @default '*'
|
|
35
|
+
*/
|
|
36
|
+
mask?: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* An array of additional words to censor.
|
|
40
|
+
*/
|
|
41
|
+
customWords?: string[];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* An array of words to exclude from censoring (whitelist).
|
|
45
|
+
*/
|
|
46
|
+
allowedWords?: string[];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* If true, keeps the first and last letters visible (e.g., "a****g").
|
|
50
|
+
* @default false
|
|
51
|
+
*/
|
|
52
|
+
keepFirstAndLast?: boolean;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* If true, handles leetspeak (e.g., "4njing") and repeated characters (e.g., "baaaaaaabiii").
|
|
56
|
+
* @default false
|
|
57
|
+
*/
|
|
58
|
+
smartMode?: boolean;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Minimum severity level to include. Words below this severity are ignored.
|
|
62
|
+
* Severity order: low < medium < high.
|
|
63
|
+
*/
|
|
64
|
+
minSeverity?: "low" | "medium" | "high";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Only include words in these categories (e.g., ['offensive', 'slur']).
|
|
68
|
+
*/
|
|
69
|
+
categories?: string[];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* If true, only return the first occurrence of each unique word.
|
|
73
|
+
* Only used by detect().
|
|
74
|
+
* @default false
|
|
75
|
+
*/
|
|
76
|
+
unique?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Censors Indonesian profanity and inappropriate words from the given text.
|
|
81
|
+
*
|
|
82
|
+
* @param text The input text to be filtered.
|
|
83
|
+
* @param options Configuration for filtering behavior.
|
|
84
|
+
* @returns The censored text.
|
|
85
|
+
*/
|
|
86
|
+
declare function censorId(text: string, options?: CensorIdOptions): string;
|
|
87
|
+
declare function censorId<T>(text: T, options?: CensorIdOptions): T;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Detects profanity in text and returns structured results.
|
|
91
|
+
*
|
|
92
|
+
* @param text The input text to inspect.
|
|
93
|
+
* @param options Detection options (same as CensorIdOptions, minus mask/keepFirstAndLast).
|
|
94
|
+
* @returns An array of match objects with metadata.
|
|
95
|
+
*/
|
|
96
|
+
declare function detect(text: string, options?: CensorIdOptions): Match[];
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Checks if the text contains profanity.
|
|
100
|
+
*
|
|
101
|
+
* @param text The input text to check.
|
|
102
|
+
* @param options Detection options.
|
|
103
|
+
* @returns True if profanity is found.
|
|
104
|
+
*/
|
|
105
|
+
declare function isProfane(text: string, options?: CensorIdOptions): boolean;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Add words to the default dictionary.
|
|
109
|
+
* Accepts strings or objects with word/severity/category.
|
|
110
|
+
*/
|
|
111
|
+
declare function addWords(
|
|
112
|
+
words: (string | { word: string; severity?: string; category?: string })[],
|
|
113
|
+
): void;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Remove words from the default dictionary.
|
|
117
|
+
*/
|
|
118
|
+
declare function removeWords(words: string[]): void;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get a copy of the current default dictionary.
|
|
122
|
+
*/
|
|
123
|
+
declare function getDictionary(): DictionaryEntry[];
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The default dictionary of Indonesian profanity used by the filter.
|
|
127
|
+
*/
|
|
128
|
+
declare const dictionary: DictionaryEntry[];
|
|
129
|
+
|
|
130
|
+
export { type CensorIdOptions, type DictionaryEntry, type Match, addWords, censorId, detect, dictionary, getDictionary, isProfane, removeWords };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
1
|
+
var b=(o,i)=>()=>(i||o((i={exports:{}}).exports,i),i.exports);var u=b((M,l)=>{var x=[{word:"anjing",severity:"high",category:"offensive"},{word:"anjir",severity:"medium",category:"slang"},{word:"anjrit",severity:"medium",category:"slang"},{word:"anying",severity:"medium",category:"slang"},{word:"asu",severity:"high",category:"offensive"},{word:"babi",severity:"high",category:"offensive"},{word:"bajing",severity:"high",category:"offensive"},{word:"bajingan",severity:"high",category:"offensive"},{word:"banci",severity:"high",category:"slur"},{word:"bangke",severity:"high",category:"offensive"},{word:"bangsat",severity:"high",category:"offensive"},{word:"bedebah",severity:"low",category:"offensive"},{word:"bege",severity:"medium",category:"offensive"},{word:"begi",severity:"medium",category:"offensive"},{word:"bego",severity:"medium",category:"offensive"},{word:"bejat",severity:"low",category:"offensive"},{word:"bencong",severity:"high",category:"slur"},{word:"berak",severity:"medium",category:"vulgar"},{word:"bispas",severity:"low",category:"vulgar"},{word:"boit",severity:"low",category:"vulgar"},{word:"bokep",severity:"low",category:"vulgar"},{word:"bokis",severity:"low",category:"vulgar"},{word:"bongol",severity:"low",category:"offensive"},{word:"brengsek",severity:"high",category:"offensive"},{word:"buaya",severity:"low",category:"offensive"},{word:"budek",severity:"low",category:"offensive"},{word:"burit",severity:"medium",category:"vulgar"},{word:"buto",severity:"medium",category:"slang"},{word:"cebong",severity:"medium",category:"slang"},{word:"celeng",severity:"high",category:"offensive"},{word:"conge",severity:"low",category:"offensive"},{word:"cuk",severity:"medium",category:"slang"},{word:"cupu",severity:"medium",category:"slang"},{word:"dajjal",severity:"high",category:"offensive"},{word:"dancuk",severity:"medium",category:"slang"},{word:"dekil",severity:"low",category:"offensive"},{word:"dungu",severity:"medium",category:"offensive"},{word:"edan",severity:"medium",category:"offensive"},{word:"engas",severity:"low",category:"vulgar"},{word:"entot",severity:"high",category:"sexual"},{word:"gadun",severity:"low",category:"vulgar"},{word:"gampang",severity:"low",category:"offensive"},{word:"ganjen",severity:"low",category:"offensive"},{word:"garong",severity:"low",category:"offensive"},{word:"gatel",severity:"low",category:"offensive"},{word:"geblek",severity:"medium",category:"offensive"},{word:"gelo",severity:"medium",category:"offensive"},{word:"gendeng",severity:"medium",category:"offensive"},{word:"goblok",severity:"medium",category:"offensive"},{word:"gondok",severity:"medium",category:"vulgar"},{word:"itil",severity:"high",category:"sexual"},{word:"jablay",severity:"high",category:"slur"},{word:"jalang",severity:"high",category:"slur"},{word:"jamet",severity:"medium",category:"slang"},{word:"jancuk",severity:"medium",category:"slang"},{word:"jembut",severity:"high",category:"sexual"},{word:"jijik",severity:"medium",category:"offensive"},{word:"kacung",severity:"low",category:"offensive"},{word:"kampret",severity:"medium",category:"slang"},{word:"kampungan",severity:"medium",category:"slang"},{word:"kancut",severity:"medium",category:"vulgar"},{word:"kanjut",severity:"medium",category:"vulgar"},{word:"kaparat",severity:"high",category:"offensive"},{word:"kehed",severity:"medium",category:"slang"},{word:"kenthu",severity:"high",category:"sexual"},{word:"kentut",severity:"low",category:"vulgar"},{word:"keparat",severity:"high",category:"offensive"},{word:"kere",severity:"low",category:"offensive"},{word:"kontol",severity:"high",category:"sexual"},{word:"koplak",severity:"medium",category:"slang"},{word:"kunyuk",severity:"high",category:"offensive"},{word:"lacur",severity:"high",category:"slur"},{word:"lendir",severity:"low",category:"vulgar"},{word:"lonte",severity:"high",category:"slur"},{word:"maho",severity:"high",category:"slur"},{word:"maling",severity:"low",category:"offensive"},{word:"mampus",severity:"medium",category:"offensive"},{word:"memek",severity:"high",category:"sexual"},{word:"monyet",severity:"high",category:"offensive"},{word:"murtad",severity:"low",category:"offensive"},{word:"najis",severity:"low",category:"offensive"},{word:"ndeso",severity:"low",category:"offensive"},{word:"ngaceng",severity:"high",category:"sexual"},{word:"ngeloco",severity:"high",category:"sexual"},{word:"ngentot",severity:"high",category:"sexual"},{word:"ngewe",severity:"high",category:"sexual"},{word:"ngocok",severity:"high",category:"sexual"},{word:"palkon",severity:"high",category:"sexual"},{word:"pansos",severity:"medium",category:"slang"},{word:"pantat",severity:"medium",category:"vulgar"},{word:"pelakor",severity:"high",category:"slur"},{word:"pepek",severity:"high",category:"sexual"},{word:"perek",severity:"high",category:"slur"},{word:"pocong",severity:"high",category:"offensive"},{word:"porno",severity:"low",category:"vulgar"},{word:"puki",severity:"high",category:"sexual"},{word:"sange",severity:"low",category:"vulgar"},{word:"sarap",severity:"medium",category:"offensive"},{word:"seks",severity:"low",category:"vulgar"},{word:"sempak",severity:"medium",category:"vulgar"},{word:"sepong",severity:"low",category:"vulgar"},{word:"setan",severity:"high",category:"offensive"},{word:"siak",severity:"low",category:"offensive"},{word:"sinting",severity:"medium",category:"offensive"},{word:"somplak",severity:"medium",category:"slang"},{word:"sontoloyo",severity:"medium",category:"slang"},{word:"tai",severity:"medium",category:"vulgar"},{word:"tempik",severity:"high",category:"sexual"},{word:"tetek",severity:"high",category:"sexual"},{word:"titit",severity:"high",category:"sexual"},{word:"toket",severity:"high",category:"sexual"},{word:"tolol",severity:"medium",category:"offensive"},{word:"tuyul",severity:"high",category:"offensive"}];l.exports=x});var n=u(),f={4:"a",3:"e",1:"i",0:"o",5:"s",7:"t",8:"b",9:"g","@":"a",$:"s","!":"i","(":"c"},m=["low","medium","high"];function h(o={}){let{customWords:i=[],allowedWords:e=[],minSeverity:a,categories:g}=o,s=[...n];if(a){let r=m.indexOf(a);r>=0&&(s=s.filter(t=>m.indexOf(t.severity)>=r))}g&&g.length>0&&(s=s.filter(r=>g.includes(r.category)));for(let r of i)typeof r=="string"?s.some(t=>t.word===r)||s.push({word:r,severity:"medium",category:"custom"}):r&&r.word&&(s.some(t=>t.word===r.word)||s.push({word:r.word,severity:r.severity||"medium",category:r.category||"custom"}));if(e.length>0){let r=new Set(e.map(t=>t.toLowerCase()));s=s.filter(t=>!r.has(t.word.toLowerCase()))}return s.sort((r,t)=>t.word.length-r.word.length),s}function p(o,i){if(o.length===0)return null;if(i){let e=o.map(a=>{let g=a.word;return`(${Array.from(g).map((r,t)=>{let y=r.toLowerCase(),c=Object.keys(f).filter(w=>f[w]===y),d=[y,...c].map(w=>w.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")),v=t===g.length-1?"+":"+[^a-zA-Z0-9]*";return`[${d.join("")}]${v}`}).join("")})`}).join("|");return new RegExp(`(?<=\\s|^|[^a-zA-Z0-9])(${e})(?=\\s|$|[^a-zA-Z0-9])`,"gi")}else{let e=o.map(a=>`(${a.word.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")})`).join("|");return new RegExp(`(?<=\\s|^|[^a-zA-Z0-9])(${e})(?=\\s|$|[^a-zA-Z0-9])`,"gi")}}function k(o,i={}){if(typeof o!="string")return[];let{unique:e=!1}=i,a=h(i);if(a.length===0)return[];let g=i.smartMode||!1,s=p(a,g);if(!s)return[];let r=[],t=new Set;for(let y of o.matchAll(s)){let c=-1;for(let v=2;v<y.length;v++)if(y[v]!==void 0){c=v-2;break}if(c<0)continue;let d=a[c];if(e){if(t.has(d.word))continue;t.add(d.word)}r.push({match:y[1],word:d.word,index:y.index,length:y[0].length,severity:d.severity,category:d.category})}return r}function j(o,i={}){return k(o,{...i,unique:!1}).length>0}function $(o,i={}){if(typeof o!="string")return o;let{mask:e="*",keepFirstAndLast:a=!1}=i,g=h(i);if(g.length===0)return o;let s=i.smartMode||!1,r=p(g,s);return r?o.replace(r,t=>{if(a&&t.length>2){let y=t[0],c=t[t.length-1],d=e.repeat(t.length-2);return y+d+c}return e.repeat(t.length)}):o}function A(o){let i=Array.isArray(o)?o:[o];for(let e of i)typeof e=="string"?n.some(a=>a.word===e)||n.push({word:e,severity:"medium",category:"custom"}):e&&e.word&&(n.some(a=>a.word===e.word)||n.push({word:e.word,severity:e.severity||"medium",category:e.category||"custom"}))}function C(o){let i=new Set(o.map(e=>e.toLowerCase()));for(let e=n.length-1;e>=0;e--)i.has(n[e].word.toLowerCase())&&n.splice(e,1)}function L(){return[...n]}module.exports={censorId:$,detect:k,isProfane:j,addWords:A,removeWords:C,getDictionary:L,dictionary:n};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../dictionary.js","../index.js"],"sourcesContent":["/**\r\n * A comprehensive list of common Indonesian profanity, slang, and inappropriate words.\r\n */\r\nconst dictionary = [\r\n \"anjing\",\r\n \"asu\",\r\n \"babi\",\r\n \"bangsat\",\r\n \"bajing\",\r\n \"bajingan\",\r\n \"bedebah\",\r\n \"berak\",\r\n \"bejat\",\r\n \"brengsek\",\r\n \"buto\",\r\n \"cuk\",\r\n \"dancuk\",\r\n \"entot\",\r\n \"goblok\",\r\n \"itil\",\r\n \"jancuk\",\r\n \"jembut\",\r\n \"kontol\",\r\n \"kunyuk\",\r\n \"lonte\",\r\n \"memek\",\r\n \"monyet\",\r\n \"ngentot\",\r\n \"pantat\",\r\n \"perek\",\r\n \"puki\",\r\n \"setan\",\r\n \"siak\",\r\n \"sinting\",\r\n \"tai\",\r\n \"tolol\"\r\n];\r\n\r\nmodule.exports = dictionary;\r\n","const defaultDictionary = require(\"./dictionary\");\r\n\r\n// Leetspeak mapping\r\nconst leetMap = {\r\n \"4\": \"a\",\r\n \"3\": \"e\",\r\n \"1\": \"i\",\r\n \"0\": \"o\",\r\n \"5\": \"s\",\r\n \"7\": \"t\",\r\n \"8\": \"b\",\r\n \"9\": \"g\",\r\n \"@\": \"a\",\r\n \"$\": \"s\",\r\n \"!\": \"i\",\r\n \"(\": \"c\"\r\n};\r\n\r\n/**\r\n * Censor Indonesian profanity and inappropriate words.\r\n *\r\n * @param {string} text - The input text to be filtered.\r\n * @param {object} options - Filtering options.\r\n * @param {string} options.mask - The character used to mask the words (default is '*').\r\n * @param {string[]} options.customWords - An array of additional words to censor.\r\n * @param {boolean} options.keepFirstAndLast - If true, keeps the first and last letters visible (default is false).\r\n * @param {boolean} options.smartMode - If true, handles leetspeak and repeated characters (default is false).\r\n * @returns {string} - The censored text.\r\n */\r\nfunction censorId(text, options = {}) {\r\n if (typeof text !== \"string\") {\r\n return text;\r\n }\r\n\r\n const {\r\n mask = \"*\",\r\n customWords = [],\r\n keepFirstAndLast = false,\r\n smartMode = false\r\n } = options;\r\n\r\n // Merge default dictionary with custom words and sort by length descending\r\n const wordsToCensor = [...new Set([...defaultDictionary, ...customWords])].sort((a, b) => b.length - a.length);\r\n\r\n let censoredText = text;\r\n\r\n // Iterate over each word and apply masking\r\n wordsToCensor.forEach(word => {\r\n let searchPattern;\r\n if (smartMode) {\r\n // Create a pattern that allows:\r\n // 1. Any number of repeats for each character (e.g., a+n+j+i+n+g+)\r\n // 2. Optional non-alphanumeric characters between letters (e.g., a[...]*n[...]*j)\r\n // 3. Mapping of leetspeak characters\r\n\r\n const charPatterns = Array.from(word).map((char, index) => {\r\n const lowerChar = char.toLowerCase();\r\n const leetChars = Object.keys(leetMap).filter(key => leetMap[key] === lowerChar);\r\n const variants = [lowerChar, ...leetChars].map(c => c.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"));\r\n\r\n // Only allow non-alphanumeric between characters, not after the last one\r\n const suffix = index === word.length - 1 ? \"+\" : \"+[^a-zA-Z0-9]*\";\r\n return `[${variants.join(\"\")}]${suffix}`;\r\n });\r\n\r\n searchPattern = charPatterns.join(\"\");\r\n } else {\r\n // Standard escaping for regex\r\n searchPattern = word.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\r\n }\r\n\r\n const regex = new RegExp(`(?<=\\\\s|^|[^a-zA-Z0-9])${searchPattern}(?=\\\\s|$|[^a-zA-Z0-9])`, \"gi\");\r\n\r\n censoredText = censoredText.replace(regex, (match) => {\r\n if (keepFirstAndLast && match.length > 2) {\r\n const first = match[0];\r\n const last = match[match.length - 1];\r\n const middle = mask.repeat(match.length - 2);\r\n return first + middle + last;\r\n }\r\n return mask.repeat(match.length);\r\n });\r\n });\r\n\r\n return censoredText;\r\n}\r\n\r\nmodule.exports = {\r\n censorId,\r\n dictionary: defaultDictionary\r\n};\r\n"],"mappings":"8DAAA,IAAAA,EAAAC,EAAA,CAAAC,EAAAC,IAAA,CAGA,IAAMC,EAAa,CACjB,SACA,MACA,OACA,UACA,SACA,WACA,UACA,QACA,QACA,WACA,OACA,MACA,SACA,QACA,SACA,OACA,SACA,SACA,SACA,SACA,QACA,QACA,SACA,UACA,SACA,QACA,OACA,QACA,OACA,UACA,MACA,OACF,EAEAD,EAAO,QAAUC,ICtCjB,IAAMC,EAAoB,IAGpBC,EAAU,CACZ,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,IAAK,IACL,EAAK,IACL,IAAK,IACL,IAAK,GACT,EAaA,SAASC,EAASC,EAAMC,EAAU,CAAC,EAAG,CAClC,GAAI,OAAOD,GAAS,SAChB,OAAOA,EAGX,GAAM,CACF,KAAAE,EAAO,IACP,YAAAC,EAAc,CAAC,EACf,iBAAAC,EAAmB,GACnB,UAAAC,EAAY,EAChB,EAAIJ,EAGEK,EAAgB,CAAC,GAAG,IAAI,IAAI,CAAC,GAAGT,EAAmB,GAAGM,CAAW,CAAC,CAAC,EAAE,KAAK,CAACI,EAAGC,IAAMA,EAAE,OAASD,EAAE,MAAM,EAEzGE,EAAeT,EAGnB,OAAAM,EAAc,QAAQI,GAAQ,CAC1B,IAAIC,EACAN,EAgBAM,EAVqB,MAAM,KAAKD,CAAI,EAAE,IAAI,CAACE,EAAMC,IAAU,CACvD,IAAMC,EAAYF,EAAK,YAAY,EAC7BG,EAAY,OAAO,KAAKjB,CAAO,EAAE,OAAOkB,GAAOlB,EAAQkB,CAAG,IAAMF,CAAS,EACzEG,EAAW,CAACH,EAAW,GAAGC,CAAS,EAAE,IAAIG,GAAKA,EAAE,QAAQ,sBAAuB,MAAM,CAAC,EAGtFC,EAASN,IAAUH,EAAK,OAAS,EAAI,IAAM,iBACjD,MAAO,IAAIO,EAAS,KAAK,EAAE,CAAC,IAAIE,CAAM,EAC1C,CAAC,EAE4B,KAAK,EAAE,EAGpCR,EAAgBD,EAAK,QAAQ,sBAAuB,MAAM,EAG9D,IAAMU,EAAQ,IAAI,OAAO,0BAA0BT,CAAa,yBAA0B,IAAI,EAE9FF,EAAeA,EAAa,QAAQW,EAAQC,GAAU,CAClD,GAAIjB,GAAoBiB,EAAM,OAAS,EAAG,CACtC,IAAMC,EAAQD,EAAM,CAAC,EACfE,EAAOF,EAAMA,EAAM,OAAS,CAAC,EAC7BG,EAAStB,EAAK,OAAOmB,EAAM,OAAS,CAAC,EAC3C,OAAOC,EAAQE,EAASD,CAC5B,CACA,OAAOrB,EAAK,OAAOmB,EAAM,MAAM,CACnC,CAAC,CACL,CAAC,EAEMZ,CACX,CAEA,OAAO,QAAU,CACb,SAAAV,EACA,WAAYF,CAChB","names":["require_dictionary","__commonJSMin","exports","module","dictionary","defaultDictionary","leetMap","censorId","text","options","mask","customWords","keepFirstAndLast","smartMode","wordsToCensor","a","b","censoredText","word","searchPattern","char","index","lowerChar","leetChars","key","variants","c","suffix","regex","match","first","last","middle"]}
|
|
1
|
+
{"version":3,"sources":["../src/dictionary.js","../src/index.js"],"sourcesContent":["/**\n * A comprehensive list of common Indonesian profanity, slang, and inappropriate words\n * with severity levels and categories.\n */\nconst dictionary = [\n {\n \"word\": \"anjing\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"anjir\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"anjrit\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"anying\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"asu\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"babi\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"bajing\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"bajingan\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"banci\",\n \"severity\": \"high\",\n \"category\": \"slur\"\n },\n {\n \"word\": \"bangke\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"bangsat\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"bedebah\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"bege\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"begi\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"bego\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"bejat\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"bencong\",\n \"severity\": \"high\",\n \"category\": \"slur\"\n },\n {\n \"word\": \"berak\",\n \"severity\": \"medium\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"bispas\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"boit\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"bokep\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"bokis\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"bongol\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"brengsek\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"buaya\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"budek\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"burit\",\n \"severity\": \"medium\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"buto\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"cebong\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"celeng\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"conge\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"cuk\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"cupu\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"dajjal\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"dancuk\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"dekil\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"dungu\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"edan\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"engas\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"entot\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"gadun\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"gampang\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"ganjen\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"garong\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"gatel\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"geblek\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"gelo\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"gendeng\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"goblok\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"gondok\",\n \"severity\": \"medium\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"itil\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"jablay\",\n \"severity\": \"high\",\n \"category\": \"slur\"\n },\n {\n \"word\": \"jalang\",\n \"severity\": \"high\",\n \"category\": \"slur\"\n },\n {\n \"word\": \"jamet\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"jancuk\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"jembut\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"jijik\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"kacung\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"kampret\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"kampungan\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"kancut\",\n \"severity\": \"medium\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"kanjut\",\n \"severity\": \"medium\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"kaparat\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"kehed\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"kenthu\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"kentut\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"keparat\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"kere\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"kontol\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"koplak\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"kunyuk\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"lacur\",\n \"severity\": \"high\",\n \"category\": \"slur\"\n },\n {\n \"word\": \"lendir\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"lonte\",\n \"severity\": \"high\",\n \"category\": \"slur\"\n },\n {\n \"word\": \"maho\",\n \"severity\": \"high\",\n \"category\": \"slur\"\n },\n {\n \"word\": \"maling\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"mampus\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"memek\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"monyet\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"murtad\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"najis\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"ndeso\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"ngaceng\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"ngeloco\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"ngentot\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"ngewe\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"ngocok\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"palkon\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"pansos\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"pantat\",\n \"severity\": \"medium\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"pelakor\",\n \"severity\": \"high\",\n \"category\": \"slur\"\n },\n {\n \"word\": \"pepek\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"perek\",\n \"severity\": \"high\",\n \"category\": \"slur\"\n },\n {\n \"word\": \"pocong\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"porno\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"puki\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"sange\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"sarap\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"seks\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"sempak\",\n \"severity\": \"medium\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"sepong\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"setan\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"siak\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"sinting\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"somplak\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"sontoloyo\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"tai\",\n \"severity\": \"medium\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"tempik\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"tetek\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"titit\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"toket\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"tolol\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"tuyul\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n }\n];\n\nmodule.exports = dictionary;\n","const defaultDictionary = require(\"./dictionary\");\n\n// Leetspeak mapping\nconst leetMap = {\n 4: \"a\",\n 3: \"e\",\n 1: \"i\",\n 0: \"o\",\n 5: \"s\",\n 7: \"t\",\n 8: \"b\",\n 9: \"g\",\n \"@\": \"a\",\n $: \"s\",\n \"!\": \"i\",\n \"(\": \"c\",\n};\n\nconst severityOrder = [\"low\", \"medium\", \"high\"];\n\n/**\n * Build a sorted, filtered list of dictionary entries.\n */\nfunction buildEntries(options = {}) {\n const {\n customWords = [],\n allowedWords = [],\n minSeverity,\n categories,\n } = options;\n\n let entries = [...defaultDictionary];\n\n // Filter by minimum severity\n if (minSeverity) {\n const minIdx = severityOrder.indexOf(minSeverity);\n if (minIdx >= 0) {\n entries = entries.filter(\n (e) => severityOrder.indexOf(e.severity) >= minIdx,\n );\n }\n }\n\n // Filter by categories\n if (categories && categories.length > 0) {\n entries = entries.filter((e) => categories.includes(e.category));\n }\n\n // Add custom words (supports strings or objects)\n for (const w of customWords) {\n if (typeof w === \"string\") {\n if (!entries.some((e) => e.word === w)) {\n entries.push({ word: w, severity: \"medium\", category: \"custom\" });\n }\n } else if (w && w.word) {\n if (!entries.some((e) => e.word === w.word)) {\n entries.push({\n word: w.word,\n severity: w.severity || \"medium\",\n category: w.category || \"custom\",\n });\n }\n }\n }\n\n // Remove allowed words\n if (allowedWords.length > 0) {\n const allowedLower = new Set(allowedWords.map((w) => w.toLowerCase()));\n entries = entries.filter((e) => !allowedLower.has(e.word.toLowerCase()));\n }\n\n entries.sort((a, b) => b.word.length - a.word.length);\n\n return entries;\n}\n\n/**\n * Build a combined regex with per-word capturing groups for metadata lookup.\n */\nfunction buildRegex(entries, smartMode) {\n if (entries.length === 0) return null;\n\n if (smartMode) {\n const patterns = entries\n .map((entry) => {\n const word = entry.word;\n const charPatterns = Array.from(word).map((char, index) => {\n const lowerChar = char.toLowerCase();\n const leetChars = Object.keys(leetMap).filter(\n (key) => leetMap[key] === lowerChar,\n );\n const variants = [lowerChar, ...leetChars].map((c) =>\n c.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"),\n );\n const suffix = index === word.length - 1 ? \"+\" : \"+[^a-zA-Z0-9]*\";\n return `[${variants.join(\"\")}]${suffix}`;\n });\n return `(${charPatterns.join(\"\")})`;\n })\n .join(\"|\");\n return new RegExp(\n `(?<=\\\\s|^|[^a-zA-Z0-9])(${patterns})(?=\\\\s|$|[^a-zA-Z0-9])`,\n \"gi\",\n );\n } else {\n const pattern = entries\n .map((entry) => `(${entry.word.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")})`)\n .join(\"|\");\n return new RegExp(\n `(?<=\\\\s|^|[^a-zA-Z0-9])(${pattern})(?=\\\\s|$|[^a-zA-Z0-9])`,\n \"gi\",\n );\n }\n}\n\n// ─── Public API ──────────────────────────────────────────────────────────────\n\n/**\n * Detect profanity in text and return structured results.\n *\n * @param {string} text - The input text to inspect.\n * @param {object} options - Detection options.\n * @param {string[]} [options.customWords] - Additional words to detect.\n * @param {string[]} [options.allowedWords] - Words to exclude from detection.\n * @param {boolean} [options.smartMode] - Handle leetspeak, repeats, punctuation.\n * @param {'low'|'medium'|'high'} [options.minSeverity] - Minimum severity to include.\n * @param {string[]} [options.categories] - Only include these categories.\n * @param {boolean} [options.unique] - Only return first occurrence of each word.\n * @returns {Array<{match: string, word: string, index: number, length: number, severity: string, category: string}>}\n */\nfunction detect(text, options = {}) {\n if (typeof text !== \"string\") return [];\n\n const { unique = false } = options;\n const entries = buildEntries(options);\n if (entries.length === 0) return [];\n\n const smartMode = options.smartMode || false;\n const regex = buildRegex(entries, smartMode);\n if (!regex) return [];\n\n const results = [];\n const seen = new Set();\n\n for (const m of text.matchAll(regex)) {\n // m[0] = full match text\n // m[1] = group 1 (whole alternation)\n // m[2..] = per-word groups\n\n let entryIdx = -1;\n for (let i = 2; i < m.length; i++) {\n if (m[i] !== undefined) {\n entryIdx = i - 2;\n break;\n }\n }\n\n if (entryIdx < 0) continue;\n\n const entry = entries[entryIdx];\n\n if (unique) {\n if (seen.has(entry.word)) continue;\n seen.add(entry.word);\n }\n\n results.push({\n match: m[1],\n word: entry.word,\n index: m.index,\n length: m[0].length,\n severity: entry.severity,\n category: entry.category,\n });\n }\n\n return results;\n}\n\n/**\n * Check if text contains profanity.\n *\n * @param {string} text - The input text to check.\n * @param {object} options - Same options as detect().\n * @returns {boolean}\n */\nfunction isProfane(text, options = {}) {\n return detect(text, { ...options, unique: false }).length > 0;\n}\n\n/**\n * Censor Indonesian profanity and inappropriate words.\n *\n * @param {string} text - The input text to be filtered.\n * @param {object} options - Filtering options.\n * @param {string} [options.mask='*'] - Character used to mask words.\n * @param {string[]} [options.customWords] - Additional words to censor.\n * @param {string[]} [options.allowedWords] - Words to exclude from censoring.\n * @param {boolean} [options.keepFirstAndLast] - Keep first/last letters visible.\n * @param {boolean} [options.smartMode] - Handle leetspeak, repeats, punctuation.\n * @param {'low'|'medium'|'high'} [options.minSeverity] - Minimum severity to censor.\n * @param {string[]} [options.categories] - Only censor these categories.\n * @returns {string}\n */\nfunction censorId(text, options = {}) {\n if (typeof text !== \"string\") return text;\n\n const { mask = \"*\", keepFirstAndLast = false } = options;\n\n const entries = buildEntries(options);\n if (entries.length === 0) return text;\n\n const smartMode = options.smartMode || false;\n const regex = buildRegex(entries, smartMode);\n if (!regex) return text;\n\n return text.replace(regex, (match) => {\n if (keepFirstAndLast && match.length > 2) {\n const first = match[0];\n const last = match[match.length - 1];\n const middle = mask.repeat(match.length - 2);\n return first + middle + last;\n }\n return mask.repeat(match.length);\n });\n}\n\n// ─── Dictionary Management ───────────────────────────────────────────────────\n\n/**\n * Add words to the default dictionary.\n *\n * @param {(string|{word:string,severity?:string,category?:string})[]} words\n */\nfunction addWords(words) {\n const list = Array.isArray(words) ? words : [words];\n for (const w of list) {\n if (typeof w === \"string\") {\n if (!defaultDictionary.some((e) => e.word === w)) {\n defaultDictionary.push({\n word: w,\n severity: \"medium\",\n category: \"custom\",\n });\n }\n } else if (w && w.word) {\n if (!defaultDictionary.some((e) => e.word === w.word)) {\n defaultDictionary.push({\n word: w.word,\n severity: w.severity || \"medium\",\n category: w.category || \"custom\",\n });\n }\n }\n }\n}\n\n/**\n * Remove words from the default dictionary.\n *\n * @param {string[]} words\n */\nfunction removeWords(words) {\n const lowerSet = new Set(words.map((w) => w.toLowerCase()));\n for (let i = defaultDictionary.length - 1; i >= 0; i--) {\n if (lowerSet.has(defaultDictionary[i].word.toLowerCase())) {\n defaultDictionary.splice(i, 1);\n }\n }\n}\n\n/**\n * Get a copy of the current default dictionary.\n *\n * @returns {Array<{word:string,severity:string,category:string}>}\n */\nfunction getDictionary() {\n return [...defaultDictionary];\n}\n\nmodule.exports = {\n censorId,\n detect,\n isProfane,\n addWords,\n removeWords,\n getDictionary,\n dictionary: defaultDictionary,\n};\n"],"mappings":"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","names":["require_dictionary","__commonJSMin","exports","module","dictionary","defaultDictionary","leetMap","severityOrder","buildEntries","options","customWords","allowedWords","minSeverity","categories","entries","minIdx","e","w","allowedLower","a","b","buildRegex","smartMode","patterns","entry","word","char","index","lowerChar","leetChars","key","variants","c","suffix","pattern","detect","text","unique","regex","results","seen","m","entryIdx","i","isProfane","censorId","mask","keepFirstAndLast","match","first","last","middle","addWords","words","list","removeWords","lowerSet","getDictionary"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
1
|
+
var l=(o,i)=>()=>(i||o((i={exports:{}}).exports,i),i.exports);var f=l((Z,u)=>{var j=[{word:"anjing",severity:"high",category:"offensive"},{word:"anjir",severity:"medium",category:"slang"},{word:"anjrit",severity:"medium",category:"slang"},{word:"anying",severity:"medium",category:"slang"},{word:"asu",severity:"high",category:"offensive"},{word:"babi",severity:"high",category:"offensive"},{word:"bajing",severity:"high",category:"offensive"},{word:"bajingan",severity:"high",category:"offensive"},{word:"banci",severity:"high",category:"slur"},{word:"bangke",severity:"high",category:"offensive"},{word:"bangsat",severity:"high",category:"offensive"},{word:"bedebah",severity:"low",category:"offensive"},{word:"bege",severity:"medium",category:"offensive"},{word:"begi",severity:"medium",category:"offensive"},{word:"bego",severity:"medium",category:"offensive"},{word:"bejat",severity:"low",category:"offensive"},{word:"bencong",severity:"high",category:"slur"},{word:"berak",severity:"medium",category:"vulgar"},{word:"bispas",severity:"low",category:"vulgar"},{word:"boit",severity:"low",category:"vulgar"},{word:"bokep",severity:"low",category:"vulgar"},{word:"bokis",severity:"low",category:"vulgar"},{word:"bongol",severity:"low",category:"offensive"},{word:"brengsek",severity:"high",category:"offensive"},{word:"buaya",severity:"low",category:"offensive"},{word:"budek",severity:"low",category:"offensive"},{word:"burit",severity:"medium",category:"vulgar"},{word:"buto",severity:"medium",category:"slang"},{word:"cebong",severity:"medium",category:"slang"},{word:"celeng",severity:"high",category:"offensive"},{word:"conge",severity:"low",category:"offensive"},{word:"cuk",severity:"medium",category:"slang"},{word:"cupu",severity:"medium",category:"slang"},{word:"dajjal",severity:"high",category:"offensive"},{word:"dancuk",severity:"medium",category:"slang"},{word:"dekil",severity:"low",category:"offensive"},{word:"dungu",severity:"medium",category:"offensive"},{word:"edan",severity:"medium",category:"offensive"},{word:"engas",severity:"low",category:"vulgar"},{word:"entot",severity:"high",category:"sexual"},{word:"gadun",severity:"low",category:"vulgar"},{word:"gampang",severity:"low",category:"offensive"},{word:"ganjen",severity:"low",category:"offensive"},{word:"garong",severity:"low",category:"offensive"},{word:"gatel",severity:"low",category:"offensive"},{word:"geblek",severity:"medium",category:"offensive"},{word:"gelo",severity:"medium",category:"offensive"},{word:"gendeng",severity:"medium",category:"offensive"},{word:"goblok",severity:"medium",category:"offensive"},{word:"gondok",severity:"medium",category:"vulgar"},{word:"itil",severity:"high",category:"sexual"},{word:"jablay",severity:"high",category:"slur"},{word:"jalang",severity:"high",category:"slur"},{word:"jamet",severity:"medium",category:"slang"},{word:"jancuk",severity:"medium",category:"slang"},{word:"jembut",severity:"high",category:"sexual"},{word:"jijik",severity:"medium",category:"offensive"},{word:"kacung",severity:"low",category:"offensive"},{word:"kampret",severity:"medium",category:"slang"},{word:"kampungan",severity:"medium",category:"slang"},{word:"kancut",severity:"medium",category:"vulgar"},{word:"kanjut",severity:"medium",category:"vulgar"},{word:"kaparat",severity:"high",category:"offensive"},{word:"kehed",severity:"medium",category:"slang"},{word:"kenthu",severity:"high",category:"sexual"},{word:"kentut",severity:"low",category:"vulgar"},{word:"keparat",severity:"high",category:"offensive"},{word:"kere",severity:"low",category:"offensive"},{word:"kontol",severity:"high",category:"sexual"},{word:"koplak",severity:"medium",category:"slang"},{word:"kunyuk",severity:"high",category:"offensive"},{word:"lacur",severity:"high",category:"slur"},{word:"lendir",severity:"low",category:"vulgar"},{word:"lonte",severity:"high",category:"slur"},{word:"maho",severity:"high",category:"slur"},{word:"maling",severity:"low",category:"offensive"},{word:"mampus",severity:"medium",category:"offensive"},{word:"memek",severity:"high",category:"sexual"},{word:"monyet",severity:"high",category:"offensive"},{word:"murtad",severity:"low",category:"offensive"},{word:"najis",severity:"low",category:"offensive"},{word:"ndeso",severity:"low",category:"offensive"},{word:"ngaceng",severity:"high",category:"sexual"},{word:"ngeloco",severity:"high",category:"sexual"},{word:"ngentot",severity:"high",category:"sexual"},{word:"ngewe",severity:"high",category:"sexual"},{word:"ngocok",severity:"high",category:"sexual"},{word:"palkon",severity:"high",category:"sexual"},{word:"pansos",severity:"medium",category:"slang"},{word:"pantat",severity:"medium",category:"vulgar"},{word:"pelakor",severity:"high",category:"slur"},{word:"pepek",severity:"high",category:"sexual"},{word:"perek",severity:"high",category:"slur"},{word:"pocong",severity:"high",category:"offensive"},{word:"porno",severity:"low",category:"vulgar"},{word:"puki",severity:"high",category:"sexual"},{word:"sange",severity:"low",category:"vulgar"},{word:"sarap",severity:"medium",category:"offensive"},{word:"seks",severity:"low",category:"vulgar"},{word:"sempak",severity:"medium",category:"vulgar"},{word:"sepong",severity:"low",category:"vulgar"},{word:"setan",severity:"high",category:"offensive"},{word:"siak",severity:"low",category:"offensive"},{word:"sinting",severity:"medium",category:"offensive"},{word:"somplak",severity:"medium",category:"slang"},{word:"sontoloyo",severity:"medium",category:"slang"},{word:"tai",severity:"medium",category:"vulgar"},{word:"tempik",severity:"high",category:"sexual"},{word:"tetek",severity:"high",category:"sexual"},{word:"titit",severity:"high",category:"sexual"},{word:"toket",severity:"high",category:"sexual"},{word:"tolol",severity:"medium",category:"offensive"},{word:"tuyul",severity:"high",category:"offensive"}];u.exports=j});var M=l((O,x)=>{var n=f(),m={4:"a",3:"e",1:"i",0:"o",5:"s",7:"t",8:"b",9:"g","@":"a",$:"s","!":"i","(":"c"},h=["low","medium","high"];function p(o={}){let{customWords:i=[],allowedWords:e=[],minSeverity:a,categories:g}=o,s=[...n];if(a){let r=h.indexOf(a);r>=0&&(s=s.filter(t=>h.indexOf(t.severity)>=r))}g&&g.length>0&&(s=s.filter(r=>g.includes(r.category)));for(let r of i)typeof r=="string"?s.some(t=>t.word===r)||s.push({word:r,severity:"medium",category:"custom"}):r&&r.word&&(s.some(t=>t.word===r.word)||s.push({word:r.word,severity:r.severity||"medium",category:r.category||"custom"}));if(e.length>0){let r=new Set(e.map(t=>t.toLowerCase()));s=s.filter(t=>!r.has(t.word.toLowerCase()))}return s.sort((r,t)=>t.word.length-r.word.length),s}function k(o,i){if(o.length===0)return null;if(i){let e=o.map(a=>{let g=a.word;return`(${Array.from(g).map((r,t)=>{let y=r.toLowerCase(),c=Object.keys(m).filter(w=>m[w]===y),d=[y,...c].map(w=>w.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")),v=t===g.length-1?"+":"+[^a-zA-Z0-9]*";return`[${d.join("")}]${v}`}).join("")})`}).join("|");return new RegExp(`(?<=\\s|^|[^a-zA-Z0-9])(${e})(?=\\s|$|[^a-zA-Z0-9])`,"gi")}else{let e=o.map(a=>`(${a.word.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")})`).join("|");return new RegExp(`(?<=\\s|^|[^a-zA-Z0-9])(${e})(?=\\s|$|[^a-zA-Z0-9])`,"gi")}}function b(o,i={}){if(typeof o!="string")return[];let{unique:e=!1}=i,a=p(i);if(a.length===0)return[];let g=i.smartMode||!1,s=k(a,g);if(!s)return[];let r=[],t=new Set;for(let y of o.matchAll(s)){let c=-1;for(let v=2;v<y.length;v++)if(y[v]!==void 0){c=v-2;break}if(c<0)continue;let d=a[c];if(e){if(t.has(d.word))continue;t.add(d.word)}r.push({match:y[1],word:d.word,index:y.index,length:y[0].length,severity:d.severity,category:d.category})}return r}function $(o,i={}){return b(o,{...i,unique:!1}).length>0}function A(o,i={}){if(typeof o!="string")return o;let{mask:e="*",keepFirstAndLast:a=!1}=i,g=p(i);if(g.length===0)return o;let s=i.smartMode||!1,r=k(g,s);return r?o.replace(r,t=>{if(a&&t.length>2){let y=t[0],c=t[t.length-1],d=e.repeat(t.length-2);return y+d+c}return e.repeat(t.length)}):o}function C(o){let i=Array.isArray(o)?o:[o];for(let e of i)typeof e=="string"?n.some(a=>a.word===e)||n.push({word:e,severity:"medium",category:"custom"}):e&&e.word&&(n.some(a=>a.word===e.word)||n.push({word:e.word,severity:e.severity||"medium",category:e.category||"custom"}))}function L(o){let i=new Set(o.map(e=>e.toLowerCase()));for(let e=n.length-1;e>=0;e--)i.has(n[e].word.toLowerCase())&&n.splice(e,1)}function z(){return[...n]}x.exports={censorId:A,detect:b,isProfane:$,addWords:C,removeWords:L,getDictionary:z,dictionary:n}});export default M();
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../dictionary.js","../index.js"],"sourcesContent":["/**\r\n * A comprehensive list of common Indonesian profanity, slang, and inappropriate words.\r\n */\r\nconst dictionary = [\r\n \"anjing\",\r\n \"asu\",\r\n \"babi\",\r\n \"bangsat\",\r\n \"bajing\",\r\n \"bajingan\",\r\n \"bedebah\",\r\n \"berak\",\r\n \"bejat\",\r\n \"brengsek\",\r\n \"buto\",\r\n \"cuk\",\r\n \"dancuk\",\r\n \"entot\",\r\n \"goblok\",\r\n \"itil\",\r\n \"jancuk\",\r\n \"jembut\",\r\n \"kontol\",\r\n \"kunyuk\",\r\n \"lonte\",\r\n \"memek\",\r\n \"monyet\",\r\n \"ngentot\",\r\n \"pantat\",\r\n \"perek\",\r\n \"puki\",\r\n \"setan\",\r\n \"siak\",\r\n \"sinting\",\r\n \"tai\",\r\n \"tolol\"\r\n];\r\n\r\nmodule.exports = dictionary;\r\n","const defaultDictionary = require(\"./dictionary\");\r\n\r\n// Leetspeak mapping\r\nconst leetMap = {\r\n \"4\": \"a\",\r\n \"3\": \"e\",\r\n \"1\": \"i\",\r\n \"0\": \"o\",\r\n \"5\": \"s\",\r\n \"7\": \"t\",\r\n \"8\": \"b\",\r\n \"9\": \"g\",\r\n \"@\": \"a\",\r\n \"$\": \"s\",\r\n \"!\": \"i\",\r\n \"(\": \"c\"\r\n};\r\n\r\n/**\r\n * Censor Indonesian profanity and inappropriate words.\r\n *\r\n * @param {string} text - The input text to be filtered.\r\n * @param {object} options - Filtering options.\r\n * @param {string} options.mask - The character used to mask the words (default is '*').\r\n * @param {string[]} options.customWords - An array of additional words to censor.\r\n * @param {boolean} options.keepFirstAndLast - If true, keeps the first and last letters visible (default is false).\r\n * @param {boolean} options.smartMode - If true, handles leetspeak and repeated characters (default is false).\r\n * @returns {string} - The censored text.\r\n */\r\nfunction censorId(text, options = {}) {\r\n if (typeof text !== \"string\") {\r\n return text;\r\n }\r\n\r\n const {\r\n mask = \"*\",\r\n customWords = [],\r\n keepFirstAndLast = false,\r\n smartMode = false\r\n } = options;\r\n\r\n // Merge default dictionary with custom words and sort by length descending\r\n const wordsToCensor = [...new Set([...defaultDictionary, ...customWords])].sort((a, b) => b.length - a.length);\r\n\r\n let censoredText = text;\r\n\r\n // Iterate over each word and apply masking\r\n wordsToCensor.forEach(word => {\r\n let searchPattern;\r\n if (smartMode) {\r\n // Create a pattern that allows:\r\n // 1. Any number of repeats for each character (e.g., a+n+j+i+n+g+)\r\n // 2. Optional non-alphanumeric characters between letters (e.g., a[...]*n[...]*j)\r\n // 3. Mapping of leetspeak characters\r\n\r\n const charPatterns = Array.from(word).map((char, index) => {\r\n const lowerChar = char.toLowerCase();\r\n const leetChars = Object.keys(leetMap).filter(key => leetMap[key] === lowerChar);\r\n const variants = [lowerChar, ...leetChars].map(c => c.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"));\r\n\r\n // Only allow non-alphanumeric between characters, not after the last one\r\n const suffix = index === word.length - 1 ? \"+\" : \"+[^a-zA-Z0-9]*\";\r\n return `[${variants.join(\"\")}]${suffix}`;\r\n });\r\n\r\n searchPattern = charPatterns.join(\"\");\r\n } else {\r\n // Standard escaping for regex\r\n searchPattern = word.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\r\n }\r\n\r\n const regex = new RegExp(`(?<=\\\\s|^|[^a-zA-Z0-9])${searchPattern}(?=\\\\s|$|[^a-zA-Z0-9])`, \"gi\");\r\n\r\n censoredText = censoredText.replace(regex, (match) => {\r\n if (keepFirstAndLast && match.length > 2) {\r\n const first = match[0];\r\n const last = match[match.length - 1];\r\n const middle = mask.repeat(match.length - 2);\r\n return first + middle + last;\r\n }\r\n return mask.repeat(match.length);\r\n });\r\n });\r\n\r\n return censoredText;\r\n}\r\n\r\nmodule.exports = {\r\n censorId,\r\n dictionary: defaultDictionary\r\n};\r\n"],"mappings":"8DAAA,IAAAA,EAAAC,EAAA,CAAAC,EAAAC,IAAA,CAGA,IAAMC,EAAa,CACjB,SACA,MACA,OACA,UACA,SACA,WACA,UACA,QACA,QACA,WACA,OACA,MACA,SACA,QACA,SACA,OACA,SACA,SACA,SACA,SACA,QACA,QACA,SACA,UACA,SACA,QACA,OACA,QACA,OACA,UACA,MACA,OACF,EAEAD,EAAO,QAAUC,ICtCjB,IAAAC,EAAAC,EAAA,CAAAC,EAAAC,IAAA,KAAMC,EAAoB,IAGpBC,EAAU,CACZ,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,EAAK,IACL,IAAK,IACL,EAAK,IACL,IAAK,IACL,IAAK,GACT,EAaA,SAASC,EAASC,EAAMC,EAAU,CAAC,EAAG,CAClC,GAAI,OAAOD,GAAS,SAChB,OAAOA,EAGX,GAAM,CACF,KAAAE,EAAO,IACP,YAAAC,EAAc,CAAC,EACf,iBAAAC,EAAmB,GACnB,UAAAC,EAAY,EAChB,EAAIJ,EAGEK,EAAgB,CAAC,GAAG,IAAI,IAAI,CAAC,GAAGT,EAAmB,GAAGM,CAAW,CAAC,CAAC,EAAE,KAAK,CAACI,EAAGC,IAAMA,EAAE,OAASD,EAAE,MAAM,EAEzGE,EAAeT,EAGnB,OAAAM,EAAc,QAAQI,GAAQ,CAC1B,IAAIC,EACAN,EAgBAM,EAVqB,MAAM,KAAKD,CAAI,EAAE,IAAI,CAACE,EAAMC,IAAU,CACvD,IAAMC,EAAYF,EAAK,YAAY,EAC7BG,EAAY,OAAO,KAAKjB,CAAO,EAAE,OAAOkB,GAAOlB,EAAQkB,CAAG,IAAMF,CAAS,EACzEG,EAAW,CAACH,EAAW,GAAGC,CAAS,EAAE,IAAIG,GAAKA,EAAE,QAAQ,sBAAuB,MAAM,CAAC,EAGtFC,EAASN,IAAUH,EAAK,OAAS,EAAI,IAAM,iBACjD,MAAO,IAAIO,EAAS,KAAK,EAAE,CAAC,IAAIE,CAAM,EAC1C,CAAC,EAE4B,KAAK,EAAE,EAGpCR,EAAgBD,EAAK,QAAQ,sBAAuB,MAAM,EAG9D,IAAMU,EAAQ,IAAI,OAAO,0BAA0BT,CAAa,yBAA0B,IAAI,EAE9FF,EAAeA,EAAa,QAAQW,EAAQC,GAAU,CAClD,GAAIjB,GAAoBiB,EAAM,OAAS,EAAG,CACtC,IAAMC,EAAQD,EAAM,CAAC,EACfE,EAAOF,EAAMA,EAAM,OAAS,CAAC,EAC7BG,EAAStB,EAAK,OAAOmB,EAAM,OAAS,CAAC,EAC3C,OAAOC,EAAQE,EAASD,CAC5B,CACA,OAAOrB,EAAK,OAAOmB,EAAM,MAAM,CACnC,CAAC,CACL,CAAC,EAEMZ,CACX,CAEAb,EAAO,QAAU,CACb,SAAAG,EACA,WAAYF,CAChB","names":["require_dictionary","__commonJSMin","exports","module","dictionary","require_index","__commonJSMin","exports","module","defaultDictionary","leetMap","censorId","text","options","mask","customWords","keepFirstAndLast","smartMode","wordsToCensor","a","b","censoredText","word","searchPattern","char","index","lowerChar","leetChars","key","variants","c","suffix","regex","match","first","last","middle"]}
|
|
1
|
+
{"version":3,"sources":["../src/dictionary.js","../src/index.js"],"sourcesContent":["/**\n * A comprehensive list of common Indonesian profanity, slang, and inappropriate words\n * with severity levels and categories.\n */\nconst dictionary = [\n {\n \"word\": \"anjing\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"anjir\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"anjrit\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"anying\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"asu\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"babi\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"bajing\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"bajingan\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"banci\",\n \"severity\": \"high\",\n \"category\": \"slur\"\n },\n {\n \"word\": \"bangke\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"bangsat\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"bedebah\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"bege\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"begi\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"bego\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"bejat\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"bencong\",\n \"severity\": \"high\",\n \"category\": \"slur\"\n },\n {\n \"word\": \"berak\",\n \"severity\": \"medium\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"bispas\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"boit\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"bokep\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"bokis\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"bongol\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"brengsek\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"buaya\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"budek\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"burit\",\n \"severity\": \"medium\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"buto\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"cebong\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"celeng\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"conge\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"cuk\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"cupu\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"dajjal\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"dancuk\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"dekil\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"dungu\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"edan\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"engas\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"entot\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"gadun\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"gampang\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"ganjen\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"garong\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"gatel\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"geblek\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"gelo\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"gendeng\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"goblok\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"gondok\",\n \"severity\": \"medium\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"itil\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"jablay\",\n \"severity\": \"high\",\n \"category\": \"slur\"\n },\n {\n \"word\": \"jalang\",\n \"severity\": \"high\",\n \"category\": \"slur\"\n },\n {\n \"word\": \"jamet\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"jancuk\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"jembut\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"jijik\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"kacung\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"kampret\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"kampungan\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"kancut\",\n \"severity\": \"medium\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"kanjut\",\n \"severity\": \"medium\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"kaparat\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"kehed\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"kenthu\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"kentut\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"keparat\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"kere\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"kontol\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"koplak\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"kunyuk\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"lacur\",\n \"severity\": \"high\",\n \"category\": \"slur\"\n },\n {\n \"word\": \"lendir\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"lonte\",\n \"severity\": \"high\",\n \"category\": \"slur\"\n },\n {\n \"word\": \"maho\",\n \"severity\": \"high\",\n \"category\": \"slur\"\n },\n {\n \"word\": \"maling\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"mampus\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"memek\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"monyet\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"murtad\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"najis\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"ndeso\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"ngaceng\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"ngeloco\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"ngentot\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"ngewe\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"ngocok\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"palkon\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"pansos\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"pantat\",\n \"severity\": \"medium\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"pelakor\",\n \"severity\": \"high\",\n \"category\": \"slur\"\n },\n {\n \"word\": \"pepek\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"perek\",\n \"severity\": \"high\",\n \"category\": \"slur\"\n },\n {\n \"word\": \"pocong\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"porno\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"puki\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"sange\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"sarap\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"seks\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"sempak\",\n \"severity\": \"medium\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"sepong\",\n \"severity\": \"low\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"setan\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"siak\",\n \"severity\": \"low\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"sinting\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"somplak\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"sontoloyo\",\n \"severity\": \"medium\",\n \"category\": \"slang\"\n },\n {\n \"word\": \"tai\",\n \"severity\": \"medium\",\n \"category\": \"vulgar\"\n },\n {\n \"word\": \"tempik\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"tetek\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"titit\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"toket\",\n \"severity\": \"high\",\n \"category\": \"sexual\"\n },\n {\n \"word\": \"tolol\",\n \"severity\": \"medium\",\n \"category\": \"offensive\"\n },\n {\n \"word\": \"tuyul\",\n \"severity\": \"high\",\n \"category\": \"offensive\"\n }\n];\n\nmodule.exports = dictionary;\n","const defaultDictionary = require(\"./dictionary\");\n\n// Leetspeak mapping\nconst leetMap = {\n 4: \"a\",\n 3: \"e\",\n 1: \"i\",\n 0: \"o\",\n 5: \"s\",\n 7: \"t\",\n 8: \"b\",\n 9: \"g\",\n \"@\": \"a\",\n $: \"s\",\n \"!\": \"i\",\n \"(\": \"c\",\n};\n\nconst severityOrder = [\"low\", \"medium\", \"high\"];\n\n/**\n * Build a sorted, filtered list of dictionary entries.\n */\nfunction buildEntries(options = {}) {\n const {\n customWords = [],\n allowedWords = [],\n minSeverity,\n categories,\n } = options;\n\n let entries = [...defaultDictionary];\n\n // Filter by minimum severity\n if (minSeverity) {\n const minIdx = severityOrder.indexOf(minSeverity);\n if (minIdx >= 0) {\n entries = entries.filter(\n (e) => severityOrder.indexOf(e.severity) >= minIdx,\n );\n }\n }\n\n // Filter by categories\n if (categories && categories.length > 0) {\n entries = entries.filter((e) => categories.includes(e.category));\n }\n\n // Add custom words (supports strings or objects)\n for (const w of customWords) {\n if (typeof w === \"string\") {\n if (!entries.some((e) => e.word === w)) {\n entries.push({ word: w, severity: \"medium\", category: \"custom\" });\n }\n } else if (w && w.word) {\n if (!entries.some((e) => e.word === w.word)) {\n entries.push({\n word: w.word,\n severity: w.severity || \"medium\",\n category: w.category || \"custom\",\n });\n }\n }\n }\n\n // Remove allowed words\n if (allowedWords.length > 0) {\n const allowedLower = new Set(allowedWords.map((w) => w.toLowerCase()));\n entries = entries.filter((e) => !allowedLower.has(e.word.toLowerCase()));\n }\n\n entries.sort((a, b) => b.word.length - a.word.length);\n\n return entries;\n}\n\n/**\n * Build a combined regex with per-word capturing groups for metadata lookup.\n */\nfunction buildRegex(entries, smartMode) {\n if (entries.length === 0) return null;\n\n if (smartMode) {\n const patterns = entries\n .map((entry) => {\n const word = entry.word;\n const charPatterns = Array.from(word).map((char, index) => {\n const lowerChar = char.toLowerCase();\n const leetChars = Object.keys(leetMap).filter(\n (key) => leetMap[key] === lowerChar,\n );\n const variants = [lowerChar, ...leetChars].map((c) =>\n c.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"),\n );\n const suffix = index === word.length - 1 ? \"+\" : \"+[^a-zA-Z0-9]*\";\n return `[${variants.join(\"\")}]${suffix}`;\n });\n return `(${charPatterns.join(\"\")})`;\n })\n .join(\"|\");\n return new RegExp(\n `(?<=\\\\s|^|[^a-zA-Z0-9])(${patterns})(?=\\\\s|$|[^a-zA-Z0-9])`,\n \"gi\",\n );\n } else {\n const pattern = entries\n .map((entry) => `(${entry.word.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")})`)\n .join(\"|\");\n return new RegExp(\n `(?<=\\\\s|^|[^a-zA-Z0-9])(${pattern})(?=\\\\s|$|[^a-zA-Z0-9])`,\n \"gi\",\n );\n }\n}\n\n// ─── Public API ──────────────────────────────────────────────────────────────\n\n/**\n * Detect profanity in text and return structured results.\n *\n * @param {string} text - The input text to inspect.\n * @param {object} options - Detection options.\n * @param {string[]} [options.customWords] - Additional words to detect.\n * @param {string[]} [options.allowedWords] - Words to exclude from detection.\n * @param {boolean} [options.smartMode] - Handle leetspeak, repeats, punctuation.\n * @param {'low'|'medium'|'high'} [options.minSeverity] - Minimum severity to include.\n * @param {string[]} [options.categories] - Only include these categories.\n * @param {boolean} [options.unique] - Only return first occurrence of each word.\n * @returns {Array<{match: string, word: string, index: number, length: number, severity: string, category: string}>}\n */\nfunction detect(text, options = {}) {\n if (typeof text !== \"string\") return [];\n\n const { unique = false } = options;\n const entries = buildEntries(options);\n if (entries.length === 0) return [];\n\n const smartMode = options.smartMode || false;\n const regex = buildRegex(entries, smartMode);\n if (!regex) return [];\n\n const results = [];\n const seen = new Set();\n\n for (const m of text.matchAll(regex)) {\n // m[0] = full match text\n // m[1] = group 1 (whole alternation)\n // m[2..] = per-word groups\n\n let entryIdx = -1;\n for (let i = 2; i < m.length; i++) {\n if (m[i] !== undefined) {\n entryIdx = i - 2;\n break;\n }\n }\n\n if (entryIdx < 0) continue;\n\n const entry = entries[entryIdx];\n\n if (unique) {\n if (seen.has(entry.word)) continue;\n seen.add(entry.word);\n }\n\n results.push({\n match: m[1],\n word: entry.word,\n index: m.index,\n length: m[0].length,\n severity: entry.severity,\n category: entry.category,\n });\n }\n\n return results;\n}\n\n/**\n * Check if text contains profanity.\n *\n * @param {string} text - The input text to check.\n * @param {object} options - Same options as detect().\n * @returns {boolean}\n */\nfunction isProfane(text, options = {}) {\n return detect(text, { ...options, unique: false }).length > 0;\n}\n\n/**\n * Censor Indonesian profanity and inappropriate words.\n *\n * @param {string} text - The input text to be filtered.\n * @param {object} options - Filtering options.\n * @param {string} [options.mask='*'] - Character used to mask words.\n * @param {string[]} [options.customWords] - Additional words to censor.\n * @param {string[]} [options.allowedWords] - Words to exclude from censoring.\n * @param {boolean} [options.keepFirstAndLast] - Keep first/last letters visible.\n * @param {boolean} [options.smartMode] - Handle leetspeak, repeats, punctuation.\n * @param {'low'|'medium'|'high'} [options.minSeverity] - Minimum severity to censor.\n * @param {string[]} [options.categories] - Only censor these categories.\n * @returns {string}\n */\nfunction censorId(text, options = {}) {\n if (typeof text !== \"string\") return text;\n\n const { mask = \"*\", keepFirstAndLast = false } = options;\n\n const entries = buildEntries(options);\n if (entries.length === 0) return text;\n\n const smartMode = options.smartMode || false;\n const regex = buildRegex(entries, smartMode);\n if (!regex) return text;\n\n return text.replace(regex, (match) => {\n if (keepFirstAndLast && match.length > 2) {\n const first = match[0];\n const last = match[match.length - 1];\n const middle = mask.repeat(match.length - 2);\n return first + middle + last;\n }\n return mask.repeat(match.length);\n });\n}\n\n// ─── Dictionary Management ───────────────────────────────────────────────────\n\n/**\n * Add words to the default dictionary.\n *\n * @param {(string|{word:string,severity?:string,category?:string})[]} words\n */\nfunction addWords(words) {\n const list = Array.isArray(words) ? words : [words];\n for (const w of list) {\n if (typeof w === \"string\") {\n if (!defaultDictionary.some((e) => e.word === w)) {\n defaultDictionary.push({\n word: w,\n severity: \"medium\",\n category: \"custom\",\n });\n }\n } else if (w && w.word) {\n if (!defaultDictionary.some((e) => e.word === w.word)) {\n defaultDictionary.push({\n word: w.word,\n severity: w.severity || \"medium\",\n category: w.category || \"custom\",\n });\n }\n }\n }\n}\n\n/**\n * Remove words from the default dictionary.\n *\n * @param {string[]} words\n */\nfunction removeWords(words) {\n const lowerSet = new Set(words.map((w) => w.toLowerCase()));\n for (let i = defaultDictionary.length - 1; i >= 0; i--) {\n if (lowerSet.has(defaultDictionary[i].word.toLowerCase())) {\n defaultDictionary.splice(i, 1);\n }\n }\n}\n\n/**\n * Get a copy of the current default dictionary.\n *\n * @returns {Array<{word:string,severity:string,category:string}>}\n */\nfunction getDictionary() {\n return [...defaultDictionary];\n}\n\nmodule.exports = {\n censorId,\n detect,\n isProfane,\n addWords,\n removeWords,\n getDictionary,\n dictionary: defaultDictionary,\n};\n"],"mappings":"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","names":["require_dictionary","__commonJSMin","exports","module","dictionary","require_index","__commonJSMin","exports","module","defaultDictionary","leetMap","severityOrder","buildEntries","options","customWords","allowedWords","minSeverity","categories","entries","minIdx","e","w","allowedLower","a","b","buildRegex","smartMode","patterns","entry","word","char","index","lowerChar","leetChars","key","variants","c","suffix","pattern","detect","text","unique","regex","results","seen","m","entryIdx","i","isProfane","censorId","mask","keepFirstAndLast","match","first","last","middle","addWords","words","list","removeWords","lowerSet","getDictionary"]}
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "censorship-id",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "A robust Indonesian profanity filter
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "A robust Indonesian profanity filter with severity levels, category detection, and leetspeak handling.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
10
11
|
"require": "./dist/index.js",
|
|
11
|
-
"import": "./dist/index.mjs"
|
|
12
|
-
"types": "./dist/index.d.ts"
|
|
12
|
+
"import": "./dist/index.mjs"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"files": [
|