@promptctl/rich-js 0.18.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -8
- package/dist/core/box.d.ts +3 -1
- package/dist/core/box.js +6 -13
- package/dist/core/cells.d.ts +61 -28
- package/dist/core/cells.js +171 -124
- package/dist/core/clock.d.ts +48 -0
- package/dist/core/clock.js +42 -0
- package/dist/core/color.d.ts +99 -18
- package/dist/core/color.js +334 -146
- package/dist/core/console.d.ts +38 -7
- package/dist/core/console.js +159 -90
- package/dist/core/easing.d.ts +86 -0
- package/dist/core/easing.js +171 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +16 -0
- package/dist/core/export-html.js +1 -4
- package/dist/core/export-lines.d.ts +22 -13
- package/dist/core/export-lines.js +37 -19
- package/dist/core/export-svg.d.ts +49 -0
- package/dist/core/export-svg.js +213 -0
- package/dist/core/fnv1a.d.ts +10 -0
- package/dist/core/fnv1a.js +17 -0
- package/dist/core/highlighter.d.ts +2 -2
- package/dist/core/highlighter.js +113 -47
- package/dist/core/json.js +1 -1
- package/dist/core/markup.d.ts +28 -7
- package/dist/core/markup.js +235 -312
- package/dist/core/measure.d.ts +3 -3
- package/dist/core/measure.js +12 -7
- package/dist/core/memo.d.ts +19 -0
- package/dist/core/memo.js +34 -0
- package/dist/core/noise.d.ts +31 -0
- package/dist/core/noise.js +94 -0
- package/dist/core/osc8.d.ts +6 -6
- package/dist/core/osc8.js +9 -21
- package/dist/core/paint.d.ts +107 -0
- package/dist/core/paint.js +161 -0
- package/dist/core/place.d.ts +1 -1
- package/dist/core/place.js +4 -6
- package/dist/core/pretty.d.ts +126 -2
- package/dist/core/pretty.js +571 -105
- package/dist/core/protocol.d.ts +43 -3
- package/dist/core/protocol.js +23 -2
- package/dist/core/render.d.ts +33 -30
- package/dist/core/render.js +43 -83
- package/dist/core/segment.d.ts +6 -5
- package/dist/core/segment.js +11 -15
- package/dist/core/selection.d.ts +12 -0
- package/dist/core/selection.js +14 -0
- package/dist/core/strip.d.ts +116 -74
- package/dist/core/strip.js +266 -217
- package/dist/core/style.d.ts +27 -7
- package/dist/core/style.js +106 -41
- package/dist/core/text.d.ts +43 -16
- package/dist/core/text.js +180 -59
- package/dist/core/wrap.d.ts +5 -0
- package/dist/core/wrap.js +16 -3
- package/dist/host/app.d.ts +95 -17
- package/dist/host/app.js +201 -61
- package/dist/host/index.d.ts +4 -1
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +84 -0
- package/dist/host/input.js +498 -0
- package/dist/host/terminal-host.d.ts +15 -2
- package/dist/host/terminal-host.js +2 -0
- package/dist/index.d.ts +26 -14
- package/dist/index.js +17 -7
- package/dist/node/prompt.d.ts +13 -0
- package/dist/node/prompt.js +30 -9
- package/dist/node/save.d.ts +4 -3
- package/dist/node/save.js +6 -3
- package/dist/node/terminal-host.d.ts +10 -2
- package/dist/node/terminal-host.js +7 -0
- package/dist/node/traceback.d.ts +2 -1
- package/dist/node/traceback.js +3 -39
- package/dist/renderables/align.js +2 -9
- package/dist/renderables/columns.d.ts +45 -29
- package/dist/renderables/columns.js +142 -103
- package/dist/renderables/constrain.js +6 -12
- package/dist/renderables/effect.d.ts +113 -0
- package/dist/renderables/effect.js +182 -0
- package/dist/renderables/effects.d.ts +252 -0
- package/dist/renderables/effects.js +565 -0
- package/dist/renderables/embed.d.ts +94 -31
- package/dist/renderables/embed.js +141 -45
- package/dist/renderables/group.d.ts +6 -3
- package/dist/renderables/group.js +5 -0
- package/dist/renderables/layout.d.ts +6 -23
- package/dist/renderables/layout.js +24 -113
- package/dist/renderables/list-view.d.ts +42 -0
- package/dist/renderables/list-view.js +78 -0
- package/dist/renderables/live.d.ts +40 -16
- package/dist/renderables/live.js +96 -83
- package/dist/renderables/markdown.d.ts +16 -2
- package/dist/renderables/markdown.js +521 -118
- package/dist/renderables/padding.d.ts +22 -1
- package/dist/renderables/padding.js +30 -24
- package/dist/renderables/panel.d.ts +31 -33
- package/dist/renderables/panel.js +137 -141
- package/dist/renderables/progress.d.ts +110 -16
- package/dist/renderables/progress.js +212 -81
- package/dist/renderables/progressBar.d.ts +24 -4
- package/dist/renderables/progressBar.js +78 -23
- package/dist/renderables/prompt.d.ts +37 -15
- package/dist/renderables/prompt.js +125 -85
- package/dist/renderables/ratio.d.ts +123 -0
- package/dist/renderables/ratio.js +213 -0
- package/dist/renderables/rule.d.ts +3 -2
- package/dist/renderables/rule.js +50 -52
- package/dist/renderables/spinner.d.ts +28 -2
- package/dist/renderables/spinner.js +52 -19
- package/dist/renderables/status.d.ts +13 -7
- package/dist/renderables/status.js +15 -29
- package/dist/renderables/syntax.d.ts +37 -7
- package/dist/renderables/syntax.js +134 -83
- package/dist/renderables/table.d.ts +58 -20
- package/dist/renderables/table.js +373 -197
- package/dist/renderables/traceback.d.ts +5 -4
- package/dist/renderables/traceback.js +239 -68
- package/dist/renderables/tree.d.ts +10 -2
- package/dist/renderables/tree.js +61 -34
- package/dist/renderables/viewport.d.ts +30 -0
- package/dist/renderables/viewport.js +61 -7
- package/dist/template-bindings/color-funcs.d.ts +31 -9
- package/dist/template-bindings/color-funcs.js +40 -14
- package/dist/template-bindings/helpers.d.ts +4 -2
- package/dist/template-bindings/helpers.js +5 -20
- package/dist/template-bindings/index.d.ts +29 -17
- package/dist/template-bindings/index.js +36 -20
- package/dist/template-bindings/palette-funcs.d.ts +3 -1
- package/dist/template-bindings/palette-funcs.js +11 -7
- package/dist/template-bindings/style-funcs.js +4 -2
- package/dist/themes/buildPalette.d.ts +24 -3
- package/dist/themes/buildPalette.js +52 -8
- package/dist/themes/colorMath.d.ts +29 -17
- package/dist/themes/colorMath.js +74 -48
- package/dist/themes/colorRef.d.ts +2 -17
- package/dist/themes/colorRef.js +13 -31
- package/dist/themes/palette.d.ts +24 -0
- package/dist/themes/palette.js +46 -3
- package/dist/themes/ramp.d.ts +21 -34
- package/dist/themes/ramp.js +23 -42
- package/dist/themes/registry.js +39 -28
- package/dist/widgets/button.d.ts +2 -6
- package/dist/widgets/button.js +9 -40
- package/dist/widgets/checkbox.d.ts +2 -5
- package/dist/widgets/checkbox.js +9 -31
- package/dist/widgets/dropdown.d.ts +2 -5
- package/dist/widgets/dropdown.js +17 -51
- package/dist/widgets/event-router.d.ts +31 -40
- package/dist/widgets/event-router.js +52 -483
- package/dist/widgets/index.d.ts +2 -1
- package/dist/widgets/ink.d.ts +22 -0
- package/dist/widgets/ink.js +31 -0
- package/dist/widgets/slider.d.ts +2 -5
- package/dist/widgets/slider.js +11 -34
- package/dist/widgets/text-input.d.ts +19 -10
- package/dist/widgets/text-input.js +156 -147
- package/dist/widgets/themed-widget.d.ts +20 -0
- package/dist/widgets/themed-widget.js +89 -0
- package/dist/widgets/toggle.d.ts +2 -5
- package/dist/widgets/toggle.js +8 -31
- package/dist/widgets/types.d.ts +8 -25
- package/dist/widgets/types.js +8 -0
- package/dist/widgets/widget-app.d.ts +24 -7
- package/dist/widgets/widget-app.js +35 -18
- package/dist/widgets/widget-base.d.ts +2 -0
- package/dist/widgets/widget-base.js +9 -2
- package/package.json +31 -17
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Table — tabular data with headers, borders, auto-sizing, and alignment.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { cellCount as cells } from "../core/cells.js";
|
|
5
5
|
import { Segment } from "../core/segment.js";
|
|
6
6
|
import { NULL_STYLE } from "../core/style.js";
|
|
7
7
|
import { HEAVY_HEAD } from "../core/box.js";
|
|
8
8
|
import { RichText } from "../core/text.js";
|
|
9
|
-
import { embed,
|
|
9
|
+
import { EmbeddedText, embed, present } from "./embed.js";
|
|
10
10
|
import { normalizePadding } from "./padding.js";
|
|
11
|
-
import {
|
|
11
|
+
import { exactWeights, ratioDistribute, ratioReduce } from "./ratio.js";
|
|
12
|
+
import { getStyle, stackedHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
13
|
+
import { Measurement } from "../core/measure.js";
|
|
12
14
|
/** A `want` no budget can satisfy: the column takes every cell its weight earns. */
|
|
13
15
|
const UNBOUNDED = Number.MAX_SAFE_INTEGER;
|
|
14
16
|
/**
|
|
@@ -37,8 +39,7 @@ const blank = (n) => (n > 0 ? [new Segment(" ".repeat(n))] : []);
|
|
|
37
39
|
* than an optimization: handing cells out one at a time made the iteration
|
|
38
40
|
* count the width itself, which stalled on a very wide table and — since
|
|
39
41
|
* `Infinity - 1 === Infinity` — never terminated at all for a `maxWidth` of
|
|
40
|
-
* `Infinity
|
|
41
|
-
* one round.
|
|
42
|
+
* `Infinity`. Capping by want reaches that case in one round.
|
|
42
43
|
*
|
|
43
44
|
* The arithmetic is exact. A share's fraction is what decides who gets a
|
|
44
45
|
* leftover cell, and in floating point two fractions that are equal — 6/14 and
|
|
@@ -98,24 +99,55 @@ function distribute(total, demands) {
|
|
|
98
99
|
return granted;
|
|
99
100
|
}
|
|
100
101
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
102
|
+
* `wants` cut back until they sum to `budget`, in the order the reference
|
|
103
|
+
* narrows a table wider than its width in `Table._calculate_column_widths`:
|
|
104
|
+
* first the widest `wrapable` columns, each round down to the next widest
|
|
105
|
+
* (`_collapse_widths`), and then, the last resort, the excess taken from every
|
|
106
|
+
* column evenly (`ratio_reduce(excess, [1] * n, widths, widths)`). Wants that
|
|
107
|
+
* fit come back as they are.
|
|
108
|
+
*
|
|
109
|
+
* A column's width is its want plus the `kept` cells — seat and padding — the
|
|
110
|
+
* table paid it first and it never gives up. One divergence: the reference's
|
|
111
|
+
* even cut runs once, and where a column too narrow to give its share leaves
|
|
112
|
+
* cells over, the table draws past its width. Here the cut repeats over the
|
|
113
|
+
* columns with cells left, so the table fits. Each round either fits or
|
|
114
|
+
* empties a column, so it ends.
|
|
107
115
|
*/
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
function reduceToFit(budget, demands) {
|
|
117
|
+
let wants = demands.map(({ want }) => want);
|
|
118
|
+
const excess = () => wants.reduce((sum, want) => sum + want, 0) - budget;
|
|
119
|
+
for (let over = excess(); over > 0; over = excess()) {
|
|
120
|
+
// A column with nothing left to give, or that may not wrap, is out of the round.
|
|
121
|
+
const widths = wants.map((want, index) => (demands[index].wrapable && want > 0 ? demands[index].kept + want : 0));
|
|
122
|
+
const widest = Math.max(...widths);
|
|
123
|
+
if (widest === 0)
|
|
124
|
+
break;
|
|
125
|
+
const next = Math.max(...widths.map((width) => (width === widest ? 0 : width)));
|
|
126
|
+
wants = ratioReduce(over, widths.map((width) => (width === widest ? 1 : 0)), wants.map((want) => Math.min(over, widest - next, want)), wants);
|
|
127
|
+
}
|
|
128
|
+
for (let over = excess(); over > 0; over = excess()) {
|
|
129
|
+
wants = ratioReduce(over, wants.map(() => 1), wants, wants);
|
|
130
|
+
}
|
|
131
|
+
return wants;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The most padding every column can have alike: each side is granted
|
|
135
|
+
* `min(want, level)`, and `level` is the highest those grants fit `budget` at.
|
|
136
|
+
* A padding level is bought for every column at once or not at all, so a
|
|
137
|
+
* budget too small for everyone's padding leaves it uneven by no column.
|
|
138
|
+
*/
|
|
139
|
+
function padLevel(wants, budget) {
|
|
140
|
+
const sorted = [...wants].sort((a, b) => a - b);
|
|
141
|
+
let left = budget;
|
|
142
|
+
for (const [rank, want] of sorted.entries()) {
|
|
143
|
+
const sharing = sorted.length - rank;
|
|
144
|
+
if (want * sharing > left)
|
|
145
|
+
return Math.floor(left / sharing);
|
|
146
|
+
left -= want;
|
|
147
|
+
}
|
|
148
|
+
return sorted.at(-1) ?? 0;
|
|
116
149
|
}
|
|
117
|
-
function layoutTable(outerWidth, demands,
|
|
118
|
-
const [, padRightWanted, , padLeftWanted] = padding;
|
|
150
|
+
function layoutTable(outerWidth, demands, frame) {
|
|
119
151
|
// Plain `number`, not the `CellCol` `cells` hands back: this budget is spent
|
|
120
152
|
// down by arithmetic, and arithmetic on a branded type produces a number.
|
|
121
153
|
let budget = cells(outerWidth);
|
|
@@ -143,32 +175,38 @@ function layoutTable(outerWidth, demands, padding, frame) {
|
|
|
143
175
|
seats.push(seat);
|
|
144
176
|
}
|
|
145
177
|
const seated = seats.length;
|
|
146
|
-
// Padding is
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
budget -= per * seated;
|
|
154
|
-
return per;
|
|
178
|
+
// Padding is bought one side at a time, to one level across every seated
|
|
179
|
+
// column (`padLevel`), so no column is padded while another that wants the
|
|
180
|
+
// same padding goes without.
|
|
181
|
+
const seatedDemands = demands.slice(0, seated);
|
|
182
|
+
const takeLevelled = (wants) => {
|
|
183
|
+
const level = padLevel(wants, budget);
|
|
184
|
+
return wants.map((want) => take(Math.min(want, level)));
|
|
155
185
|
};
|
|
156
|
-
const padLeft =
|
|
157
|
-
const padRight =
|
|
186
|
+
const padLeft = takeLevelled(seatedDemands.map((demand) => demand.pad[0]));
|
|
187
|
+
const padRight = takeLevelled(seatedDemands.map((demand) => demand.pad[1]));
|
|
158
188
|
// Reservations are paid in column order, each already holding the cell the
|
|
159
189
|
// seating pass gave it. A budget too small to cover them all runs out
|
|
160
190
|
// partway, which costs the trailing columns their width but never costs the
|
|
161
191
|
// table its frame.
|
|
162
|
-
const seatedDemands = demands.slice(0, seated);
|
|
163
192
|
const reserved = seatedDemands.map((demand, index) => take(demand.reserved - seats[index]));
|
|
164
|
-
// What is left to apportion is the rest of what each column wanted
|
|
193
|
+
// What is left to apportion is the rest of what each column wanted: its
|
|
194
|
+
// minimum first, for every column, and only then the width past it. A table
|
|
165
195
|
// whose columns all fit leaves this budget partly unspent.
|
|
166
|
-
const
|
|
167
|
-
|
|
196
|
+
const spent = (granted) => granted.reduce((sum, width) => sum + width, 0);
|
|
197
|
+
const base = seatedDemands.map((_, index) => seats[index] + reserved[index]);
|
|
198
|
+
const holding = (...passes) => base.map((start, index) => passes.reduce((sum, granted) => sum + granted[index], start));
|
|
199
|
+
const needed = reduceToFit(budget, seatedDemands.map((demand, index) => ({
|
|
200
|
+
want: Math.max(0, demand.minimum - base[index]),
|
|
201
|
+
kept: padLeft[index] + base[index] + padRight[index],
|
|
202
|
+
wrapable: demand.wrapable,
|
|
203
|
+
})));
|
|
204
|
+
const afterNeeded = holding(needed);
|
|
205
|
+
const beyond = distribute(budget - spent(needed), seatedDemands.map((demand, index) => ({
|
|
206
|
+
want: Math.max(0, demand.want - afterNeeded[index]),
|
|
168
207
|
weight: demand.weight,
|
|
169
208
|
})));
|
|
170
|
-
const
|
|
171
|
-
const holding = (...passes) => seatedDemands.map((_, index) => passes.reduce((sum, granted) => sum + granted[index], seats[index] + reserved[index]));
|
|
209
|
+
const wanted = needed.map((width, index) => width + beyond[index]);
|
|
172
210
|
// The unspent part goes first to the columns with nothing to size to, each
|
|
173
211
|
// alike, up to its `fill` — the reference's `Measurement(1, max_width)`, a
|
|
174
212
|
// maximum of the whole offer that no content measured.
|
|
@@ -177,23 +215,56 @@ function layoutTable(outerWidth, demands, padding, frame) {
|
|
|
177
215
|
want: Math.max(0, demand.fill - afterWant[index]),
|
|
178
216
|
weight: demand.fill > 0 ? 1 : 0,
|
|
179
217
|
})));
|
|
180
|
-
// Then to the ratio columns,
|
|
181
|
-
|
|
218
|
+
// Then to the ratio columns, by the reference's flexible split:
|
|
219
|
+
// `ratio_distribute(max_width - fixed_widths, ratios, flex_minimum)`. What
|
|
220
|
+
// is split is every cell the bounded columns leave, the cells the ratio
|
|
221
|
+
// columns already hold and their padding included, because Rich's widths
|
|
222
|
+
// are padded widths; each is held to at least its padding and its floor.
|
|
223
|
+
// The floor is the reference's `column.width or 1`, reading `minWidth`
|
|
224
|
+
// where there is no `width`, where the reference reads none and draws a
|
|
225
|
+
// `min_width=12` ratio column five cells wide.
|
|
226
|
+
// A ratio column is paid the rest of its part out of the budget, in column
|
|
227
|
+
// order, so a part the minimums push past what is left is cut there rather
|
|
228
|
+
// than drawn past the width.
|
|
229
|
+
const afterFill = holding(wanted, filled);
|
|
230
|
+
const flexible = seatedDemands.flatMap((demand, index) => (demand.ratio > 0 ? [index] : []));
|
|
231
|
+
const padded = (index, content) => padLeft[index] + content + padRight[index];
|
|
232
|
+
// `UNBOUNDED` holds an unbounded offer, as it does for the stretch below;
|
|
233
|
+
// it caps the whole split, so the total stays an integer a double holds.
|
|
234
|
+
const pool = budget - spent(wanted) - spent(filled);
|
|
235
|
+
let unshared = Math.min(pool, UNBOUNDED);
|
|
236
|
+
const parts = ratioDistribute(Math.min(pool + flexible.reduce((sum, index) => sum + padded(index, afterFill[index]), 0), UNBOUNDED), flexible.map((index) => seatedDemands[index].ratio), flexible.map((index) => padded(index, seatedDemands[index].want)));
|
|
237
|
+
const shared = seatedDemands.map(() => 0);
|
|
238
|
+
flexible.forEach((index, slot) => {
|
|
239
|
+
shared[index] = Math.min(Math.max(0, parts[slot] - padded(index, afterFill[index])), unshared);
|
|
240
|
+
unshared -= shared[index];
|
|
241
|
+
});
|
|
182
242
|
// What the shares leave goes to the columns that stretch, by the width each
|
|
183
|
-
// already holds: its natural width, since nothing is left
|
|
184
|
-
// column is short, or the cap a fill stopped at. The
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
243
|
+
// already holds with its padding: its natural width, since nothing is left
|
|
244
|
+
// over while any column is short, or the cap a fill stopped at. The split is
|
|
245
|
+
// the reference's `ratio_distribute(max_width - table_width, widths)`,
|
|
246
|
+
// rounding included; the columns it runs over are this port's. A table that
|
|
247
|
+
// does not expand has no column that stretches, and an empty list takes
|
|
248
|
+
// nothing, which is how it stays narrower than the width it was offered. The
|
|
249
|
+
// order is the reference's: Rich pads an expanding table only
|
|
250
|
+
// once `table_width < max_width`, never while it is collapsing a column.
|
|
190
251
|
const held = holding(wanted, filled, shared);
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
252
|
+
const stretchers = seatedDemands.flatMap((demand, index) => (demand.stretch ? [index] : []));
|
|
253
|
+
const stretches = ratioDistribute(
|
|
254
|
+
// An unbounded offer is held to `UNBOUNDED`, this model's own infinity, as
|
|
255
|
+
// `demandCells` holds a want: `Infinity` is not an integer to split.
|
|
256
|
+
Math.min(budget - spent(wanted) - spent(filled) - spent(shared), UNBOUNDED), stretchers.map((index) => padded(index, held[index])));
|
|
257
|
+
// A table that does not fit gives back each column's `floor`: Rich
|
|
258
|
+
// re-measures a collapsed table's columns at the widths it gave them, and a
|
|
259
|
+
// column with nothing to draw measures 0 there however many cells the collapse
|
|
260
|
+
// left it. Those cells go unspent, as they do in Rich.
|
|
261
|
+
const natural = demands.reduce((sum, demand) => sum + demand.pad[0] + demand.want + demand.pad[1], frame.edge * 2 + Math.max(0, demands.length - 1) * frame.divider);
|
|
262
|
+
const squeezed = natural > outerWidth;
|
|
263
|
+
const columns = held.map((width, index) => squeezed ? width - Math.min(width, seatedDemands[index].floor) : width);
|
|
264
|
+
stretchers.forEach((index, slot) => {
|
|
265
|
+
columns[index] += stretches[slot];
|
|
266
|
+
});
|
|
267
|
+
const cellWidths = columns.map((width, index) => padded(index, width));
|
|
197
268
|
return {
|
|
198
269
|
edge,
|
|
199
270
|
divider: frame.divider,
|
|
@@ -232,6 +303,7 @@ export class Column {
|
|
|
232
303
|
ratio;
|
|
233
304
|
noWrap;
|
|
234
305
|
overflow;
|
|
306
|
+
highlight;
|
|
235
307
|
_cells;
|
|
236
308
|
constructor(options) {
|
|
237
309
|
this.header = options?.header;
|
|
@@ -246,15 +318,15 @@ export class Column {
|
|
|
246
318
|
this.ratio = options?.ratio;
|
|
247
319
|
this.noWrap = options?.noWrap ?? false;
|
|
248
320
|
this.overflow = options?.overflow ?? "ellipsis";
|
|
321
|
+
this.highlight = options?.highlight ?? false;
|
|
249
322
|
this._cells = [];
|
|
250
323
|
}
|
|
251
324
|
/**
|
|
252
|
-
* The two stamped cells,
|
|
325
|
+
* The two stamped cells, stamped on assignment rather than at the constructor.
|
|
253
326
|
* `Table.columns` hands out the live column and both fields are public, so a
|
|
254
327
|
* constructor-only stamp held only until the first `columns[0].footer = mine`
|
|
255
|
-
* — which installed content
|
|
256
|
-
*
|
|
257
|
-
* assumes `embeddedText` has been through. [LAW:parse-dont-validate] The setter
|
|
328
|
+
* — which installed content still owned by the caller into a slot every
|
|
329
|
+
* reader below assumes `embed` has been through. [LAW:parse-dont-validate] The setter
|
|
258
330
|
* is the border, so the guarantee holds for the object's whole lifetime and
|
|
259
331
|
* the constructor is one caller of it rather than the one place it is true.
|
|
260
332
|
*
|
|
@@ -263,20 +335,20 @@ export class Column {
|
|
|
263
335
|
* a column always has one and `show_footer` alone decides whether it is
|
|
264
336
|
* drawn. Modelling the absence as `undefined` instead made "no column has a
|
|
265
337
|
* footer" a state the render path could ask about — and it did, skipping the
|
|
266
|
-
* row a caller had asked for. `
|
|
338
|
+
* row a caller had asked for. `embed` already maps nothing onto empty,
|
|
267
339
|
* which is why the setters take `undefined` rather than defaulting around it.
|
|
268
340
|
*/
|
|
269
341
|
get header() {
|
|
270
342
|
return this._header;
|
|
271
343
|
}
|
|
272
344
|
set header(content) {
|
|
273
|
-
this._header =
|
|
345
|
+
this._header = embed(content);
|
|
274
346
|
}
|
|
275
347
|
get footer() {
|
|
276
348
|
return this._footer;
|
|
277
349
|
}
|
|
278
350
|
set footer(content) {
|
|
279
|
-
this._footer =
|
|
351
|
+
this._footer = embed(content);
|
|
280
352
|
}
|
|
281
353
|
get flexible() {
|
|
282
354
|
return columnShare(this) > 0;
|
|
@@ -291,11 +363,6 @@ export class Column {
|
|
|
291
363
|
}
|
|
292
364
|
copy() {
|
|
293
365
|
const col = new Column({
|
|
294
|
-
// No `.copy()` here: the crossing the constructor routes through copies
|
|
295
|
-
// a `RichText` already, and a second copy is a second home for that rule.
|
|
296
|
-
// [LAW:single-enforcer]
|
|
297
|
-
header: this.header,
|
|
298
|
-
footer: this.footer,
|
|
299
366
|
justify: this.justify,
|
|
300
367
|
width: this.width,
|
|
301
368
|
minWidth: this.minWidth,
|
|
@@ -303,10 +370,15 @@ export class Column {
|
|
|
303
370
|
ratio: this.ratio,
|
|
304
371
|
noWrap: this.noWrap,
|
|
305
372
|
overflow: this.overflow,
|
|
373
|
+
highlight: this.highlight,
|
|
306
374
|
});
|
|
307
375
|
col.headerStyle = this.headerStyle;
|
|
308
376
|
col.footerStyle = this.footerStyle;
|
|
309
377
|
col.style = this.style;
|
|
378
|
+
// Shared, not copied, as a row's cells are: `embed` already copied a
|
|
379
|
+
// caller's `RichText`, and what it hands back is never changed.
|
|
380
|
+
col._header = this._header;
|
|
381
|
+
col._footer = this._footer;
|
|
310
382
|
return col;
|
|
311
383
|
}
|
|
312
384
|
}
|
|
@@ -314,14 +386,16 @@ export class Table {
|
|
|
314
386
|
_columns;
|
|
315
387
|
_rows;
|
|
316
388
|
box;
|
|
317
|
-
|
|
318
|
-
|
|
389
|
+
_title;
|
|
390
|
+
_caption;
|
|
319
391
|
expand;
|
|
320
392
|
showHeader;
|
|
321
393
|
showFooter;
|
|
322
394
|
showLines;
|
|
323
395
|
showEdge;
|
|
324
396
|
padding;
|
|
397
|
+
collapsePadding;
|
|
398
|
+
padEdge;
|
|
325
399
|
style;
|
|
326
400
|
headerStyle;
|
|
327
401
|
footerStyle;
|
|
@@ -333,14 +407,23 @@ export class Table {
|
|
|
333
407
|
tableWidth;
|
|
334
408
|
minWidth;
|
|
335
409
|
rowStyles;
|
|
410
|
+
highlight;
|
|
411
|
+
get title() {
|
|
412
|
+
return this._title;
|
|
413
|
+
}
|
|
414
|
+
get caption() {
|
|
415
|
+
return this._caption;
|
|
416
|
+
}
|
|
336
417
|
constructor(options) {
|
|
337
418
|
this._columns = [];
|
|
338
419
|
this._rows = [];
|
|
339
420
|
this.box = options?.box !== undefined ? options.box : HEAVY_HEAD;
|
|
421
|
+
// An empty title draws no row, as the reference's `if self.title` draws
|
|
422
|
+
// none; markup that styles nothing is a title, and draws a blank one.
|
|
340
423
|
const titleVal = options?.title;
|
|
341
|
-
this.
|
|
424
|
+
this._title = present(titleVal) ? new EmbeddedText(titleVal) : undefined;
|
|
342
425
|
const captionVal = options?.caption;
|
|
343
|
-
this.
|
|
426
|
+
this._caption = present(captionVal) ? new EmbeddedText(captionVal) : undefined;
|
|
344
427
|
// A declared width is the table's size, not a ceiling on it: the
|
|
345
428
|
// reference's `expand` is `self._expand or self.width is not None`, and a
|
|
346
429
|
// port that sized to content inside it drew Rich code narrower than Rich.
|
|
@@ -350,6 +433,8 @@ export class Table {
|
|
|
350
433
|
this.showLines = options?.showLines ?? false;
|
|
351
434
|
this.showEdge = options?.showEdge !== false;
|
|
352
435
|
this.padding = normalizePadding(options?.padding ?? [0, 1, 0, 1]);
|
|
436
|
+
this.collapsePadding = options?.collapsePadding ?? false;
|
|
437
|
+
this.padEdge = options?.padEdge ?? true;
|
|
353
438
|
this.style = options?.style ?? NULL_STYLE;
|
|
354
439
|
this.headerStyle = options?.headerStyle ?? "table.header";
|
|
355
440
|
this.footerStyle = options?.footerStyle ?? "table.footer";
|
|
@@ -361,6 +446,7 @@ export class Table {
|
|
|
361
446
|
this.tableWidth = options?.width;
|
|
362
447
|
this.minWidth = options?.minWidth;
|
|
363
448
|
this.rowStyles = options?.rowStyles ?? [];
|
|
449
|
+
this.highlight = options?.highlight ?? false;
|
|
364
450
|
}
|
|
365
451
|
get columns() {
|
|
366
452
|
return this._columns;
|
|
@@ -369,7 +455,7 @@ export class Table {
|
|
|
369
455
|
return this._rows.length;
|
|
370
456
|
}
|
|
371
457
|
addColumn(header, options) {
|
|
372
|
-
const col = new Column({ ...options, header: header ?? options?.header });
|
|
458
|
+
const col = new Column({ ...options, header: header ?? options?.header, highlight: options?.highlight ?? this.highlight });
|
|
373
459
|
this._columns.push(col);
|
|
374
460
|
return this;
|
|
375
461
|
}
|
|
@@ -426,11 +512,12 @@ export class Table {
|
|
|
426
512
|
const borderStyle = getStyle(options, this.style).add(getStyle(options, this.borderStyle));
|
|
427
513
|
const border = borderStyle.isNull ? undefined : borderStyle;
|
|
428
514
|
// The one division of the width every row below is measured against.
|
|
429
|
-
const
|
|
515
|
+
const outerWidth = this._outerWidth(options);
|
|
516
|
+
const geometry = this._geometry(options, outerWidth);
|
|
430
517
|
const edge = geometry.edge === 1;
|
|
431
518
|
// Title
|
|
432
|
-
if (this.
|
|
433
|
-
yield* this._renderTitle(options, this.
|
|
519
|
+
if (this._title) {
|
|
520
|
+
yield* this._renderTitle(options, this._title, geometry.totalWidth, this.titleStyle, this.titleJustify);
|
|
434
521
|
}
|
|
435
522
|
// Top border
|
|
436
523
|
if (box && this.showEdge) {
|
|
@@ -442,11 +529,16 @@ export class Table {
|
|
|
442
529
|
const footerStyle = getStyle(options, this.footerStyle);
|
|
443
530
|
const columnStyles = this._columns.map((c) => getStyle(options, c.style));
|
|
444
531
|
const stripes = this.rowStyles.map((s) => getStyle(options, s));
|
|
532
|
+
// Each row's padding above and below, by where it stands among every row
|
|
533
|
+
// the table draws, header and footer included, as the reference counts them.
|
|
534
|
+
const firstBody = this.showHeader ? 1 : 0;
|
|
535
|
+
const lastRow = firstBody + this._rows.length + (this.showFooter ? 1 : 0) - 1;
|
|
536
|
+
const rowPadding = (position) => this._rowPadding(position === 0, position === lastRow);
|
|
445
537
|
// Header row
|
|
446
538
|
if (this.showHeader) {
|
|
447
539
|
const headerCells = this._columns.map((c) => c.header);
|
|
448
540
|
const headerStyles = this._columns.map((c) => headerStyle.add(getStyle(options, c.headerStyle)));
|
|
449
|
-
yield* this._renderRow(options, headerCells, headerStyles, NULL_STYLE, geometry, box, "head", border);
|
|
541
|
+
yield* this._renderRow(options, headerCells, headerStyles, NULL_STYLE, geometry, rowPadding(0), box, "head", border);
|
|
450
542
|
// Header separator
|
|
451
543
|
if (box) {
|
|
452
544
|
yield* box.getRow(geometry.cellWidths, "head", border, edge);
|
|
@@ -455,10 +547,10 @@ export class Table {
|
|
|
455
547
|
// Data rows
|
|
456
548
|
for (let rowIdx = 0; rowIdx < this._rows.length; rowIdx++) {
|
|
457
549
|
const row = this._rows[rowIdx];
|
|
458
|
-
const rowCells = this._columns.map((_, colIdx) => row.cells[colIdx] ??
|
|
550
|
+
const rowCells = this._columns.map((_, colIdx) => row.cells[colIdx] ?? new EmbeddedText(undefined));
|
|
459
551
|
const rowStyle = stripes[rowIdx % stripes.length] ?? NULL_STYLE;
|
|
460
552
|
const cellStyles = columnStyles.map((s) => s.add(rowStyle));
|
|
461
|
-
yield* this._renderRow(options, rowCells, cellStyles, rowStyle, geometry, box, "row", border);
|
|
553
|
+
yield* this._renderRow(options, rowCells, cellStyles, rowStyle, geometry, rowPadding(firstBody + rowIdx), box, "row", border);
|
|
462
554
|
// Row separator
|
|
463
555
|
const showSep = this.showLines || row.endSection;
|
|
464
556
|
if (showSep && box && rowIdx < this._rows.length - 1) {
|
|
@@ -472,21 +564,21 @@ export class Table {
|
|
|
472
564
|
}
|
|
473
565
|
const footerCells = this._columns.map((c) => c.footer);
|
|
474
566
|
const footerStyles = this._columns.map((c) => footerStyle.add(getStyle(options, c.footerStyle)));
|
|
475
|
-
yield* this._renderRow(options, footerCells, footerStyles, NULL_STYLE, geometry, box, "foot", border);
|
|
567
|
+
yield* this._renderRow(options, footerCells, footerStyles, NULL_STYLE, geometry, rowPadding(lastRow), box, "foot", border);
|
|
476
568
|
}
|
|
477
569
|
// Bottom border
|
|
478
570
|
if (box && this.showEdge) {
|
|
479
571
|
yield* box.getBottom(geometry.cellWidths, border, edge);
|
|
480
572
|
}
|
|
481
573
|
// Caption
|
|
482
|
-
if (this.
|
|
483
|
-
yield* this._renderTitle(options, this.
|
|
574
|
+
if (this._caption) {
|
|
575
|
+
yield* this._renderTitle(options, this._caption, geometry.totalWidth, this.captionStyle, this.captionJustify);
|
|
484
576
|
}
|
|
485
577
|
}
|
|
486
578
|
/**
|
|
487
579
|
* [LAW:one-source-of-truth] Both ends of the range are widths the geometry
|
|
488
580
|
* actually produced — the maximum from the demands as they stand, the
|
|
489
|
-
* minimum from the same layout with every column asking for
|
|
581
|
+
* minimum from the same layout with every column asking for its minimum.
|
|
490
582
|
* Neither end stretches or fills into an offer: both only spend cells an
|
|
491
583
|
* offer happens to leave over, and a renderable reports the width its content
|
|
492
584
|
* wants rather than the width it was offered. A declared `width` is not an
|
|
@@ -503,7 +595,7 @@ export class Table {
|
|
|
503
595
|
const options = withCellWidth(rawOptions);
|
|
504
596
|
const outerWidth = this._outerWidth(options);
|
|
505
597
|
const frame = this._frame();
|
|
506
|
-
const demands = this._columnDemands();
|
|
598
|
+
const demands = this._columnDemands(options, outerWidth);
|
|
507
599
|
// A declared width is the one stretch a measurement reports, because it is
|
|
508
600
|
// a size the table was given rather than an offer it grew into — the
|
|
509
601
|
// reference measures `self.width` as its maximum, so a `Panel` fitted round
|
|
@@ -514,30 +606,34 @@ export class Table {
|
|
|
514
606
|
// back to the content, as an unbounded `expand` does.
|
|
515
607
|
const declared = this.tableWidth === undefined
|
|
516
608
|
? undefined
|
|
517
|
-
: layoutTable(outerWidth, demands,
|
|
609
|
+
: layoutTable(outerWidth, demands, frame).totalWidth;
|
|
518
610
|
const laidOut = declared !== undefined && declared < UNBOUNDED
|
|
519
611
|
? declared
|
|
520
|
-
: layoutTable(outerWidth, demands.map((demand) => ({ ...demand, fill: 0, stretch: false })),
|
|
612
|
+
: layoutTable(outerWidth, demands.map((demand) => ({ ...demand, fill: 0, stretch: false })), frame).totalWidth;
|
|
521
613
|
// `UNBOUNDED` is this table's own infinity, so a layout that reached it has
|
|
522
614
|
// no natural width to report — a column asked for every cell there is. Said
|
|
523
615
|
// as the number, it escapes as a width a caller would try to draw:
|
|
524
616
|
// `minWidth: Infinity` measured 18014398509481988 and rendered
|
|
525
617
|
// `RangeError: Invalid string length` out of the top border.
|
|
526
618
|
const maximum = laidOut >= UNBOUNDED ? Infinity : laidOut;
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
stretch: false,
|
|
534
|
-
})), this.padding, frame).totalWidth;
|
|
619
|
+
// The narrowest this table draws without cutting a cell is every column at
|
|
620
|
+
// its own minimum — the reference's sum of `_measure_column` minimums — so a
|
|
621
|
+
// parent squeezing it still leaves each figure whole. One divergence: a
|
|
622
|
+
// `noWrap` column's minimum is its whole line, where the reference measures
|
|
623
|
+
// its longest word and then never narrows it that far when it draws.
|
|
624
|
+
const tightest = layoutTable(outerWidth, demands.map((demand) => ({ ...demand, want: demand.minimum, fill: 0, ratio: 0, stretch: false })), frame).totalWidth;
|
|
535
625
|
return {
|
|
536
626
|
minimum: Math.min(maximum, Math.max(tightest, cells(this.minWidth ?? 0))),
|
|
537
627
|
maximum,
|
|
538
628
|
};
|
|
539
629
|
}
|
|
540
630
|
// --- Static ---
|
|
631
|
+
/**
|
|
632
|
+
* A table with no frame and no header, padded between its columns and never
|
|
633
|
+
* at its edges, so an expanded grid reaches both sides of its width. The
|
|
634
|
+
* reference's `grid` defaults, but for `padding`: Rich's is 0, which runs
|
|
635
|
+
* adjacent columns together, and this port's is one cell to the right.
|
|
636
|
+
*/
|
|
541
637
|
static grid(options) {
|
|
542
638
|
return new Table({
|
|
543
639
|
...options,
|
|
@@ -545,6 +641,8 @@ export class Table {
|
|
|
545
641
|
showHeader: false,
|
|
546
642
|
showEdge: false,
|
|
547
643
|
padding: options?.padding ?? [0, 1, 0, 0],
|
|
644
|
+
collapsePadding: options?.collapsePadding ?? true,
|
|
645
|
+
padEdge: options?.padEdge ?? false,
|
|
548
646
|
});
|
|
549
647
|
}
|
|
550
648
|
// --- Private ---
|
|
@@ -569,8 +667,8 @@ export class Table {
|
|
|
569
667
|
_outerWidth(options) {
|
|
570
668
|
return Math.min(this.tableWidth === undefined ? options.maxWidth : cells(this.tableWidth), options.maxWidth);
|
|
571
669
|
}
|
|
572
|
-
_geometry(outerWidth) {
|
|
573
|
-
return layoutTable(outerWidth, this._columnDemands(
|
|
670
|
+
_geometry(options, outerWidth) {
|
|
671
|
+
return layoutTable(outerWidth, this._columnDemands(options, outerWidth), this._frame());
|
|
574
672
|
}
|
|
575
673
|
/**
|
|
576
674
|
* [LAW:dataflow-not-control-flow] The three ways a column can be sized —
|
|
@@ -578,59 +676,122 @@ export class Table {
|
|
|
578
676
|
* produce, and `expand` only in its `stretch`. They are resolved once, here, into uniform data, so
|
|
579
677
|
* `layoutTable` runs the same apportionment for every table and no sizing
|
|
580
678
|
* mode gets its own path through the width division.
|
|
679
|
+
*
|
|
680
|
+
* Cells are measured against the width left once the frame is paid, as the
|
|
681
|
+
* reference's `_measure_column` measures them. Padding and the floor are laid
|
|
682
|
+
* over whichever sizing the column has: they are the same for all three.
|
|
581
683
|
*/
|
|
582
|
-
_columnDemands() {
|
|
684
|
+
_columnDemands(options, outerWidth) {
|
|
685
|
+
const frame = this._frame();
|
|
686
|
+
const inner = {
|
|
687
|
+
...options,
|
|
688
|
+
maxWidth: Math.max(0, outerWidth - frame.edge * 2 - frame.divider * Math.max(0, this._columns.length - 1)),
|
|
689
|
+
};
|
|
583
690
|
return this._columns.map((col, index) => {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
// competes for its natural width like any other, and only the cells left
|
|
607
|
-
// once every column has that are shared out. A larger want looks
|
|
608
|
-
// equivalent and is not — it lets a one-cell column claim cells while its
|
|
609
|
-
// neighbour is still truncated. The stretch weighs by width, so the
|
|
610
|
-
// widest column grows most, which is what keeps a `Progress` bar from
|
|
611
|
-
// getting no more of the slack than its percentage label.
|
|
612
|
-
//
|
|
613
|
-
// One deliberate divergence: Rich stretches declared-width columns as
|
|
614
|
-
// well, so `width: 6` under `expand` renders 21 cells wide — an option
|
|
615
|
-
// quietly meaning something else, the defect rich-justify-0cr exists to
|
|
616
|
-
// remove. A reservation's stretch is zero above.
|
|
617
|
-
//
|
|
618
|
-
// A column with no cell at all — no rows, and neither header nor footer
|
|
619
|
-
// drawn — has nothing to size to, which is not sizing to nothing. The
|
|
620
|
-
// reference measures it `Measurement(1, max_width)`: one cell of content,
|
|
621
|
-
// and a maximum of the whole offer, so a rowless table fills it. Reading
|
|
622
|
-
// "no cells" as zero sized it to a two-cell box instead. The one cell is
|
|
623
|
-
// its want, so a `minWidth` is paid like any column's, and the offer is
|
|
624
|
-
// its `fill`, held to its `maxWidth`.
|
|
691
|
+
const pad = this._columnPadding(index);
|
|
692
|
+
const sizing = this._columnSizing(col, index, inner);
|
|
693
|
+
const floor = pad[0] + sizing.want + pad[1] === 0 ? 1 : 0;
|
|
694
|
+
return { ...sizing, minimum: sizing.minimum + floor, want: sizing.want + floor, pad, floor };
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
_columnSizing(col, index, options) {
|
|
698
|
+
// A flexible column takes its share of whatever the bounded columns
|
|
699
|
+
// leave; every other column asks for its natural width, whether or not a
|
|
700
|
+
// neighbour is flexible. The split is the reference's —
|
|
701
|
+
// `fixed_widths = [0 if column.flexible else _range.maximum ...]` — with
|
|
702
|
+
// one divergence: Rich splits by ratio only when the table expands, and
|
|
703
|
+
// here a ratio is honoured either way. Its bounded part is its floor —
|
|
704
|
+
// the reference's `column.width or 1`, reading `minWidth` where there is
|
|
705
|
+
// no `width` — bid for in cells like any content column, so a squeezed
|
|
706
|
+
// table still pays it. A `width` beside a `ratio` is that floor, not a
|
|
707
|
+
// reservation, as the reference's `flex_minimum` reads it.
|
|
708
|
+
// [LAW:one-source-of-truth] `columnShare` decides first, so a column that
|
|
709
|
+
// `Column.flexible` calls elastic is one the division shares out.
|
|
710
|
+
const share = columnShare(col);
|
|
711
|
+
if (share > 0) {
|
|
712
|
+
const floor = Math.max(1, demandCells(col.width ?? col.minWidth ?? 0));
|
|
625
713
|
return {
|
|
626
714
|
reserved: 0,
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
715
|
+
minimum: floor,
|
|
716
|
+
want: floor,
|
|
717
|
+
weight: floor,
|
|
718
|
+
fill: 0,
|
|
719
|
+
ratio: share,
|
|
720
|
+
stretch: false,
|
|
721
|
+
wrapable: col.width === undefined && !col.noWrap,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
if (col.width !== undefined) {
|
|
725
|
+
// [LAW:single-enforcer] floored where it is parsed, the same rule
|
|
726
|
+
// `normalizePadding` applies to a negative padding side.
|
|
727
|
+
const declared = demandCells(col.width);
|
|
728
|
+
return {
|
|
729
|
+
reserved: declared,
|
|
730
|
+
minimum: declared,
|
|
731
|
+
want: declared,
|
|
732
|
+
weight: 0,
|
|
733
|
+
fill: 0,
|
|
630
734
|
ratio: 0,
|
|
631
|
-
stretch:
|
|
735
|
+
stretch: false,
|
|
736
|
+
wrapable: false,
|
|
632
737
|
};
|
|
633
|
-
}
|
|
738
|
+
}
|
|
739
|
+
const range = this._cellRange(col, index, options);
|
|
740
|
+
const natural = demandCells(this._bounded(col, range?.maximum ?? 1));
|
|
741
|
+
// `expand` is a stretch rather than a larger want: the column still
|
|
742
|
+
// competes for its natural width like any other, and only the cells left
|
|
743
|
+
// once every column has that are shared out. A larger want looks
|
|
744
|
+
// equivalent and is not — it lets a one-cell column claim cells while its
|
|
745
|
+
// neighbour is still truncated. The stretch weighs by width, so the
|
|
746
|
+
// widest column grows most, which is what keeps a `Progress` bar from
|
|
747
|
+
// getting no more of the slack than its percentage label.
|
|
748
|
+
//
|
|
749
|
+
// One deliberate divergence: Rich stretches declared-width columns as
|
|
750
|
+
// well, so `width: 6` under `expand` renders 21 cells wide — an option
|
|
751
|
+
// quietly meaning something else, the defect rich-justify-0cr exists to
|
|
752
|
+
// remove. A reservation's stretch is zero above.
|
|
753
|
+
//
|
|
754
|
+
// A column with no cell at all — no rows, and neither header nor footer
|
|
755
|
+
// drawn — has nothing to size to, which is not sizing to nothing. The
|
|
756
|
+
// reference measures it `Measurement(1, max_width)`: one cell of content,
|
|
757
|
+
// and a maximum of the whole offer, so a rowless table fills it. Reading
|
|
758
|
+
// "no cells" as zero sized it to a two-cell box instead. The one cell is
|
|
759
|
+
// its want, so a `minWidth` is paid like any column's, and the offer is
|
|
760
|
+
// its `fill`, held to its `maxWidth`.
|
|
761
|
+
//
|
|
762
|
+
// A column that may not wrap has no narrower width than its natural one
|
|
763
|
+
// that keeps its content whole — the reference leaves `no_wrap` columns out
|
|
764
|
+
// of the ones it collapses.
|
|
765
|
+
return {
|
|
766
|
+
reserved: 0,
|
|
767
|
+
minimum: col.noWrap ? natural : Math.min(natural, demandCells(this._bounded(col, range?.minimum ?? 1))),
|
|
768
|
+
want: natural,
|
|
769
|
+
weight: natural,
|
|
770
|
+
fill: range === undefined ? demandCells(this._bounded(col, UNBOUNDED)) : 0,
|
|
771
|
+
ratio: 0,
|
|
772
|
+
stretch: this.expand,
|
|
773
|
+
wrapable: !col.noWrap,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* The padding either side of column `index`'s content, and above and below
|
|
778
|
+
* one row of cells: the reference's `get_padding`. `collapsePadding` lets a
|
|
779
|
+
* cell's left padding and its row's bottom padding merge into the padding
|
|
780
|
+
* beside and below them; `padEdge: false` drops every side that meets the
|
|
781
|
+
* table's edge. Each side is the configured padding unless one of those two
|
|
782
|
+
* reduces it.
|
|
783
|
+
*/
|
|
784
|
+
_columnPadding(index) {
|
|
785
|
+
const [, right, , left] = this.padding;
|
|
786
|
+
const first = index === 0;
|
|
787
|
+
const last = index === this._columns.length - 1;
|
|
788
|
+
const collapsed = this.collapsePadding && !first ? Math.max(0, left - right) : left;
|
|
789
|
+
return [!this.padEdge && first ? 0 : collapsed, !this.padEdge && last ? 0 : right];
|
|
790
|
+
}
|
|
791
|
+
_rowPadding(first, last) {
|
|
792
|
+
const [top, , bottom] = this.padding;
|
|
793
|
+
const collapsed = this.collapsePadding && !last ? Math.max(0, top - bottom) : bottom;
|
|
794
|
+
return [!this.padEdge && first ? 0 : top, !this.padEdge && last ? 0 : collapsed];
|
|
634
795
|
}
|
|
635
796
|
/**
|
|
636
797
|
* Every cell column `index` draws, in draw order.
|
|
@@ -647,31 +808,33 @@ export class Table {
|
|
|
647
808
|
if (this.showHeader)
|
|
648
809
|
yield col.header;
|
|
649
810
|
for (const row of this._rows)
|
|
650
|
-
yield row.cells[index] ??
|
|
811
|
+
yield row.cells[index] ?? new EmbeddedText(undefined);
|
|
651
812
|
if (this.showFooter)
|
|
652
813
|
yield col.footer;
|
|
653
814
|
}
|
|
654
815
|
/**
|
|
655
|
-
* The widest cell in a column,
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
816
|
+
* The widest any cell in a column draws, and the narrowest the column can be
|
|
817
|
+
* without cutting one — the reference's `_measure_column` — or `undefined`
|
|
818
|
+
* when the column has no cell to measure. Zero is a column whose cells draw
|
|
819
|
+
* nothing — a gutter asks for its padding and nothing else — and is not the
|
|
820
|
+
* same answer. [LAW:types-are-the-program]
|
|
659
821
|
*/
|
|
660
|
-
|
|
661
|
-
let
|
|
822
|
+
_cellRange(col, index, options) {
|
|
823
|
+
let range;
|
|
662
824
|
for (const cell of this._columnCells(col, index)) {
|
|
663
825
|
// The stamped cell, so the width a column asks for is the width its text
|
|
664
826
|
// will occupy — measuring the raw value sized this column to
|
|
665
827
|
// `[red]Solo[/red]`, fifteen cells for four cells of text.
|
|
666
|
-
// [LAW:one-source-of-truth]
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
828
|
+
// [LAW:one-source-of-truth] Measured as the cell measures itself, so a
|
|
829
|
+
// multi-line cell asks for its widest line and a `Panel` for its frame;
|
|
830
|
+
// one that cannot measure itself asks for every cell there is, as in Rich.
|
|
831
|
+
const measured = Measurement.get(options, cell);
|
|
832
|
+
range = {
|
|
833
|
+
minimum: Math.max(range?.minimum ?? 0, measured.minimum),
|
|
834
|
+
maximum: Math.max(range?.maximum ?? 0, measured.maximum),
|
|
835
|
+
};
|
|
673
836
|
}
|
|
674
|
-
return
|
|
837
|
+
return range;
|
|
675
838
|
}
|
|
676
839
|
/**
|
|
677
840
|
* A natural width held to the column's own `minWidth`/`maxWidth`.
|
|
@@ -684,7 +847,7 @@ export class Table {
|
|
|
684
847
|
_bounded(col, natural) {
|
|
685
848
|
return Math.min(Math.max(natural, cells(col.minWidth ?? 0)), cells(col.maxWidth ?? Infinity));
|
|
686
849
|
}
|
|
687
|
-
*_renderRow(options, cells, styles, rowStyle, geometry, box, level, border) {
|
|
850
|
+
*_renderRow(options, cells, styles, rowStyle, geometry, [top, bottom], box, level, border) {
|
|
688
851
|
const { padLeft, padRight, columns } = geometry;
|
|
689
852
|
// [LAW:dataflow-not-control-flow] Header, body and footer share this path;
|
|
690
853
|
// the level crosses as a value the box answers with glyphs, not a branch.
|
|
@@ -699,22 +862,36 @@ export class Table {
|
|
|
699
862
|
// Render each cell onto the canvas the geometry gave its column. Columns
|
|
700
863
|
// the width could not seat are absent from `columns` and so are never
|
|
701
864
|
// rendered at all.
|
|
865
|
+
const paddedWidths = columns.map((cellWidth, index) => padLeft[index] + cellWidth + padRight[index]);
|
|
702
866
|
const cellLines = columns.map((cellWidth, index) => {
|
|
703
867
|
const col = this._columns[index];
|
|
704
868
|
const cell = cells[index] ?? embed("");
|
|
705
869
|
// The render's own options with the column's canvas laid over them, so a
|
|
706
|
-
// cell resolves its style names against the same theme as the table.
|
|
707
|
-
// table
|
|
708
|
-
// `
|
|
870
|
+
// cell resolves its style names against the same theme as the table. A
|
|
871
|
+
// table stacks its cells, so each is handed the table's rows as a
|
|
872
|
+
// ceiling, never a region to fill — the `Height` contract's
|
|
873
|
+
// `stackedHeight`, where the reference hands a cell `height=None`.
|
|
874
|
+
// Highlighted as its column says, Rich's `highlight=column.highlight`.
|
|
709
875
|
const segs = [...cell.render({
|
|
710
876
|
...options,
|
|
877
|
+
highlight: col.highlight,
|
|
711
878
|
maxWidth: cellWidth,
|
|
712
879
|
justify: col.justify,
|
|
713
880
|
overflow: col.overflow,
|
|
714
881
|
noWrap: col.noWrap,
|
|
715
|
-
height:
|
|
882
|
+
height: stackedHeight(options.height),
|
|
716
883
|
})];
|
|
717
|
-
|
|
884
|
+
// The padding on every side is part of the cell, as the reference's
|
|
885
|
+
// `Padding` makes it: a content line sits between its left and right
|
|
886
|
+
// blanks, and a line above or below is one blank the cell's width.
|
|
887
|
+
const left = blank(padLeft[index]);
|
|
888
|
+
const right = blank(padRight[index]);
|
|
889
|
+
const padLine = () => blank(paddedWidths[index]);
|
|
890
|
+
return [
|
|
891
|
+
...Array.from({ length: top }, padLine),
|
|
892
|
+
...Segment.splitAndCropLines(segs, cellWidth).map((line) => [...left, ...line, ...right]),
|
|
893
|
+
...Array.from({ length: bottom }, padLine),
|
|
894
|
+
];
|
|
718
895
|
});
|
|
719
896
|
const maxLines = cellLines.reduce((most, lines) => Math.max(most, lines.length), 1);
|
|
720
897
|
// How many blank lines stand above each cell's content. A header sits on
|
|
@@ -729,16 +906,15 @@ export class Table {
|
|
|
729
906
|
if (colIdx > 0 && divider) {
|
|
730
907
|
yield divider;
|
|
731
908
|
}
|
|
732
|
-
const cellWidth = columns[colIdx];
|
|
733
909
|
const style = styles[colIdx];
|
|
734
910
|
// A cell with fewer lines than the row contributes blanks, so every
|
|
735
911
|
// column spans the same number of rows and the frame stays rectangular.
|
|
736
|
-
const line = cellLines[colIdx][lineIdx - drop[colIdx]] ?? [
|
|
912
|
+
const line = cellLines[colIdx][lineIdx - drop[colIdx]] ?? blank(paddedWidths[colIdx]);
|
|
737
913
|
// The cell's style is the base its content's own spans layer over, and
|
|
738
|
-
// it covers the whole cell — its
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
yield* Segment.applyStyle(
|
|
914
|
+
// it covers the whole cell — its padding and its blank lines too, as
|
|
915
|
+
// the reference's does — so a background fills the column rather than
|
|
916
|
+
// sitting behind the text alone.
|
|
917
|
+
yield* Segment.applyStyle(line, style.isNull ? undefined : style);
|
|
742
918
|
}
|
|
743
919
|
if (frame && geometry.edge === 1) {
|
|
744
920
|
yield new Segment(frame.right, border);
|
|
@@ -747,24 +923,25 @@ export class Table {
|
|
|
747
923
|
}
|
|
748
924
|
}
|
|
749
925
|
*_renderTitle(options, text, tableWidth, ownStyle, justify) {
|
|
750
|
-
const style = getStyle(options, ownStyle);
|
|
751
|
-
const titleStyle = style.isNull ? undefined : style;
|
|
752
926
|
// The table owns the canvas; the caller's text still says how it meets the
|
|
753
|
-
// edge. A `RichText`'s own `justify`
|
|
754
|
-
//
|
|
755
|
-
//
|
|
756
|
-
//
|
|
757
|
-
//
|
|
758
|
-
//
|
|
759
|
-
|
|
760
|
-
|
|
927
|
+
// edge. A `RichText`'s own `justify` outranks `titleJustify`, as a `Text`'s
|
|
928
|
+
// does in the reference (rich-table-qj6i.kpp). Its own `overflow` does too,
|
|
929
|
+
// but for `"ignore"`: that title would have no edge and run straight
|
|
930
|
+
// through the frame. Every other method, and `noWrap`, cuts within the
|
|
931
|
+
// bound, so those stay theirs. `text` hands back a copy to clear it on.
|
|
932
|
+
//
|
|
933
|
+
// Never highlighted, and markup as the console says: Rich draws a title
|
|
934
|
+
// and caption through `render_str(highlight=False)`.
|
|
935
|
+
//
|
|
936
|
+
// The table's title style is the *base* a string title's own spans layer
|
|
937
|
+
// over, as `render_str(style=)` sets it: a `[red]` title inside an italic
|
|
938
|
+
// table title arrives as italic-red, and the line breaks and the `end` the
|
|
939
|
+
// text emits stay unstyled. A `RichText` title takes none of it, because
|
|
940
|
+
// the reference renders a `Text` title without the title style
|
|
941
|
+
// (rich-table-qj6i).
|
|
942
|
+
const source = text.text({ ...options, highlight: false }, getStyle(options, ownStyle));
|
|
761
943
|
if (source.overflow === "ignore")
|
|
762
944
|
source.overflow = undefined;
|
|
763
|
-
// The table's title style is the *base* the content's own spans layer over,
|
|
764
|
-
// which is what the reference emits: a `[red]` title inside an italic table
|
|
765
|
-
// title arrives as italic-red, not one or the other. Rendering `text.plain`
|
|
766
|
-
// here read the characters and dropped every span attached to them, so a
|
|
767
|
-
// styled title lost its styling and parsed markup silently did nothing.
|
|
768
945
|
//
|
|
769
946
|
// Rendered at the table's own width with nothing suppressed, because that
|
|
770
947
|
// is what the reference hands its title: an annotation too wide for the
|
|
@@ -776,20 +953,19 @@ export class Table {
|
|
|
776
953
|
// rules — that a wrap's trailing whitespace is not content to centre
|
|
777
954
|
// around, that `left` fills the canvas and an unset justify does not — and
|
|
778
955
|
// a second copy of those is a second answer waiting to disagree.
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
}
|
|
956
|
+
//
|
|
957
|
+
// Yielded as the text emits it, every line and its own `end` included, as
|
|
958
|
+
// the reference's `console.render` yields it: a title of "one\ntwo" is two
|
|
959
|
+
// lines, and `end="!!"` runs on into the top border there too. Splitting
|
|
960
|
+
// and re-ending each line moved a non-newline `end` onto the title's own
|
|
961
|
+
// row, wider than the table.
|
|
962
|
+
yield* source.render({
|
|
963
|
+
...options,
|
|
964
|
+
maxWidth: tableWidth,
|
|
965
|
+
justify,
|
|
966
|
+
overflow: undefined,
|
|
967
|
+
noWrap: false,
|
|
968
|
+
height: undefined,
|
|
969
|
+
});
|
|
794
970
|
}
|
|
795
971
|
}
|