-
Notifications
You must be signed in to change notification settings - Fork 49
Expand file tree
/
Copy pathwrapSpannedStringByWord.ts
More file actions
109 lines (97 loc) · 2.95 KB
/
wrapSpannedStringByWord.ts
File metadata and controls
109 lines (97 loc) · 2.95 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
import { SpannedString } from './SpannedString';
const SPACE_REGEX = /\s/;
/**
* Returns an array of indices where the string should be broken to fit in lines
* of up to `width` characters.
*
* This handles double-width CJK characters
* (https://en.wikipedia.org/wiki/Duospaced_font), and will break words if they
* cannot fit in a line.
*/
function getLineBreaksForString(
string: string,
charWidths: number[],
width: number
): number[] {
const lineBreaks: number[] = [];
let budget = width;
let curLineEnd = 0;
function flushLine() {
lineBreaks.push(curLineEnd);
budget = width;
}
function pushWord(startIndex: number, endIndex: number) {
let wordWidth = 0;
for (let i = startIndex; i < endIndex; i++) {
wordWidth += charWidths[i];
}
// word can fit on current line
if (wordWidth <= budget) {
curLineEnd = endIndex;
budget -= wordWidth;
return;
}
// word can fit in the new line, so start a new one
if (wordWidth <= width) {
flushLine();
curLineEnd = endIndex;
budget -= wordWidth;
return;
}
// word is too long to fit in any line, so lets break it and push each
// part
for (let i = startIndex; i < endIndex; i++) {
const charLength = charWidths[i];
if (budget < charLength) {
flushLine();
}
budget -= charLength;
curLineEnd++;
}
}
let prevIndex = 0;
let curIndex = 1;
let prevIsSpace = SPACE_REGEX.test(string[prevIndex]);
// Add one word at a time
while (curIndex < string.length) {
const isSpace = SPACE_REGEX.test(string[curIndex]);
if (isSpace) {
pushWord(prevIndex, curIndex);
prevIndex = curIndex;
} else if (prevIsSpace) {
pushWord(prevIndex, curIndex);
prevIndex = curIndex;
}
prevIsSpace = isSpace;
curIndex++;
}
if (prevIndex < curIndex) {
pushWord(prevIndex, curIndex);
}
if (budget < width) {
flushLine();
}
return lineBreaks;
}
export function* wrapSpannedStringByWord<T>(
spannedString: SpannedString<T>,
width: number
): Iterable<SpannedString<T>> {
// Short circuit if no wrapping is required
const string = spannedString.getString();
const charWidths = spannedString.getCharWidths();
const stringWidth = charWidths.reduce((a, b) => a + b, 0);
if (stringWidth < width) {
yield spannedString;
return;
}
const lineBreaks = getLineBreaksForString(string, charWidths, width);
let prevLineBreak = 0;
for (const lineBreak of lineBreaks) {
yield spannedString.slice(prevLineBreak, lineBreak);
prevLineBreak = lineBreak;
}
if (prevLineBreak < stringWidth - 1) {
yield spannedString.slice(prevLineBreak);
}
}