Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [Unreleased]

### Added
- Technical articles explaining Arabic affix removal and banned-word matching, including design decisions, examples, and known limitations.
- Integration tests for CJS, ESM, and browser UMD artifacts.
- Arabic NLP boundary and ReDoS resilience test suites.
- Direct CDN links (jsdelivr & unpkg) and GitHub release bundle guide in documentation.
Expand All @@ -26,6 +27,8 @@
- Release publishing now validates the tag/version and runs formatting, lint, and tests before publishing the verified build.

### Fixed
- Retain each original word in `removeArabicAffixes` when the complete stripped result has fewer than three Arabic letters, excluding marks, tatweel, digits, and other scripts from the count. This changes outputs such as `نور`, `بنت`, and `اليد`; banned-word recognition retains its existing stripping behavior.
- Process affix removal independently across whitespace, punctuation, and symbol boundaries while preserving interior separators. Defer Unicode property regular expressions until this function is called, so unsupported engines can still load and use unrelated APIs.
- Recognize exact banned-word matches before stripping affixes, so listed words such as `فلسطين` are obfuscated while retaining existing affix-based similarity matching.
- Calculate `similarityScore` lengths and edit distance after lowercasing so Unicode case expansion cannot produce negative scores or incorrect similarity ratios.
- Prevent invalid levels from hanging `tashfeer`, `tashfeerBannedWords`, and `toOldArabicAndTashfeerBannedWords`: non-number values throw `TypeError`; negative, fractional, and non-finite numbers throw `RangeError`. Random-index generation handles empty input and caps the requested count at the word length ([#35](https://github.com/Seen-Arabic/Arabic-Services-JavaScript/issues/35), [PR #36](https://github.com/Seen-Arabic/Arabic-Services-JavaScript/pull/36)).
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,16 @@ Can be used in Node.js and the browser.
- **Convert To Old Arabic And Tashfeer Banned Words**: Transform Arabic text into old script and replace Banned Arabic text with visually similar characters for encoding purposes. (Banned words are words that considered as hate speech in social media)
- **Tashfeer**: Replaces Arabic text with visually similar characters for encoding purposes.
- **Tashfeer Banned Words**: Replaces Banned Arabic text with visually similar characters for encoding purposes. (Banned words are words that considered as hate speech in social media)
- **Remove Arabic Affixes**: Removes predefined affixes (prefixes and suffixes) from an Arabic word if it starts or ends with those affixes.
- **Remove Arabic Affixes**: Removes listed prefixes and suffixes from letter/mark/number sequences, preserving interior whitespace, punctuation, and symbols. Retains the original word when the complete result has fewer than three Arabic letters. This heuristic can retain legitimate two-letter stems (e.g. `اليد` stays `اليد`) and still strip original letters in longer words; it is not morphological analysis. This function requires Unicode property escape support; other APIs do not acquire that requirement on import. Banned-word matching retains its previous stripping rules.
- **Word To Letters**: Convert Arabic word to its pronounced letters.
- ... and more!

<h3 align=center><a href="https://github.com/Seen-Arabic/Arabic-Services-JavaScript/wiki/Features-Documentation">➡️ Check out the usage and detailed documentation on each feature ⬅️</a></h3>

## 🚀 Getting Started

For implementation details, examples, and known limitations, see the [technical documentation](docs/README.md).

Arabic-Services was designed to work in both the browser and NodeJS applications.

### Browser
Expand Down
4 changes: 3 additions & 1 deletion README_AR.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,16 @@
- **التحويل إلى العربية القديمة وتشفير الكلمات المحظورة:** تحويل النص العربي إلى الخط العربي القديم واستبدال النصوص العربية المحظورة بأحرف مشابهة بصريًا لأغراض الترميز. (الكلمات المحظورة هي الكلمات التي تعتبر كلمات مسيئة في وسائل التواصل الاجتماعي)
- **التشفير:** استبدال النص العربي بأحرف مشابهة بصريًا لأغراض الترميز.
- **تشفير الكلمات المحظورة:** استبدال النصوص العربية المحظورة بأحرف مشابهة بصريًا لأغراض الترميز. (الكلمات المحظورة هي الكلمات التي تعتبر كلمات مسيئة في وسائل التواصل الاجتماعي)
- **إزالة اللواحق العربية:** إزالة اللواحق المحددة (البادئات واللاحقات) من كلمة عربية إذا بدأت أو انتهت بهذه اللواحق.
- **إزالة اللواحق العربية:** إزالة السوابق واللواحق المحددة من تتابعات الحروف والعلامات والأرقام، مع الحفاظ على الفواصل الداخلية والأسطر وعلامات الترقيم والرموز. تُبقي الكلمة الأصلية إذا احتوت نتيجة القص الكاملة على أقل من ثلاثة أحرف عربية. هذه قواعد تقريبية؛ فقد تُبقي كلمات ذات أصل ثنائي مثل `اليد` كما هي، وقد تحذف حروفًا أصلية من كلمات أطول، وليست تحليلًا صرفيًا. تتطلب هذه الدالة دعم Unicode property escapes، دون فرض هذا المتطلب عند تحميل بقية الخدمات. تحتفظ مطابقة الكلمات المدرجة بقواعد القص السابقة.
- **الكلمة إلى حروفها:** تحويل الكلمة العربية إلى حروفها المنطوقة.
- ...والمزيد!

<h3 align=center><a href="https://github.com/Seen-Arabic/Arabic-Services-JavaScript/wiki/Features%E2%80%90Documentation%E2%80%90AR">➡️ شاهد استخدام كل ميزة من المكتبة وطريقة استخدامها في الكود ⬅️</a></h3>

## 🚀 التثبيت

لشرح الخوارزميات وأسباب القرارات والأمثلة والحدود المعروفة، راجع [التوثيق التقني باللغة الإنجليزية](docs/README.md).

تم تصميم خدمات العربية لتعمل في كل من المتصفح وتطبيقات NodeJS.

### المتصفح (Browser)
Expand Down
14 changes: 14 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Technical documentation

These articles explain how the current source works, why its rules exist, and where its results can be misleading. They describe the repository revision you are reading, including behavior recorded under [Unreleased](../CHANGELOG.md); an installed npm release may behave differently.

| Article | What it explains |
| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| [Arabic affix removal](algorithms/arabic-affixes.md) | Listed prefix/suffix removal, the short-result guard, word boundaries, and linguistic limitations. |
| [Banned-word matching](algorithms/banned-word-matching.md) | Exact matching, similarity matching, and why matching keeps its own stripping policy. |

For installation and public API usage, start with the [English README](../README.md) or [Arabic README](../README_AR.md).

## Keeping articles accurate

When changing documented behavior, update its article and regression examples together. Explain the decision and its trade-offs; link to the implementation and tests rather than copying their full contents. Include known failures alongside successful examples. Treat experiment results as evidence for a particular input set, not as a general accuracy guarantee.
69 changes: 69 additions & 0 deletions docs/algorithms/arabic-affixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Arabic affix removal: a short-result guard

`removeArabicAffixes(text: string): string` removes predefined prefixes and suffixes. It can be useful when a caller deliberately wants a lossy text transformation. It does not identify grammatical affixes, recover roots, or guarantee that different forms of a word produce the same output.

This article describes the current source, including the guard and boundary handling recorded under [Unreleased](../../CHANGELOG.md).

## Why the guard exists

The original rules remove letters by their position and membership in a list. They cannot tell whether the initial `ن` in `نور` is an affix or part of the word. Applying those rules without a guard turns `نور` into `ور` and `بنت` into `نت`.

The current policy keeps the original word if the **complete stripped result** contains fewer than three Arabic-script letters. Three is a conservative cutoff for this transformation, not a linguistic claim that Arabic stems must have three letters.

## How a word is processed

1. Trim whitespace from the beginning and end of the whole input.
2. Find each contiguous sequence of Unicode letters, combining marks, and numbers. Adjacent Arabic, Latin, and numeric characters belong to the same sequence; this is not language-aware tokenization.
3. Try one listed prefix, checking two characters before one character.
4. On the remaining text, try one listed suffix, again checking two characters before one character. Neither step repeats until no affix remains.
5. Count letters that have both Unicode `Script=Arabic` and category `Letter` in the candidate. Keep the candidate only if at least three are found; otherwise keep the original sequence.

The current lists live in [arabic-letters.ts](../../src/constants/arabic-letters.ts):

- Prefixes: `أ`, `ا`, `إ`, `ال`, `ي`, `ت`, `ن`, `ب`.
- Suffixes: `ة`, `ه`, `ي`, `ى`, `ية`, `ين`, `ون`, `هم`.

Diacritics, tatweel, digits, punctuation, and other scripts do not satisfy the minimum. Interior whitespace, punctuation, and symbols are preserved. The function does not remove diacritics or normalize spelling before trying the lists.

## Examples and counterexamples

| Input | Candidate after listed stripping | Returned text | Reason |
| -------- | -------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------ |
| `نور` | `ور` | `نور` | Two Arabic letters remain. |
| `بنت` | `نت` | `بنت` | Two Arabic letters remain. |
| `الكتاب` | `كتاب` | `كتاب` | Four Arabic letters remain. |
| `كتابهم` | `كتاب` | `كتاب` | The listed suffix is removed. |
| `اليد` | `يد` | `اليد` | A legitimate two-letter stem is also rejected. |
| `يحبون` | `حب` | `يحبون` | The complete candidate is too short; the function does not retry suffix-only removal to produce `يحب`. |
| `تفاح` | `فاح` | `فاح` | The guard cannot detect removal of an original letter from a longer word. |
| `فلسطين` | `فلسط` | `فلسط` | A listed ending can be part of the original word. |

```javascript
import { removeArabicAffixes } from 'arabic-services';

removeArabicAffixes(' نور،الكتاب\nبنت '); // 'نور،كتاب\nبنت'
removeArabicAffixes('المدرسون يحبون طلابهم'); // 'مدرس يحبون طلاب'
```

The fallback is atomic: it rejects the entire stripped candidate rather than approving each deletion separately. Otherwise a partial deletion could survive even when a later deletion should make us retain the original word.

## Why this is not a stemmer replacement

Expanding the prefix list would change which words are cut; it would not resolve whether those letters are original. Replacing this function with a search stemmer or morphological analyzer would also change its output contract and potentially require dictionaries or dependencies. The chosen fix addresses short destructive results while retaining the existing lists and keeping zero runtime dependencies.

This trade-off leaves real limitations: short stems may not be reduced, longer words may still be damaged, and stripping an initial letter before a combining mark can leave that mark without its original base. Use a separate morphological solution when grammatical interpretation is required. Do not use this transformation as a prerequisite for assuming that a word is correctly recognized.

## Runtime and module boundaries

Calling this function requires Unicode property escape support. Its regular expressions are constructed inside the function, so loading the package or calling unrelated APIs does not introduce that requirement. An unsupported engine will throw a `SyntaxError` when the function is called; use a runtime with that support if affix removal is needed.

The underlying stripping routine is shared internally with [banned-word matching](banned-word-matching.md), which deliberately does not apply this guard. That routine is not exported from the package root. Named ESM consumers can import `removeArabicAffixes` without retaining the banned-word matching code and list in their bundle.

## Implementation and verification

- [Affix implementation](../../src/scripts/affixes.ts) and [public script exports](../../src/scripts/index.ts).
- [Guard, Unicode, separator, and matching regression tests](../../tests/affixes.test.ts).
- [Sentence examples](../../tests/scripts.test.ts) and [short-input boundaries](../../tests/edge-cases.test.ts).
- [Built-module and runtime-isolation checks](../../tests/artifacts.test.ts) and [Webpack tree-shaking checks](../../tests/tree-shaking.test.ts).

Run `npm run test:unit` for source behavior. Run `npm test` to also rebuild and validate package artifacts. These checks establish the tested contract, not general linguistic correctness.
Loading
Loading