Files
ragflow/web/src/utils/text-direction.ts
Attili-sys 21bc1ab7ec Feature rtl support (#13118)
### What problem does this PR solve?

This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).

Previously, RTL content had multiple rendering issues:

- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components  
- Improper positioning of blockquotes and “think” sections  
- Incorrect table alignment  
- Citation placement ambiguity in RTL prompts  
- UI layout inconsistencies when mixing LTR and RTL text  

This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.

#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
  - `،` (comma)
  - `؛` (semicolon)
  - `؟` (question mark)
  - `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.

#### Frontend
- Introduced a new utility: `text-direction.ts`
  - Detects text direction based on Unicode ranges.
  - Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
  - Provides `getDirAttribute()` for automatic `dir` assignment.

- Applied dynamic `dir` attributes across:
  - Markdown rendering
  - Chat messages
  - Search results
  - Tables
  - Hover cards and reference popovers

- Added proper RTL styling in LESS:
  - Text alignment adjustments
  - Blockquote border flipping
  - Section indentation correction
  - Table direction switching
  - Use of `<bdi>` for figure labels to prevent bidirectional conflicts

#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.

---

### Type of change

- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)

---------

Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 13:03:44 +08:00

102 lines
2.7 KiB
TypeScript

/**
* RTL (Right-to-Left) text direction utilities
* Supports Arabic, Hebrew, Persian/Farsi, Urdu, and other RTL scripts
*/
// Unicode ranges for RTL scripts
const RTL_RANGES: [number, number][] = [
[0x0600, 0x06ff], // Arabic
[0x0750, 0x077f], // Arabic Supplement
[0x08a0, 0x08ff], // Arabic Extended-A
[0xfb50, 0xfdff], // Arabic Presentation Forms-A
[0xfe70, 0xfeff], // Arabic Presentation Forms-B
[0x0590, 0x05ff], // Hebrew
[0xfb1d, 0xfb4f], // Hebrew Presentation Forms
[0x0700, 0x074f], // Syriac
[0x0780, 0x07bf], // Thaana (Maldivian)
[0x0840, 0x085f], // Mandaic
[0x0860, 0x086f], // Syriac Supplement
];
/**
* Check if a character code is in RTL Unicode range
*/
const isRTLCharCode = (charCode: number): boolean => {
return RTL_RANGES.some(
([start, end]) => charCode >= start && charCode <= end,
);
};
/**
* Find the first "strong" directional character in text
* Strong characters are letters (not numbers, punctuation, or whitespace)
* Returns 'rtl', 'ltr', or 'neutral' if no strong character found
*/
export const getTextDirection = (text: string): 'rtl' | 'ltr' | 'neutral' => {
if (!text) return 'neutral';
for (const char of text) {
const code = char.charCodeAt(0);
// Skip whitespace, numbers, and common punctuation
if (
code <= 0x40 || // Control chars, digits, basic punctuation
(code >= 0x5b && code <= 0x60) || // [ \ ] ^ _ `
(code >= 0x7b && code <= 0x7f) // { | } ~ DEL
) {
continue;
}
// Check if RTL
if (isRTLCharCode(code)) {
return 'rtl';
}
// If we found a non-RTL letter, it's LTR
// Latin, Greek, Cyrillic, etc.
if (
(code >= 0x41 && code <= 0x5a) || // A-Z
(code >= 0x61 && code <= 0x7a) || // a-z
(code >= 0x00c0 && code <= 0x024f) || // Latin Extended
(code >= 0x0370 && code <= 0x03ff) || // Greek
(code >= 0x0400 && code <= 0x04ff) // Cyrillic
) {
return 'ltr';
}
}
return 'neutral';
};
/**
* Check if text contains any RTL characters
* Useful for detecting mixed content
*/
export const containsRTL = (text: string): boolean => {
if (!text) return false;
for (const char of text) {
if (isRTLCharCode(char.charCodeAt(0))) {
return true;
}
}
return false;
};
/**
* Check if text is predominantly RTL
* Returns true if first strong character is RTL
*/
export const isRTL = (text: string): boolean => {
return getTextDirection(text) === 'rtl';
};
/**
* Get the appropriate dir attribute value for HTML elements
* Returns 'rtl', 'ltr', or 'auto' (for neutral/mixed content)
*/
export const getDirAttribute = (text: string): 'rtl' | 'ltr' | 'auto' => {
const direction = getTextDirection(text);
return direction === 'neutral' ? 'auto' : direction;
};