1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151 |
x5
x5
x5
x5
x33
x6
x6
x6
x33
x33
x33
x33
x33
x5
x5
x16
x16
x33
x3
x3
x3
x33
x5
x5
x5
x5
x5
x5
x33
x7
x33
x1
x1
x33
x33
x5
x17
x15
x15
x15
x14
x17
x5
x8
x8
x8
x8
x6
x8 |
|
// Copyright 2018-2026 the Deno authors. MIT license.
// This module is browser compatible.
/** Options for {@linkcode truncate}. */
export interface TruncateOptions {
/**
* The string used to indicate where truncation occurred.
*
* @default {"…"}
*/
suffix?: string;
/**
* Where to truncate.
*
* - `"end"`: `"very long te…"`
* - `"middle"`: `"very l…g text"` (useful for file paths)
* - `"start"`: `"…ery long text"`
*
* @default {"end"}
*/
position?: "end" | "middle" | "start";
}
/**
* Truncates a string to at most `maxLength` UTF-16 code units. When truncation
* occurs the suffix is included within the `maxLength` budget. Surrogate pairs
* are never split, so the result may be shorter than `maxLength` when a cut
* would land inside a pair. When the suffix itself contains surrogate pairs and
* `maxLength` is very small, the result can even be empty.
*
* Note: this function is not grapheme-cluster-aware. Combining characters, flag
* emoji, and ZWJ sequences may still be visually split.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*
* @param str The string to truncate.
* @param maxLength The maximum length of the returned string (must be a
* non-negative integer).
* @param options The truncation options.
* @returns The truncated string.
* @throws {RangeError} If `maxLength` is not a non-negative integer.
* @throws {TypeError} If `position` is not a valid position.
*
* @example End truncation (default)
* ```ts
* import { truncate } from "@std/text/unstable-truncate";
* import { assertEquals } from "@std/assert";
*
* assertEquals(truncate("Hello, world!", 8), "Hello, …");
* assertEquals(truncate("Short", 10), "Short");
* ```
*
* @example Middle truncation
* ```ts
* import { truncate } from "@std/text/unstable-truncate";
* import { assertEquals } from "@std/assert";
*
* assertEquals(
* truncate("src/components/Button.tsx", 18, { position: "middle" }),
* "src/comp…utton.tsx",
* );
* ```
*
* @example Start truncation
* ```ts
* import { truncate } from "@std/text/unstable-truncate";
* import { assertEquals } from "@std/assert";
*
* assertEquals(
* truncate("Hello, world!", 8, { position: "start" }),
* "… world!",
* );
* ```
*
* @example Custom suffix
* ```ts
* import { truncate } from "@std/text/unstable-truncate";
* import { assertEquals } from "@std/assert";
*
* assertEquals(
* truncate("Hello, world!", 10, { suffix: "→" }),
* "Hello, wo→",
* );
* ```
*/
export function truncate(
str: string,
maxLength: number,
options?: TruncateOptions,
): string {
if (!Number.isInteger(maxLength) || maxLength < 0) {
throw new RangeError(
`Cannot truncate: maxLength must be a non-negative integer, received ${maxLength}`,
);
}
if (maxLength === 0) return "";
if (str.length <= maxLength) return str;
const suffix = options?.suffix ?? "\u2026";
const position = options?.position ?? "end";
if (maxLength <= suffix.length) {
return suffix.slice(0, adjustSplitBack(suffix, maxLength));
}
const budget = maxLength - suffix.length;
switch (position) {
case "start": {
const start = str.length - budget;
return suffix + str.slice(adjustSplitForward(str, start));
}
case "middle": {
const leftLen = Math.floor(budget / 2);
const rightLen = budget - leftLen;
const rightStart = str.length - rightLen;
return str.slice(0, adjustSplitBack(str, leftLen)) + suffix +
str.slice(adjustSplitForward(str, rightStart));
}
case "end":
return str.slice(0, adjustSplitBack(str, budget)) + suffix;
default:
throw new TypeError(
`Cannot truncate: position must be "end", "middle", or "start", received "${position}"`,
);
}
}
/**
* If `index` lands on a low surrogate, back up one position so we don't split
* a surrogate pair.
*/
function adjustSplitBack(str: string, index: number): number {
if (index > 0 && index < str.length) {
const code = str.charCodeAt(index);
if (code >= 0xDC00 && code <= 0xDFFF) return index - 1;
}
return index;
}
/**
* If `index` lands on a low surrogate, advance one position so we don't split
* a surrogate pair.
*/
function adjustSplitForward(str: string, index: number): number {
if (index > 0 && index < str.length) {
const code = str.charCodeAt(index);
if (code >= 0xDC00 && code <= 0xDFFF) return index + 1;
}
return index;
}
|