From 00a0ea5137df2e47dd90e06f5b9f8ee8a3e84be7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 24 Oct 2026 23:23:44 +0000 Subject: [PATCH 1/2] test(shared): takeover tests for truncate surrogate pairs (#13413) Co-Authored-By: Claude Opus 5.5 --- packages/shared/src/String.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 packages/shared/src/String.test.ts diff --git a/packages/shared/src/String.test.ts b/packages/shared/src/String.test.ts new file mode 100644 index 0000000000000..ffc8f6772053d --- /dev/null +++ b/packages/shared/src/String.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { truncate } from "./String.ts"; + +describe("truncate", () => { + it("cuts before a surrogate pair that would be split", () => { + expect(truncate("abcd😀 more", 5)).toBe("abcd..."); + }); + + it("keeps a surrogate pair that fits exactly", () => { + expect(truncate("abc😀 more", 5)).toBe("abc😀..."); + }); + + it("keeps ASCII and BMP truncation lengths", () => { + expect(truncate(" hello world ", 5)).toBe("hello..."); + expect(truncate("日本語のテキスト", 3)).toBe("日本語..."); + expect(truncate("short", 5)).toBe("short"); + }); +}); From 95c8f6b2250479002501e44ac03560d1ba747a90 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 24 Oct 2026 23:24:47 +0000 Subject: [PATCH 2/2] fix(shared): truncate no longer splits surrogate pairs (#13413) Co-Authored-By: Claude Opus 5.5 --- packages/shared/src/String.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/String.ts b/packages/shared/src/String.ts index c93d0c90cb6bb..feebdf66dc7ac 100644 --- a/packages/shared/src/String.ts +++ b/packages/shared/src/String.ts @@ -1,8 +1,19 @@ +const isHighSurrogate = (code: number) => code >= 0xd800 && code <= 0xdbff; +const isLowSurrogate = (code: number) => code >= 0xdc00 && code <= 0xdfff; + export function truncate(text: string, maxLength = 50): string { const trimmed = text.trim(); if (trimmed.length <= maxLength) { return trimmed; } - return `${trimmed.slice(0, maxLength)}...`; + let head = trimmed.slice(0, maxLength); + // Cutting between the halves of a surrogate pair leaves a lone surrogate that renders as U+FFFD. + if ( + isHighSurrogate(head.charCodeAt(head.length - 1)) && + isLowSurrogate(trimmed.charCodeAt(head.length)) + ) { + head = head.slice(0, -1); + } + return `${head}...`; }