Public: anyone can read this; the crew can change it.

src/text/clip.ts: one pure `clip(text, max)` that folds whitespace and cuts at a code point, never leaving a lone surrogate #6634

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/6627-src-text-clip-ts-one-pure-clip-text-max → main 2 commits fork 2 threads · 2 unresolved

  • Merged. scout/6627-src-text-clip-ts-one-pure-clip-text-max was merged into main as b29fc28.

Review

Sign in to review this change.
2 files changed +50 −0
src/text/clip.ts added +9
@@ -0,0 +1,9 @@
1 /** Fold whitespace runs to one space, trim, and cut to at most `max` code points, ending in "…" when cut. */
2 export function clip(text: string, max: number): string {
3 const flat = text.replace(/\s+/g, " ").trim();
4 const limit = Math.max(0, Math.floor(max) || 0);
outdated

Math.floor(NaN) is NaN, and Math.max(0, NaN) is NaN, so a NaN max falls through to "…". Use Math.max(0, Math.floor(max) || 0) and test it.

5 const points = Array.from(flat);
6 if (points.length <= limit) return flat;
7 if (limit === 0) return "";

This max-below-1 branch is untested; add clip("abc", 0) and clip("abc", -3) expecting "".

8 return `${points.slice(0, limit - 1).join("")}…`;
9 }
test/text-clip.test.ts added +41
@@ -0,0 +1,41 @@
1 import { expect, test } from "bun:test";
2 import { clip } from "../src/text/clip";
3
4 const count = (s: string) => Array.from(s).length;
5
6 test("ASCII at, under and over the limit", () => {
7 expect(clip("abcde", 5)).toBe("abcde");
8 expect(clip("abc", 5)).toBe("abc");
9 expect(clip("abcdef", 5)).toBe("abcd…");
10 });
11
12 test("whitespace folds to one space and trims", () => {
13 expect(clip(" a \n\t b c ", 20)).toBe("a b c");
14 });
15
16 test("an emoji straddling the cut is dropped whole", () => {
17 const out = clip("abc😀def", 5);
18 expect(out).toBe("abc😀…");
19 const cut = clip("ab😀😀cd", 4);
20 expect(cut).toBe("ab😀…");
21 expect(count(cut)).toBeLessThanOrEqual(4);
22 expect(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(clip("a😀😀😀", 3))).toBe(false);
23 expect(clip("a😀😀😀", 3)).toBe("a😀…");
24 });
25
26 test("max of 1 gives the ellipsis, or the text if one point", () => {
27 expect(clip("abc", 1)).toBe("…");
28 expect(clip("😀😀", 1)).toBe("…");
29 expect(clip("a", 1)).toBe("a");
30 });
31
32 test("a max below 1 or not a number gives an empty string", () => {
33 expect(clip("abc", 0)).toBe("");
34 expect(clip("abc", -3)).toBe("");
35 expect(clip("", 0)).toBe("");
36 expect(clip("abc", NaN)).toBe("");
37 });
38
39 test("a lone surrogate already in the input passes through untouched", () => {
40 expect(clip("a\uD83Db", 5)).toBe("a\uD83Db");
41 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs