mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-03 14:27:32 +08:00
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>
This commit is contained in:
@@ -10,7 +10,8 @@ describe('preprocessLaTeX', () => {
|
||||
});
|
||||
|
||||
it('does not cut block math at \\right] (Closes #13134)', () => {
|
||||
const content = '\\[ C_{seq}(y|x) = \\frac{1}{|y|} \\sum_{t=1}^{|y|} \\right] \\]';
|
||||
const content =
|
||||
'\\[ C_{seq}(y|x) = \\frac{1}{|y|} \\sum_{t=1}^{|y|} \\right] \\]';
|
||||
const result = preprocessLaTeX(content);
|
||||
expect(result).toContain('\\right]');
|
||||
expect(result).toContain('\\frac{1}{|y|}');
|
||||
|
||||
@@ -5,6 +5,11 @@ import {
|
||||
import { IMessage, Message } from '@/interfaces/database/chat';
|
||||
import { omit } from 'lodash';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import {
|
||||
citationMarkerReg,
|
||||
normalizeCitationDigits,
|
||||
parseCitationIndex,
|
||||
} from './citation-utils';
|
||||
|
||||
export const isConversationIdExist = (conversationId: string) => {
|
||||
return conversationId !== EmptyConversationId && conversationId !== '';
|
||||
@@ -93,8 +98,9 @@ export function setChatVariableEnabledFieldValuePage() {
|
||||
return variableCheckBoxFieldMap;
|
||||
}
|
||||
|
||||
const oldReg = /(#{2}\d+\${2})/g;
|
||||
export const currentReg = /\[ID:(\d+)\]/g;
|
||||
const oldReg = /(#{2}[0-9\u0660-\u0669\u06F0-\u06F9]+\${2})/g;
|
||||
export const currentReg = citationMarkerReg;
|
||||
export { normalizeCitationDigits, parseCitationIndex };
|
||||
|
||||
// To be compatible with the old index matching mode
|
||||
export const replaceTextByOldReg = (text: string) => {
|
||||
|
||||
24
web/src/utils/citation-utils.ts
Normal file
24
web/src/utils/citation-utils.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export const normalizeCitationDigits = (text: string) => {
|
||||
if (!text) return text;
|
||||
return text.replace(/[٠-٩۰-۹]/g, (char) => {
|
||||
const code = char.charCodeAt(0);
|
||||
if (code >= 0x0660 && code <= 0x0669) {
|
||||
return String.fromCharCode(code - 0x0660 + 0x30);
|
||||
}
|
||||
if (code >= 0x06f0 && code <= 0x06f9) {
|
||||
return String.fromCharCode(code - 0x06f0 + 0x30);
|
||||
}
|
||||
return char;
|
||||
});
|
||||
};
|
||||
|
||||
export const parseCitationIndex = (value: string) => {
|
||||
const normalized = normalizeCitationDigits(value);
|
||||
const markerMatch = normalized.match(/\[(?:ID:)?(\d+)\]/);
|
||||
if (markerMatch) return Number(markerMatch[1]);
|
||||
if (/^\d+$/.test(normalized)) return Number(normalized);
|
||||
return Number.NaN;
|
||||
};
|
||||
|
||||
export const citationMarkerReg =
|
||||
/\[(?:ID:)?([0-9\u0660-\u0669\u06F0-\u06F9]+)\]/g;
|
||||
101
web/src/utils/text-direction.ts
Normal file
101
web/src/utils/text-direction.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
Reference in New Issue
Block a user