mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 09:11:22 +00:00
TUI: preserve RTL text order in terminal output
This commit is contained in:
@@ -251,4 +251,25 @@ describe("sanitizeRenderableText", () => {
|
||||
|
||||
expect(sanitized).toBe(input);
|
||||
});
|
||||
|
||||
it("wraps rtl lines with directional isolation marks", () => {
|
||||
const input = "مرحبا بالعالم";
|
||||
const sanitized = sanitizeRenderableText(input);
|
||||
|
||||
expect(sanitized).toBe("\u2067مرحبا بالعالم\u2069");
|
||||
});
|
||||
|
||||
it("only wraps lines that contain rtl script", () => {
|
||||
const input = "hello\nمرحبا";
|
||||
const sanitized = sanitizeRenderableText(input);
|
||||
|
||||
expect(sanitized).toBe("hello\n\u2067مرحبا\u2069");
|
||||
});
|
||||
|
||||
it("does not double-wrap lines that already include bidi controls", () => {
|
||||
const input = "\u2067مرحبا\u2069";
|
||||
const sanitized = sanitizeRenderableText(input);
|
||||
|
||||
expect(sanitized).toBe(input);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,10 @@ const BINARY_LINE_REPLACEMENT_THRESHOLD = 12;
|
||||
const URL_PREFIX_RE = /^(https?:\/\/|file:\/\/)/i;
|
||||
const WINDOWS_DRIVE_RE = /^[a-zA-Z]:[\\/]/;
|
||||
const FILE_LIKE_RE = /^[a-zA-Z0-9._-]+$/;
|
||||
const RTL_SCRIPT_RE = /[\u0590-\u08ff\ufb1d-\ufdff\ufe70-\ufefc]/;
|
||||
const BIDI_CONTROL_RE = /[\u202a-\u202e\u2066-\u2069]/;
|
||||
const RTL_ISOLATE_START = "\u2067";
|
||||
const RTL_ISOLATE_END = "\u2069";
|
||||
|
||||
function hasControlChars(text: string): boolean {
|
||||
for (const char of text) {
|
||||
@@ -91,6 +95,23 @@ function redactBinaryLikeLine(line: string): string {
|
||||
return line;
|
||||
}
|
||||
|
||||
function isolateRtlLine(line: string): string {
|
||||
if (!RTL_SCRIPT_RE.test(line) || BIDI_CONTROL_RE.test(line)) {
|
||||
return line;
|
||||
}
|
||||
return `${RTL_ISOLATE_START}${line}${RTL_ISOLATE_END}`;
|
||||
}
|
||||
|
||||
function applyRtlIsolation(text: string): string {
|
||||
if (!RTL_SCRIPT_RE.test(text)) {
|
||||
return text;
|
||||
}
|
||||
return text
|
||||
.split("\n")
|
||||
.map((line) => isolateRtlLine(line))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function sanitizeRenderableText(text: string): string {
|
||||
if (!text) {
|
||||
return text;
|
||||
@@ -101,7 +122,7 @@ export function sanitizeRenderableText(text: string): string {
|
||||
const hasLongTokens = LONG_TOKEN_TEST_RE.test(text);
|
||||
const hasControls = hasControlChars(text);
|
||||
if (!hasAnsi && !hasReplacementChars && !hasLongTokens && !hasControls) {
|
||||
return text;
|
||||
return applyRtlIsolation(text);
|
||||
}
|
||||
|
||||
const withoutAnsi = hasAnsi ? stripAnsi(text) : text;
|
||||
@@ -112,9 +133,10 @@ export function sanitizeRenderableText(text: string): string {
|
||||
.map((line) => redactBinaryLikeLine(line))
|
||||
.join("\n")
|
||||
: withoutControlChars;
|
||||
return LONG_TOKEN_TEST_RE.test(redacted)
|
||||
const tokenSafe = LONG_TOKEN_TEST_RE.test(redacted)
|
||||
? redacted.replace(LONG_TOKEN_RE, normalizeLongTokenForDisplay)
|
||||
: redacted;
|
||||
return applyRtlIsolation(tokenSafe);
|
||||
}
|
||||
|
||||
export function resolveFinalAssistantText(params: {
|
||||
|
||||
Reference in New Issue
Block a user