iraqi-dialect-bad-words 1.2.1 → 1.4.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 CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  A profanity filter for the Iraqi Arabic dialect. Detects and masks bad words in user-generated text.
8
8
 
9
- Works in **React Native / Expo**, **React** (Vite, Next.js, CRA, …) and **Node.js**, with TypeScript types included. No dependencies.
9
+ Works in **React Native / Expo**, **React** (Vite, Next.js, CRA, …), **Vue** (Vite, Nuxt) and **Node.js**, with TypeScript types included. No dependencies.
10
10
 
11
11
  ## Install
12
12
 
@@ -55,6 +55,37 @@ export function CommentInput() {
55
55
  }
56
56
  ```
57
57
 
58
+ ### Vue example
59
+
60
+ Create the filter once in its own module and import it where you need it. Keep it out of `ref()` and `reactive()`: the filter never changes, and wrapping it in reactive proxies only slows every check down.
61
+
62
+ ```ts
63
+ // src/profanity.ts
64
+ import IraqiBadWordsFilter from 'iraqi-dialect-bad-words';
65
+
66
+ export const filter = new IraqiBadWordsFilter();
67
+ ```
68
+
69
+ ```vue
70
+ <script setup lang="ts">
71
+ import { ref, computed } from 'vue';
72
+ import { filter } from '@/profanity';
73
+
74
+ const text = ref('');
75
+ const isProfane = computed(() => filter.isProfane(text.value));
76
+ </script>
77
+
78
+ <template>
79
+ <textarea v-model="text" dir="auto" />
80
+ <p v-if="isProfane">Please keep it respectful.</p>
81
+ <button :disabled="isProfane">Send</button>
82
+ </template>
83
+ ```
84
+
85
+ To highlight bad words, render the text in pieces split at the `start` and `end` of each match from [`inspect()`](#inspecttext-string-options-profanitymatch) rather than with `v-html`, so user text can't inject HTML.
86
+
87
+ In any framework, check the text on your server too (a Nuxt server route or any Node backend can use the same package): a check in the browser can be skipped by calling your API directly.
88
+
58
89
  ## API
59
90
 
60
91
  ### `new IraqiBadWordsFilter(options?)`
@@ -64,16 +95,48 @@ Creates a filter with the built-in word list.
64
95
  ```ts
65
96
  new IraqiBadWordsFilter(); // profanity only
66
97
  new IraqiBadWordsFilter({ includeInsults: true }); // also milder insults
98
+ new IraqiBadWordsFilter({ includePhrases: true }); // also vulgar phrases
67
99
  new IraqiBadWordsFilter({ stripSuffixes: true }); // also unlisted suffix forms
100
+ new IraqiBadWordsFilter({ dialects: ['mosul'] }); // also Mosul ر → غ forms
68
101
  new IraqiBadWordsFilter({ customWords: ['word1', 'word2'] });
102
+ new IraqiBadWordsFilter({ safeWords: ['word3'] }); // clean words not to flag
69
103
  new IraqiBadWordsFilter(['word1', 'word2']); // same as customWords
70
104
  ```
71
105
 
72
106
  | Option | Default | Description |
73
107
  |---|---|---|
74
- | `customWords` | `[]` | Extra words to filter. |
108
+ | `customWords` | `[]` | Extra words to filter, on top of the built-in list. Can come from your own JSON file (see below). |
109
+ | `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
110
  | `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"). |
111
+ | `includePhrases` | `false` | Also filter the phrases in [`phrases.json`](src/phrases.json): clean words that are vulgar together, like `تعال العب بيه`. Off by default because the same words can be meant literally ("come play with it"). |
112
+ | `customPhrases` | `[]` | Extra phrases to filter, whether or not `includePhrases` is on. Can come from your own JSON file. |
76
113
  | `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. |
114
+ | `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. |
115
+
116
+ #### Using your own word files
117
+
118
+ 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).
119
+
120
+ ```json
121
+ ["زفت", "نذل"]
122
+ ```
123
+
124
+ Import them and pass them as `customWords` and `safeWords`. The built-in lists stay on, and your words are added to them:
125
+
126
+ ```ts
127
+ import IraqiBadWordsFilter from 'iraqi-dialect-bad-words';
128
+ import myWords from './my-words.json'; // TypeScript needs "resolveJsonModule": true
129
+ import mySafeWords from './my-safe-words.json';
130
+ // CommonJS: const myWords = require('./my-words.json');
131
+
132
+ const filter = new IraqiBadWordsFilter({ customWords: myWords, safeWords: mySafeWords });
133
+ ```
134
+
135
+ 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.
136
+
137
+ 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()`.
138
+
139
+ 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
140
 
78
141
  ### `isProfane(text: string): boolean`
79
142
 
@@ -89,38 +152,80 @@ Lists each bad word in the text and explains why it matched. Use it to highlight
89
152
 
90
153
  ```ts
91
154
  filter.inspect('روح للكواد!');
92
- // [{ token: 'للكواد', start: 4, end: 10, normalized: 'لكواد',
93
- // entry: 'كواد', source: 'words', prefix: 'ل' }]
155
+ // [{ token: 'للكواد', start: 4, end: 10, normalized: 'لكواد', entry: 'كواد',
156
+ // source: 'words', prefix: 'ل', suffix: '', rules: ['stretched', 'prefix'] }]
94
157
  ```
95
158
 
96
159
  | Field | Description |
97
160
  |---|---|
98
161
  | `token` | The word exactly as written. |
99
162
  | `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. |
163
+ | `normalized` | The word after normalization (Arabizi is folded too, so `Qa7ba` shows as `gahba`). For a word with گ, the reading that matched (ق or ك). |
164
+ | `entry` | The list entry it matched, spelled as in the list. For a Mosul form, the listed ر spelling. |
165
+ | `source` | `'words'`, `'insults'`, `'phrases'` or `'custom'`. A word on more than one list keeps the first: built-in, then insults, then custom. |
103
166
  | `prefix` | The attached prefix stripped before matching (after normalization, so `لل` shows as `ل`), or `''` for a direct match. |
104
167
  | `suffix` | The suffix stripped before matching (only with `stripSuffixes`), or `''`. |
168
+ | `rules` | The rules the word needed to match the entry (see below). Empty when it's spelled exactly like the entry. |
105
169
  | `suppressedBy` | Only with `includeSuppressed`: the safe stem that cleared this match. |
106
170
 
107
171
  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
172
 
109
173
  ```ts
110
174
  filter.inspect('وبعيرك', { includeSuppressed: true });
111
- // [{ token: 'وبعيرك', …, entry: 'عيرك', prefix: 'وب', suppressedBy: 'بعير' }]
175
+ // [{ token: 'وبعيرك', …, entry: 'عيرك', prefix: 'وب', rules: ['prefix'], suppressedBy: 'بعير' }]
112
176
  ```
113
177
 
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.
178
+ `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:
179
+
180
+ | Rule | Example |
181
+ |---|---|
182
+ | `unicode-forms` | `ﻋﻴﺮ`, `gahba`, `𝐠𝐚𝐡𝐛𝐚` (presentation forms, full-width and mathematical letters) |
183
+ | `diacritics` | `عَارَة`, `عـــارة` |
184
+ | `invisible` | `عا‌رة` (zero-width character inside) |
185
+ | `spelling` | `عاره` (ة/ه, ى/ي, أ/ا) |
186
+ | `keyboard` | `عیر`, `کس` (Persian/Kurdish letters) |
187
+ | `lookalike` | `gаhba` (Cyrillic `а` in a Latin word) |
188
+ | `arabic-digits` | `ga٧ba` |
189
+ | `stretched` | `عاااارة`, `للكواد` |
190
+ | `glued-digits` | `عارة3` |
191
+ | `arabizi` | `qa7ba` → `gahba` |
192
+ | `gaf-as-qaf` / `gaf-as-kaf` | `گحبة` → قحبة, `گسها` → كسها |
193
+ | `prefix` / `suffix` | `بطيزك`, `ياعارة`, `طيزهم` (with `stripSuffixes`) |
194
+ | `mosul` | `شغموطة` → شرموطة (with `dialects: ['mosul']`) |
195
+ | `phrase` | `تعال العب بيه` (with `includePhrases`) |
196
+ | `split` | `ع*ي*ر`, `ع ي ر` (pieces joined again) |
197
+ | `symbols` | `g@hba`, `$harmoota` (@ as a, $ as s) |
198
+
199
+ 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
200
 
116
201
  ### `addWords(words: string[]): void`
117
202
 
118
203
  Adds words to this filter.
119
204
 
205
+ ### `addSafeWords(words: string[]): void`
206
+
207
+ Adds safe stems to this filter, like the `safeWords` option.
208
+
120
209
  ### `removeWords(words: string[]): void`
121
210
 
122
211
  Removes words from this filter, for example a built-in word you don't want flagged in your app.
123
212
 
213
+ ### `addPhrases(phrases: string[]): void` / `removePhrases(phrases: string[]): void`
214
+
215
+ Adds phrases to this filter, like the `customPhrases` option, or removes them, including built-in ones.
216
+
217
+ A phrase is its words separated by spaces. A word can list alternatives separated by `|`, so one entry covers every combination:
218
+
219
+ ```json
220
+ [
221
+ "قوم|كوم بيه|بينو"
222
+ ]
223
+ ```
224
+
225
+ matches `قوم بيه`, `قوم بينو`, `كوم بيه` and `كوم بينو`. A phrase matches when its words appear one after another with only spaces or punctuation between them (`تعال، العب بيه!`). Each word is normalized like any other, so `گوم` matches the `قوم` alternative without listing it. The first word can carry an attached prefix (`وتعال العب بيه`) or the Iraqi progressive `د` (`دقوم بيه`). Other forms aren't stripped or guessed, so list each one you want as an alternative.
226
+
227
+ The whole phrase is reported as one match with the rule `phrase`, its `entry` being the line as listed, and `clean()` masks all of it. `removePhrases()` takes a line as listed; the order of its alternatives doesn't matter.
228
+
124
229
  ## What it catches
125
230
 
126
231
  Matching is done per whole word, after normalizing both the text and the word list, so these all match the same entry:
@@ -132,19 +237,28 @@ Matching is done per whole word, after normalizing both the text and the word li
132
237
  | Tatweel | `عـــارة` |
133
238
  | ة / ه, ى / ي, أ / إ / آ / ا | `عاره` |
134
239
  | Persian/Kurdish keyboard letters ی ک ە | `عیر`, `کس` |
240
+ | گ read as ق (Iraqi) and as ك (Kurdish typists) | `گحبة` = `قحبة`, `گس` = `كس` |
135
241
  | Invisible characters hidden inside a word (zero-width, direction marks, soft hyphen) | `عا‌رة` |
136
- | Attached prefixes و ف ب ل ك ال | `والعارة`, `للعارة` |
242
+ | Letters split by symbols, or by spaces between single letters | `ع*ي*ر`, `ع.ي.ر`, `ع ي ر`, `عي * ر`, `شرم.وطة`, `g-a-h-b-a` |
243
+ | Arabic presentation forms, full-width and mathematical letters | `ﻋﻴﺮ`, `gahba`, `𝐠𝐚𝐡𝐛𝐚` |
244
+ | Greek or Cyrillic look-alike letters in a Latin word | `gаhba` (Cyrillic `а`) |
245
+ | Attached prefixes و ف ب ل ك ال, and يا / ويا | `والعارة`, `للعارة`, `ياعارة`, `يالعارة`, `وياعارة` |
137
246
  | Pronoun suffixes (with `stripSuffixes`) | `طيزهم`, `وطيزكم` |
138
247
  | Punctuation next to the word | `عارة!` `عارة،` |
139
248
  | 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` |
249
+ | Arabizi (Latin letters and numbers, Western or Arabic-Indic), including spelling variants | `ga7ba` = `ga٧ba` = `qahba` = `gahba`, `kosomak` = `kusumak`, `3ayri` = `ayri`, `6eezak` = `teezak` (6 = ط), `9awad` = `8awad` = `gawad` (9, 8 = ق) |
250
+ | @ and $ standing in for letters in Arabizi | `g@hba`, `$harmoota` |
141
251
 
142
252
  Because matching is whole-word, a clean word that merely contains a bad word is not flagged.
143
253
 
254
+ Split words are found by joining neighbouring pieces when the gap between them is short, on one line, and either has no spaces (`عي*ر`), sits between single letters (`ع ي ر`), or has a symbol between pieces of one or two letters (`عي * ر`). Normal sentences like `انا و انت` or `د. علي` stay apart, and the joined word must match an entry exactly. A split safe stem stays safe: `ب ع ي ر` is بعير (camel).
255
+
144
256
  ## Known limitations
145
257
 
146
- - Phrases (multiple words) can't be list entries; each entry is a single word.
258
+ - Phrases match only as listed, word for word: another word in between (`تعال هنا العب بيه`) or an unlisted form (`بيهم`) isn't matched.
147
259
  - Words glued together without a space (e.g. the same word typed twice) aren't detected.
260
+ - A letter replaced by a symbol (`ع*ر`, `ك*بة`) isn't detected: the symbol could stand for any letter, and `ع*ر` could just as well be عمر.
261
+ - Longer pieces split with spaces around the symbol (`شرم * وطة`) aren't joined, so normal sentences aren't either.
148
262
  - No word list is complete. Use this as one layer of moderation, not the only one.
149
263
 
150
264
  ## Contributing
@@ -154,10 +268,11 @@ The word list is the heart of this package, and native speakers are the best jud
154
268
  - **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.
155
269
  - **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).
156
270
  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).
271
+ A safe stem written with گ protects only words spelled with گ, so `گلب` ("heart", Iraqi spelling of قلب) can be safe while `كلب` is not.
157
272
 
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.
273
+ - **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`, `includePhrases` or `stripSuffixes`). Every sentence there runs as a test, so later word-list changes can't silently flip it.
159
274
 
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`.
275
+ 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`. Phrases of clean words that are vulgar together belong in [`src/phrases.json`](src/phrases.json), also opt-in.
161
276
 
162
277
  ### Development
163
278
 
@@ -166,8 +281,11 @@ npm install
166
281
  npm test # run tests
167
282
  npm run typecheck # type-check
168
283
  npm run build # build dist/
284
+ npm run check -- article.txt --phrases # list every match in a text file
169
285
  ```
170
286
 
287
+ `npm run check` runs the filter over a text file (or stdin, e.g. `pbpaste | npm run check`) and prints each match with its line and column, the entry it matched, its source and rules, and the text around it. Flags turn on options: `--insults`, `--phrases`, `--suffixes`, `--mosul`, and `--clean` to also print the masked text. Running it on articles you know are clean is the quickest way to find false positives: each match is either a missing safe stem or a word that belongs in an opt-in list.
288
+
171
289
  ## License
172
290
 
173
291
  [MIT](LICENSE)
package/dist/index.d.mts CHANGED
@@ -1,20 +1,55 @@
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 = "unicode-forms" | "diacritics" | "invisible" | "spelling" | "keyboard" | "lookalike" | "arabic-digits" | "stretched" | "glued-digits" | "arabizi" | "gaf-as-qaf" | "gaf-as-kaf" | "prefix" | "suffix" | "mosul" | "split" | "symbols" | "phrase";
1
6
  interface IraqiBadWordsFilterOptions {
2
- /** Extra words to filter, on top of the built-in list. */
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 ("عندي كلب", "جلب لي هدية").
7
23
  */
8
24
  includeInsults?: boolean;
25
+ /**
26
+ * Also filter phrases (phrases.json) whose words are clean on their own but
27
+ * vulgar together (تعال العب بيه). Off by default because the same words can
28
+ * be meant literally ("come play with it").
29
+ */
30
+ includePhrases?: boolean;
31
+ /**
32
+ * Extra phrases to filter, whatever includePhrases is. Can be your own JSON
33
+ * file: `customPhrases: require('./my-phrases.json')`.
34
+ */
35
+ customPhrases?: string[];
9
36
  /**
10
37
  * Also match words with a pronoun suffix the list doesn't spell out
11
38
  * (طيزهم -> طيز). Off by default: it widens matching, so it can flag
12
39
  * clean words that happen to end like a bad word plus a suffix.
13
40
  */
14
41
  stripSuffixes?: boolean;
42
+ /**
43
+ * Regional pronunciations to also match. "mosul": ر said as غ, so every entry
44
+ * and safe stem with ر also matches its غ form (عير -> عيغ, بعير -> بعيغ).
45
+ * The ر forms keep matching. Off by default.
46
+ */
47
+ dialects?: Dialect[];
15
48
  }
49
+ /** A regional pronunciation the filter can also match. */
50
+ type Dialect = "mosul";
16
51
  /** Which list a dictionary entry came from. */
17
- type WordSource = "words" | "insults" | "custom";
52
+ type WordSource = "words" | "insults" | "phrases" | "custom";
18
53
  interface InspectOptions {
19
54
  /**
20
55
  * Also return words that matched but were cleared by a safe stem
@@ -24,7 +59,7 @@ interface InspectOptions {
24
59
  }
25
60
  /** A bad word found by inspect(). */
26
61
  interface ProfanityMatch {
27
- /** The word exactly as written in the input. */
62
+ /** The word (or phrase) exactly as written in the input. */
28
63
  token: string;
29
64
  /**
30
65
  * Offset of the word in the input; text.slice(start, end) === token.
@@ -33,9 +68,15 @@ interface ProfanityMatch {
33
68
  start: number;
34
69
  /** Offset just past the end of the word in the input. */
35
70
  end: number;
36
- /** The word after normalization (diacritics, stretched letters, … removed). */
71
+ /**
72
+ * The word after normalization (diacritics, stretched letters, … removed).
73
+ * For a word with گ, the reading that matched: گ as ق or as ك.
74
+ */
37
75
  normalized: string;
38
- /** The dictionary entry it matched, spelled as in its list. */
76
+ /**
77
+ * The dictionary entry (word or phrase) it matched, spelled as in its list. For a Mosul form
78
+ * (rule "mosul"), the listed ر spelling: الشرموطة for الشغموطة.
79
+ */
39
80
  entry: string;
40
81
  /** The list the entry came from. */
41
82
  source: WordSource;
@@ -43,8 +84,10 @@ interface ProfanityMatch {
43
84
  prefix: string;
44
85
  /** The suffix stripped before matching (only with stripSuffixes), or "". */
45
86
  suffix: string;
87
+ /** The rules the word needed to match the entry, e.g. ["prefix", "arabizi"]. */
88
+ rules: MatchRule[];
46
89
  /**
47
- * The safe stem that cleared this match, spelled as in safe-stems.json.
90
+ * The safe stem that cleared this match, spelled as in safe-stems.json or safeWords.
48
91
  * Only set on matches returned with `includeSuppressed`; these are not profane.
49
92
  */
50
93
  suppressedBy?: string;
@@ -53,42 +96,135 @@ declare class IraqiBadWordsFilter {
53
96
  private words;
54
97
  private safeStems;
55
98
  private stripSuffixes;
99
+ private mosul;
100
+ private entryStarts;
101
+ private longest;
102
+ private normalized;
103
+ private phrases;
56
104
  constructor(options?: string[] | IraqiBadWordsFilterOptions);
57
105
  /**
58
- * Normalizes Arabic text so spelling variants compare equal:
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
- * (عاااارة -> عارة).
106
+ * Normalizes a word or token for comparison: removes diacritics, tatweel and
107
+ * invisible characters, unifies letter variants (أ/ا, ة/ه, ی/ي, ک/ك, …),
108
+ * reads Arabic-Indic digits, collapses stretched letters (عاااارة -> عارة),
109
+ * ignores digits in Arabic words and folds Arabizi. See NORMALIZE_STEPS.
63
110
  */
64
- private normalizeArabic;
111
+ private normalize;
65
112
  /**
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.
113
+ * The ways a normalized word can be read. Iraqi Arabic writes ق as گ, but
114
+ * Kurdish typists sometimes use گ for ك, so a word with گ is tried both ways.
70
115
  */
71
- private foldArabizi;
72
- /** Normalizes a word or token for comparison. */
73
- private normalize;
116
+ private readings;
117
+ /** A list word plus its regional forms (عير -> عير, عيغ with the Mosul dialect). */
118
+ private dialectForms;
119
+ /** The key a list word is stored under. A گ in a list entry means ق. */
120
+ private entryKey;
121
+ /**
122
+ * A word list with surrounding whitespace trimmed and entries that are empty
123
+ * after normalization skipped. A stray "" in a user's file would otherwise
124
+ * match a lone tatweel as a word, or clear every match as a safe stem.
125
+ */
126
+ private cleanList;
127
+ /**
128
+ * Adds safe stems from one list. A safe stem written with گ protects only
129
+ * words spelled with گ (گلب "heart"), so it isn't read as ق or ك.
130
+ */
131
+ private addSafeStems;
74
132
  /**
75
133
  * Adds words from one list. A word already present keeps its first source.
76
134
  */
77
135
  private addEntries;
136
+ /**
137
+ * Reads a listed phrase: words separated by spaces, each with its alternatives
138
+ * separated by | (قوم|كوم بيه|بينو). Punctuation in a word is ignored, and
139
+ * alternatives that are empty after normalization (a lone tatweel) are left
140
+ * out. With the Mosul dialect, an alternative with ر also has its غ form.
141
+ * Phrases that match the same text get the same id.
142
+ */
143
+ private parsePhrase;
144
+ /**
145
+ * Adds phrases from one list, indexed by each alternative of their first word.
146
+ * A phrase already present keeps its first source.
147
+ */
148
+ private addPhraseEntries;
149
+ /**
150
+ * The first word of a phrase with attached prefixes removed, and also the
151
+ * Iraqi progressive د (دقوم بيه, ودقوم بيه).
152
+ */
153
+ private phraseCandidates;
78
154
  /**
79
155
  * Checks a single word, also trying it with attached prefixes removed
80
156
  * (والكحبة -> كحبة, بطيزك -> طيزك), and suffixes removed when stripSuffixes is on.
81
157
  * Returns the matched entry, or null.
82
158
  * A match cleared by a safe stem is returned with `suppressedBy` set.
159
+ * Pass `spelled` when the token is already normalized.
83
160
  */
84
161
  private matchWord;
85
162
  /**
86
- * Finds every match in the text, including ones cleared by a safe stem.
163
+ * Works out which rules a match needed, by comparing the typed word with the
164
+ * listed spelling (plus any stripped prefix and suffix). A normalization rule
165
+ * counts only if leaving it out would keep them apart: عاااارة vs عارة needs
166
+ * "stretched" but not "spelling", since both have ة.
167
+ */
168
+ private explain;
169
+ /** The word plus each form of it with an attached prefix removed. */
170
+ private prefixCandidates;
171
+ /** The first safe stem one of the candidates starts with, as listed. */
172
+ private findSafe;
173
+ /** Matches one normalized reading of a word. */
174
+ private matchReading;
175
+ /**
176
+ * Finds every match in the text, including ones cleared by a safe stem: whole
177
+ * words, phrases, words split into pieces, and Arabizi words with symbols as
178
+ * letters. Matches from different passes can overlap; inspect() resolves that.
179
+ */
180
+ private findMatches;
181
+ /**
182
+ * Finds listed phrases: their words one after another, with only spaces or
183
+ * punctuation between them, each one of its alternatives. Each word is
184
+ * normalized like any other, and the first can carry an attached prefix
185
+ * (وتعال العب بيه) or the progressive د (دقوم بيه). The longest phrase wins.
186
+ */
187
+ private findPhraseMatches;
188
+ /**
189
+ * Matches each word in the text.
87
190
  * A token with invisible characters is matched whole first (عا‌رة -> عارة);
88
191
  * if that finds nothing profane, each visible piece is tried on its own, so an
89
192
  * invisible character between two words can't hide a bad word either.
90
193
  */
91
- private findMatches;
194
+ private findWordMatches;
195
+ /**
196
+ * Whether two neighbouring pieces can be one split word. The gap between them
197
+ * must be short and on one line, and either have no spaces (عي*ر, شرم.وطة),
198
+ * sit between single letters (ع ي ر), or have a symbol between pieces of at
199
+ * most two letters (عي * ر). Normal sentences (انا و انت, كلمة، كلمة) stay apart.
200
+ * `size` gives a piece's normalized length, worked out only when needed.
201
+ */
202
+ private joinable;
203
+ /**
204
+ * Whether a normalized word, or a longer one made by joining more pieces to
205
+ * it, could still match an entry: it must be the start of a prefix, or a
206
+ * prefix plus the start of an entry (or of an entry and a suffix). A word
207
+ * with no letters yet (digits only) always could.
208
+ */
209
+ private couldMatch;
210
+ /**
211
+ * Finds bad words split into pieces by symbols or spaces (ع*ي*ر, ع ي ر,
212
+ * g-a-h-b-a) by joining neighbouring pieces again. From each piece, the
213
+ * longest joined stretch that matches wins, and its span covers the separators.
214
+ */
215
+ private findSplitMatches;
216
+ /**
217
+ * Finds Arabizi words with @ or $ standing in for a letter (g@hba, $harmoota).
218
+ * A word glued to other letters, like an Arabic word, is left to the other passes.
219
+ */
220
+ private findSymbolMatches;
221
+ /**
222
+ * Puts matches in text order and drops overlapping ones, so each span is
223
+ * reported and masked once. Real matches go before ones cleared by a safe
224
+ * stem, then earlier and longer before later or shorter: ع*ي*ر is one match,
225
+ * not also its pieces.
226
+ */
227
+ private resolveOverlaps;
92
228
  /**
93
229
  * Checks if a given text contains profane words.
94
230
  */
@@ -106,10 +242,23 @@ declare class IraqiBadWordsFilter {
106
242
  * Dynamically add words to the active dictionary.
107
243
  */
108
244
  addWords(newWords: string[]): void;
245
+ /**
246
+ * Add safe stems to this filter, like the safeWords option.
247
+ */
248
+ addSafeWords(newWords: string[]): void;
109
249
  /**
110
250
  * Remove words from the active filter dictionary.
111
251
  */
112
252
  removeWords(wordsToRemove: string[]): void;
253
+ /**
254
+ * Add phrases to this filter, like the customPhrases option.
255
+ */
256
+ addPhrases(newPhrases: string[]): void;
257
+ /**
258
+ * Remove phrases from this filter, for example a built-in phrase you don't want
259
+ * flagged. Pass the phrase as listed, alternatives included.
260
+ */
261
+ removePhrases(phrasesToRemove: string[]): void;
113
262
  }
114
263
 
115
- export { type InspectOptions, IraqiBadWordsFilter, type IraqiBadWordsFilterOptions, type ProfanityMatch, type WordSource, IraqiBadWordsFilter as default };
264
+ export { type Dialect, type InspectOptions, IraqiBadWordsFilter, type IraqiBadWordsFilterOptions, type MatchRule, type ProfanityMatch, type WordSource, IraqiBadWordsFilter as default };