iraqi-dialect-bad-words 1.2.0 → 1.3.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 +63 -8
- package/dist/index.d.mts +85 -18
- package/dist/index.d.ts +85 -18
- package/dist/index.js +208 -50
- package/dist/index.mjs +208 -50
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,15 +65,44 @@ Creates a filter with the built-in word list.
|
|
|
65
65
|
new IraqiBadWordsFilter(); // profanity only
|
|
66
66
|
new IraqiBadWordsFilter({ includeInsults: true }); // also milder insults
|
|
67
67
|
new IraqiBadWordsFilter({ stripSuffixes: true }); // also unlisted suffix forms
|
|
68
|
+
new IraqiBadWordsFilter({ dialects: ['mosul'] }); // also Mosul ر → غ forms
|
|
68
69
|
new IraqiBadWordsFilter({ customWords: ['word1', 'word2'] });
|
|
70
|
+
new IraqiBadWordsFilter({ safeWords: ['word3'] }); // clean words not to flag
|
|
69
71
|
new IraqiBadWordsFilter(['word1', 'word2']); // same as customWords
|
|
70
72
|
```
|
|
71
73
|
|
|
72
74
|
| Option | Default | Description |
|
|
73
75
|
|---|---|---|
|
|
74
|
-
| `customWords` | `[]` | Extra words to filter. |
|
|
76
|
+
| `customWords` | `[]` | Extra words to filter, on top of the built-in list. Can come from your own JSON file (see below). |
|
|
77
|
+
| `safeWords` | `[]` | Extra safe stems, on top of the built-in [`safe-stems.json`](src/safe-stems.json): clean words that would otherwise match once a prefix or suffix is stripped, like the built-in `بعير` ("camel" = `ب` + `عير`). Use it for clean words your app sees flagged, especially ones your own `customWords` cause. Their prefixed and suffixed forms are safe too (`وبعيرك`). Can come from your own JSON file (see below). |
|
|
75
78
|
| `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
79
|
| `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. |
|
|
80
|
+
| `dialects` | `[]` | Regional pronunciations to also match. `'mosul'`: ر said as غ, so every entry with ر also matches its غ form (`شرموطة` → `شغموطة`); the ر forms keep matching. Safe stems get their غ form too. |
|
|
81
|
+
|
|
82
|
+
#### Using your own word files
|
|
83
|
+
|
|
84
|
+
Keep your app's extra words, and clean words you don't want flagged, in JSON files: arrays of single words, the same format as [`src/words.json`](src/words.json) and [`src/safe-stems.json`](src/safe-stems.json).
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
["زفت", "نذل"]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Import them and pass them as `customWords` and `safeWords`. The built-in lists stay on, and your words are added to them:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import IraqiBadWordsFilter from 'iraqi-dialect-bad-words';
|
|
94
|
+
import myWords from './my-words.json'; // TypeScript needs "resolveJsonModule": true
|
|
95
|
+
import mySafeWords from './my-safe-words.json';
|
|
96
|
+
// CommonJS: const myWords = require('./my-words.json');
|
|
97
|
+
|
|
98
|
+
const filter = new IraqiBadWordsFilter({ customWords: myWords, safeWords: mySafeWords });
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Your words go through the same normalization as the built-in lists, so you don't need to list diacritic, ة/ه, stretched or prefixed spellings. Whitespace around a word is trimmed and blank entries are skipped. `inspect()` reports a match from your word file with `source: 'custom'`, and a match cleared by your safe words with `suppressedBy` set to that safe word.
|
|
102
|
+
|
|
103
|
+
A safe word only clears a match that needed a prefix or suffix stripped, like the built-in safe stems. To stop a listed word itself from being flagged, use `removeWords()`.
|
|
104
|
+
|
|
105
|
+
React Native, Expo, Vite and Next.js bundle the JSON files into your app, so this works the same in each. The filter doesn't read files at runtime. If your lists come from a server, fetch them and pass the arrays to `addWords()` and `addSafeWords()`.
|
|
77
106
|
|
|
78
107
|
### `isProfane(text: string): boolean`
|
|
79
108
|
|
|
@@ -89,34 +118,55 @@ Lists each bad word in the text and explains why it matched. Use it to highlight
|
|
|
89
118
|
|
|
90
119
|
```ts
|
|
91
120
|
filter.inspect('روح للكواد!');
|
|
92
|
-
// [{ token: 'للكواد', start: 4, end: 10, normalized: 'لكواد',
|
|
93
|
-
//
|
|
121
|
+
// [{ token: 'للكواد', start: 4, end: 10, normalized: 'لكواد', entry: 'كواد',
|
|
122
|
+
// source: 'words', prefix: 'ل', suffix: '', rules: ['stretched', 'prefix'] }]
|
|
94
123
|
```
|
|
95
124
|
|
|
96
125
|
| Field | Description |
|
|
97
126
|
|---|---|
|
|
98
127
|
| `token` | The word exactly as written. |
|
|
99
128
|
| `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. |
|
|
129
|
+
| `normalized` | The word after normalization (Arabizi is folded too, so `Qa7ba` shows as `gahba`). For a word with گ, the reading that matched (ق or ك). |
|
|
130
|
+
| `entry` | The list entry it matched, spelled as in the list. For a Mosul form, the listed ر spelling. |
|
|
102
131
|
| `source` | `'words'`, `'insults'` or `'custom'`. A word on more than one list keeps the first: built-in, then insults, then custom. |
|
|
103
132
|
| `prefix` | The attached prefix stripped before matching (after normalization, so `لل` shows as `ل`), or `''` for a direct match. |
|
|
104
133
|
| `suffix` | The suffix stripped before matching (only with `stripSuffixes`), or `''`. |
|
|
134
|
+
| `rules` | The rules the word needed to match the entry (see below). Empty when it's spelled exactly like the entry. |
|
|
105
135
|
| `suppressedBy` | Only with `includeSuppressed`: the safe stem that cleared this match. |
|
|
106
136
|
|
|
107
137
|
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
138
|
|
|
109
139
|
```ts
|
|
110
140
|
filter.inspect('وبعيرك', { includeSuppressed: true });
|
|
111
|
-
// [{ token: 'وبعيرك', …, entry: 'عيرك', prefix: 'وب', suppressedBy: 'بعير' }]
|
|
141
|
+
// [{ token: 'وبعيرك', …, entry: 'عيرك', prefix: 'وب', rules: ['prefix'], suppressedBy: 'بعير' }]
|
|
112
142
|
```
|
|
113
143
|
|
|
114
|
-
|
|
144
|
+
`rules` lists only the rules that made a difference between the typed word and the entry, so a report shows straight away why something matched:
|
|
145
|
+
|
|
146
|
+
| Rule | Example |
|
|
147
|
+
|---|---|
|
|
148
|
+
| `diacritics` | `عَارَة`, `عـــارة` |
|
|
149
|
+
| `invisible` | `عارة` (zero-width character inside) |
|
|
150
|
+
| `spelling` | `عاره` (ة/ه, ى/ي, أ/ا) |
|
|
151
|
+
| `keyboard` | `عیر`, `کس` (Persian/Kurdish letters) |
|
|
152
|
+
| `arabic-digits` | `ga٧ba` |
|
|
153
|
+
| `stretched` | `عاااارة`, `للكواد` |
|
|
154
|
+
| `glued-digits` | `عارة3` |
|
|
155
|
+
| `arabizi` | `qa7ba` → `gahba` |
|
|
156
|
+
| `gaf-as-qaf` / `gaf-as-kaf` | `گحبة` → قحبة, `گسها` → كسها |
|
|
157
|
+
| `prefix` / `suffix` | `بطيزك`, `طيزهم` (with `stripSuffixes`) |
|
|
158
|
+
| `mosul` | `شغموطة` → شرموطة (with `dialects: ['mosul']`) |
|
|
159
|
+
|
|
160
|
+
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 matched and which rules it needed; the issue form has a field for it.
|
|
115
161
|
|
|
116
162
|
### `addWords(words: string[]): void`
|
|
117
163
|
|
|
118
164
|
Adds words to this filter.
|
|
119
165
|
|
|
166
|
+
### `addSafeWords(words: string[]): void`
|
|
167
|
+
|
|
168
|
+
Adds safe stems to this filter, like the `safeWords` option.
|
|
169
|
+
|
|
120
170
|
### `removeWords(words: string[]): void`
|
|
121
171
|
|
|
122
172
|
Removes words from this filter, for example a built-in word you don't want flagged in your app.
|
|
@@ -131,10 +181,14 @@ Matching is done per whole word, after normalizing both the text and the word li
|
|
|
131
181
|
| Stretched letters | `عاااارة` |
|
|
132
182
|
| Tatweel | `عـــارة` |
|
|
133
183
|
| ة / ه, ى / ي, أ / إ / آ / ا | `عاره` |
|
|
184
|
+
| Persian/Kurdish keyboard letters ی ک ە | `عیر`, `کس` |
|
|
185
|
+
| گ read as ق (Iraqi) and as ك (Kurdish typists) | `گحبة` = `قحبة`, `گس` = `كس` |
|
|
186
|
+
| Invisible characters hidden inside a word (zero-width, direction marks, soft hyphen) | `عارة` |
|
|
134
187
|
| Attached prefixes و ف ب ل ك ال | `والعارة`, `للعارة` |
|
|
135
188
|
| Pronoun suffixes (with `stripSuffixes`) | `طيزهم`, `وطيزكم` |
|
|
136
189
|
| Punctuation next to the word | `عارة!` `عارة،` |
|
|
137
|
-
|
|
|
190
|
+
| Digits glued to or inside an Arabic word | `عارة3`, `عا٣رة` |
|
|
191
|
+
| Arabizi (Latin letters and numbers, Western or Arabic-Indic), including spelling variants | `ga7ba` = `ga٧ba` = `qahba` = `gahba`, `kosomak` = `kusumak`, `3ayri` = `ayri` |
|
|
138
192
|
|
|
139
193
|
Because matching is whole-word, a clean word that merely contains a bad word is not flagged.
|
|
140
194
|
|
|
@@ -151,6 +205,7 @@ The word list is the heart of this package, and native speakers are the best jud
|
|
|
151
205
|
- **A word is missing:** open a [“Suggest a word” issue](https://github.com/karam-fikre/iraqi-dialect-bad-words/issues/new?template=add-word.yml), or add it to [`src/words.json`](src/words.json) in a pull request.
|
|
152
206
|
- **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).
|
|
153
207
|
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).
|
|
208
|
+
A safe stem written with گ protects only words spelled with گ, so `گلب` ("heart", Iraqi spelling of قلب) can be safe while `كلب` is not.
|
|
154
209
|
|
|
155
210
|
- **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.
|
|
156
211
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A rule a word needed to match its list entry, reported in ProfanityMatch.rules.
|
|
3
|
+
* Only rules that made a difference are reported: عارة matching عارة has none.
|
|
4
|
+
*/
|
|
5
|
+
type MatchRule = "diacritics" | "invisible" | "spelling" | "keyboard" | "arabic-digits" | "stretched" | "glued-digits" | "arabizi" | "gaf-as-qaf" | "gaf-as-kaf" | "prefix" | "suffix" | "mosul";
|
|
1
6
|
interface IraqiBadWordsFilterOptions {
|
|
2
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Extra words to filter, on top of the built-in list. Can be your own JSON
|
|
9
|
+
* word file: `customWords: require('./my-words.json')`.
|
|
10
|
+
*/
|
|
3
11
|
customWords?: string[];
|
|
12
|
+
/**
|
|
13
|
+
* Extra safe stems, on top of the built-in safe-stems.json: clean words that
|
|
14
|
+
* would otherwise match once a prefix or suffix is stripped, like the
|
|
15
|
+
* built-in بعير ("camel" = ب + عير). Their prefixed and suffixed forms are
|
|
16
|
+
* safe too (وبعيرك).
|
|
17
|
+
* Can be your own JSON file: `safeWords: require('./my-safe-words.json')`.
|
|
18
|
+
*/
|
|
19
|
+
safeWords?: string[];
|
|
4
20
|
/**
|
|
5
21
|
* Also filter milder insults (كلب, حمار, قندرة, …). Off by default because
|
|
6
22
|
* many of them are everyday words ("عندي كلب", "جلب لي هدية").
|
|
@@ -12,7 +28,15 @@ interface IraqiBadWordsFilterOptions {
|
|
|
12
28
|
* clean words that happen to end like a bad word plus a suffix.
|
|
13
29
|
*/
|
|
14
30
|
stripSuffixes?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Regional pronunciations to also match. "mosul": ر said as غ, so every entry
|
|
33
|
+
* and safe stem with ر also matches its غ form (عير -> عيغ, بعير -> بعيغ).
|
|
34
|
+
* The ر forms keep matching. Off by default.
|
|
35
|
+
*/
|
|
36
|
+
dialects?: Dialect[];
|
|
15
37
|
}
|
|
38
|
+
/** A regional pronunciation the filter can also match. */
|
|
39
|
+
type Dialect = "mosul";
|
|
16
40
|
/** Which list a dictionary entry came from. */
|
|
17
41
|
type WordSource = "words" | "insults" | "custom";
|
|
18
42
|
interface InspectOptions {
|
|
@@ -33,9 +57,15 @@ interface ProfanityMatch {
|
|
|
33
57
|
start: number;
|
|
34
58
|
/** Offset just past the end of the word in the input. */
|
|
35
59
|
end: number;
|
|
36
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* The word after normalization (diacritics, stretched letters, … removed).
|
|
62
|
+
* For a word with گ, the reading that matched: گ as ق or as ك.
|
|
63
|
+
*/
|
|
37
64
|
normalized: string;
|
|
38
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* The dictionary entry it matched, spelled as in its list. For a Mosul form
|
|
67
|
+
* (rule "mosul"), the listed ر spelling: الشرموطة for الشغموطة.
|
|
68
|
+
*/
|
|
39
69
|
entry: string;
|
|
40
70
|
/** The list the entry came from. */
|
|
41
71
|
source: WordSource;
|
|
@@ -43,8 +73,10 @@ interface ProfanityMatch {
|
|
|
43
73
|
prefix: string;
|
|
44
74
|
/** The suffix stripped before matching (only with stripSuffixes), or "". */
|
|
45
75
|
suffix: string;
|
|
76
|
+
/** The rules the word needed to match the entry, e.g. ["prefix", "arabizi"]. */
|
|
77
|
+
rules: MatchRule[];
|
|
46
78
|
/**
|
|
47
|
-
* The safe stem that cleared this match, spelled as in safe-stems.json.
|
|
79
|
+
* The safe stem that cleared this match, spelled as in safe-stems.json or safeWords.
|
|
48
80
|
* Only set on matches returned with `includeSuppressed`; these are not profane.
|
|
49
81
|
*/
|
|
50
82
|
suppressedBy?: string;
|
|
@@ -53,22 +85,35 @@ declare class IraqiBadWordsFilter {
|
|
|
53
85
|
private words;
|
|
54
86
|
private safeStems;
|
|
55
87
|
private stripSuffixes;
|
|
88
|
+
private mosul;
|
|
56
89
|
constructor(options?: string[] | IraqiBadWordsFilterOptions);
|
|
57
90
|
/**
|
|
58
|
-
* Normalizes
|
|
59
|
-
*
|
|
60
|
-
*
|
|
91
|
+
* Normalizes a word or token for comparison: removes diacritics, tatweel and
|
|
92
|
+
* invisible characters, unifies letter variants (أ/ا, ة/ه, ی/ي, ک/ك, …),
|
|
93
|
+
* reads Arabic-Indic digits, collapses stretched letters (عاااارة -> عارة),
|
|
94
|
+
* ignores digits in Arabic words and folds Arabizi. See NORMALIZE_STEPS.
|
|
95
|
+
*/
|
|
96
|
+
private normalize;
|
|
97
|
+
/**
|
|
98
|
+
* The ways a normalized word can be read. Iraqi Arabic writes ق as گ, but
|
|
99
|
+
* Kurdish typists sometimes use گ for ك, so a word with گ is tried both ways.
|
|
61
100
|
*/
|
|
62
|
-
private
|
|
101
|
+
private readings;
|
|
102
|
+
/** A list word plus its regional forms (عير -> عير, عيغ with the Mosul dialect). */
|
|
103
|
+
private dialectForms;
|
|
104
|
+
/** The key a list word is stored under. A گ in a list entry means ق. */
|
|
105
|
+
private entryKey;
|
|
63
106
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* folded; expects text already passed through normalizeArabic.
|
|
107
|
+
* A word list with surrounding whitespace trimmed and entries that are empty
|
|
108
|
+
* after normalization skipped. A stray "" in a user's file would otherwise
|
|
109
|
+
* match a lone tatweel as a word, or clear every match as a safe stem.
|
|
68
110
|
*/
|
|
69
|
-
private
|
|
70
|
-
/**
|
|
71
|
-
|
|
111
|
+
private cleanList;
|
|
112
|
+
/**
|
|
113
|
+
* Adds safe stems from one list. A safe stem written with گ protects only
|
|
114
|
+
* words spelled with گ (گلب "heart"), so it isn't read as ق or ك.
|
|
115
|
+
*/
|
|
116
|
+
private addSafeStems;
|
|
72
117
|
/**
|
|
73
118
|
* Adds words from one list. A word already present keeps its first source.
|
|
74
119
|
*/
|
|
@@ -80,8 +125,26 @@ declare class IraqiBadWordsFilter {
|
|
|
80
125
|
* A match cleared by a safe stem is returned with `suppressedBy` set.
|
|
81
126
|
*/
|
|
82
127
|
private matchWord;
|
|
83
|
-
/**
|
|
84
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Works out which rules a match needed, by comparing the typed word with the
|
|
130
|
+
* listed spelling (plus any stripped prefix and suffix). A normalization rule
|
|
131
|
+
* counts only if leaving it out would keep them apart: عاااارة vs عارة needs
|
|
132
|
+
* "stretched" but not "spelling", since both have ة.
|
|
133
|
+
*/
|
|
134
|
+
private explain;
|
|
135
|
+
/** The word plus each form of it with an attached prefix removed. */
|
|
136
|
+
private prefixCandidates;
|
|
137
|
+
/** The first safe stem one of the candidates starts with, as listed. */
|
|
138
|
+
private findSafe;
|
|
139
|
+
/** Matches one normalized reading of a word. */
|
|
140
|
+
private matchReading;
|
|
141
|
+
/**
|
|
142
|
+
* Finds every match in the text, including ones cleared by a safe stem.
|
|
143
|
+
* A token with invisible characters is matched whole first (عارة -> عارة);
|
|
144
|
+
* if that finds nothing profane, each visible piece is tried on its own, so an
|
|
145
|
+
* invisible character between two words can't hide a bad word either.
|
|
146
|
+
*/
|
|
147
|
+
private findMatches;
|
|
85
148
|
/**
|
|
86
149
|
* Checks if a given text contains profane words.
|
|
87
150
|
*/
|
|
@@ -99,10 +162,14 @@ declare class IraqiBadWordsFilter {
|
|
|
99
162
|
* Dynamically add words to the active dictionary.
|
|
100
163
|
*/
|
|
101
164
|
addWords(newWords: string[]): void;
|
|
165
|
+
/**
|
|
166
|
+
* Add safe stems to this filter, like the safeWords option.
|
|
167
|
+
*/
|
|
168
|
+
addSafeWords(newWords: string[]): void;
|
|
102
169
|
/**
|
|
103
170
|
* Remove words from the active filter dictionary.
|
|
104
171
|
*/
|
|
105
172
|
removeWords(wordsToRemove: string[]): void;
|
|
106
173
|
}
|
|
107
174
|
|
|
108
|
-
export { type InspectOptions, IraqiBadWordsFilter, type IraqiBadWordsFilterOptions, type ProfanityMatch, type WordSource, IraqiBadWordsFilter as default };
|
|
175
|
+
export { type Dialect, type InspectOptions, IraqiBadWordsFilter, type IraqiBadWordsFilterOptions, type MatchRule, type ProfanityMatch, type WordSource, IraqiBadWordsFilter as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A rule a word needed to match its list entry, reported in ProfanityMatch.rules.
|
|
3
|
+
* Only rules that made a difference are reported: عارة matching عارة has none.
|
|
4
|
+
*/
|
|
5
|
+
type MatchRule = "diacritics" | "invisible" | "spelling" | "keyboard" | "arabic-digits" | "stretched" | "glued-digits" | "arabizi" | "gaf-as-qaf" | "gaf-as-kaf" | "prefix" | "suffix" | "mosul";
|
|
1
6
|
interface IraqiBadWordsFilterOptions {
|
|
2
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Extra words to filter, on top of the built-in list. Can be your own JSON
|
|
9
|
+
* word file: `customWords: require('./my-words.json')`.
|
|
10
|
+
*/
|
|
3
11
|
customWords?: string[];
|
|
12
|
+
/**
|
|
13
|
+
* Extra safe stems, on top of the built-in safe-stems.json: clean words that
|
|
14
|
+
* would otherwise match once a prefix or suffix is stripped, like the
|
|
15
|
+
* built-in بعير ("camel" = ب + عير). Their prefixed and suffixed forms are
|
|
16
|
+
* safe too (وبعيرك).
|
|
17
|
+
* Can be your own JSON file: `safeWords: require('./my-safe-words.json')`.
|
|
18
|
+
*/
|
|
19
|
+
safeWords?: string[];
|
|
4
20
|
/**
|
|
5
21
|
* Also filter milder insults (كلب, حمار, قندرة, …). Off by default because
|
|
6
22
|
* many of them are everyday words ("عندي كلب", "جلب لي هدية").
|
|
@@ -12,7 +28,15 @@ interface IraqiBadWordsFilterOptions {
|
|
|
12
28
|
* clean words that happen to end like a bad word plus a suffix.
|
|
13
29
|
*/
|
|
14
30
|
stripSuffixes?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Regional pronunciations to also match. "mosul": ر said as غ, so every entry
|
|
33
|
+
* and safe stem with ر also matches its غ form (عير -> عيغ, بعير -> بعيغ).
|
|
34
|
+
* The ر forms keep matching. Off by default.
|
|
35
|
+
*/
|
|
36
|
+
dialects?: Dialect[];
|
|
15
37
|
}
|
|
38
|
+
/** A regional pronunciation the filter can also match. */
|
|
39
|
+
type Dialect = "mosul";
|
|
16
40
|
/** Which list a dictionary entry came from. */
|
|
17
41
|
type WordSource = "words" | "insults" | "custom";
|
|
18
42
|
interface InspectOptions {
|
|
@@ -33,9 +57,15 @@ interface ProfanityMatch {
|
|
|
33
57
|
start: number;
|
|
34
58
|
/** Offset just past the end of the word in the input. */
|
|
35
59
|
end: number;
|
|
36
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* The word after normalization (diacritics, stretched letters, … removed).
|
|
62
|
+
* For a word with گ, the reading that matched: گ as ق or as ك.
|
|
63
|
+
*/
|
|
37
64
|
normalized: string;
|
|
38
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* The dictionary entry it matched, spelled as in its list. For a Mosul form
|
|
67
|
+
* (rule "mosul"), the listed ر spelling: الشرموطة for الشغموطة.
|
|
68
|
+
*/
|
|
39
69
|
entry: string;
|
|
40
70
|
/** The list the entry came from. */
|
|
41
71
|
source: WordSource;
|
|
@@ -43,8 +73,10 @@ interface ProfanityMatch {
|
|
|
43
73
|
prefix: string;
|
|
44
74
|
/** The suffix stripped before matching (only with stripSuffixes), or "". */
|
|
45
75
|
suffix: string;
|
|
76
|
+
/** The rules the word needed to match the entry, e.g. ["prefix", "arabizi"]. */
|
|
77
|
+
rules: MatchRule[];
|
|
46
78
|
/**
|
|
47
|
-
* The safe stem that cleared this match, spelled as in safe-stems.json.
|
|
79
|
+
* The safe stem that cleared this match, spelled as in safe-stems.json or safeWords.
|
|
48
80
|
* Only set on matches returned with `includeSuppressed`; these are not profane.
|
|
49
81
|
*/
|
|
50
82
|
suppressedBy?: string;
|
|
@@ -53,22 +85,35 @@ declare class IraqiBadWordsFilter {
|
|
|
53
85
|
private words;
|
|
54
86
|
private safeStems;
|
|
55
87
|
private stripSuffixes;
|
|
88
|
+
private mosul;
|
|
56
89
|
constructor(options?: string[] | IraqiBadWordsFilterOptions);
|
|
57
90
|
/**
|
|
58
|
-
* Normalizes
|
|
59
|
-
*
|
|
60
|
-
*
|
|
91
|
+
* Normalizes a word or token for comparison: removes diacritics, tatweel and
|
|
92
|
+
* invisible characters, unifies letter variants (أ/ا, ة/ه, ی/ي, ک/ك, …),
|
|
93
|
+
* reads Arabic-Indic digits, collapses stretched letters (عاااارة -> عارة),
|
|
94
|
+
* ignores digits in Arabic words and folds Arabizi. See NORMALIZE_STEPS.
|
|
95
|
+
*/
|
|
96
|
+
private normalize;
|
|
97
|
+
/**
|
|
98
|
+
* The ways a normalized word can be read. Iraqi Arabic writes ق as گ, but
|
|
99
|
+
* Kurdish typists sometimes use گ for ك, so a word with گ is tried both ways.
|
|
61
100
|
*/
|
|
62
|
-
private
|
|
101
|
+
private readings;
|
|
102
|
+
/** A list word plus its regional forms (عير -> عير, عيغ with the Mosul dialect). */
|
|
103
|
+
private dialectForms;
|
|
104
|
+
/** The key a list word is stored under. A گ in a list entry means ق. */
|
|
105
|
+
private entryKey;
|
|
63
106
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* folded; expects text already passed through normalizeArabic.
|
|
107
|
+
* A word list with surrounding whitespace trimmed and entries that are empty
|
|
108
|
+
* after normalization skipped. A stray "" in a user's file would otherwise
|
|
109
|
+
* match a lone tatweel as a word, or clear every match as a safe stem.
|
|
68
110
|
*/
|
|
69
|
-
private
|
|
70
|
-
/**
|
|
71
|
-
|
|
111
|
+
private cleanList;
|
|
112
|
+
/**
|
|
113
|
+
* Adds safe stems from one list. A safe stem written with گ protects only
|
|
114
|
+
* words spelled with گ (گلب "heart"), so it isn't read as ق or ك.
|
|
115
|
+
*/
|
|
116
|
+
private addSafeStems;
|
|
72
117
|
/**
|
|
73
118
|
* Adds words from one list. A word already present keeps its first source.
|
|
74
119
|
*/
|
|
@@ -80,8 +125,26 @@ declare class IraqiBadWordsFilter {
|
|
|
80
125
|
* A match cleared by a safe stem is returned with `suppressedBy` set.
|
|
81
126
|
*/
|
|
82
127
|
private matchWord;
|
|
83
|
-
/**
|
|
84
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Works out which rules a match needed, by comparing the typed word with the
|
|
130
|
+
* listed spelling (plus any stripped prefix and suffix). A normalization rule
|
|
131
|
+
* counts only if leaving it out would keep them apart: عاااارة vs عارة needs
|
|
132
|
+
* "stretched" but not "spelling", since both have ة.
|
|
133
|
+
*/
|
|
134
|
+
private explain;
|
|
135
|
+
/** The word plus each form of it with an attached prefix removed. */
|
|
136
|
+
private prefixCandidates;
|
|
137
|
+
/** The first safe stem one of the candidates starts with, as listed. */
|
|
138
|
+
private findSafe;
|
|
139
|
+
/** Matches one normalized reading of a word. */
|
|
140
|
+
private matchReading;
|
|
141
|
+
/**
|
|
142
|
+
* Finds every match in the text, including ones cleared by a safe stem.
|
|
143
|
+
* A token with invisible characters is matched whole first (عارة -> عارة);
|
|
144
|
+
* if that finds nothing profane, each visible piece is tried on its own, so an
|
|
145
|
+
* invisible character between two words can't hide a bad word either.
|
|
146
|
+
*/
|
|
147
|
+
private findMatches;
|
|
85
148
|
/**
|
|
86
149
|
* Checks if a given text contains profane words.
|
|
87
150
|
*/
|
|
@@ -99,10 +162,14 @@ declare class IraqiBadWordsFilter {
|
|
|
99
162
|
* Dynamically add words to the active dictionary.
|
|
100
163
|
*/
|
|
101
164
|
addWords(newWords: string[]): void;
|
|
165
|
+
/**
|
|
166
|
+
* Add safe stems to this filter, like the safeWords option.
|
|
167
|
+
*/
|
|
168
|
+
addSafeWords(newWords: string[]): void;
|
|
102
169
|
/**
|
|
103
170
|
* Remove words from the active filter dictionary.
|
|
104
171
|
*/
|
|
105
172
|
removeWords(wordsToRemove: string[]): void;
|
|
106
173
|
}
|
|
107
174
|
|
|
108
|
-
export { type InspectOptions, IraqiBadWordsFilter, type IraqiBadWordsFilterOptions, type ProfanityMatch, type WordSource, IraqiBadWordsFilter as default };
|
|
175
|
+
export { type Dialect, type InspectOptions, IraqiBadWordsFilter, type IraqiBadWordsFilterOptions, type MatchRule, type ProfanityMatch, type WordSource, IraqiBadWordsFilter as default };
|
package/dist/index.js
CHANGED
|
@@ -233,56 +233,134 @@ var insults_default = [
|
|
|
233
233
|
var safe_stems_default = [
|
|
234
234
|
"\u0628\u0639\u064A\u0631",
|
|
235
235
|
"\u0628\u0639\u064A\u063A",
|
|
236
|
+
"\u06AF\u0644\u0628",
|
|
237
|
+
"\u06AF\u0644\u0627\u0628",
|
|
238
|
+
"\u0637\u06AF\u0639\u0629",
|
|
236
239
|
"\u0628\u0643\u0633",
|
|
237
240
|
"\u0648\u0643\u0633",
|
|
238
|
-
"\u0641\u0643\u0633"
|
|
241
|
+
"\u0641\u0643\u0633",
|
|
242
|
+
"\u0628\u062B\u0648\u0631",
|
|
243
|
+
"\u062B\u0648\u0631\u0629",
|
|
244
|
+
"\u062B\u0648\u0631\u064A",
|
|
245
|
+
"\u0628\u0627\u0644\u0639\u062C\u0644"
|
|
239
246
|
];
|
|
240
247
|
|
|
241
248
|
// src/index.ts
|
|
242
|
-
var
|
|
249
|
+
var INVISIBLE = "\\u00AD\\u061C\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF";
|
|
250
|
+
var INVISIBLE_CHARS = new RegExp(`[${INVISIBLE}]`, "g");
|
|
251
|
+
var HAS_INVISIBLE = new RegExp(`[${INVISIBLE}]`);
|
|
252
|
+
var VISIBLE_RUN = new RegExp(`[^${INVISIBLE}]+`, "g");
|
|
253
|
+
var WORD_PATTERN = new RegExp(
|
|
254
|
+
`[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}]+`,
|
|
255
|
+
"g"
|
|
256
|
+
);
|
|
243
257
|
var CONJUNCTIONS = ["", "\u0648", "\u0641"];
|
|
244
258
|
var PREPOSITIONS = ["", "\u0628", "\u0644", "\u0643"];
|
|
245
259
|
var ARTICLES = ["", "\u0627\u0644"];
|
|
246
260
|
var SUFFIXES = ["\u064A", "\u0643", "\u062C", "\u0647", "\u0647\u0627", "\u0646\u0627", "\u0643\u0645", "\u0647\u0645"];
|
|
261
|
+
var HAS_ARABIC = /[\u0600-\uFEFF]/;
|
|
262
|
+
function foldArabizi(word) {
|
|
263
|
+
if (!/^[a-z0-9]+$/.test(word) || !/[a-z]/.test(word)) return word;
|
|
264
|
+
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");
|
|
265
|
+
}
|
|
266
|
+
var NORMALIZE_STEPS = [
|
|
267
|
+
{ rule: "diacritics", apply: (t) => t.replace(/[\u064B-\u065F\u0670\u0640]/g, "") },
|
|
268
|
+
{ rule: "invisible", apply: (t) => t.replace(INVISIBLE_CHARS, "") },
|
|
269
|
+
{ rule: "spelling", apply: (t) => t.replace(/[أإآٱ]/g, "\u0627").replace(/ة/g, "\u0647").replace(/ى/g, "\u064A") },
|
|
270
|
+
{ rule: "keyboard", apply: (t) => t.replace(/[ەہۀ]/g, "\u0647").replace(/ی/g, "\u064A").replace(/[کڪ]/g, "\u0643") },
|
|
271
|
+
{
|
|
272
|
+
rule: "arabic-digits",
|
|
273
|
+
apply: (t) => t.replace(/[٠-٩]/g, (digit) => String(digit.charCodeAt(0) - 1632)).replace(/[۰-۹]/g, (digit) => String(digit.charCodeAt(0) - 1776))
|
|
274
|
+
},
|
|
275
|
+
{ rule: "stretched", apply: (t) => t.replace(/(.)\1+/gu, "$1") },
|
|
276
|
+
{ rule: null, apply: (t) => t.toLowerCase() },
|
|
277
|
+
// Digits glued to an Arabic word (عارة3, عا٣رة) are padding; in Arabizi they're letters
|
|
278
|
+
{
|
|
279
|
+
rule: "glued-digits",
|
|
280
|
+
apply: (t) => HAS_ARABIC.test(t) && /[0-9]/.test(t) ? t.replace(/[0-9]/g, "").replace(/(.)\1+/gu, "$1") : t
|
|
281
|
+
},
|
|
282
|
+
{ rule: "arabizi", apply: (t) => HAS_ARABIC.test(t) ? t : foldArabizi(t) }
|
|
283
|
+
];
|
|
284
|
+
function normalizeSteps(text, skip = -1) {
|
|
285
|
+
return NORMALIZE_STEPS.reduce((t, step, i) => i === skip ? t : step.apply(t), text);
|
|
286
|
+
}
|
|
247
287
|
var IraqiBadWordsFilter = class {
|
|
248
288
|
constructor(options = {}) {
|
|
249
|
-
|
|
289
|
+
// Clean words that would otherwise match after a prefix is stripped
|
|
290
|
+
// (e.g. بعير "camel" = ب + عير). Also covers their prefixed and suffixed
|
|
291
|
+
// forms (وبعيرك). Listed in safe-stems.json, plus the safeWords option.
|
|
292
|
+
this.safeStems = [];
|
|
293
|
+
const {
|
|
294
|
+
customWords = [],
|
|
295
|
+
safeWords = [],
|
|
296
|
+
includeInsults = false,
|
|
297
|
+
stripSuffixes = false,
|
|
298
|
+
dialects = []
|
|
299
|
+
} = Array.isArray(options) ? { customWords: options } : options;
|
|
250
300
|
this.words = /* @__PURE__ */ new Map();
|
|
251
301
|
this.stripSuffixes = stripSuffixes;
|
|
252
|
-
this.
|
|
302
|
+
this.mosul = dialects.includes("mosul");
|
|
303
|
+
this.addSafeStems(safe_stems_default);
|
|
304
|
+
this.addSafeStems(safeWords);
|
|
253
305
|
this.addEntries(words_default, "words");
|
|
254
306
|
if (includeInsults) this.addEntries(insults_default, "insults");
|
|
255
307
|
this.addEntries(customWords, "custom");
|
|
256
308
|
}
|
|
257
309
|
/**
|
|
258
|
-
* Normalizes
|
|
259
|
-
*
|
|
260
|
-
*
|
|
310
|
+
* Normalizes a word or token for comparison: removes diacritics, tatweel and
|
|
311
|
+
* invisible characters, unifies letter variants (أ/ا, ة/ه, ی/ي, ک/ك, …),
|
|
312
|
+
* reads Arabic-Indic digits, collapses stretched letters (عاااارة -> عارة),
|
|
313
|
+
* ignores digits in Arabic words and folds Arabizi. See NORMALIZE_STEPS.
|
|
261
314
|
*/
|
|
262
|
-
|
|
263
|
-
return text
|
|
315
|
+
normalize(text) {
|
|
316
|
+
return normalizeSteps(text);
|
|
264
317
|
}
|
|
265
318
|
/**
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
* teezak / tizak. Only words made purely of Latin letters and digits are
|
|
269
|
-
* folded; expects text already passed through normalizeArabic.
|
|
319
|
+
* The ways a normalized word can be read. Iraqi Arabic writes ق as گ, but
|
|
320
|
+
* Kurdish typists sometimes use گ for ك, so a word with گ is tried both ways.
|
|
270
321
|
*/
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
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");
|
|
322
|
+
readings(word) {
|
|
323
|
+
return word.includes("\u06AF") ? [word.replace(/گ/g, "\u0642"), word.replace(/گ/g, "\u0643")] : [word];
|
|
274
324
|
}
|
|
275
|
-
/**
|
|
276
|
-
|
|
277
|
-
return this.
|
|
325
|
+
/** A list word plus its regional forms (عير -> عير, عيغ with the Mosul dialect). */
|
|
326
|
+
dialectForms(word) {
|
|
327
|
+
return this.mosul && word.includes("\u0631") ? [word, word.replace(/ر/g, "\u063A")] : [word];
|
|
328
|
+
}
|
|
329
|
+
/** The key a list word is stored under. A گ in a list entry means ق. */
|
|
330
|
+
entryKey(word) {
|
|
331
|
+
return this.normalize(word).replace(/گ/g, "\u0642");
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* A word list with surrounding whitespace trimmed and entries that are empty
|
|
335
|
+
* after normalization skipped. A stray "" in a user's file would otherwise
|
|
336
|
+
* match a lone tatweel as a word, or clear every match as a safe stem.
|
|
337
|
+
*/
|
|
338
|
+
cleanList(list) {
|
|
339
|
+
return list.filter((word) => typeof word === "string").map((word) => word.trim()).filter((word) => this.entryKey(word) !== "");
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Adds safe stems from one list. A safe stem written with گ protects only
|
|
343
|
+
* words spelled with گ (گلب "heart"), so it isn't read as ق or ك.
|
|
344
|
+
*/
|
|
345
|
+
addSafeStems(stems) {
|
|
346
|
+
for (const stem of this.cleanList(stems)) {
|
|
347
|
+
for (const word of this.dialectForms(stem)) {
|
|
348
|
+
this.safeStems.push({ normalized: this.normalize(word), word });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
278
351
|
}
|
|
279
352
|
/**
|
|
280
353
|
* Adds words from one list. A word already present keeps its first source.
|
|
281
354
|
*/
|
|
282
|
-
addEntries(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
355
|
+
addEntries(listWords, source) {
|
|
356
|
+
const newWords = this.cleanList(listWords);
|
|
357
|
+
const forms = [
|
|
358
|
+
...newWords.map((word) => ({ word, form: word })),
|
|
359
|
+
...newWords.flatMap((word) => this.dialectForms(word).slice(1).map((form) => ({ word, form })))
|
|
360
|
+
];
|
|
361
|
+
for (const { word, form } of forms) {
|
|
362
|
+
const key = this.entryKey(form);
|
|
363
|
+
if (!this.words.has(key)) this.words.set(key, { word, form, source });
|
|
286
364
|
}
|
|
287
365
|
}
|
|
288
366
|
/**
|
|
@@ -292,11 +370,44 @@ var IraqiBadWordsFilter = class {
|
|
|
292
370
|
* A match cleared by a safe stem is returned with `suppressedBy` set.
|
|
293
371
|
*/
|
|
294
372
|
matchWord(token) {
|
|
295
|
-
const
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
373
|
+
const spelled = this.normalize(token);
|
|
374
|
+
const spelledSafe = spelled.includes("\u06AF") ? this.findSafe(this.prefixCandidates(spelled)) : void 0;
|
|
375
|
+
let suppressed = null;
|
|
376
|
+
for (const reading of this.readings(spelled)) {
|
|
377
|
+
const found = this.matchReading(reading);
|
|
378
|
+
if (!found) continue;
|
|
379
|
+
const { form, ...rest } = found;
|
|
380
|
+
const match = { ...rest, rules: this.explain(token, found, spelled, reading) };
|
|
381
|
+
if (match.suppressedBy === void 0 && spelledSafe) match.suppressedBy = spelledSafe;
|
|
382
|
+
if (match.suppressedBy === void 0) return match;
|
|
383
|
+
suppressed = suppressed ?? match;
|
|
384
|
+
}
|
|
385
|
+
return suppressed;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Works out which rules a match needed, by comparing the typed word with the
|
|
389
|
+
* listed spelling (plus any stripped prefix and suffix). A normalization rule
|
|
390
|
+
* counts only if leaving it out would keep them apart: عاااارة vs عارة needs
|
|
391
|
+
* "stretched" but not "spelling", since both have ة.
|
|
392
|
+
*/
|
|
393
|
+
explain(token, found, spelled, reading) {
|
|
394
|
+
const listed = found.prefix + found.form + found.suffix;
|
|
395
|
+
const gaf = reading === spelled ? "" : reading === spelled.replace(/گ/g, "\u0642") ? "\u0642" : "\u0643";
|
|
396
|
+
const same = (typed, list) => typed.replace(/گ/g, gaf || "\u06AF") === list.replace(/گ/g, "\u0642");
|
|
397
|
+
const rules = [];
|
|
398
|
+
NORMALIZE_STEPS.forEach(({ rule }, i) => {
|
|
399
|
+
if (rule && !same(normalizeSteps(token, i), normalizeSteps(listed, i))) rules.push(rule);
|
|
400
|
+
});
|
|
401
|
+
if (gaf && normalizeSteps(token) !== normalizeSteps(listed)) {
|
|
402
|
+
rules.push(gaf === "\u0642" ? "gaf-as-qaf" : "gaf-as-kaf");
|
|
299
403
|
}
|
|
404
|
+
if (found.prefix) rules.push("prefix");
|
|
405
|
+
if (found.suffix) rules.push("suffix");
|
|
406
|
+
if (found.form !== found.entry) rules.push("mosul");
|
|
407
|
+
return rules;
|
|
408
|
+
}
|
|
409
|
+
/** The word plus each form of it with an attached prefix removed. */
|
|
410
|
+
prefixCandidates(word) {
|
|
300
411
|
const candidates = [{ prefix: "", stem: word, suffix: "" }];
|
|
301
412
|
for (const conjunction of CONJUNCTIONS) {
|
|
302
413
|
for (const preposition of PREPOSITIONS) {
|
|
@@ -308,6 +419,30 @@ var IraqiBadWordsFilter = class {
|
|
|
308
419
|
}
|
|
309
420
|
}
|
|
310
421
|
}
|
|
422
|
+
return candidates;
|
|
423
|
+
}
|
|
424
|
+
/** The first safe stem one of the candidates starts with, as listed. */
|
|
425
|
+
findSafe(candidates) {
|
|
426
|
+
for (const { stem } of candidates) {
|
|
427
|
+
const safe = this.safeStems.find(({ normalized }) => stem.startsWith(normalized));
|
|
428
|
+
if (safe) return safe.word;
|
|
429
|
+
}
|
|
430
|
+
return void 0;
|
|
431
|
+
}
|
|
432
|
+
/** Matches one normalized reading of a word. */
|
|
433
|
+
matchReading(word) {
|
|
434
|
+
const direct = this.words.get(word);
|
|
435
|
+
if (direct) {
|
|
436
|
+
return {
|
|
437
|
+
normalized: word,
|
|
438
|
+
entry: direct.word,
|
|
439
|
+
form: direct.form,
|
|
440
|
+
source: direct.source,
|
|
441
|
+
prefix: "",
|
|
442
|
+
suffix: ""
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
const candidates = this.prefixCandidates(word);
|
|
311
446
|
if (this.stripSuffixes) {
|
|
312
447
|
for (const { prefix, stem } of [...candidates]) {
|
|
313
448
|
for (const suffix of SUFFIXES) {
|
|
@@ -319,34 +454,55 @@ var IraqiBadWordsFilter = class {
|
|
|
319
454
|
}
|
|
320
455
|
const match = candidates.find(({ stem }) => this.words.has(stem));
|
|
321
456
|
if (!match) return null;
|
|
322
|
-
const { word: entry, source } = this.words.get(match.stem);
|
|
457
|
+
const { word: entry, form, source } = this.words.get(match.stem);
|
|
323
458
|
const result = {
|
|
324
459
|
normalized: word,
|
|
325
460
|
entry,
|
|
461
|
+
form,
|
|
326
462
|
source,
|
|
327
463
|
prefix: match.prefix,
|
|
328
464
|
suffix: match.suffix
|
|
329
465
|
};
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (safe) return { ...result, suppressedBy: safe.word };
|
|
333
|
-
}
|
|
334
|
-
return result;
|
|
466
|
+
const safe = this.findSafe(candidates);
|
|
467
|
+
return safe ? { ...result, suppressedBy: safe } : result;
|
|
335
468
|
}
|
|
336
|
-
/**
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
469
|
+
/**
|
|
470
|
+
* Finds every match in the text, including ones cleared by a safe stem.
|
|
471
|
+
* A token with invisible characters is matched whole first (عارة -> عارة);
|
|
472
|
+
* if that finds nothing profane, each visible piece is tried on its own, so an
|
|
473
|
+
* invisible character between two words can't hide a bad word either.
|
|
474
|
+
*/
|
|
475
|
+
*findMatches(text) {
|
|
476
|
+
const pattern = new RegExp(WORD_PATTERN.source, "g");
|
|
477
|
+
let found;
|
|
478
|
+
while (found = pattern.exec(text)) {
|
|
479
|
+
const token = found[0];
|
|
480
|
+
const start = found.index;
|
|
481
|
+
const whole = this.matchWord(token);
|
|
482
|
+
const wholeMatch = whole && { token, start, end: start + token.length, ...whole };
|
|
483
|
+
if (whole && whole.suppressedBy === void 0 || !HAS_INVISIBLE.test(token)) {
|
|
484
|
+
if (wholeMatch) yield wholeMatch;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
const pieces = [];
|
|
488
|
+
const piecePattern = new RegExp(VISIBLE_RUN.source, "g");
|
|
489
|
+
let piece;
|
|
490
|
+
while (piece = piecePattern.exec(token)) {
|
|
491
|
+
const match = this.matchWord(piece[0]);
|
|
492
|
+
const pieceStart = start + piece.index;
|
|
493
|
+
if (match) pieces.push({ token: piece[0], start: pieceStart, end: pieceStart + piece[0].length, ...match });
|
|
494
|
+
}
|
|
495
|
+
if (pieces.length) yield* pieces;
|
|
496
|
+
else if (wholeMatch) yield wholeMatch;
|
|
497
|
+
}
|
|
340
498
|
}
|
|
341
499
|
/**
|
|
342
500
|
* Checks if a given text contains profane words.
|
|
343
501
|
*/
|
|
344
502
|
isProfane(text) {
|
|
345
503
|
if (!text) return false;
|
|
346
|
-
for (const
|
|
347
|
-
if (
|
|
348
|
-
return true;
|
|
349
|
-
}
|
|
504
|
+
for (const match of this.findMatches(text)) {
|
|
505
|
+
if (match.suppressedBy === void 0) return true;
|
|
350
506
|
}
|
|
351
507
|
return false;
|
|
352
508
|
}
|
|
@@ -357,14 +513,8 @@ var IraqiBadWordsFilter = class {
|
|
|
357
513
|
inspect(text, options = {}) {
|
|
358
514
|
if (!text) return [];
|
|
359
515
|
const matches = [];
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
while (found = pattern.exec(text)) {
|
|
363
|
-
const token = found[0];
|
|
364
|
-
const match = this.matchWord(token);
|
|
365
|
-
if (match && (match.suppressedBy === void 0 || options.includeSuppressed)) {
|
|
366
|
-
matches.push({ token, start: found.index, end: found.index + token.length, ...match });
|
|
367
|
-
}
|
|
516
|
+
for (const match of this.findMatches(text)) {
|
|
517
|
+
if (match.suppressedBy === void 0 || options.includeSuppressed) matches.push(match);
|
|
368
518
|
}
|
|
369
519
|
return matches;
|
|
370
520
|
}
|
|
@@ -387,11 +537,19 @@ var IraqiBadWordsFilter = class {
|
|
|
387
537
|
addWords(newWords) {
|
|
388
538
|
this.addEntries(newWords, "custom");
|
|
389
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* Add safe stems to this filter, like the safeWords option.
|
|
542
|
+
*/
|
|
543
|
+
addSafeWords(newWords) {
|
|
544
|
+
this.addSafeStems(newWords);
|
|
545
|
+
}
|
|
390
546
|
/**
|
|
391
547
|
* Remove words from the active filter dictionary.
|
|
392
548
|
*/
|
|
393
549
|
removeWords(wordsToRemove) {
|
|
394
|
-
wordsToRemove.
|
|
550
|
+
for (const word of wordsToRemove.flatMap((w) => this.dialectForms(w))) {
|
|
551
|
+
this.words.delete(this.entryKey(word));
|
|
552
|
+
}
|
|
395
553
|
}
|
|
396
554
|
};
|
|
397
555
|
var index_default = IraqiBadWordsFilter;
|
package/dist/index.mjs
CHANGED
|
@@ -206,56 +206,134 @@ var insults_default = [
|
|
|
206
206
|
var safe_stems_default = [
|
|
207
207
|
"\u0628\u0639\u064A\u0631",
|
|
208
208
|
"\u0628\u0639\u064A\u063A",
|
|
209
|
+
"\u06AF\u0644\u0628",
|
|
210
|
+
"\u06AF\u0644\u0627\u0628",
|
|
211
|
+
"\u0637\u06AF\u0639\u0629",
|
|
209
212
|
"\u0628\u0643\u0633",
|
|
210
213
|
"\u0648\u0643\u0633",
|
|
211
|
-
"\u0641\u0643\u0633"
|
|
214
|
+
"\u0641\u0643\u0633",
|
|
215
|
+
"\u0628\u062B\u0648\u0631",
|
|
216
|
+
"\u062B\u0648\u0631\u0629",
|
|
217
|
+
"\u062B\u0648\u0631\u064A",
|
|
218
|
+
"\u0628\u0627\u0644\u0639\u062C\u0644"
|
|
212
219
|
];
|
|
213
220
|
|
|
214
221
|
// src/index.ts
|
|
215
|
-
var
|
|
222
|
+
var INVISIBLE = "\\u00AD\\u061C\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF";
|
|
223
|
+
var INVISIBLE_CHARS = new RegExp(`[${INVISIBLE}]`, "g");
|
|
224
|
+
var HAS_INVISIBLE = new RegExp(`[${INVISIBLE}]`);
|
|
225
|
+
var VISIBLE_RUN = new RegExp(`[^${INVISIBLE}]+`, "g");
|
|
226
|
+
var WORD_PATTERN = new RegExp(
|
|
227
|
+
`[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}]+`,
|
|
228
|
+
"g"
|
|
229
|
+
);
|
|
216
230
|
var CONJUNCTIONS = ["", "\u0648", "\u0641"];
|
|
217
231
|
var PREPOSITIONS = ["", "\u0628", "\u0644", "\u0643"];
|
|
218
232
|
var ARTICLES = ["", "\u0627\u0644"];
|
|
219
233
|
var SUFFIXES = ["\u064A", "\u0643", "\u062C", "\u0647", "\u0647\u0627", "\u0646\u0627", "\u0643\u0645", "\u0647\u0645"];
|
|
234
|
+
var HAS_ARABIC = /[\u0600-\uFEFF]/;
|
|
235
|
+
function foldArabizi(word) {
|
|
236
|
+
if (!/^[a-z0-9]+$/.test(word) || !/[a-z]/.test(word)) return word;
|
|
237
|
+
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");
|
|
238
|
+
}
|
|
239
|
+
var NORMALIZE_STEPS = [
|
|
240
|
+
{ rule: "diacritics", apply: (t) => t.replace(/[\u064B-\u065F\u0670\u0640]/g, "") },
|
|
241
|
+
{ rule: "invisible", apply: (t) => t.replace(INVISIBLE_CHARS, "") },
|
|
242
|
+
{ rule: "spelling", apply: (t) => t.replace(/[أإآٱ]/g, "\u0627").replace(/ة/g, "\u0647").replace(/ى/g, "\u064A") },
|
|
243
|
+
{ rule: "keyboard", apply: (t) => t.replace(/[ەہۀ]/g, "\u0647").replace(/ی/g, "\u064A").replace(/[کڪ]/g, "\u0643") },
|
|
244
|
+
{
|
|
245
|
+
rule: "arabic-digits",
|
|
246
|
+
apply: (t) => t.replace(/[٠-٩]/g, (digit) => String(digit.charCodeAt(0) - 1632)).replace(/[۰-۹]/g, (digit) => String(digit.charCodeAt(0) - 1776))
|
|
247
|
+
},
|
|
248
|
+
{ rule: "stretched", apply: (t) => t.replace(/(.)\1+/gu, "$1") },
|
|
249
|
+
{ rule: null, apply: (t) => t.toLowerCase() },
|
|
250
|
+
// Digits glued to an Arabic word (عارة3, عا٣رة) are padding; in Arabizi they're letters
|
|
251
|
+
{
|
|
252
|
+
rule: "glued-digits",
|
|
253
|
+
apply: (t) => HAS_ARABIC.test(t) && /[0-9]/.test(t) ? t.replace(/[0-9]/g, "").replace(/(.)\1+/gu, "$1") : t
|
|
254
|
+
},
|
|
255
|
+
{ rule: "arabizi", apply: (t) => HAS_ARABIC.test(t) ? t : foldArabizi(t) }
|
|
256
|
+
];
|
|
257
|
+
function normalizeSteps(text, skip = -1) {
|
|
258
|
+
return NORMALIZE_STEPS.reduce((t, step, i) => i === skip ? t : step.apply(t), text);
|
|
259
|
+
}
|
|
220
260
|
var IraqiBadWordsFilter = class {
|
|
221
261
|
constructor(options = {}) {
|
|
222
|
-
|
|
262
|
+
// Clean words that would otherwise match after a prefix is stripped
|
|
263
|
+
// (e.g. بعير "camel" = ب + عير). Also covers their prefixed and suffixed
|
|
264
|
+
// forms (وبعيرك). Listed in safe-stems.json, plus the safeWords option.
|
|
265
|
+
this.safeStems = [];
|
|
266
|
+
const {
|
|
267
|
+
customWords = [],
|
|
268
|
+
safeWords = [],
|
|
269
|
+
includeInsults = false,
|
|
270
|
+
stripSuffixes = false,
|
|
271
|
+
dialects = []
|
|
272
|
+
} = Array.isArray(options) ? { customWords: options } : options;
|
|
223
273
|
this.words = /* @__PURE__ */ new Map();
|
|
224
274
|
this.stripSuffixes = stripSuffixes;
|
|
225
|
-
this.
|
|
275
|
+
this.mosul = dialects.includes("mosul");
|
|
276
|
+
this.addSafeStems(safe_stems_default);
|
|
277
|
+
this.addSafeStems(safeWords);
|
|
226
278
|
this.addEntries(words_default, "words");
|
|
227
279
|
if (includeInsults) this.addEntries(insults_default, "insults");
|
|
228
280
|
this.addEntries(customWords, "custom");
|
|
229
281
|
}
|
|
230
282
|
/**
|
|
231
|
-
* Normalizes
|
|
232
|
-
*
|
|
233
|
-
*
|
|
283
|
+
* Normalizes a word or token for comparison: removes diacritics, tatweel and
|
|
284
|
+
* invisible characters, unifies letter variants (أ/ا, ة/ه, ی/ي, ک/ك, …),
|
|
285
|
+
* reads Arabic-Indic digits, collapses stretched letters (عاااارة -> عارة),
|
|
286
|
+
* ignores digits in Arabic words and folds Arabizi. See NORMALIZE_STEPS.
|
|
234
287
|
*/
|
|
235
|
-
|
|
236
|
-
return text
|
|
288
|
+
normalize(text) {
|
|
289
|
+
return normalizeSteps(text);
|
|
237
290
|
}
|
|
238
291
|
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* teezak / tizak. Only words made purely of Latin letters and digits are
|
|
242
|
-
* folded; expects text already passed through normalizeArabic.
|
|
292
|
+
* The ways a normalized word can be read. Iraqi Arabic writes ق as گ, but
|
|
293
|
+
* Kurdish typists sometimes use گ for ك, so a word with گ is tried both ways.
|
|
243
294
|
*/
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
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");
|
|
295
|
+
readings(word) {
|
|
296
|
+
return word.includes("\u06AF") ? [word.replace(/گ/g, "\u0642"), word.replace(/گ/g, "\u0643")] : [word];
|
|
247
297
|
}
|
|
248
|
-
/**
|
|
249
|
-
|
|
250
|
-
return this.
|
|
298
|
+
/** A list word plus its regional forms (عير -> عير, عيغ with the Mosul dialect). */
|
|
299
|
+
dialectForms(word) {
|
|
300
|
+
return this.mosul && word.includes("\u0631") ? [word, word.replace(/ر/g, "\u063A")] : [word];
|
|
301
|
+
}
|
|
302
|
+
/** The key a list word is stored under. A گ in a list entry means ق. */
|
|
303
|
+
entryKey(word) {
|
|
304
|
+
return this.normalize(word).replace(/گ/g, "\u0642");
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* A word list with surrounding whitespace trimmed and entries that are empty
|
|
308
|
+
* after normalization skipped. A stray "" in a user's file would otherwise
|
|
309
|
+
* match a lone tatweel as a word, or clear every match as a safe stem.
|
|
310
|
+
*/
|
|
311
|
+
cleanList(list) {
|
|
312
|
+
return list.filter((word) => typeof word === "string").map((word) => word.trim()).filter((word) => this.entryKey(word) !== "");
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Adds safe stems from one list. A safe stem written with گ protects only
|
|
316
|
+
* words spelled with گ (گلب "heart"), so it isn't read as ق or ك.
|
|
317
|
+
*/
|
|
318
|
+
addSafeStems(stems) {
|
|
319
|
+
for (const stem of this.cleanList(stems)) {
|
|
320
|
+
for (const word of this.dialectForms(stem)) {
|
|
321
|
+
this.safeStems.push({ normalized: this.normalize(word), word });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
251
324
|
}
|
|
252
325
|
/**
|
|
253
326
|
* Adds words from one list. A word already present keeps its first source.
|
|
254
327
|
*/
|
|
255
|
-
addEntries(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
328
|
+
addEntries(listWords, source) {
|
|
329
|
+
const newWords = this.cleanList(listWords);
|
|
330
|
+
const forms = [
|
|
331
|
+
...newWords.map((word) => ({ word, form: word })),
|
|
332
|
+
...newWords.flatMap((word) => this.dialectForms(word).slice(1).map((form) => ({ word, form })))
|
|
333
|
+
];
|
|
334
|
+
for (const { word, form } of forms) {
|
|
335
|
+
const key = this.entryKey(form);
|
|
336
|
+
if (!this.words.has(key)) this.words.set(key, { word, form, source });
|
|
259
337
|
}
|
|
260
338
|
}
|
|
261
339
|
/**
|
|
@@ -265,11 +343,44 @@ var IraqiBadWordsFilter = class {
|
|
|
265
343
|
* A match cleared by a safe stem is returned with `suppressedBy` set.
|
|
266
344
|
*/
|
|
267
345
|
matchWord(token) {
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
346
|
+
const spelled = this.normalize(token);
|
|
347
|
+
const spelledSafe = spelled.includes("\u06AF") ? this.findSafe(this.prefixCandidates(spelled)) : void 0;
|
|
348
|
+
let suppressed = null;
|
|
349
|
+
for (const reading of this.readings(spelled)) {
|
|
350
|
+
const found = this.matchReading(reading);
|
|
351
|
+
if (!found) continue;
|
|
352
|
+
const { form, ...rest } = found;
|
|
353
|
+
const match = { ...rest, rules: this.explain(token, found, spelled, reading) };
|
|
354
|
+
if (match.suppressedBy === void 0 && spelledSafe) match.suppressedBy = spelledSafe;
|
|
355
|
+
if (match.suppressedBy === void 0) return match;
|
|
356
|
+
suppressed = suppressed ?? match;
|
|
357
|
+
}
|
|
358
|
+
return suppressed;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Works out which rules a match needed, by comparing the typed word with the
|
|
362
|
+
* listed spelling (plus any stripped prefix and suffix). A normalization rule
|
|
363
|
+
* counts only if leaving it out would keep them apart: عاااارة vs عارة needs
|
|
364
|
+
* "stretched" but not "spelling", since both have ة.
|
|
365
|
+
*/
|
|
366
|
+
explain(token, found, spelled, reading) {
|
|
367
|
+
const listed = found.prefix + found.form + found.suffix;
|
|
368
|
+
const gaf = reading === spelled ? "" : reading === spelled.replace(/گ/g, "\u0642") ? "\u0642" : "\u0643";
|
|
369
|
+
const same = (typed, list) => typed.replace(/گ/g, gaf || "\u06AF") === list.replace(/گ/g, "\u0642");
|
|
370
|
+
const rules = [];
|
|
371
|
+
NORMALIZE_STEPS.forEach(({ rule }, i) => {
|
|
372
|
+
if (rule && !same(normalizeSteps(token, i), normalizeSteps(listed, i))) rules.push(rule);
|
|
373
|
+
});
|
|
374
|
+
if (gaf && normalizeSteps(token) !== normalizeSteps(listed)) {
|
|
375
|
+
rules.push(gaf === "\u0642" ? "gaf-as-qaf" : "gaf-as-kaf");
|
|
272
376
|
}
|
|
377
|
+
if (found.prefix) rules.push("prefix");
|
|
378
|
+
if (found.suffix) rules.push("suffix");
|
|
379
|
+
if (found.form !== found.entry) rules.push("mosul");
|
|
380
|
+
return rules;
|
|
381
|
+
}
|
|
382
|
+
/** The word plus each form of it with an attached prefix removed. */
|
|
383
|
+
prefixCandidates(word) {
|
|
273
384
|
const candidates = [{ prefix: "", stem: word, suffix: "" }];
|
|
274
385
|
for (const conjunction of CONJUNCTIONS) {
|
|
275
386
|
for (const preposition of PREPOSITIONS) {
|
|
@@ -281,6 +392,30 @@ var IraqiBadWordsFilter = class {
|
|
|
281
392
|
}
|
|
282
393
|
}
|
|
283
394
|
}
|
|
395
|
+
return candidates;
|
|
396
|
+
}
|
|
397
|
+
/** The first safe stem one of the candidates starts with, as listed. */
|
|
398
|
+
findSafe(candidates) {
|
|
399
|
+
for (const { stem } of candidates) {
|
|
400
|
+
const safe = this.safeStems.find(({ normalized }) => stem.startsWith(normalized));
|
|
401
|
+
if (safe) return safe.word;
|
|
402
|
+
}
|
|
403
|
+
return void 0;
|
|
404
|
+
}
|
|
405
|
+
/** Matches one normalized reading of a word. */
|
|
406
|
+
matchReading(word) {
|
|
407
|
+
const direct = this.words.get(word);
|
|
408
|
+
if (direct) {
|
|
409
|
+
return {
|
|
410
|
+
normalized: word,
|
|
411
|
+
entry: direct.word,
|
|
412
|
+
form: direct.form,
|
|
413
|
+
source: direct.source,
|
|
414
|
+
prefix: "",
|
|
415
|
+
suffix: ""
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
const candidates = this.prefixCandidates(word);
|
|
284
419
|
if (this.stripSuffixes) {
|
|
285
420
|
for (const { prefix, stem } of [...candidates]) {
|
|
286
421
|
for (const suffix of SUFFIXES) {
|
|
@@ -292,34 +427,55 @@ var IraqiBadWordsFilter = class {
|
|
|
292
427
|
}
|
|
293
428
|
const match = candidates.find(({ stem }) => this.words.has(stem));
|
|
294
429
|
if (!match) return null;
|
|
295
|
-
const { word: entry, source } = this.words.get(match.stem);
|
|
430
|
+
const { word: entry, form, source } = this.words.get(match.stem);
|
|
296
431
|
const result = {
|
|
297
432
|
normalized: word,
|
|
298
433
|
entry,
|
|
434
|
+
form,
|
|
299
435
|
source,
|
|
300
436
|
prefix: match.prefix,
|
|
301
437
|
suffix: match.suffix
|
|
302
438
|
};
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (safe) return { ...result, suppressedBy: safe.word };
|
|
306
|
-
}
|
|
307
|
-
return result;
|
|
439
|
+
const safe = this.findSafe(candidates);
|
|
440
|
+
return safe ? { ...result, suppressedBy: safe } : result;
|
|
308
441
|
}
|
|
309
|
-
/**
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
442
|
+
/**
|
|
443
|
+
* Finds every match in the text, including ones cleared by a safe stem.
|
|
444
|
+
* A token with invisible characters is matched whole first (عارة -> عارة);
|
|
445
|
+
* if that finds nothing profane, each visible piece is tried on its own, so an
|
|
446
|
+
* invisible character between two words can't hide a bad word either.
|
|
447
|
+
*/
|
|
448
|
+
*findMatches(text) {
|
|
449
|
+
const pattern = new RegExp(WORD_PATTERN.source, "g");
|
|
450
|
+
let found;
|
|
451
|
+
while (found = pattern.exec(text)) {
|
|
452
|
+
const token = found[0];
|
|
453
|
+
const start = found.index;
|
|
454
|
+
const whole = this.matchWord(token);
|
|
455
|
+
const wholeMatch = whole && { token, start, end: start + token.length, ...whole };
|
|
456
|
+
if (whole && whole.suppressedBy === void 0 || !HAS_INVISIBLE.test(token)) {
|
|
457
|
+
if (wholeMatch) yield wholeMatch;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
const pieces = [];
|
|
461
|
+
const piecePattern = new RegExp(VISIBLE_RUN.source, "g");
|
|
462
|
+
let piece;
|
|
463
|
+
while (piece = piecePattern.exec(token)) {
|
|
464
|
+
const match = this.matchWord(piece[0]);
|
|
465
|
+
const pieceStart = start + piece.index;
|
|
466
|
+
if (match) pieces.push({ token: piece[0], start: pieceStart, end: pieceStart + piece[0].length, ...match });
|
|
467
|
+
}
|
|
468
|
+
if (pieces.length) yield* pieces;
|
|
469
|
+
else if (wholeMatch) yield wholeMatch;
|
|
470
|
+
}
|
|
313
471
|
}
|
|
314
472
|
/**
|
|
315
473
|
* Checks if a given text contains profane words.
|
|
316
474
|
*/
|
|
317
475
|
isProfane(text) {
|
|
318
476
|
if (!text) return false;
|
|
319
|
-
for (const
|
|
320
|
-
if (
|
|
321
|
-
return true;
|
|
322
|
-
}
|
|
477
|
+
for (const match of this.findMatches(text)) {
|
|
478
|
+
if (match.suppressedBy === void 0) return true;
|
|
323
479
|
}
|
|
324
480
|
return false;
|
|
325
481
|
}
|
|
@@ -330,14 +486,8 @@ var IraqiBadWordsFilter = class {
|
|
|
330
486
|
inspect(text, options = {}) {
|
|
331
487
|
if (!text) return [];
|
|
332
488
|
const matches = [];
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
while (found = pattern.exec(text)) {
|
|
336
|
-
const token = found[0];
|
|
337
|
-
const match = this.matchWord(token);
|
|
338
|
-
if (match && (match.suppressedBy === void 0 || options.includeSuppressed)) {
|
|
339
|
-
matches.push({ token, start: found.index, end: found.index + token.length, ...match });
|
|
340
|
-
}
|
|
489
|
+
for (const match of this.findMatches(text)) {
|
|
490
|
+
if (match.suppressedBy === void 0 || options.includeSuppressed) matches.push(match);
|
|
341
491
|
}
|
|
342
492
|
return matches;
|
|
343
493
|
}
|
|
@@ -360,11 +510,19 @@ var IraqiBadWordsFilter = class {
|
|
|
360
510
|
addWords(newWords) {
|
|
361
511
|
this.addEntries(newWords, "custom");
|
|
362
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* Add safe stems to this filter, like the safeWords option.
|
|
515
|
+
*/
|
|
516
|
+
addSafeWords(newWords) {
|
|
517
|
+
this.addSafeStems(newWords);
|
|
518
|
+
}
|
|
363
519
|
/**
|
|
364
520
|
* Remove words from the active filter dictionary.
|
|
365
521
|
*/
|
|
366
522
|
removeWords(wordsToRemove) {
|
|
367
|
-
wordsToRemove.
|
|
523
|
+
for (const word of wordsToRemove.flatMap((w) => this.dialectForms(w))) {
|
|
524
|
+
this.words.delete(this.entryKey(word));
|
|
525
|
+
}
|
|
368
526
|
}
|
|
369
527
|
};
|
|
370
528
|
var index_default = IraqiBadWordsFilter;
|
package/package.json
CHANGED