-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Expand file tree
/
Copy pathlayoutParagraph.ts
More file actions
164 lines (141 loc) Β· 4.1 KB
/
layoutParagraph.ts
File metadata and controls
164 lines (141 loc) Β· 4.1 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
import omit from '../run/omit';
import stringHeight from '../attributedString/height';
import generateLineRects from './generateLineRects';
import {
AttributedString,
Container,
Rect,
LayoutOptions,
Paragraph,
} from '../types';
import { Engines } from '../engines';
const ATTACHMENT_CODE = '\ufffc'; // 65532
/**
* Remove attachment attribute if no char present
*
* @param line - Line
* @returns Line
*/
const purgeAttachments = (line: AttributedString) => {
const shouldPurge = !line.string.includes(ATTACHMENT_CODE);
if (!shouldPurge) return line;
const runs = line.runs.map((run) => omit('attachment', run));
return Object.assign({}, line, { runs });
};
/**
* Layout paragraphs inside rectangle
*
* @param rects - Rects
* @param lines - Attributed strings
* @param indent
* @returns layout blocks
*/
const layoutLines = (
rects: Rect[],
lines: AttributedString[],
indent: number,
) => {
let rect = rects.shift();
let currentY = rect.y;
return lines.map((line, i) => {
const lineIndent = i === 0 ? indent : 0;
const style = line.runs?.[0]?.attributes || {};
const height = Math.max(stringHeight(line), style.lineHeight);
if (currentY + height > rect.y + rect.height && rects.length > 0) {
rect = rects.shift();
currentY = rect.y;
}
const newLine: AttributedString = {
string: line.string,
runs: line.runs,
box: {
x: rect.x + lineIndent,
y: currentY,
width: rect.width - lineIndent,
height,
},
};
currentY += height;
return purgeAttachments(newLine);
});
};
type layoutParagraphEngines = Pick<Engines, 'linebreaker'>;
// Mirror Firefox's `text-wrap: balance` cap. Above this, the spec lets us fall
// back to plain wrap to avoid pathological binary-search cost on body copy.
const BALANCE_LINE_LIMIT = 10;
// Width precision (pt) for the balance binary search.
const BALANCE_PRECISION = 1;
/**
* Find the smallest container width that still yields the same number of
* wrapped lines as the natural width. Equalizes line lengths for
* `text-wrap: balance` (titles, short headings).
*/
const computeBalancedWidth = (
linebreak: ReturnType<Engines['linebreaker']>,
paragraph: AttributedString,
width: number,
naturalLineCount: number,
): number => {
let lo = 0;
let hi = width;
while (hi - lo > BALANCE_PRECISION) {
const mid = (lo + hi) / 2;
const lines = linebreak(paragraph, [mid]);
if (lines.length === naturalLineCount) {
hi = mid;
} else {
lo = mid;
}
}
return hi;
};
/**
* Performs line breaking and layout
*
* @param engines - Engines
* @param options - Layout options
*/
const layoutParagraph = (
engines: layoutParagraphEngines,
options: LayoutOptions = {},
) => {
/**
* @param container - Container
* @param paragraph - Attributed string
* @returns Layout block
*/
return (container: Container, paragraph: AttributedString): Paragraph => {
const height = stringHeight(paragraph);
const indent = paragraph.runs?.[0]?.attributes?.indent || 0;
const rects = generateLineRects(container, height);
const linebreak = engines.linebreaker(options);
let availableWidths: number[];
if (options.textWrap === 'nowrap') {
availableWidths = [Infinity];
} else if (options.textWrap === 'balance') {
const naturalWidths = rects.map((r) => r.width);
const naturalLines = linebreak(paragraph, naturalWidths);
if (
naturalLines.length > 1 &&
naturalLines.length <= BALANCE_LINE_LIMIT
) {
const balanced = computeBalancedWidth(
linebreak,
paragraph,
naturalWidths[0],
naturalLines.length,
);
availableWidths = [balanced];
} else {
availableWidths = naturalWidths;
availableWidths.unshift(availableWidths[0] - indent);
}
} else {
availableWidths = rects.map((r) => r.width);
availableWidths.unshift(availableWidths[0] - indent);
}
const lines = linebreak(paragraph, availableWidths);
return layoutLines(rects, lines, indent);
};
};
export default layoutParagraph;