Repository navigation
Expand file tree
/
Copy pathtextwrap.ts
More file actions
194 lines (170 loc) · 5.47 KB
/
Copy pathtextwrap.ts
File metadata and controls
194 lines (170 loc) · 5.47 KB
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
import { BdfFont } from "./bdffont";
export type WrapTextOptions = {
preserveLeadingWhitespace?: boolean;
breakLongWords?: boolean;
};
export function findWrapOpportunities(text: string): number[] {
const offsets: number[] = [];
for (let i = 0; i < text.length; ) {
const codePoint = text.codePointAt(i) ?? 0;
const char = String.fromCodePoint(codePoint);
const next = i + char.length;
if (isWhitespace(char)) {
let end = next;
while (end < text.length) {
const innerCodePoint = text.codePointAt(end) ?? 0;
const innerChar = String.fromCodePoint(innerCodePoint);
if (!isWhitespace(innerChar) || innerChar === "\n") break;
end += innerChar.length;
}
offsets.push(end);
i = end;
continue;
}
if (isBreakAfter(char)) {
offsets.push(next);
}
i = next;
}
return offsets;
}
export function wrapText(font: BdfFont, text: string, targetWidth: number, opts: WrapTextOptions = {}): string[] {
if (targetWidth <= 0) {
return text.length ? text.split("\n") : [""];
}
const paragraphs = text.replace(/\r/g, "").split("\n");
const lines: string[] = [];
for (const paragraph of paragraphs) {
lines.push(...wrapParagraph(font, paragraph, targetWidth, opts));
}
return lines.length ? lines : [""];
}
export function truncateText(font: BdfFont, text: string, maxWidth: number): string {
if (font.measureText(text) <= maxWidth) return text;
let out = text;
while (out.length > 1 && font.measureText(`${out}...`) > maxWidth) {
out = out.slice(0, -1);
}
return `${out}...`;
}
export function truncateLeft(font: BdfFont, text: string, maxWidth: number): string {
if (font.measureText(text) <= maxWidth) return text;
let out = text;
while (out.length > 1 && font.measureText(`...${out}`) > maxWidth) {
out = out.slice(1);
}
return `...${out}`;
}
function wrapParagraph(font: BdfFont, paragraph: string, targetWidth: number, opts: WrapTextOptions): string[] {
if (paragraph.length === 0) {
return [""];
}
const widths = measureOffsets(font, paragraph);
const wrapOffsets = findWrapOpportunities(paragraph);
if (wrapOffsets[wrapOffsets.length - 1] !== paragraph.length) {
wrapOffsets.push(paragraph.length);
}
const lines: string[] = [];
let start = opts.preserveLeadingWhitespace ? 0 : skipLeadingWhitespace(paragraph, 0);
while (start < paragraph.length) {
let best = -1;
for (const candidate of wrapOffsets) {
if (candidate <= start) continue;
if (measureSpan(widths, candidate) - measureSpan(widths, start) <= targetWidth) {
best = candidate;
continue;
}
break;
}
const splitOffset = furthestFittingOffset(widths, start, targetWidth);
if (opts.breakLongWords && splitOffset > start && (best < 0 || splitOffset > trimEndOffset(paragraph, best))) {
best = splitOffset;
}
if (best < 0) {
best = splitOffset;
}
if (best <= start) {
best = nextOffset(widths, start);
}
const lineEnd = trimEndOffset(paragraph, best);
const line = paragraph.slice(start, lineEnd);
lines.push(line);
start = opts.preserveLeadingWhitespace && lines.length === 1 ? best : skipLeadingWhitespace(paragraph, best);
}
return lines.length ? lines : [""];
}
function measureOffsets(font: BdfFont, text: string): Map<number, number> {
const widths = new Map<number, number>();
let total = 0;
widths.set(0, 0);
for (let i = 0; i < text.length; ) {
const codePoint = text.codePointAt(i) ?? 32;
const char = String.fromCodePoint(codePoint);
i += char.length;
total += font.getGlyph(codePoint)?.dwidthX ?? 0;
widths.set(i, total);
}
return widths;
}
function furthestFittingOffset(widths: Map<number, number>, start: number, targetWidth: number): number {
const startWidth = measureSpan(widths, start);
let best = start;
for (const offset of widths.keys()) {
if (offset <= start) continue;
if (measureSpan(widths, offset) - startWidth <= targetWidth) {
best = offset;
continue;
}
break;
}
return best;
}
function nextOffset(widths: Map<number, number>, start: number): number {
for (const offset of widths.keys()) {
if (offset > start) return offset;
}
return start;
}
function measureSpan(widths: Map<number, number>, offset: number): number {
return widths.get(offset) ?? 0;
}
function skipLeadingWhitespace(text: string, offset: number): number {
let cursor = offset;
while (cursor < text.length) {
const codePoint = text.codePointAt(cursor) ?? 0;
const char = String.fromCodePoint(codePoint);
if (!isWhitespace(char) || char === "\n") {
break;
}
cursor += char.length;
}
return cursor;
}
function trimEndOffset(text: string, offset: number): number {
let cursor = offset;
while (cursor > 0) {
const previous = previousOffset(text, cursor);
const char = text.slice(previous, cursor);
if (!isWhitespace(char) || char === "\n") {
break;
}
cursor = previous;
}
return cursor;
}
function previousOffset(text: string, offset: number): number {
let cursor = 0;
let previous = 0;
while (cursor < offset) {
previous = cursor;
const codePoint = text.codePointAt(cursor) ?? 0;
cursor += String.fromCodePoint(codePoint).length;
}
return previous;
}
function isWhitespace(char: string): boolean {
return /\s/.test(char);
}
function isBreakAfter(char: string): boolean {
return char === "-" || char === "/" || char === "," || char === ";" || char === ":";
}