Files
DeepSeek-TUI/web/lib/truncate.ts
Hunter Bown d1fac6e9f5 fix(web): stop cutting GitHub titles through the middle of a character
Defect: the homepage wire strip shortened long titles with
`title.slice(0, 70)`, and the roadmap summaries with
`stripped.slice(0, 137)`. `slice` counts UTF-16 code units, so when the
cut lands between the two halves of a surrogate pair the output ends in
a lone surrogate. GitHub issue, pull request and release titles carry
emoji routinely; the reader sees U+FFFD — the black-diamond question
mark — immediately before the ellipsis. Verified: a title of 69 ASCII
characters followed by U+1F40B yields a string whose last code unit is
0xD83D.

The same `.length` test also miscounted: an astral character was two
characters toward the budget, so a 40-emoji title was treated as
80 characters and truncated at 35.

Fix: one code-point-safe `truncateChars` in lib/truncate.ts, used by
both surfaces. Character budgets and thresholds are unchanged (70 for
the ticker; cut at 140, keep 137 for roadmap notes), so nothing but the
broken cut moves.

Also corrected a false comment in summarizeReleaseBody: it claimed to
strip trailing emoji, which it has never done.

Evidence: lib/truncate.test.ts. Restoring `value.slice(...)` in the
helper fails "never splits an astral character in half" (a lone
surrogate survives the well-formed-pair filter) and "counts code
points, not UTF-16 code units". The last case pins both call sites onto
the shared rule.

npm test 322 passed, npm run lint clean, npx tsc --noEmit clean.

Implemented with agent assistance.
Signed-off-by: Hunter Bown <hmbown@gmail.com>
2026-08-20 16:07:10 -07:00

30 lines
1.0 KiB
TypeScript

/**
* truncate.ts — code-point-safe truncation for text the repository owns
* rather than the site.
*
* `String.prototype.slice` counts UTF-16 code units, so a cut can land
* between the two halves of a surrogate pair. GitHub issue, pull request,
* and release titles routinely carry emoji, and the resulting lone surrogate
* is not a character: it renders as U+FFFD (the black-diamond question
* mark) in every browser, immediately before the ellipsis that says the text
* was shortened.
*/
/**
* `value` unchanged when it is at most `limit` characters long; otherwise its
* first `keep` characters (default: `limit`) followed by `ellipsis`.
*
* Characters are Unicode code points, so an astral character is either kept
* whole or dropped whole.
*/
export function truncateChars(
value: string,
limit: number,
keep: number = limit,
ellipsis = "…",
): string {
const chars = Array.from(value);
if (chars.length <= limit) return value;
return chars.slice(0, Math.max(0, keep)).join("") + ellipsis;
}