iraqi-dialect-bad-words 1.1.0 → 1.2.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 +36 -1
- package/dist/index.d.mts +67 -2
- package/dist/index.d.ts +67 -2
- package/dist/index.js +105 -20
- package/dist/index.mjs +105 -20
- 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.
|
|
@@ -100,8 +132,9 @@ Matching is done per whole word, after normalizing both the text and the word li
|
|
|
100
132
|
| Tatweel | `عـــارة` |
|
|
101
133
|
| ة / ه, ى / ي, أ / إ / آ / ا | `عاره` |
|
|
102
134
|
| Attached prefixes و ف ب ل ك ال | `والعارة`, `للعارة` |
|
|
135
|
+
| Pronoun suffixes (with `stripSuffixes`) | `طيزهم`, `وطيزكم` |
|
|
103
136
|
| Punctuation next to the word | `عارة!` `عارة،` |
|
|
104
|
-
| Arabizi (Latin letters and numbers) | `ga7ba`, `3ayri` |
|
|
137
|
+
| Arabizi (Latin letters and numbers), including spelling variants | `ga7ba` = `qahba` = `gahba`, `kosomak` = `kusumak`, `3ayri` = `ayri` |
|
|
105
138
|
|
|
106
139
|
Because matching is whole-word, a clean word that merely contains a bad word is not flagged.
|
|
107
140
|
|
|
@@ -119,6 +152,8 @@ The word list is the heart of this package, and native speakers are the best jud
|
|
|
119
152
|
- **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
153
|
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
154
|
|
|
155
|
+
- **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
|
+
|
|
122
157
|
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
158
|
|
|
124
159
|
### Development
|
package/dist/index.d.mts
CHANGED
|
@@ -6,10 +6,53 @@ 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:
|
|
@@ -17,15 +60,37 @@ declare class IraqiBadWordsFilter {
|
|
|
17
60
|
* forms, and collapses stretched letters (عاااارة -> عارة).
|
|
18
61
|
*/
|
|
19
62
|
private normalizeArabic;
|
|
63
|
+
/**
|
|
64
|
+
* Folds Arabizi (Arabic written in Latin letters and digits) so spelling
|
|
65
|
+
* variants compare equal: ga7ba / qahba / gahba, kosomak / kusomak,
|
|
66
|
+
* teezak / tizak. Only words made purely of Latin letters and digits are
|
|
67
|
+
* folded; expects text already passed through normalizeArabic.
|
|
68
|
+
*/
|
|
69
|
+
private foldArabizi;
|
|
70
|
+
/** Normalizes a word or token for comparison. */
|
|
71
|
+
private normalize;
|
|
72
|
+
/**
|
|
73
|
+
* Adds words from one list. A word already present keeps its first source.
|
|
74
|
+
*/
|
|
75
|
+
private addEntries;
|
|
20
76
|
/**
|
|
21
77
|
* Checks a single word, also trying it with attached prefixes removed
|
|
22
|
-
* (والكحبة -> كحبة, بطيزك -> طيزك).
|
|
78
|
+
* (والكحبة -> كحبة, بطيزك -> طيزك), and suffixes removed when stripSuffixes is on.
|
|
79
|
+
* Returns the matched entry, or null.
|
|
80
|
+
* A match cleared by a safe stem is returned with `suppressedBy` set.
|
|
23
81
|
*/
|
|
82
|
+
private matchWord;
|
|
83
|
+
/** True when the word is profane, i.e. matched and not cleared by a safe stem. */
|
|
24
84
|
private isProfaneWord;
|
|
25
85
|
/**
|
|
26
86
|
* Checks if a given text contains profane words.
|
|
27
87
|
*/
|
|
28
88
|
isProfane(text: string): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Lists every bad word in the text with its position in the original input
|
|
91
|
+
* and why it matched, e.g. to highlight words or review a match before blocking.
|
|
92
|
+
*/
|
|
93
|
+
inspect(text: string, options?: InspectOptions): ProfanityMatch[];
|
|
29
94
|
/**
|
|
30
95
|
* Replaces profane words with a placeholder string.
|
|
31
96
|
*/
|
|
@@ -40,4 +105,4 @@ declare class IraqiBadWordsFilter {
|
|
|
40
105
|
removeWords(wordsToRemove: string[]): void;
|
|
41
106
|
}
|
|
42
107
|
|
|
43
|
-
export { IraqiBadWordsFilter, type IraqiBadWordsFilterOptions, IraqiBadWordsFilter as default };
|
|
108
|
+
export { type InspectOptions, IraqiBadWordsFilter, type IraqiBadWordsFilterOptions, type ProfanityMatch, type WordSource, IraqiBadWordsFilter as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -6,10 +6,53 @@ 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:
|
|
@@ -17,15 +60,37 @@ declare class IraqiBadWordsFilter {
|
|
|
17
60
|
* forms, and collapses stretched letters (عاااارة -> عارة).
|
|
18
61
|
*/
|
|
19
62
|
private normalizeArabic;
|
|
63
|
+
/**
|
|
64
|
+
* Folds Arabizi (Arabic written in Latin letters and digits) so spelling
|
|
65
|
+
* variants compare equal: ga7ba / qahba / gahba, kosomak / kusomak,
|
|
66
|
+
* teezak / tizak. Only words made purely of Latin letters and digits are
|
|
67
|
+
* folded; expects text already passed through normalizeArabic.
|
|
68
|
+
*/
|
|
69
|
+
private foldArabizi;
|
|
70
|
+
/** Normalizes a word or token for comparison. */
|
|
71
|
+
private normalize;
|
|
72
|
+
/**
|
|
73
|
+
* Adds words from one list. A word already present keeps its first source.
|
|
74
|
+
*/
|
|
75
|
+
private addEntries;
|
|
20
76
|
/**
|
|
21
77
|
* Checks a single word, also trying it with attached prefixes removed
|
|
22
|
-
* (والكحبة -> كحبة, بطيزك -> طيزك).
|
|
78
|
+
* (والكحبة -> كحبة, بطيزك -> طيزك), and suffixes removed when stripSuffixes is on.
|
|
79
|
+
* Returns the matched entry, or null.
|
|
80
|
+
* A match cleared by a safe stem is returned with `suppressedBy` set.
|
|
23
81
|
*/
|
|
82
|
+
private matchWord;
|
|
83
|
+
/** True when the word is profane, i.e. matched and not cleared by a safe stem. */
|
|
24
84
|
private isProfaneWord;
|
|
25
85
|
/**
|
|
26
86
|
* Checks if a given text contains profane words.
|
|
27
87
|
*/
|
|
28
88
|
isProfane(text: string): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Lists every bad word in the text with its position in the original input
|
|
91
|
+
* and why it matched, e.g. to highlight words or review a match before blocking.
|
|
92
|
+
*/
|
|
93
|
+
inspect(text: string, options?: InspectOptions): ProfanityMatch[];
|
|
29
94
|
/**
|
|
30
95
|
* Replaces profane words with a placeholder string.
|
|
31
96
|
*/
|
|
@@ -40,4 +105,4 @@ declare class IraqiBadWordsFilter {
|
|
|
40
105
|
removeWords(wordsToRemove: string[]): void;
|
|
41
106
|
}
|
|
42
107
|
|
|
43
|
-
export { IraqiBadWordsFilter, type IraqiBadWordsFilterOptions, IraqiBadWordsFilter as default };
|
|
108
|
+
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,6 +232,7 @@ 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"
|
|
@@ -236,12 +243,16 @@ var WORD_PATTERN = /[A-Za-z0-9_À-ɏ-؋؍-ؚ-؞ؠ-٩ٮ-ۓە-ۿݐ-ݿࢠ-ࣿ
|
|
|
236
243
|
var CONJUNCTIONS = ["", "\u0648", "\u0641"];
|
|
237
244
|
var PREPOSITIONS = ["", "\u0628", "\u0644", "\u0643"];
|
|
238
245
|
var ARTICLES = ["", "\u0627\u0644"];
|
|
246
|
+
var SUFFIXES = ["\u064A", "\u0643", "\u062C", "\u0647", "\u0647\u0627", "\u0646\u0627", "\u0643\u0645", "\u0647\u0645"];
|
|
239
247
|
var IraqiBadWordsFilter = class {
|
|
240
248
|
constructor(options = {}) {
|
|
241
|
-
const { customWords = [], includeInsults = false } = Array.isArray(options) ? { customWords: options } : options;
|
|
242
|
-
this.words = /* @__PURE__ */ new
|
|
243
|
-
this.
|
|
244
|
-
this.
|
|
249
|
+
const { customWords = [], includeInsults = false, stripSuffixes = false } = Array.isArray(options) ? { customWords: options } : options;
|
|
250
|
+
this.words = /* @__PURE__ */ new Map();
|
|
251
|
+
this.stripSuffixes = stripSuffixes;
|
|
252
|
+
this.safeStems = safe_stems_default.map((word) => ({ normalized: this.normalize(word), word }));
|
|
253
|
+
this.addEntries(words_default, "words");
|
|
254
|
+
if (includeInsults) this.addEntries(insults_default, "insults");
|
|
255
|
+
this.addEntries(customWords, "custom");
|
|
245
256
|
}
|
|
246
257
|
/**
|
|
247
258
|
* Normalizes Arabic text so spelling variants compare equal:
|
|
@@ -249,30 +260,83 @@ var IraqiBadWordsFilter = class {
|
|
|
249
260
|
* forms, and collapses stretched letters (عاااارة -> عارة).
|
|
250
261
|
*/
|
|
251
262
|
normalizeArabic(text) {
|
|
252
|
-
return text.replace(/[
|
|
263
|
+
return text.replace(/[ً-ٰٟـ]/g, "").replace(/[أإآٱ]/g, "\u0627").replace(/ة/g, "\u0647").replace(/ى/g, "\u064A").replace(/(.)\1+/gu, "$1").toLowerCase();
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Folds Arabizi (Arabic written in Latin letters and digits) so spelling
|
|
267
|
+
* variants compare equal: ga7ba / qahba / gahba, kosomak / kusomak,
|
|
268
|
+
* teezak / tizak. Only words made purely of Latin letters and digits are
|
|
269
|
+
* folded; expects text already passed through normalizeArabic.
|
|
270
|
+
*/
|
|
271
|
+
foldArabizi(word) {
|
|
272
|
+
if (!/^[a-z0-9]+$/.test(word) || !/[a-z]/.test(word)) return word;
|
|
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");
|
|
274
|
+
}
|
|
275
|
+
/** Normalizes a word or token for comparison. */
|
|
276
|
+
normalize(text) {
|
|
277
|
+
return this.foldArabizi(this.normalizeArabic(text));
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Adds words from one list. A word already present keeps its first source.
|
|
281
|
+
*/
|
|
282
|
+
addEntries(newWords, source) {
|
|
283
|
+
for (const word of newWords) {
|
|
284
|
+
const normalized = this.normalize(word);
|
|
285
|
+
if (!this.words.has(normalized)) this.words.set(normalized, { word, source });
|
|
286
|
+
}
|
|
253
287
|
}
|
|
254
288
|
/**
|
|
255
289
|
* Checks a single word, also trying it with attached prefixes removed
|
|
256
|
-
* (والكحبة -> كحبة, بطيزك -> طيزك).
|
|
290
|
+
* (والكحبة -> كحبة, بطيزك -> طيزك), and suffixes removed when stripSuffixes is on.
|
|
291
|
+
* Returns the matched entry, or null.
|
|
292
|
+
* A match cleared by a safe stem is returned with `suppressedBy` set.
|
|
257
293
|
*/
|
|
258
|
-
|
|
259
|
-
const word = this.
|
|
260
|
-
|
|
261
|
-
|
|
294
|
+
matchWord(token) {
|
|
295
|
+
const word = this.normalize(token);
|
|
296
|
+
const direct = this.words.get(word);
|
|
297
|
+
if (direct) {
|
|
298
|
+
return { normalized: word, entry: direct.word, source: direct.source, prefix: "", suffix: "" };
|
|
299
|
+
}
|
|
300
|
+
const candidates = [{ prefix: "", stem: word, suffix: "" }];
|
|
262
301
|
for (const conjunction of CONJUNCTIONS) {
|
|
263
302
|
for (const preposition of PREPOSITIONS) {
|
|
264
303
|
for (const article of ARTICLES) {
|
|
265
304
|
const prefix = conjunction + preposition + article;
|
|
266
305
|
if (prefix && word.startsWith(prefix) && word.length - prefix.length >= 2) {
|
|
267
|
-
|
|
306
|
+
candidates.push({ prefix, stem: word.slice(prefix.length), suffix: "" });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (this.stripSuffixes) {
|
|
312
|
+
for (const { prefix, stem } of [...candidates]) {
|
|
313
|
+
for (const suffix of SUFFIXES) {
|
|
314
|
+
if (stem.endsWith(suffix) && stem.length - suffix.length >= 2) {
|
|
315
|
+
candidates.push({ prefix, stem: stem.slice(0, -suffix.length), suffix });
|
|
268
316
|
}
|
|
269
317
|
}
|
|
270
318
|
}
|
|
271
319
|
}
|
|
272
|
-
|
|
273
|
-
|
|
320
|
+
const match = candidates.find(({ stem }) => this.words.has(stem));
|
|
321
|
+
if (!match) return null;
|
|
322
|
+
const { word: entry, source } = this.words.get(match.stem);
|
|
323
|
+
const result = {
|
|
324
|
+
normalized: word,
|
|
325
|
+
entry,
|
|
326
|
+
source,
|
|
327
|
+
prefix: match.prefix,
|
|
328
|
+
suffix: match.suffix
|
|
329
|
+
};
|
|
330
|
+
for (const { stem } of candidates) {
|
|
331
|
+
const safe = this.safeStems.find(({ normalized }) => stem.startsWith(normalized));
|
|
332
|
+
if (safe) return { ...result, suppressedBy: safe.word };
|
|
274
333
|
}
|
|
275
|
-
return
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
/** True when the word is profane, i.e. matched and not cleared by a safe stem. */
|
|
337
|
+
isProfaneWord(token) {
|
|
338
|
+
const match = this.matchWord(token);
|
|
339
|
+
return match !== null && match.suppressedBy === void 0;
|
|
276
340
|
}
|
|
277
341
|
/**
|
|
278
342
|
* Checks if a given text contains profane words.
|
|
@@ -286,27 +350,48 @@ var IraqiBadWordsFilter = class {
|
|
|
286
350
|
}
|
|
287
351
|
return false;
|
|
288
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Lists every bad word in the text with its position in the original input
|
|
355
|
+
* and why it matched, e.g. to highlight words or review a match before blocking.
|
|
356
|
+
*/
|
|
357
|
+
inspect(text, options = {}) {
|
|
358
|
+
if (!text) return [];
|
|
359
|
+
const matches = [];
|
|
360
|
+
const pattern = new RegExp(WORD_PATTERN.source, "g");
|
|
361
|
+
let found;
|
|
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
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return matches;
|
|
370
|
+
}
|
|
289
371
|
/**
|
|
290
372
|
* Replaces profane words with a placeholder string.
|
|
291
373
|
*/
|
|
292
374
|
clean(text, placeholder = "*") {
|
|
293
375
|
if (!text) return "";
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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);
|
|
298
383
|
}
|
|
299
384
|
/**
|
|
300
385
|
* Dynamically add words to the active dictionary.
|
|
301
386
|
*/
|
|
302
387
|
addWords(newWords) {
|
|
303
|
-
|
|
388
|
+
this.addEntries(newWords, "custom");
|
|
304
389
|
}
|
|
305
390
|
/**
|
|
306
391
|
* Remove words from the active filter dictionary.
|
|
307
392
|
*/
|
|
308
393
|
removeWords(wordsToRemove) {
|
|
309
|
-
wordsToRemove.forEach((word) => this.words.delete(this.
|
|
394
|
+
wordsToRemove.forEach((word) => this.words.delete(this.normalize(word)));
|
|
310
395
|
}
|
|
311
396
|
};
|
|
312
397
|
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,6 +205,7 @@ 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"
|
|
@@ -209,12 +216,16 @@ var WORD_PATTERN = /[A-Za-z0-9_À-ɏ-؋؍-ؚ-؞ؠ-٩ٮ-ۓە-ۿݐ-ݿࢠ-ࣿ
|
|
|
209
216
|
var CONJUNCTIONS = ["", "\u0648", "\u0641"];
|
|
210
217
|
var PREPOSITIONS = ["", "\u0628", "\u0644", "\u0643"];
|
|
211
218
|
var ARTICLES = ["", "\u0627\u0644"];
|
|
219
|
+
var SUFFIXES = ["\u064A", "\u0643", "\u062C", "\u0647", "\u0647\u0627", "\u0646\u0627", "\u0643\u0645", "\u0647\u0645"];
|
|
212
220
|
var IraqiBadWordsFilter = class {
|
|
213
221
|
constructor(options = {}) {
|
|
214
|
-
const { customWords = [], includeInsults = false } = Array.isArray(options) ? { customWords: options } : options;
|
|
215
|
-
this.words = /* @__PURE__ */ new
|
|
216
|
-
this.
|
|
217
|
-
this.
|
|
222
|
+
const { customWords = [], includeInsults = false, stripSuffixes = false } = Array.isArray(options) ? { customWords: options } : options;
|
|
223
|
+
this.words = /* @__PURE__ */ new Map();
|
|
224
|
+
this.stripSuffixes = stripSuffixes;
|
|
225
|
+
this.safeStems = safe_stems_default.map((word) => ({ normalized: this.normalize(word), word }));
|
|
226
|
+
this.addEntries(words_default, "words");
|
|
227
|
+
if (includeInsults) this.addEntries(insults_default, "insults");
|
|
228
|
+
this.addEntries(customWords, "custom");
|
|
218
229
|
}
|
|
219
230
|
/**
|
|
220
231
|
* Normalizes Arabic text so spelling variants compare equal:
|
|
@@ -222,30 +233,83 @@ var IraqiBadWordsFilter = class {
|
|
|
222
233
|
* forms, and collapses stretched letters (عاااارة -> عارة).
|
|
223
234
|
*/
|
|
224
235
|
normalizeArabic(text) {
|
|
225
|
-
return text.replace(/[
|
|
236
|
+
return text.replace(/[ً-ٰٟـ]/g, "").replace(/[أإآٱ]/g, "\u0627").replace(/ة/g, "\u0647").replace(/ى/g, "\u064A").replace(/(.)\1+/gu, "$1").toLowerCase();
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Folds Arabizi (Arabic written in Latin letters and digits) so spelling
|
|
240
|
+
* variants compare equal: ga7ba / qahba / gahba, kosomak / kusomak,
|
|
241
|
+
* teezak / tizak. Only words made purely of Latin letters and digits are
|
|
242
|
+
* folded; expects text already passed through normalizeArabic.
|
|
243
|
+
*/
|
|
244
|
+
foldArabizi(word) {
|
|
245
|
+
if (!/^[a-z0-9]+$/.test(word) || !/[a-z]/.test(word)) return word;
|
|
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");
|
|
247
|
+
}
|
|
248
|
+
/** Normalizes a word or token for comparison. */
|
|
249
|
+
normalize(text) {
|
|
250
|
+
return this.foldArabizi(this.normalizeArabic(text));
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Adds words from one list. A word already present keeps its first source.
|
|
254
|
+
*/
|
|
255
|
+
addEntries(newWords, source) {
|
|
256
|
+
for (const word of newWords) {
|
|
257
|
+
const normalized = this.normalize(word);
|
|
258
|
+
if (!this.words.has(normalized)) this.words.set(normalized, { word, source });
|
|
259
|
+
}
|
|
226
260
|
}
|
|
227
261
|
/**
|
|
228
262
|
* Checks a single word, also trying it with attached prefixes removed
|
|
229
|
-
* (والكحبة -> كحبة, بطيزك -> طيزك).
|
|
263
|
+
* (والكحبة -> كحبة, بطيزك -> طيزك), and suffixes removed when stripSuffixes is on.
|
|
264
|
+
* Returns the matched entry, or null.
|
|
265
|
+
* A match cleared by a safe stem is returned with `suppressedBy` set.
|
|
230
266
|
*/
|
|
231
|
-
|
|
232
|
-
const word = this.
|
|
233
|
-
|
|
234
|
-
|
|
267
|
+
matchWord(token) {
|
|
268
|
+
const word = this.normalize(token);
|
|
269
|
+
const direct = this.words.get(word);
|
|
270
|
+
if (direct) {
|
|
271
|
+
return { normalized: word, entry: direct.word, source: direct.source, prefix: "", suffix: "" };
|
|
272
|
+
}
|
|
273
|
+
const candidates = [{ prefix: "", stem: word, suffix: "" }];
|
|
235
274
|
for (const conjunction of CONJUNCTIONS) {
|
|
236
275
|
for (const preposition of PREPOSITIONS) {
|
|
237
276
|
for (const article of ARTICLES) {
|
|
238
277
|
const prefix = conjunction + preposition + article;
|
|
239
278
|
if (prefix && word.startsWith(prefix) && word.length - prefix.length >= 2) {
|
|
240
|
-
|
|
279
|
+
candidates.push({ prefix, stem: word.slice(prefix.length), suffix: "" });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (this.stripSuffixes) {
|
|
285
|
+
for (const { prefix, stem } of [...candidates]) {
|
|
286
|
+
for (const suffix of SUFFIXES) {
|
|
287
|
+
if (stem.endsWith(suffix) && stem.length - suffix.length >= 2) {
|
|
288
|
+
candidates.push({ prefix, stem: stem.slice(0, -suffix.length), suffix });
|
|
241
289
|
}
|
|
242
290
|
}
|
|
243
291
|
}
|
|
244
292
|
}
|
|
245
|
-
|
|
246
|
-
|
|
293
|
+
const match = candidates.find(({ stem }) => this.words.has(stem));
|
|
294
|
+
if (!match) return null;
|
|
295
|
+
const { word: entry, source } = this.words.get(match.stem);
|
|
296
|
+
const result = {
|
|
297
|
+
normalized: word,
|
|
298
|
+
entry,
|
|
299
|
+
source,
|
|
300
|
+
prefix: match.prefix,
|
|
301
|
+
suffix: match.suffix
|
|
302
|
+
};
|
|
303
|
+
for (const { stem } of candidates) {
|
|
304
|
+
const safe = this.safeStems.find(({ normalized }) => stem.startsWith(normalized));
|
|
305
|
+
if (safe) return { ...result, suppressedBy: safe.word };
|
|
247
306
|
}
|
|
248
|
-
return
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
/** True when the word is profane, i.e. matched and not cleared by a safe stem. */
|
|
310
|
+
isProfaneWord(token) {
|
|
311
|
+
const match = this.matchWord(token);
|
|
312
|
+
return match !== null && match.suppressedBy === void 0;
|
|
249
313
|
}
|
|
250
314
|
/**
|
|
251
315
|
* Checks if a given text contains profane words.
|
|
@@ -259,27 +323,48 @@ var IraqiBadWordsFilter = class {
|
|
|
259
323
|
}
|
|
260
324
|
return false;
|
|
261
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Lists every bad word in the text with its position in the original input
|
|
328
|
+
* and why it matched, e.g. to highlight words or review a match before blocking.
|
|
329
|
+
*/
|
|
330
|
+
inspect(text, options = {}) {
|
|
331
|
+
if (!text) return [];
|
|
332
|
+
const matches = [];
|
|
333
|
+
const pattern = new RegExp(WORD_PATTERN.source, "g");
|
|
334
|
+
let found;
|
|
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
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return matches;
|
|
343
|
+
}
|
|
262
344
|
/**
|
|
263
345
|
* Replaces profane words with a placeholder string.
|
|
264
346
|
*/
|
|
265
347
|
clean(text, placeholder = "*") {
|
|
266
348
|
if (!text) return "";
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
349
|
+
let result = "";
|
|
350
|
+
let last = 0;
|
|
351
|
+
for (const { start, end } of this.inspect(text)) {
|
|
352
|
+
result += text.slice(last, start) + placeholder.repeat(end - start);
|
|
353
|
+
last = end;
|
|
354
|
+
}
|
|
355
|
+
return result + text.slice(last);
|
|
271
356
|
}
|
|
272
357
|
/**
|
|
273
358
|
* Dynamically add words to the active dictionary.
|
|
274
359
|
*/
|
|
275
360
|
addWords(newWords) {
|
|
276
|
-
|
|
361
|
+
this.addEntries(newWords, "custom");
|
|
277
362
|
}
|
|
278
363
|
/**
|
|
279
364
|
* Remove words from the active filter dictionary.
|
|
280
365
|
*/
|
|
281
366
|
removeWords(wordsToRemove) {
|
|
282
|
-
wordsToRemove.forEach((word) => this.words.delete(this.
|
|
367
|
+
wordsToRemove.forEach((word) => this.words.delete(this.normalize(word)));
|
|
283
368
|
}
|
|
284
369
|
};
|
|
285
370
|
var index_default = IraqiBadWordsFilter;
|
package/package.json
CHANGED