iraqi-dialect-bad-words 1.1.0 → 1.2.1
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 +39 -1
- package/dist/index.d.mts +77 -5
- package/dist/index.d.ts +77 -5
- package/dist/index.js +139 -27
- package/dist/index.mjs +139 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,6 +64,7 @@ Creates a filter with the built-in word list.
|
|
|
64
64
|
```ts
|
|
65
65
|
new IraqiBadWordsFilter(); // profanity only
|
|
66
66
|
new IraqiBadWordsFilter({ includeInsults: true }); // also milder insults
|
|
67
|
+
new IraqiBadWordsFilter({ stripSuffixes: true }); // also unlisted suffix forms
|
|
67
68
|
new IraqiBadWordsFilter({ customWords: ['word1', 'word2'] });
|
|
68
69
|
new IraqiBadWordsFilter(['word1', 'word2']); // same as customWords
|
|
69
70
|
```
|
|
@@ -72,6 +73,7 @@ new IraqiBadWordsFilter(['word1', 'word2']); // same as customWords
|
|
|
72
73
|
|---|---|---|
|
|
73
74
|
| `customWords` | `[]` | Extra words to filter. |
|
|
74
75
|
| `includeInsults` | `false` | Also filter milder insults like كلب, حمار, قندرة, نعال. Off by default because many of them are everyday words, so turning it on flags sentences like `عندي كلب` ("I have a dog"). |
|
|
76
|
+
| `stripSuffixes` | `false` | Also match words with a pronoun suffix (ي ك ج ه ها نا كم هم) that the list doesn't spell out, e.g. `طيزهم` → `طيز`. Off by default because it widens matching: a clean word that ends like a bad word plus a suffix gets flagged too. |
|
|
75
77
|
|
|
76
78
|
### `isProfane(text: string): boolean`
|
|
77
79
|
|
|
@@ -81,6 +83,36 @@ Returns `true` if any word in the text is on the list.
|
|
|
81
83
|
|
|
82
84
|
Replaces each bad word with the placeholder, repeated to the word's length. Spaces and punctuation are kept.
|
|
83
85
|
|
|
86
|
+
### `inspect(text: string, options?): ProfanityMatch[]`
|
|
87
|
+
|
|
88
|
+
Lists each bad word in the text and explains why it matched. Use it to highlight words, to treat lists differently (e.g. block `words` but only warn on `insults`), or to let someone review a match before your app blocks it.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
filter.inspect('روح للكواد!');
|
|
92
|
+
// [{ token: 'للكواد', start: 4, end: 10, normalized: 'لكواد',
|
|
93
|
+
// entry: 'كواد', source: 'words', prefix: 'ل' }]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
| Field | Description |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `token` | The word exactly as written. |
|
|
99
|
+
| `start`, `end` | Offsets in the **original** text, so `text.slice(start, end) === token`. Counted in JavaScript string units (UTF-16); convert them if another platform counts characters differently. |
|
|
100
|
+
| `normalized` | The word after normalization (Arabizi is folded too, so `Qa7ba` shows as `gahba`). |
|
|
101
|
+
| `entry` | The list entry it matched, spelled as in the list. |
|
|
102
|
+
| `source` | `'words'`, `'insults'` or `'custom'`. A word on more than one list keeps the first: built-in, then insults, then custom. |
|
|
103
|
+
| `prefix` | The attached prefix stripped before matching (after normalization, so `لل` shows as `ل`), or `''` for a direct match. |
|
|
104
|
+
| `suffix` | The suffix stripped before matching (only with `stripSuffixes`), or `''`. |
|
|
105
|
+
| `suppressedBy` | Only with `includeSuppressed`: the safe stem that cleared this match. |
|
|
106
|
+
|
|
107
|
+
Pass `{ includeSuppressed: true }` to also see words that matched but were cleared by a safe stem. They are not profane (`isProfane` and `clean` ignore them), but they show why a word *wasn't* flagged:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
filter.inspect('وبعيرك', { includeSuppressed: true });
|
|
111
|
+
// [{ token: 'وبعيرك', …, entry: 'عيرك', prefix: 'وب', suppressedBy: 'بعير' }]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Including the output of `inspect` in a [“Clean word flagged” issue](https://github.com/karam-fikre/iraqi-dialect-bad-words/issues/new?template=false-positive.yml) shows exactly which entry and prefix rule fired.
|
|
115
|
+
|
|
84
116
|
### `addWords(words: string[]): void`
|
|
85
117
|
|
|
86
118
|
Adds words to this filter.
|
|
@@ -99,9 +131,13 @@ Matching is done per whole word, after normalizing both the text and the word li
|
|
|
99
131
|
| Stretched letters | `عاااارة` |
|
|
100
132
|
| Tatweel | `عـــارة` |
|
|
101
133
|
| ة / ه, ى / ي, أ / إ / آ / ا | `عاره` |
|
|
134
|
+
| Persian/Kurdish keyboard letters ی ک ە | `عیر`, `کس` |
|
|
135
|
+
| Invisible characters hidden inside a word (zero-width, direction marks, soft hyphen) | `عارة` |
|
|
102
136
|
| Attached prefixes و ف ب ل ك ال | `والعارة`, `للعارة` |
|
|
137
|
+
| Pronoun suffixes (with `stripSuffixes`) | `طيزهم`, `وطيزكم` |
|
|
103
138
|
| Punctuation next to the word | `عارة!` `عارة،` |
|
|
104
|
-
|
|
|
139
|
+
| Digits glued to or inside an Arabic word | `عارة3`, `عا٣رة` |
|
|
140
|
+
| Arabizi (Latin letters and numbers, Western or Arabic-Indic), including spelling variants | `ga7ba` = `ga٧ba` = `qahba` = `gahba`, `kosomak` = `kusumak`, `3ayri` = `ayri` |
|
|
105
141
|
|
|
106
142
|
Because matching is whole-word, a clean word that merely contains a bad word is not flagged.
|
|
107
143
|
|
|
@@ -119,6 +155,8 @@ The word list is the heart of this package, and native speakers are the best jud
|
|
|
119
155
|
- **A clean word is flagged:** open a [“Clean word flagged” issue](https://github.com/karam-fikre/iraqi-dialect-bad-words/issues/new?template=false-positive.yml).
|
|
120
156
|
If it's flagged only because of a prefix (e.g. `بعير` "camel" = `ب` + a bad word), the fix is adding it to [`src/safe-stems.json`](src/safe-stems.json).
|
|
121
157
|
|
|
158
|
+
- **A sentence is judged wrong:** add it to [`test/fixtures/sentences.json`](test/fixtures/sentences.json) with the verdict a native speaker expects (`"profane": true` or `false`, plus `"options"` if it needs `includeInsults` or `stripSuffixes`). Every sentence there runs as a test, so later word-list changes can't silently flip it.
|
|
159
|
+
|
|
122
160
|
Words that also have a common, normal meaning (like كلب "dog") belong in [`src/insults.json`](src/insults.json), which is opt-in, not in `words.json`.
|
|
123
161
|
|
|
124
162
|
### Development
|
package/dist/index.d.mts
CHANGED
|
@@ -6,26 +6,98 @@ interface IraqiBadWordsFilterOptions {
|
|
|
6
6
|
* many of them are everyday words ("عندي كلب", "جلب لي هدية").
|
|
7
7
|
*/
|
|
8
8
|
includeInsults?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Also match words with a pronoun suffix the list doesn't spell out
|
|
11
|
+
* (طيزهم -> طيز). Off by default: it widens matching, so it can flag
|
|
12
|
+
* clean words that happen to end like a bad word plus a suffix.
|
|
13
|
+
*/
|
|
14
|
+
stripSuffixes?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Which list a dictionary entry came from. */
|
|
17
|
+
type WordSource = "words" | "insults" | "custom";
|
|
18
|
+
interface InspectOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Also return words that matched but were cleared by a safe stem
|
|
21
|
+
* (e.g. بعير "camel" = ب + عير). They carry `suppressedBy`.
|
|
22
|
+
*/
|
|
23
|
+
includeSuppressed?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** A bad word found by inspect(). */
|
|
26
|
+
interface ProfanityMatch {
|
|
27
|
+
/** The word exactly as written in the input. */
|
|
28
|
+
token: string;
|
|
29
|
+
/**
|
|
30
|
+
* Offset of the word in the input; text.slice(start, end) === token.
|
|
31
|
+
* Counted in JavaScript string units (UTF-16), like String.prototype.slice.
|
|
32
|
+
*/
|
|
33
|
+
start: number;
|
|
34
|
+
/** Offset just past the end of the word in the input. */
|
|
35
|
+
end: number;
|
|
36
|
+
/** The word after normalization (diacritics, stretched letters, … removed). */
|
|
37
|
+
normalized: string;
|
|
38
|
+
/** The dictionary entry it matched, spelled as in its list. */
|
|
39
|
+
entry: string;
|
|
40
|
+
/** The list the entry came from. */
|
|
41
|
+
source: WordSource;
|
|
42
|
+
/** The prefix stripped before matching (e.g. "وال"), or "" for a direct match. */
|
|
43
|
+
prefix: string;
|
|
44
|
+
/** The suffix stripped before matching (only with stripSuffixes), or "". */
|
|
45
|
+
suffix: string;
|
|
46
|
+
/**
|
|
47
|
+
* The safe stem that cleared this match, spelled as in safe-stems.json.
|
|
48
|
+
* Only set on matches returned with `includeSuppressed`; these are not profane.
|
|
49
|
+
*/
|
|
50
|
+
suppressedBy?: string;
|
|
9
51
|
}
|
|
10
52
|
declare class IraqiBadWordsFilter {
|
|
11
53
|
private words;
|
|
12
54
|
private safeStems;
|
|
55
|
+
private stripSuffixes;
|
|
13
56
|
constructor(options?: string[] | IraqiBadWordsFilterOptions);
|
|
14
57
|
/**
|
|
15
58
|
* Normalizes Arabic text so spelling variants compare equal:
|
|
16
|
-
* removes diacritics (Tashkeel) and
|
|
17
|
-
* forms
|
|
59
|
+
* removes diacritics (Tashkeel), tatweel and invisible characters, unifies
|
|
60
|
+
* Alef/Teh Marbuta/Alef Maksura forms and the Persian/Kurdish keyboard letters
|
|
61
|
+
* ی ک ە, turns Arabic-Indic digits into 0-9, and collapses stretched letters
|
|
62
|
+
* (عاااارة -> عارة).
|
|
18
63
|
*/
|
|
19
64
|
private normalizeArabic;
|
|
65
|
+
/**
|
|
66
|
+
* Folds Arabizi (Arabic written in Latin letters and digits) so spelling
|
|
67
|
+
* variants compare equal: ga7ba / qahba / gahba, kosomak / kusomak,
|
|
68
|
+
* teezak / tizak. Only words made purely of Latin letters and digits are
|
|
69
|
+
* folded; expects text already passed through normalizeArabic.
|
|
70
|
+
*/
|
|
71
|
+
private foldArabizi;
|
|
72
|
+
/** Normalizes a word or token for comparison. */
|
|
73
|
+
private normalize;
|
|
74
|
+
/**
|
|
75
|
+
* Adds words from one list. A word already present keeps its first source.
|
|
76
|
+
*/
|
|
77
|
+
private addEntries;
|
|
20
78
|
/**
|
|
21
79
|
* Checks a single word, also trying it with attached prefixes removed
|
|
22
|
-
* (والكحبة -> كحبة, بطيزك -> طيزك).
|
|
80
|
+
* (والكحبة -> كحبة, بطيزك -> طيزك), and suffixes removed when stripSuffixes is on.
|
|
81
|
+
* Returns the matched entry, or null.
|
|
82
|
+
* A match cleared by a safe stem is returned with `suppressedBy` set.
|
|
23
83
|
*/
|
|
24
|
-
private
|
|
84
|
+
private matchWord;
|
|
85
|
+
/**
|
|
86
|
+
* Finds every match in the text, including ones cleared by a safe stem.
|
|
87
|
+
* A token with invisible characters is matched whole first (عارة -> عارة);
|
|
88
|
+
* if that finds nothing profane, each visible piece is tried on its own, so an
|
|
89
|
+
* invisible character between two words can't hide a bad word either.
|
|
90
|
+
*/
|
|
91
|
+
private findMatches;
|
|
25
92
|
/**
|
|
26
93
|
* Checks if a given text contains profane words.
|
|
27
94
|
*/
|
|
28
95
|
isProfane(text: string): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Lists every bad word in the text with its position in the original input
|
|
98
|
+
* and why it matched, e.g. to highlight words or review a match before blocking.
|
|
99
|
+
*/
|
|
100
|
+
inspect(text: string, options?: InspectOptions): ProfanityMatch[];
|
|
29
101
|
/**
|
|
30
102
|
* Replaces profane words with a placeholder string.
|
|
31
103
|
*/
|
|
@@ -40,4 +112,4 @@ declare class IraqiBadWordsFilter {
|
|
|
40
112
|
removeWords(wordsToRemove: string[]): void;
|
|
41
113
|
}
|
|
42
114
|
|
|
43
|
-
export { IraqiBadWordsFilter, type IraqiBadWordsFilterOptions, IraqiBadWordsFilter as default };
|
|
115
|
+
export { type InspectOptions, IraqiBadWordsFilter, type IraqiBadWordsFilterOptions, type ProfanityMatch, type WordSource, IraqiBadWordsFilter as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -6,26 +6,98 @@ interface IraqiBadWordsFilterOptions {
|
|
|
6
6
|
* many of them are everyday words ("عندي كلب", "جلب لي هدية").
|
|
7
7
|
*/
|
|
8
8
|
includeInsults?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Also match words with a pronoun suffix the list doesn't spell out
|
|
11
|
+
* (طيزهم -> طيز). Off by default: it widens matching, so it can flag
|
|
12
|
+
* clean words that happen to end like a bad word plus a suffix.
|
|
13
|
+
*/
|
|
14
|
+
stripSuffixes?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Which list a dictionary entry came from. */
|
|
17
|
+
type WordSource = "words" | "insults" | "custom";
|
|
18
|
+
interface InspectOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Also return words that matched but were cleared by a safe stem
|
|
21
|
+
* (e.g. بعير "camel" = ب + عير). They carry `suppressedBy`.
|
|
22
|
+
*/
|
|
23
|
+
includeSuppressed?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** A bad word found by inspect(). */
|
|
26
|
+
interface ProfanityMatch {
|
|
27
|
+
/** The word exactly as written in the input. */
|
|
28
|
+
token: string;
|
|
29
|
+
/**
|
|
30
|
+
* Offset of the word in the input; text.slice(start, end) === token.
|
|
31
|
+
* Counted in JavaScript string units (UTF-16), like String.prototype.slice.
|
|
32
|
+
*/
|
|
33
|
+
start: number;
|
|
34
|
+
/** Offset just past the end of the word in the input. */
|
|
35
|
+
end: number;
|
|
36
|
+
/** The word after normalization (diacritics, stretched letters, … removed). */
|
|
37
|
+
normalized: string;
|
|
38
|
+
/** The dictionary entry it matched, spelled as in its list. */
|
|
39
|
+
entry: string;
|
|
40
|
+
/** The list the entry came from. */
|
|
41
|
+
source: WordSource;
|
|
42
|
+
/** The prefix stripped before matching (e.g. "وال"), or "" for a direct match. */
|
|
43
|
+
prefix: string;
|
|
44
|
+
/** The suffix stripped before matching (only with stripSuffixes), or "". */
|
|
45
|
+
suffix: string;
|
|
46
|
+
/**
|
|
47
|
+
* The safe stem that cleared this match, spelled as in safe-stems.json.
|
|
48
|
+
* Only set on matches returned with `includeSuppressed`; these are not profane.
|
|
49
|
+
*/
|
|
50
|
+
suppressedBy?: string;
|
|
9
51
|
}
|
|
10
52
|
declare class IraqiBadWordsFilter {
|
|
11
53
|
private words;
|
|
12
54
|
private safeStems;
|
|
55
|
+
private stripSuffixes;
|
|
13
56
|
constructor(options?: string[] | IraqiBadWordsFilterOptions);
|
|
14
57
|
/**
|
|
15
58
|
* Normalizes Arabic text so spelling variants compare equal:
|
|
16
|
-
* removes diacritics (Tashkeel) and
|
|
17
|
-
* forms
|
|
59
|
+
* removes diacritics (Tashkeel), tatweel and invisible characters, unifies
|
|
60
|
+
* Alef/Teh Marbuta/Alef Maksura forms and the Persian/Kurdish keyboard letters
|
|
61
|
+
* ی ک ە, turns Arabic-Indic digits into 0-9, and collapses stretched letters
|
|
62
|
+
* (عاااارة -> عارة).
|
|
18
63
|
*/
|
|
19
64
|
private normalizeArabic;
|
|
65
|
+
/**
|
|
66
|
+
* Folds Arabizi (Arabic written in Latin letters and digits) so spelling
|
|
67
|
+
* variants compare equal: ga7ba / qahba / gahba, kosomak / kusomak,
|
|
68
|
+
* teezak / tizak. Only words made purely of Latin letters and digits are
|
|
69
|
+
* folded; expects text already passed through normalizeArabic.
|
|
70
|
+
*/
|
|
71
|
+
private foldArabizi;
|
|
72
|
+
/** Normalizes a word or token for comparison. */
|
|
73
|
+
private normalize;
|
|
74
|
+
/**
|
|
75
|
+
* Adds words from one list. A word already present keeps its first source.
|
|
76
|
+
*/
|
|
77
|
+
private addEntries;
|
|
20
78
|
/**
|
|
21
79
|
* Checks a single word, also trying it with attached prefixes removed
|
|
22
|
-
* (والكحبة -> كحبة, بطيزك -> طيزك).
|
|
80
|
+
* (والكحبة -> كحبة, بطيزك -> طيزك), and suffixes removed when stripSuffixes is on.
|
|
81
|
+
* Returns the matched entry, or null.
|
|
82
|
+
* A match cleared by a safe stem is returned with `suppressedBy` set.
|
|
23
83
|
*/
|
|
24
|
-
private
|
|
84
|
+
private matchWord;
|
|
85
|
+
/**
|
|
86
|
+
* Finds every match in the text, including ones cleared by a safe stem.
|
|
87
|
+
* A token with invisible characters is matched whole first (عارة -> عارة);
|
|
88
|
+
* if that finds nothing profane, each visible piece is tried on its own, so an
|
|
89
|
+
* invisible character between two words can't hide a bad word either.
|
|
90
|
+
*/
|
|
91
|
+
private findMatches;
|
|
25
92
|
/**
|
|
26
93
|
* Checks if a given text contains profane words.
|
|
27
94
|
*/
|
|
28
95
|
isProfane(text: string): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Lists every bad word in the text with its position in the original input
|
|
98
|
+
* and why it matched, e.g. to highlight words or review a match before blocking.
|
|
99
|
+
*/
|
|
100
|
+
inspect(text: string, options?: InspectOptions): ProfanityMatch[];
|
|
29
101
|
/**
|
|
30
102
|
* Replaces profane words with a placeholder string.
|
|
31
103
|
*/
|
|
@@ -40,4 +112,4 @@ declare class IraqiBadWordsFilter {
|
|
|
40
112
|
removeWords(wordsToRemove: string[]): void;
|
|
41
113
|
}
|
|
42
114
|
|
|
43
|
-
export { IraqiBadWordsFilter, type IraqiBadWordsFilterOptions, IraqiBadWordsFilter as default };
|
|
115
|
+
export { type InspectOptions, IraqiBadWordsFilter, type IraqiBadWordsFilterOptions, type ProfanityMatch, type WordSource, IraqiBadWordsFilter as default };
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,12 @@ var words_default = [
|
|
|
41
41
|
"\u0639\u064A\u0631\u0647",
|
|
42
42
|
"\u0639\u064A\u0631\u0647\u0627",
|
|
43
43
|
"\u0639\u064A\u0631\u064A\u0646",
|
|
44
|
+
"\u0639\u064A\u063A",
|
|
45
|
+
"\u0639\u064A\u063A\u064A",
|
|
46
|
+
"\u0639\u064A\u063A\u0643",
|
|
47
|
+
"\u0639\u064A\u063A\u0647",
|
|
48
|
+
"\u0639\u064A\u063A\u0647\u0627",
|
|
49
|
+
"\u0639\u064A\u063A\u064A\u0646",
|
|
44
50
|
"\u0632\u0628",
|
|
45
51
|
"\u0632\u0628\u064A",
|
|
46
52
|
"\u0632\u0628\u0643",
|
|
@@ -226,87 +232,193 @@ var insults_default = [
|
|
|
226
232
|
// src/safe-stems.json
|
|
227
233
|
var safe_stems_default = [
|
|
228
234
|
"\u0628\u0639\u064A\u0631",
|
|
235
|
+
"\u0628\u0639\u064A\u063A",
|
|
229
236
|
"\u0628\u0643\u0633",
|
|
230
237
|
"\u0648\u0643\u0633",
|
|
231
238
|
"\u0641\u0643\u0633"
|
|
232
239
|
];
|
|
233
240
|
|
|
234
241
|
// src/index.ts
|
|
235
|
-
var
|
|
242
|
+
var INVISIBLE = "\\u00AD\\u061C\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF";
|
|
243
|
+
var INVISIBLE_CHARS = new RegExp(`[${INVISIBLE}]`, "g");
|
|
244
|
+
var HAS_INVISIBLE = new RegExp(`[${INVISIBLE}]`);
|
|
245
|
+
var VISIBLE_RUN = new RegExp(`[^${INVISIBLE}]+`, "g");
|
|
246
|
+
var WORD_PATTERN = new RegExp(
|
|
247
|
+
`[A-Za-z0-9_\xC0-\u024F\u0600-\u060B\u060D-\u061A\u061C-\u061E\u0620-\u0669\u066E-\u06D3\u06D5-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFC${INVISIBLE}]+`,
|
|
248
|
+
"g"
|
|
249
|
+
);
|
|
236
250
|
var CONJUNCTIONS = ["", "\u0648", "\u0641"];
|
|
237
251
|
var PREPOSITIONS = ["", "\u0628", "\u0644", "\u0643"];
|
|
238
252
|
var ARTICLES = ["", "\u0627\u0644"];
|
|
253
|
+
var SUFFIXES = ["\u064A", "\u0643", "\u062C", "\u0647", "\u0647\u0627", "\u0646\u0627", "\u0643\u0645", "\u0647\u0645"];
|
|
239
254
|
var IraqiBadWordsFilter = class {
|
|
240
255
|
constructor(options = {}) {
|
|
241
|
-
const { customWords = [], includeInsults = false } = Array.isArray(options) ? { customWords: options } : options;
|
|
242
|
-
this.words = /* @__PURE__ */ new
|
|
243
|
-
this.
|
|
244
|
-
this.
|
|
256
|
+
const { customWords = [], includeInsults = false, stripSuffixes = false } = Array.isArray(options) ? { customWords: options } : options;
|
|
257
|
+
this.words = /* @__PURE__ */ new Map();
|
|
258
|
+
this.stripSuffixes = stripSuffixes;
|
|
259
|
+
this.safeStems = safe_stems_default.map((word) => ({ normalized: this.normalize(word), word }));
|
|
260
|
+
this.addEntries(words_default, "words");
|
|
261
|
+
if (includeInsults) this.addEntries(insults_default, "insults");
|
|
262
|
+
this.addEntries(customWords, "custom");
|
|
245
263
|
}
|
|
246
264
|
/**
|
|
247
265
|
* Normalizes Arabic text so spelling variants compare equal:
|
|
248
|
-
* removes diacritics (Tashkeel) and
|
|
249
|
-
* forms
|
|
266
|
+
* removes diacritics (Tashkeel), tatweel and invisible characters, unifies
|
|
267
|
+
* Alef/Teh Marbuta/Alef Maksura forms and the Persian/Kurdish keyboard letters
|
|
268
|
+
* ی ک ە, turns Arabic-Indic digits into 0-9, and collapses stretched letters
|
|
269
|
+
* (عاااارة -> عارة).
|
|
250
270
|
*/
|
|
251
271
|
normalizeArabic(text) {
|
|
252
|
-
return text.replace(/[
|
|
272
|
+
return text.replace(/[\u064B-\u065F\u0670\u0640]/g, "").replace(INVISIBLE_CHARS, "").replace(/[أإآٱ]/g, "\u0627").replace(/[ةەہۀ]/g, "\u0647").replace(/[ىی]/g, "\u064A").replace(/[کڪ]/g, "\u0643").replace(/[٠-٩]/g, (digit) => String(digit.charCodeAt(0) - 1632)).replace(/[۰-۹]/g, (digit) => String(digit.charCodeAt(0) - 1776)).replace(/(.)\1+/gu, "$1").toLowerCase();
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Folds Arabizi (Arabic written in Latin letters and digits) so spelling
|
|
276
|
+
* variants compare equal: ga7ba / qahba / gahba, kosomak / kusomak,
|
|
277
|
+
* teezak / tizak. Only words made purely of Latin letters and digits are
|
|
278
|
+
* folded; expects text already passed through normalizeArabic.
|
|
279
|
+
*/
|
|
280
|
+
foldArabizi(word) {
|
|
281
|
+
if (!/^[a-z0-9]+$/.test(word) || !/[a-z]/.test(word)) return word;
|
|
282
|
+
return word.replace(/7/g, "h").replace(/5/g, "kh").replace(/[23]/g, "").replace(/q/g, "g").replace(/ou|o/g, "u").replace(/e/g, "i").replace(/(.)\1+/g, "$1");
|
|
283
|
+
}
|
|
284
|
+
/** Normalizes a word or token for comparison. */
|
|
285
|
+
normalize(text) {
|
|
286
|
+
const word = this.normalizeArabic(text);
|
|
287
|
+
return /[\u0600-\uFEFF]/.test(word) ? word.replace(/[0-9]/g, "").replace(/(.)\1+/gu, "$1") : this.foldArabizi(word);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Adds words from one list. A word already present keeps its first source.
|
|
291
|
+
*/
|
|
292
|
+
addEntries(newWords, source) {
|
|
293
|
+
for (const word of newWords) {
|
|
294
|
+
const normalized = this.normalize(word);
|
|
295
|
+
if (!this.words.has(normalized)) this.words.set(normalized, { word, source });
|
|
296
|
+
}
|
|
253
297
|
}
|
|
254
298
|
/**
|
|
255
299
|
* Checks a single word, also trying it with attached prefixes removed
|
|
256
|
-
* (والكحبة -> كحبة, بطيزك -> طيزك).
|
|
300
|
+
* (والكحبة -> كحبة, بطيزك -> طيزك), and suffixes removed when stripSuffixes is on.
|
|
301
|
+
* Returns the matched entry, or null.
|
|
302
|
+
* A match cleared by a safe stem is returned with `suppressedBy` set.
|
|
257
303
|
*/
|
|
258
|
-
|
|
259
|
-
const word = this.
|
|
260
|
-
|
|
261
|
-
|
|
304
|
+
matchWord(token) {
|
|
305
|
+
const word = this.normalize(token);
|
|
306
|
+
const direct = this.words.get(word);
|
|
307
|
+
if (direct) {
|
|
308
|
+
return { normalized: word, entry: direct.word, source: direct.source, prefix: "", suffix: "" };
|
|
309
|
+
}
|
|
310
|
+
const candidates = [{ prefix: "", stem: word, suffix: "" }];
|
|
262
311
|
for (const conjunction of CONJUNCTIONS) {
|
|
263
312
|
for (const preposition of PREPOSITIONS) {
|
|
264
313
|
for (const article of ARTICLES) {
|
|
265
314
|
const prefix = conjunction + preposition + article;
|
|
266
315
|
if (prefix && word.startsWith(prefix) && word.length - prefix.length >= 2) {
|
|
267
|
-
|
|
316
|
+
candidates.push({ prefix, stem: word.slice(prefix.length), suffix: "" });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (this.stripSuffixes) {
|
|
322
|
+
for (const { prefix, stem } of [...candidates]) {
|
|
323
|
+
for (const suffix of SUFFIXES) {
|
|
324
|
+
if (stem.endsWith(suffix) && stem.length - suffix.length >= 2) {
|
|
325
|
+
candidates.push({ prefix, stem: stem.slice(0, -suffix.length), suffix });
|
|
268
326
|
}
|
|
269
327
|
}
|
|
270
328
|
}
|
|
271
329
|
}
|
|
272
|
-
|
|
273
|
-
|
|
330
|
+
const match = candidates.find(({ stem }) => this.words.has(stem));
|
|
331
|
+
if (!match) return null;
|
|
332
|
+
const { word: entry, source } = this.words.get(match.stem);
|
|
333
|
+
const result = {
|
|
334
|
+
normalized: word,
|
|
335
|
+
entry,
|
|
336
|
+
source,
|
|
337
|
+
prefix: match.prefix,
|
|
338
|
+
suffix: match.suffix
|
|
339
|
+
};
|
|
340
|
+
for (const { stem } of candidates) {
|
|
341
|
+
const safe = this.safeStems.find(({ normalized }) => stem.startsWith(normalized));
|
|
342
|
+
if (safe) return { ...result, suppressedBy: safe.word };
|
|
343
|
+
}
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Finds every match in the text, including ones cleared by a safe stem.
|
|
348
|
+
* A token with invisible characters is matched whole first (عارة -> عارة);
|
|
349
|
+
* if that finds nothing profane, each visible piece is tried on its own, so an
|
|
350
|
+
* invisible character between two words can't hide a bad word either.
|
|
351
|
+
*/
|
|
352
|
+
*findMatches(text) {
|
|
353
|
+
const pattern = new RegExp(WORD_PATTERN.source, "g");
|
|
354
|
+
let found;
|
|
355
|
+
while (found = pattern.exec(text)) {
|
|
356
|
+
const token = found[0];
|
|
357
|
+
const start = found.index;
|
|
358
|
+
const whole = this.matchWord(token);
|
|
359
|
+
const wholeMatch = whole && { token, start, end: start + token.length, ...whole };
|
|
360
|
+
if (whole && whole.suppressedBy === void 0 || !HAS_INVISIBLE.test(token)) {
|
|
361
|
+
if (wholeMatch) yield wholeMatch;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const pieces = [];
|
|
365
|
+
const piecePattern = new RegExp(VISIBLE_RUN.source, "g");
|
|
366
|
+
let piece;
|
|
367
|
+
while (piece = piecePattern.exec(token)) {
|
|
368
|
+
const match = this.matchWord(piece[0]);
|
|
369
|
+
const pieceStart = start + piece.index;
|
|
370
|
+
if (match) pieces.push({ token: piece[0], start: pieceStart, end: pieceStart + piece[0].length, ...match });
|
|
371
|
+
}
|
|
372
|
+
if (pieces.length) yield* pieces;
|
|
373
|
+
else if (wholeMatch) yield wholeMatch;
|
|
274
374
|
}
|
|
275
|
-
return stems.some((stem) => this.words.has(stem));
|
|
276
375
|
}
|
|
277
376
|
/**
|
|
278
377
|
* Checks if a given text contains profane words.
|
|
279
378
|
*/
|
|
280
379
|
isProfane(text) {
|
|
281
380
|
if (!text) return false;
|
|
282
|
-
for (const
|
|
283
|
-
if (
|
|
284
|
-
return true;
|
|
285
|
-
}
|
|
381
|
+
for (const match of this.findMatches(text)) {
|
|
382
|
+
if (match.suppressedBy === void 0) return true;
|
|
286
383
|
}
|
|
287
384
|
return false;
|
|
288
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* Lists every bad word in the text with its position in the original input
|
|
388
|
+
* and why it matched, e.g. to highlight words or review a match before blocking.
|
|
389
|
+
*/
|
|
390
|
+
inspect(text, options = {}) {
|
|
391
|
+
if (!text) return [];
|
|
392
|
+
const matches = [];
|
|
393
|
+
for (const match of this.findMatches(text)) {
|
|
394
|
+
if (match.suppressedBy === void 0 || options.includeSuppressed) matches.push(match);
|
|
395
|
+
}
|
|
396
|
+
return matches;
|
|
397
|
+
}
|
|
289
398
|
/**
|
|
290
399
|
* Replaces profane words with a placeholder string.
|
|
291
400
|
*/
|
|
292
401
|
clean(text, placeholder = "*") {
|
|
293
402
|
if (!text) return "";
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
403
|
+
let result = "";
|
|
404
|
+
let last = 0;
|
|
405
|
+
for (const { start, end } of this.inspect(text)) {
|
|
406
|
+
result += text.slice(last, start) + placeholder.repeat(end - start);
|
|
407
|
+
last = end;
|
|
408
|
+
}
|
|
409
|
+
return result + text.slice(last);
|
|
298
410
|
}
|
|
299
411
|
/**
|
|
300
412
|
* Dynamically add words to the active dictionary.
|
|
301
413
|
*/
|
|
302
414
|
addWords(newWords) {
|
|
303
|
-
|
|
415
|
+
this.addEntries(newWords, "custom");
|
|
304
416
|
}
|
|
305
417
|
/**
|
|
306
418
|
* Remove words from the active filter dictionary.
|
|
307
419
|
*/
|
|
308
420
|
removeWords(wordsToRemove) {
|
|
309
|
-
wordsToRemove.forEach((word) => this.words.delete(this.
|
|
421
|
+
wordsToRemove.forEach((word) => this.words.delete(this.normalize(word)));
|
|
310
422
|
}
|
|
311
423
|
};
|
|
312
424
|
var index_default = IraqiBadWordsFilter;
|
package/dist/index.mjs
CHANGED
|
@@ -14,6 +14,12 @@ var words_default = [
|
|
|
14
14
|
"\u0639\u064A\u0631\u0647",
|
|
15
15
|
"\u0639\u064A\u0631\u0647\u0627",
|
|
16
16
|
"\u0639\u064A\u0631\u064A\u0646",
|
|
17
|
+
"\u0639\u064A\u063A",
|
|
18
|
+
"\u0639\u064A\u063A\u064A",
|
|
19
|
+
"\u0639\u064A\u063A\u0643",
|
|
20
|
+
"\u0639\u064A\u063A\u0647",
|
|
21
|
+
"\u0639\u064A\u063A\u0647\u0627",
|
|
22
|
+
"\u0639\u064A\u063A\u064A\u0646",
|
|
17
23
|
"\u0632\u0628",
|
|
18
24
|
"\u0632\u0628\u064A",
|
|
19
25
|
"\u0632\u0628\u0643",
|
|
@@ -199,87 +205,193 @@ var insults_default = [
|
|
|
199
205
|
// src/safe-stems.json
|
|
200
206
|
var safe_stems_default = [
|
|
201
207
|
"\u0628\u0639\u064A\u0631",
|
|
208
|
+
"\u0628\u0639\u064A\u063A",
|
|
202
209
|
"\u0628\u0643\u0633",
|
|
203
210
|
"\u0648\u0643\u0633",
|
|
204
211
|
"\u0641\u0643\u0633"
|
|
205
212
|
];
|
|
206
213
|
|
|
207
214
|
// src/index.ts
|
|
208
|
-
var
|
|
215
|
+
var INVISIBLE = "\\u00AD\\u061C\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF";
|
|
216
|
+
var INVISIBLE_CHARS = new RegExp(`[${INVISIBLE}]`, "g");
|
|
217
|
+
var HAS_INVISIBLE = new RegExp(`[${INVISIBLE}]`);
|
|
218
|
+
var VISIBLE_RUN = new RegExp(`[^${INVISIBLE}]+`, "g");
|
|
219
|
+
var WORD_PATTERN = new RegExp(
|
|
220
|
+
`[A-Za-z0-9_\xC0-\u024F\u0600-\u060B\u060D-\u061A\u061C-\u061E\u0620-\u0669\u066E-\u06D3\u06D5-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFC${INVISIBLE}]+`,
|
|
221
|
+
"g"
|
|
222
|
+
);
|
|
209
223
|
var CONJUNCTIONS = ["", "\u0648", "\u0641"];
|
|
210
224
|
var PREPOSITIONS = ["", "\u0628", "\u0644", "\u0643"];
|
|
211
225
|
var ARTICLES = ["", "\u0627\u0644"];
|
|
226
|
+
var SUFFIXES = ["\u064A", "\u0643", "\u062C", "\u0647", "\u0647\u0627", "\u0646\u0627", "\u0643\u0645", "\u0647\u0645"];
|
|
212
227
|
var IraqiBadWordsFilter = class {
|
|
213
228
|
constructor(options = {}) {
|
|
214
|
-
const { customWords = [], includeInsults = false } = Array.isArray(options) ? { customWords: options } : options;
|
|
215
|
-
this.words = /* @__PURE__ */ new
|
|
216
|
-
this.
|
|
217
|
-
this.
|
|
229
|
+
const { customWords = [], includeInsults = false, stripSuffixes = false } = Array.isArray(options) ? { customWords: options } : options;
|
|
230
|
+
this.words = /* @__PURE__ */ new Map();
|
|
231
|
+
this.stripSuffixes = stripSuffixes;
|
|
232
|
+
this.safeStems = safe_stems_default.map((word) => ({ normalized: this.normalize(word), word }));
|
|
233
|
+
this.addEntries(words_default, "words");
|
|
234
|
+
if (includeInsults) this.addEntries(insults_default, "insults");
|
|
235
|
+
this.addEntries(customWords, "custom");
|
|
218
236
|
}
|
|
219
237
|
/**
|
|
220
238
|
* Normalizes Arabic text so spelling variants compare equal:
|
|
221
|
-
* removes diacritics (Tashkeel) and
|
|
222
|
-
* forms
|
|
239
|
+
* removes diacritics (Tashkeel), tatweel and invisible characters, unifies
|
|
240
|
+
* Alef/Teh Marbuta/Alef Maksura forms and the Persian/Kurdish keyboard letters
|
|
241
|
+
* ی ک ە, turns Arabic-Indic digits into 0-9, and collapses stretched letters
|
|
242
|
+
* (عاااارة -> عارة).
|
|
223
243
|
*/
|
|
224
244
|
normalizeArabic(text) {
|
|
225
|
-
return text.replace(/[
|
|
245
|
+
return text.replace(/[\u064B-\u065F\u0670\u0640]/g, "").replace(INVISIBLE_CHARS, "").replace(/[أإآٱ]/g, "\u0627").replace(/[ةەہۀ]/g, "\u0647").replace(/[ىی]/g, "\u064A").replace(/[کڪ]/g, "\u0643").replace(/[٠-٩]/g, (digit) => String(digit.charCodeAt(0) - 1632)).replace(/[۰-۹]/g, (digit) => String(digit.charCodeAt(0) - 1776)).replace(/(.)\1+/gu, "$1").toLowerCase();
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Folds Arabizi (Arabic written in Latin letters and digits) so spelling
|
|
249
|
+
* variants compare equal: ga7ba / qahba / gahba, kosomak / kusomak,
|
|
250
|
+
* teezak / tizak. Only words made purely of Latin letters and digits are
|
|
251
|
+
* folded; expects text already passed through normalizeArabic.
|
|
252
|
+
*/
|
|
253
|
+
foldArabizi(word) {
|
|
254
|
+
if (!/^[a-z0-9]+$/.test(word) || !/[a-z]/.test(word)) return word;
|
|
255
|
+
return word.replace(/7/g, "h").replace(/5/g, "kh").replace(/[23]/g, "").replace(/q/g, "g").replace(/ou|o/g, "u").replace(/e/g, "i").replace(/(.)\1+/g, "$1");
|
|
256
|
+
}
|
|
257
|
+
/** Normalizes a word or token for comparison. */
|
|
258
|
+
normalize(text) {
|
|
259
|
+
const word = this.normalizeArabic(text);
|
|
260
|
+
return /[\u0600-\uFEFF]/.test(word) ? word.replace(/[0-9]/g, "").replace(/(.)\1+/gu, "$1") : this.foldArabizi(word);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Adds words from one list. A word already present keeps its first source.
|
|
264
|
+
*/
|
|
265
|
+
addEntries(newWords, source) {
|
|
266
|
+
for (const word of newWords) {
|
|
267
|
+
const normalized = this.normalize(word);
|
|
268
|
+
if (!this.words.has(normalized)) this.words.set(normalized, { word, source });
|
|
269
|
+
}
|
|
226
270
|
}
|
|
227
271
|
/**
|
|
228
272
|
* Checks a single word, also trying it with attached prefixes removed
|
|
229
|
-
* (والكحبة -> كحبة, بطيزك -> طيزك).
|
|
273
|
+
* (والكحبة -> كحبة, بطيزك -> طيزك), and suffixes removed when stripSuffixes is on.
|
|
274
|
+
* Returns the matched entry, or null.
|
|
275
|
+
* A match cleared by a safe stem is returned with `suppressedBy` set.
|
|
230
276
|
*/
|
|
231
|
-
|
|
232
|
-
const word = this.
|
|
233
|
-
|
|
234
|
-
|
|
277
|
+
matchWord(token) {
|
|
278
|
+
const word = this.normalize(token);
|
|
279
|
+
const direct = this.words.get(word);
|
|
280
|
+
if (direct) {
|
|
281
|
+
return { normalized: word, entry: direct.word, source: direct.source, prefix: "", suffix: "" };
|
|
282
|
+
}
|
|
283
|
+
const candidates = [{ prefix: "", stem: word, suffix: "" }];
|
|
235
284
|
for (const conjunction of CONJUNCTIONS) {
|
|
236
285
|
for (const preposition of PREPOSITIONS) {
|
|
237
286
|
for (const article of ARTICLES) {
|
|
238
287
|
const prefix = conjunction + preposition + article;
|
|
239
288
|
if (prefix && word.startsWith(prefix) && word.length - prefix.length >= 2) {
|
|
240
|
-
|
|
289
|
+
candidates.push({ prefix, stem: word.slice(prefix.length), suffix: "" });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (this.stripSuffixes) {
|
|
295
|
+
for (const { prefix, stem } of [...candidates]) {
|
|
296
|
+
for (const suffix of SUFFIXES) {
|
|
297
|
+
if (stem.endsWith(suffix) && stem.length - suffix.length >= 2) {
|
|
298
|
+
candidates.push({ prefix, stem: stem.slice(0, -suffix.length), suffix });
|
|
241
299
|
}
|
|
242
300
|
}
|
|
243
301
|
}
|
|
244
302
|
}
|
|
245
|
-
|
|
246
|
-
|
|
303
|
+
const match = candidates.find(({ stem }) => this.words.has(stem));
|
|
304
|
+
if (!match) return null;
|
|
305
|
+
const { word: entry, source } = this.words.get(match.stem);
|
|
306
|
+
const result = {
|
|
307
|
+
normalized: word,
|
|
308
|
+
entry,
|
|
309
|
+
source,
|
|
310
|
+
prefix: match.prefix,
|
|
311
|
+
suffix: match.suffix
|
|
312
|
+
};
|
|
313
|
+
for (const { stem } of candidates) {
|
|
314
|
+
const safe = this.safeStems.find(({ normalized }) => stem.startsWith(normalized));
|
|
315
|
+
if (safe) return { ...result, suppressedBy: safe.word };
|
|
316
|
+
}
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Finds every match in the text, including ones cleared by a safe stem.
|
|
321
|
+
* A token with invisible characters is matched whole first (عارة -> عارة);
|
|
322
|
+
* if that finds nothing profane, each visible piece is tried on its own, so an
|
|
323
|
+
* invisible character between two words can't hide a bad word either.
|
|
324
|
+
*/
|
|
325
|
+
*findMatches(text) {
|
|
326
|
+
const pattern = new RegExp(WORD_PATTERN.source, "g");
|
|
327
|
+
let found;
|
|
328
|
+
while (found = pattern.exec(text)) {
|
|
329
|
+
const token = found[0];
|
|
330
|
+
const start = found.index;
|
|
331
|
+
const whole = this.matchWord(token);
|
|
332
|
+
const wholeMatch = whole && { token, start, end: start + token.length, ...whole };
|
|
333
|
+
if (whole && whole.suppressedBy === void 0 || !HAS_INVISIBLE.test(token)) {
|
|
334
|
+
if (wholeMatch) yield wholeMatch;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const pieces = [];
|
|
338
|
+
const piecePattern = new RegExp(VISIBLE_RUN.source, "g");
|
|
339
|
+
let piece;
|
|
340
|
+
while (piece = piecePattern.exec(token)) {
|
|
341
|
+
const match = this.matchWord(piece[0]);
|
|
342
|
+
const pieceStart = start + piece.index;
|
|
343
|
+
if (match) pieces.push({ token: piece[0], start: pieceStart, end: pieceStart + piece[0].length, ...match });
|
|
344
|
+
}
|
|
345
|
+
if (pieces.length) yield* pieces;
|
|
346
|
+
else if (wholeMatch) yield wholeMatch;
|
|
247
347
|
}
|
|
248
|
-
return stems.some((stem) => this.words.has(stem));
|
|
249
348
|
}
|
|
250
349
|
/**
|
|
251
350
|
* Checks if a given text contains profane words.
|
|
252
351
|
*/
|
|
253
352
|
isProfane(text) {
|
|
254
353
|
if (!text) return false;
|
|
255
|
-
for (const
|
|
256
|
-
if (
|
|
257
|
-
return true;
|
|
258
|
-
}
|
|
354
|
+
for (const match of this.findMatches(text)) {
|
|
355
|
+
if (match.suppressedBy === void 0) return true;
|
|
259
356
|
}
|
|
260
357
|
return false;
|
|
261
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Lists every bad word in the text with its position in the original input
|
|
361
|
+
* and why it matched, e.g. to highlight words or review a match before blocking.
|
|
362
|
+
*/
|
|
363
|
+
inspect(text, options = {}) {
|
|
364
|
+
if (!text) return [];
|
|
365
|
+
const matches = [];
|
|
366
|
+
for (const match of this.findMatches(text)) {
|
|
367
|
+
if (match.suppressedBy === void 0 || options.includeSuppressed) matches.push(match);
|
|
368
|
+
}
|
|
369
|
+
return matches;
|
|
370
|
+
}
|
|
262
371
|
/**
|
|
263
372
|
* Replaces profane words with a placeholder string.
|
|
264
373
|
*/
|
|
265
374
|
clean(text, placeholder = "*") {
|
|
266
375
|
if (!text) return "";
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
376
|
+
let result = "";
|
|
377
|
+
let last = 0;
|
|
378
|
+
for (const { start, end } of this.inspect(text)) {
|
|
379
|
+
result += text.slice(last, start) + placeholder.repeat(end - start);
|
|
380
|
+
last = end;
|
|
381
|
+
}
|
|
382
|
+
return result + text.slice(last);
|
|
271
383
|
}
|
|
272
384
|
/**
|
|
273
385
|
* Dynamically add words to the active dictionary.
|
|
274
386
|
*/
|
|
275
387
|
addWords(newWords) {
|
|
276
|
-
|
|
388
|
+
this.addEntries(newWords, "custom");
|
|
277
389
|
}
|
|
278
390
|
/**
|
|
279
391
|
* Remove words from the active filter dictionary.
|
|
280
392
|
*/
|
|
281
393
|
removeWords(wordsToRemove) {
|
|
282
|
-
wordsToRemove.forEach((word) => this.words.delete(this.
|
|
394
|
+
wordsToRemove.forEach((word) => this.words.delete(this.normalize(word)));
|
|
283
395
|
}
|
|
284
396
|
};
|
|
285
397
|
var index_default = IraqiBadWordsFilter;
|
package/package.json
CHANGED