iraqi-dialect-bad-words 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -131,10 +131,13 @@ Matching is done per whole word, after normalizing both the text and the word li
131
131
  | Stretched letters | `عاااارة` |
132
132
  | Tatweel | `عـــارة` |
133
133
  | ة / ه, ى / ي, أ / إ / آ / ا | `عاره` |
134
+ | Persian/Kurdish keyboard letters ی ک ە | `عیر`, `کس` |
135
+ | Invisible characters hidden inside a word (zero-width, direction marks, soft hyphen) | `عا‌رة` |
134
136
  | Attached prefixes و ف ب ل ك ال | `والعارة`, `للعارة` |
135
137
  | Pronoun suffixes (with `stripSuffixes`) | `طيزهم`, `وطيزكم` |
136
138
  | Punctuation next to the word | `عارة!` `عارة،` |
137
- | Arabizi (Latin letters and numbers), including spelling variants | `ga7ba` = `qahba` = `gahba`, `kosomak` = `kusumak`, `3ayri` = `ayri` |
139
+ | Digits glued to or inside an Arabic word | `عارة3`, `عا٣رة` |
140
+ | Arabizi (Latin letters and numbers, Western or Arabic-Indic), including spelling variants | `ga7ba` = `ga٧ba` = `qahba` = `gahba`, `kosomak` = `kusumak`, `3ayri` = `ayri` |
138
141
 
139
142
  Because matching is whole-word, a clean word that merely contains a bad word is not flagged.
140
143
 
package/dist/index.d.mts CHANGED
@@ -56,8 +56,10 @@ declare class IraqiBadWordsFilter {
56
56
  constructor(options?: string[] | IraqiBadWordsFilterOptions);
57
57
  /**
58
58
  * Normalizes Arabic text so spelling variants compare equal:
59
- * removes diacritics (Tashkeel) and tatweel, unifies Alef/Teh Marbuta/Alef Maksura
60
- * forms, and collapses stretched letters (عاااارة -> عارة).
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
+ * (عاااارة -> عارة).
61
63
  */
62
64
  private normalizeArabic;
63
65
  /**
@@ -80,8 +82,13 @@ declare class IraqiBadWordsFilter {
80
82
  * A match cleared by a safe stem is returned with `suppressedBy` set.
81
83
  */
82
84
  private matchWord;
83
- /** True when the word is profane, i.e. matched and not cleared by a safe stem. */
84
- private isProfaneWord;
85
+ /**
86
+ * Finds every match in the text, including ones cleared by a safe stem.
87
+ * A token with invisible characters is matched whole first (عا‌رة -> عارة);
88
+ * if that finds nothing profane, each visible piece is tried on its own, so an
89
+ * invisible character between two words can't hide a bad word either.
90
+ */
91
+ private findMatches;
85
92
  /**
86
93
  * Checks if a given text contains profane words.
87
94
  */
package/dist/index.d.ts CHANGED
@@ -56,8 +56,10 @@ declare class IraqiBadWordsFilter {
56
56
  constructor(options?: string[] | IraqiBadWordsFilterOptions);
57
57
  /**
58
58
  * Normalizes Arabic text so spelling variants compare equal:
59
- * removes diacritics (Tashkeel) and tatweel, unifies Alef/Teh Marbuta/Alef Maksura
60
- * forms, and collapses stretched letters (عاااارة -> عارة).
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
+ * (عاااارة -> عارة).
61
63
  */
62
64
  private normalizeArabic;
63
65
  /**
@@ -80,8 +82,13 @@ declare class IraqiBadWordsFilter {
80
82
  * A match cleared by a safe stem is returned with `suppressedBy` set.
81
83
  */
82
84
  private matchWord;
83
- /** True when the word is profane, i.e. matched and not cleared by a safe stem. */
84
- private isProfaneWord;
85
+ /**
86
+ * Finds every match in the text, including ones cleared by a safe stem.
87
+ * A token with invisible characters is matched whole first (عا‌رة -> عارة);
88
+ * if that finds nothing profane, each visible piece is tried on its own, so an
89
+ * invisible character between two words can't hide a bad word either.
90
+ */
91
+ private findMatches;
85
92
  /**
86
93
  * Checks if a given text contains profane words.
87
94
  */
package/dist/index.js CHANGED
@@ -239,7 +239,14 @@ var safe_stems_default = [
239
239
  ];
240
240
 
241
241
  // src/index.ts
242
- var WORD_PATTERN = /[A-Za-z0-9_À-ɏ؀-؋؍-ؚ؜-؞ؠ-٩ٮ-ۓە-ۿݐ-ݿࢠ-ࣿﭐ-﷿ﹰ-ﻼ]+/g;
242
+ var INVISIBLE = "\\u00AD\\u061C\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF";
243
+ var INVISIBLE_CHARS = new RegExp(`[${INVISIBLE}]`, "g");
244
+ var HAS_INVISIBLE = new RegExp(`[${INVISIBLE}]`);
245
+ var VISIBLE_RUN = new RegExp(`[^${INVISIBLE}]+`, "g");
246
+ var WORD_PATTERN = new RegExp(
247
+ `[A-Za-z0-9_\xC0-\u024F\u0600-\u060B\u060D-\u061A\u061C-\u061E\u0620-\u0669\u066E-\u06D3\u06D5-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFC${INVISIBLE}]+`,
248
+ "g"
249
+ );
243
250
  var CONJUNCTIONS = ["", "\u0648", "\u0641"];
244
251
  var PREPOSITIONS = ["", "\u0628", "\u0644", "\u0643"];
245
252
  var ARTICLES = ["", "\u0627\u0644"];
@@ -256,11 +263,13 @@ var IraqiBadWordsFilter = class {
256
263
  }
257
264
  /**
258
265
  * Normalizes Arabic text so spelling variants compare equal:
259
- * removes diacritics (Tashkeel) and tatweel, unifies Alef/Teh Marbuta/Alef Maksura
260
- * forms, and collapses stretched letters (عاااارة -> عارة).
266
+ * removes diacritics (Tashkeel), tatweel and invisible characters, unifies
267
+ * Alef/Teh Marbuta/Alef Maksura forms and the Persian/Kurdish keyboard letters
268
+ * ی ک ە, turns Arabic-Indic digits into 0-9, and collapses stretched letters
269
+ * (عاااارة -> عارة).
261
270
  */
262
271
  normalizeArabic(text) {
263
- return text.replace(/[ً-ٰٟـ]/g, "").replace(/[أإآٱ]/g, "\u0627").replace(/ة/g, "\u0647").replace(/ى/g, "\u064A").replace(/(.)\1+/gu, "$1").toLowerCase();
272
+ return text.replace(/[\u064B-\u065F\u0670\u0640]/g, "").replace(INVISIBLE_CHARS, "").replace(/[أإآٱ]/g, "\u0627").replace(/[ةەہۀ]/g, "\u0647").replace(/[ىی]/g, "\u064A").replace(/[کڪ]/g, "\u0643").replace(/[٠-٩]/g, (digit) => String(digit.charCodeAt(0) - 1632)).replace(/[۰-۹]/g, (digit) => String(digit.charCodeAt(0) - 1776)).replace(/(.)\1+/gu, "$1").toLowerCase();
264
273
  }
265
274
  /**
266
275
  * Folds Arabizi (Arabic written in Latin letters and digits) so spelling
@@ -274,7 +283,8 @@ var IraqiBadWordsFilter = class {
274
283
  }
275
284
  /** Normalizes a word or token for comparison. */
276
285
  normalize(text) {
277
- return this.foldArabizi(this.normalizeArabic(text));
286
+ const word = this.normalizeArabic(text);
287
+ return /[\u0600-\uFEFF]/.test(word) ? word.replace(/[0-9]/g, "").replace(/(.)\1+/gu, "$1") : this.foldArabizi(word);
278
288
  }
279
289
  /**
280
290
  * Adds words from one list. A word already present keeps its first source.
@@ -333,20 +343,43 @@ var IraqiBadWordsFilter = class {
333
343
  }
334
344
  return result;
335
345
  }
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;
346
+ /**
347
+ * Finds every match in the text, including ones cleared by a safe stem.
348
+ * A token with invisible characters is matched whole first (عا‌رة -> عارة);
349
+ * if that finds nothing profane, each visible piece is tried on its own, so an
350
+ * invisible character between two words can't hide a bad word either.
351
+ */
352
+ *findMatches(text) {
353
+ const pattern = new RegExp(WORD_PATTERN.source, "g");
354
+ let found;
355
+ while (found = pattern.exec(text)) {
356
+ const token = found[0];
357
+ const start = found.index;
358
+ const whole = this.matchWord(token);
359
+ const wholeMatch = whole && { token, start, end: start + token.length, ...whole };
360
+ if (whole && whole.suppressedBy === void 0 || !HAS_INVISIBLE.test(token)) {
361
+ if (wholeMatch) yield wholeMatch;
362
+ continue;
363
+ }
364
+ const pieces = [];
365
+ const piecePattern = new RegExp(VISIBLE_RUN.source, "g");
366
+ let piece;
367
+ while (piece = piecePattern.exec(token)) {
368
+ const match = this.matchWord(piece[0]);
369
+ const pieceStart = start + piece.index;
370
+ if (match) pieces.push({ token: piece[0], start: pieceStart, end: pieceStart + piece[0].length, ...match });
371
+ }
372
+ if (pieces.length) yield* pieces;
373
+ else if (wholeMatch) yield wholeMatch;
374
+ }
340
375
  }
341
376
  /**
342
377
  * Checks if a given text contains profane words.
343
378
  */
344
379
  isProfane(text) {
345
380
  if (!text) return false;
346
- for (const token of text.match(WORD_PATTERN) ?? []) {
347
- if (this.isProfaneWord(token)) {
348
- return true;
349
- }
381
+ for (const match of this.findMatches(text)) {
382
+ if (match.suppressedBy === void 0) return true;
350
383
  }
351
384
  return false;
352
385
  }
@@ -357,14 +390,8 @@ var IraqiBadWordsFilter = class {
357
390
  inspect(text, options = {}) {
358
391
  if (!text) return [];
359
392
  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
- }
393
+ for (const match of this.findMatches(text)) {
394
+ if (match.suppressedBy === void 0 || options.includeSuppressed) matches.push(match);
368
395
  }
369
396
  return matches;
370
397
  }
package/dist/index.mjs CHANGED
@@ -212,7 +212,14 @@ var safe_stems_default = [
212
212
  ];
213
213
 
214
214
  // src/index.ts
215
- var WORD_PATTERN = /[A-Za-z0-9_À-ɏ؀-؋؍-ؚ؜-؞ؠ-٩ٮ-ۓە-ۿݐ-ݿࢠ-ࣿﭐ-﷿ﹰ-ﻼ]+/g;
215
+ var INVISIBLE = "\\u00AD\\u061C\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u2069\\uFEFF";
216
+ var INVISIBLE_CHARS = new RegExp(`[${INVISIBLE}]`, "g");
217
+ var HAS_INVISIBLE = new RegExp(`[${INVISIBLE}]`);
218
+ var VISIBLE_RUN = new RegExp(`[^${INVISIBLE}]+`, "g");
219
+ var WORD_PATTERN = new RegExp(
220
+ `[A-Za-z0-9_\xC0-\u024F\u0600-\u060B\u060D-\u061A\u061C-\u061E\u0620-\u0669\u066E-\u06D3\u06D5-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFC${INVISIBLE}]+`,
221
+ "g"
222
+ );
216
223
  var CONJUNCTIONS = ["", "\u0648", "\u0641"];
217
224
  var PREPOSITIONS = ["", "\u0628", "\u0644", "\u0643"];
218
225
  var ARTICLES = ["", "\u0627\u0644"];
@@ -229,11 +236,13 @@ var IraqiBadWordsFilter = class {
229
236
  }
230
237
  /**
231
238
  * Normalizes Arabic text so spelling variants compare equal:
232
- * removes diacritics (Tashkeel) and tatweel, unifies Alef/Teh Marbuta/Alef Maksura
233
- * forms, and collapses stretched letters (عاااارة -> عارة).
239
+ * removes diacritics (Tashkeel), tatweel and invisible characters, unifies
240
+ * Alef/Teh Marbuta/Alef Maksura forms and the Persian/Kurdish keyboard letters
241
+ * ی ک ە, turns Arabic-Indic digits into 0-9, and collapses stretched letters
242
+ * (عاااارة -> عارة).
234
243
  */
235
244
  normalizeArabic(text) {
236
- return text.replace(/[ً-ٰٟـ]/g, "").replace(/[أإآٱ]/g, "\u0627").replace(/ة/g, "\u0647").replace(/ى/g, "\u064A").replace(/(.)\1+/gu, "$1").toLowerCase();
245
+ return text.replace(/[\u064B-\u065F\u0670\u0640]/g, "").replace(INVISIBLE_CHARS, "").replace(/[أإآٱ]/g, "\u0627").replace(/[ةەہۀ]/g, "\u0647").replace(/[ىی]/g, "\u064A").replace(/[کڪ]/g, "\u0643").replace(/[٠-٩]/g, (digit) => String(digit.charCodeAt(0) - 1632)).replace(/[۰-۹]/g, (digit) => String(digit.charCodeAt(0) - 1776)).replace(/(.)\1+/gu, "$1").toLowerCase();
237
246
  }
238
247
  /**
239
248
  * Folds Arabizi (Arabic written in Latin letters and digits) so spelling
@@ -247,7 +256,8 @@ var IraqiBadWordsFilter = class {
247
256
  }
248
257
  /** Normalizes a word or token for comparison. */
249
258
  normalize(text) {
250
- return this.foldArabizi(this.normalizeArabic(text));
259
+ const word = this.normalizeArabic(text);
260
+ return /[\u0600-\uFEFF]/.test(word) ? word.replace(/[0-9]/g, "").replace(/(.)\1+/gu, "$1") : this.foldArabizi(word);
251
261
  }
252
262
  /**
253
263
  * Adds words from one list. A word already present keeps its first source.
@@ -306,20 +316,43 @@ var IraqiBadWordsFilter = class {
306
316
  }
307
317
  return result;
308
318
  }
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;
319
+ /**
320
+ * Finds every match in the text, including ones cleared by a safe stem.
321
+ * A token with invisible characters is matched whole first (عا‌رة -> عارة);
322
+ * if that finds nothing profane, each visible piece is tried on its own, so an
323
+ * invisible character between two words can't hide a bad word either.
324
+ */
325
+ *findMatches(text) {
326
+ const pattern = new RegExp(WORD_PATTERN.source, "g");
327
+ let found;
328
+ while (found = pattern.exec(text)) {
329
+ const token = found[0];
330
+ const start = found.index;
331
+ const whole = this.matchWord(token);
332
+ const wholeMatch = whole && { token, start, end: start + token.length, ...whole };
333
+ if (whole && whole.suppressedBy === void 0 || !HAS_INVISIBLE.test(token)) {
334
+ if (wholeMatch) yield wholeMatch;
335
+ continue;
336
+ }
337
+ const pieces = [];
338
+ const piecePattern = new RegExp(VISIBLE_RUN.source, "g");
339
+ let piece;
340
+ while (piece = piecePattern.exec(token)) {
341
+ const match = this.matchWord(piece[0]);
342
+ const pieceStart = start + piece.index;
343
+ if (match) pieces.push({ token: piece[0], start: pieceStart, end: pieceStart + piece[0].length, ...match });
344
+ }
345
+ if (pieces.length) yield* pieces;
346
+ else if (wholeMatch) yield wholeMatch;
347
+ }
313
348
  }
314
349
  /**
315
350
  * Checks if a given text contains profane words.
316
351
  */
317
352
  isProfane(text) {
318
353
  if (!text) return false;
319
- for (const token of text.match(WORD_PATTERN) ?? []) {
320
- if (this.isProfaneWord(token)) {
321
- return true;
322
- }
354
+ for (const match of this.findMatches(text)) {
355
+ if (match.suppressedBy === void 0) return true;
323
356
  }
324
357
  return false;
325
358
  }
@@ -330,14 +363,8 @@ var IraqiBadWordsFilter = class {
330
363
  inspect(text, options = {}) {
331
364
  if (!text) return [];
332
365
  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
- }
366
+ for (const match of this.findMatches(text)) {
367
+ if (match.suppressedBy === void 0 || options.includeSuppressed) matches.push(match);
341
368
  }
342
369
  return matches;
343
370
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iraqi-dialect-bad-words",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "An Iraqi dialect profanity filter for React, React Native Expo, and Node.js",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",