@promptctl/rich-js 0.17.0 → 0.18.1
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 +13 -26
- package/dist/core/anchor.d.ts +45 -0
- package/dist/core/anchor.js +39 -0
- package/dist/core/ansi.d.ts +65 -0
- package/dist/core/ansi.js +231 -0
- package/dist/core/box.d.ts +0 -1
- package/dist/core/box.js +6 -18
- package/dist/core/cells.d.ts +16 -5
- package/dist/core/cells.js +31 -10
- package/dist/core/color.d.ts +4 -8
- package/dist/core/color.js +28 -19
- package/dist/core/console.d.ts +13 -4
- package/dist/core/console.js +91 -36
- package/dist/core/export-html.d.ts +40 -5
- package/dist/core/export-html.js +111 -17
- package/dist/core/markup.d.ts +27 -5
- package/dist/core/markup.js +282 -170
- package/dist/core/measure.d.ts +1 -1
- package/dist/core/measure.js +2 -1
- package/dist/core/place.d.ts +30 -0
- package/dist/core/place.js +51 -0
- package/dist/core/pretty.d.ts +3 -0
- package/dist/core/pretty.js +5 -1
- package/dist/core/protocol.d.ts +136 -6
- package/dist/core/protocol.js +64 -2
- package/dist/core/render.d.ts +2 -2
- package/dist/core/render.js +2 -2
- package/dist/core/segment.d.ts +23 -4
- package/dist/core/segment.js +75 -16
- package/dist/core/spinnerData.d.ts +2 -0
- package/dist/core/spinnerData.js +3 -1
- package/dist/core/strip.d.ts +3 -7
- package/dist/core/strip.js +32 -35
- package/dist/core/style.d.ts +25 -0
- package/dist/core/style.js +61 -89
- package/dist/core/text.d.ts +16 -15
- package/dist/core/text.js +62 -76
- package/dist/host/app.d.ts +121 -0
- package/dist/host/app.js +255 -0
- package/dist/host/host-environment.d.ts +26 -0
- package/dist/host/host-environment.js +44 -0
- package/dist/host/index.d.ts +4 -2
- package/dist/host/index.js +2 -1
- package/dist/host/terminal-host.d.ts +43 -7
- package/dist/host/terminal-host.js +57 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -1
- package/dist/node/terminal-host.d.ts +31 -2
- package/dist/node/terminal-host.js +69 -2
- package/dist/node/traceback.d.ts +16 -6
- package/dist/node/traceback.js +30 -8
- package/dist/renderables/align.d.ts +2 -2
- package/dist/renderables/align.js +6 -19
- package/dist/renderables/columns.js +4 -20
- package/dist/renderables/embed.d.ts +50 -0
- package/dist/renderables/embed.js +71 -0
- package/dist/renderables/group.js +3 -1
- package/dist/renderables/layout.js +31 -34
- package/dist/renderables/live.d.ts +6 -3
- package/dist/renderables/live.js +89 -45
- package/dist/renderables/markdown.d.ts +1 -1
- package/dist/renderables/markdown.js +6 -4
- package/dist/renderables/padding.js +3 -2
- package/dist/renderables/panel.js +26 -41
- package/dist/renderables/progress.js +5 -8
- package/dist/renderables/progressBar.js +6 -5
- package/dist/renderables/rule.d.ts +3 -2
- package/dist/renderables/rule.js +9 -9
- package/dist/renderables/spinner.d.ts +5 -3
- package/dist/renderables/spinner.js +14 -8
- package/dist/renderables/syntax.d.ts +1 -1
- package/dist/renderables/syntax.js +2 -1
- package/dist/renderables/table.d.ts +26 -14
- package/dist/renderables/table.js +239 -141
- package/dist/renderables/traceback.js +8 -9
- package/dist/renderables/tree.d.ts +17 -3
- package/dist/renderables/tree.js +62 -71
- package/dist/renderables/viewport.d.ts +114 -0
- package/dist/renderables/viewport.js +198 -0
- package/dist/template-bindings/color-funcs.js +11 -13
- package/dist/template-bindings/index.js +4 -2
- package/dist/template-bindings/palette-funcs.js +6 -16
- package/dist/template-bindings/style-funcs.js +4 -0
- package/dist/themes/data/ansi-tables.d.ts +14 -0
- package/dist/themes/data/ansi-tables.js +49 -0
- package/dist/themes/data/atom-one-dark.js +19 -0
- package/dist/themes/data/atom-one-light.js +19 -0
- package/dist/themes/data/catppuccin-frappe.js +19 -0
- package/dist/themes/data/catppuccin-latte.js +19 -0
- package/dist/themes/data/catppuccin-macchiato.js +19 -0
- package/dist/themes/data/catppuccin-mocha.js +19 -0
- package/dist/themes/data/cyberpunk.js +2 -0
- package/dist/themes/data/default.js +2 -0
- package/dist/themes/data/dracula.js +19 -0
- package/dist/themes/data/flexoki.js +19 -0
- package/dist/themes/data/gruvbox.js +19 -0
- package/dist/themes/data/monokai.js +19 -0
- package/dist/themes/data/nord.js +19 -0
- package/dist/themes/data/rose-pine-dawn.js +19 -0
- package/dist/themes/data/rose-pine-moon.js +19 -0
- package/dist/themes/data/rose-pine.js +19 -0
- package/dist/themes/data/solarized-dark.js +19 -0
- package/dist/themes/data/solarized-light.js +19 -0
- package/dist/themes/data/svg-export.js +19 -0
- package/dist/themes/data/textual-ansi.js +2 -0
- package/dist/themes/data/textual-dark.js +2 -0
- package/dist/themes/data/textual-light.js +2 -0
- package/dist/themes/data/tokyo-night.js +19 -0
- package/dist/themes/data/types.d.ts +16 -0
- package/dist/themes/data/types.js +10 -1
- package/dist/themes/registry.d.ts +3 -2
- package/dist/themes/registry.js +13 -1
- package/dist/themes/terminalThemes.d.ts +2 -7
- package/dist/themes/terminalThemes.js +4 -9
- package/dist/widgets/button.d.ts +1 -1
- package/dist/widgets/button.js +4 -2
- package/dist/widgets/checkbox.d.ts +2 -2
- package/dist/widgets/checkbox.js +5 -3
- package/dist/widgets/dropdown.d.ts +2 -2
- package/dist/widgets/dropdown.js +13 -16
- package/dist/widgets/event-router.d.ts +21 -28
- package/dist/widgets/event-router.js +80 -73
- package/dist/widgets/focus-manager.d.ts +25 -6
- package/dist/widgets/focus-manager.js +63 -65
- package/dist/widgets/hit.d.ts +64 -0
- package/dist/widgets/hit.js +113 -0
- package/dist/widgets/index.d.ts +5 -7
- package/dist/widgets/index.js +3 -4
- package/dist/widgets/slider.d.ts +2 -2
- package/dist/widgets/slider.js +7 -7
- package/dist/widgets/static-item.d.ts +9 -15
- package/dist/widgets/static-item.js +9 -15
- package/dist/widgets/text-input.d.ts +2 -2
- package/dist/widgets/text-input.js +17 -14
- package/dist/widgets/toggle.d.ts +1 -1
- package/dist/widgets/toggle.js +3 -2
- package/dist/widgets/types.d.ts +14 -35
- package/dist/widgets/types.js +0 -1
- package/dist/widgets/widget-app.d.ts +63 -0
- package/dist/widgets/widget-app.js +199 -0
- package/dist/widgets/widget-base.d.ts +4 -6
- package/dist/widgets/widget-base.js +22 -30
- package/package.json +12 -4
- package/dist/host/host-stream.d.ts +0 -30
- package/dist/host/host-stream.js +0 -35
- package/dist/widgets/screen.d.ts +0 -98
- package/dist/widgets/screen.js +0 -530
|
@@ -6,48 +6,9 @@ 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 {
|
|
9
|
+
import { embed, embeddedText } from "./embed.js";
|
|
10
10
|
import { normalizePadding } from "./padding.js";
|
|
11
11
|
import { getStyle, withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
12
|
-
/**
|
|
13
|
-
* The one crossing where caller text becomes styled table content.
|
|
14
|
-
*
|
|
15
|
-
* Cells, headers, footers, the title and the caption each used to build their
|
|
16
|
-
* own `RichText` straight from the constructor — which does not parse markup —
|
|
17
|
-
* so the markup rule had five homes and was absent from all five, and
|
|
18
|
-
* `[red]Solo[/red]` reached the terminal with its tags intact.
|
|
19
|
-
* [LAW:single-enforcer]
|
|
20
|
-
*
|
|
21
|
-
* Parsing is unconditional because that is what the reference does rather than
|
|
22
|
-
* because it is the simpler branch: Rich's `Console.__init__` declares
|
|
23
|
-
* `markup: bool = True`, and all five positions reach the wire through Rich's
|
|
24
|
-
* own `render_str`. A table-level opt-out would be a mode with no reference
|
|
25
|
-
* behaviour to define. [LAW:no-mode-explosion]
|
|
26
|
-
*
|
|
27
|
-
* `end` is cleared on both arms, not passed in, because a table cell is a
|
|
28
|
-
* fragment rather than a line — the same two steps Rich takes for a printed
|
|
29
|
-
* string. The `RichText` arm copies first: clearing in place would reach back
|
|
30
|
-
* into the caller's object.
|
|
31
|
-
*/
|
|
32
|
-
function toCellText(content) {
|
|
33
|
-
const text = content instanceof RichText ? content.copy() : renderMarkup(String(content ?? ""));
|
|
34
|
-
text.end = "";
|
|
35
|
-
return text;
|
|
36
|
-
}
|
|
37
|
-
function toRenderable(content) {
|
|
38
|
-
// [LAW:single-enforcer] `toCellText` is the one place that clears a cell's
|
|
39
|
-
// `end` — route a directly-passed `RichText` through it rather than letting
|
|
40
|
-
// it leave via the passthrough arm below untouched (rich-text-5ai code
|
|
41
|
-
// review): the passthrough exists for a genuine non-text `Renderable` (a
|
|
42
|
-
// nested `Panel` or `Table`), which carries no `end` to clear.
|
|
43
|
-
if (content instanceof RichText) {
|
|
44
|
-
return toCellText(content);
|
|
45
|
-
}
|
|
46
|
-
if (typeof content === "object" && content !== null && "render" in content) {
|
|
47
|
-
return content;
|
|
48
|
-
}
|
|
49
|
-
return toCellText(content);
|
|
50
|
-
}
|
|
51
12
|
/** A `want` no budget can satisfy: the column takes every cell its weight earns. */
|
|
52
13
|
const UNBOUNDED = Number.MAX_SAFE_INTEGER;
|
|
53
14
|
/**
|
|
@@ -57,6 +18,8 @@ const UNBOUNDED = Number.MAX_SAFE_INTEGER;
|
|
|
57
18
|
* every other elastic column in the table.
|
|
58
19
|
*/
|
|
59
20
|
const demandCells = (n) => Math.min(cells(n), UNBOUNDED);
|
|
21
|
+
/** `n` cells of padding, as nothing at all when `n` is zero. */
|
|
22
|
+
const blank = (n) => (n > 0 ? [new Segment(" ".repeat(n))] : []);
|
|
60
23
|
/**
|
|
61
24
|
* Hand out `total` cells across `demands`, weighted, and never past a demand's
|
|
62
25
|
* `want`.
|
|
@@ -76,47 +39,81 @@ const demandCells = (n) => Math.min(cells(n), UNBOUNDED);
|
|
|
76
39
|
* `Infinity - 1 === Infinity` — never terminated at all for a `maxWidth` of
|
|
77
40
|
* `Infinity` held open by a ratio column. Capping by want reaches that case in
|
|
78
41
|
* one round.
|
|
42
|
+
*
|
|
43
|
+
* The arithmetic is exact. A share's fraction is what decides who gets a
|
|
44
|
+
* leftover cell, and in floating point two fractions that are equal — 6/14 and
|
|
45
|
+
* 6/14 for 16 cells over weights 3 and 10 — come out 0.4285714285714284 and
|
|
46
|
+
* 0.4285714285714288, so the tie went wherever the rounding error pointed.
|
|
47
|
+
* The weights are read as whole numbers in one proportion (`exactWeights`) and
|
|
48
|
+
* everything after is integer division.
|
|
79
49
|
*/
|
|
80
50
|
function distribute(total, demands) {
|
|
81
51
|
const granted = demands.map(() => 0);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
52
|
+
const bidders = demands.flatMap((demand, index) => demand.weight > 0 && demand.want > 0 ? [{ index, demand }] : []);
|
|
53
|
+
const weights = exactWeights(bidders.map(({ demand }) => demand.weight));
|
|
54
|
+
let open = bidders.map(({ index, demand }, slot) => ({
|
|
55
|
+
index,
|
|
56
|
+
want: BigInt(demand.want),
|
|
57
|
+
weight: weights[slot],
|
|
58
|
+
}));
|
|
59
|
+
const sum = (values) => values.reduce((acc, value) => acc + value, 0n);
|
|
60
|
+
// A budget past every open want grants each its want either way, and that
|
|
61
|
+
// bound is what keeps an unbounded offer — `Infinity` is a legal outer width
|
|
62
|
+
// — out of integer arithmetic that cannot hold it.
|
|
63
|
+
const wantSum = sum(open.map(({ want }) => want));
|
|
64
|
+
let remaining = total >= wantSum ? wantSum : BigInt(Math.max(0, total));
|
|
65
|
+
while (open.length > 0 && remaining > 0n) {
|
|
66
|
+
const weightSum = sum(open.map(({ weight }) => weight));
|
|
67
|
+
const capped = open.filter(({ want, weight }) => remaining * weight >= want * weightSum);
|
|
90
68
|
if (capped.length === 0)
|
|
91
69
|
break;
|
|
92
|
-
for (const index of capped) {
|
|
93
|
-
granted[index] =
|
|
94
|
-
remaining -=
|
|
70
|
+
for (const { index, want } of capped) {
|
|
71
|
+
granted[index] = Number(want);
|
|
72
|
+
remaining -= want;
|
|
95
73
|
}
|
|
96
|
-
open = open.filter((
|
|
74
|
+
open = open.filter((bid) => !capped.includes(bid));
|
|
97
75
|
}
|
|
98
|
-
if (open.length > 0 && remaining >
|
|
99
|
-
const weightSum =
|
|
100
|
-
const
|
|
101
|
-
const whole =
|
|
102
|
-
|
|
76
|
+
if (open.length > 0 && remaining > 0n) {
|
|
77
|
+
const weightSum = sum(open.map(({ weight }) => weight));
|
|
78
|
+
const products = open.map(({ weight }) => remaining * weight);
|
|
79
|
+
const whole = products.map((product) => product / weightSum);
|
|
80
|
+
const fraction = products.map((product) => product % weightSum);
|
|
81
|
+
let residue = remaining - sum(whole);
|
|
103
82
|
// The cells the shares left as fractions go to the largest fraction first,
|
|
104
83
|
// ties to the leftmost column, so the total lands exactly on `remaining`.
|
|
84
|
+
// `Number` of a nonzero difference is never 0, so the sign is exact.
|
|
105
85
|
const byFraction = open
|
|
106
86
|
.map((_, slot) => slot)
|
|
107
|
-
.sort((a, b) =>
|
|
87
|
+
.sort((a, b) => Number(fraction[b] - fraction[a]) || a - b);
|
|
108
88
|
for (const slot of byFraction) {
|
|
109
|
-
if (residue <=
|
|
89
|
+
if (residue <= 0n)
|
|
110
90
|
break;
|
|
111
91
|
whole[slot]++;
|
|
112
92
|
residue--;
|
|
113
93
|
}
|
|
114
|
-
open.forEach((index, slot) => {
|
|
115
|
-
granted[index] = whole[slot];
|
|
94
|
+
open.forEach(({ index }, slot) => {
|
|
95
|
+
granted[index] = Number(whole[slot]);
|
|
116
96
|
});
|
|
117
97
|
}
|
|
118
98
|
return granted;
|
|
119
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Weights as integers in one exact proportion. Each is read as the decimal it
|
|
102
|
+
* prints as — the number its caller wrote, `0.1` rather than the binary double
|
|
103
|
+
* nearest it — and all are scaled by the one power of ten that makes them
|
|
104
|
+
* whole, so `0.1 : 0.3 : 1` is exactly `1 : 3 : 10`. A weight that does not
|
|
105
|
+
* print as a decimal, `Infinity`, is refused by `BigInt` with a SyntaxError —
|
|
106
|
+
* the loud end of what `demandCells` exists to prevent.
|
|
107
|
+
*/
|
|
108
|
+
function exactWeights(weights) {
|
|
109
|
+
const decimals = weights.map((weight) => {
|
|
110
|
+
const [digits = "", power = "0"] = String(weight).split("e");
|
|
111
|
+
const [whole = "", fraction = ""] = digits.split(".");
|
|
112
|
+
return { mantissa: BigInt(whole + fraction), exponent: fraction.length - Number(power) };
|
|
113
|
+
});
|
|
114
|
+
const common = Math.max(...decimals.map(({ exponent }) => exponent));
|
|
115
|
+
return decimals.map(({ mantissa, exponent }) => mantissa * 10n ** BigInt(common - exponent));
|
|
116
|
+
}
|
|
120
117
|
function layoutTable(outerWidth, demands, padding, frame) {
|
|
121
118
|
const [, padRightWanted, , padLeftWanted] = padding;
|
|
122
119
|
// Plain `number`, not the `CellCol` `cells` hands back: this budget is spent
|
|
@@ -170,12 +167,32 @@ function layoutTable(outerWidth, demands, padding, frame) {
|
|
|
170
167
|
want: Math.max(0, demand.want - seats[index] - reserved[index]),
|
|
171
168
|
weight: demand.weight,
|
|
172
169
|
})));
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
const
|
|
170
|
+
const spent = (granted) => granted.reduce((sum, cells) => sum + cells, 0);
|
|
171
|
+
const holding = (...passes) => seatedDemands.map((_, index) => passes.reduce((sum, granted) => sum + granted[index], seats[index] + reserved[index]));
|
|
172
|
+
// The unspent part goes first to the columns with nothing to size to, each
|
|
173
|
+
// alike, up to its `fill` — the reference's `Measurement(1, max_width)`, a
|
|
174
|
+
// maximum of the whole offer that no content measured.
|
|
175
|
+
const afterWant = holding(wanted);
|
|
176
|
+
const filled = distribute(budget - spent(wanted), seatedDemands.map((demand, index) => ({
|
|
177
|
+
want: Math.max(0, demand.fill - afterWant[index]),
|
|
178
|
+
weight: demand.fill > 0 ? 1 : 0,
|
|
179
|
+
})));
|
|
180
|
+
// Then to the ratio columns, in proportion.
|
|
181
|
+
const shared = distribute(budget - spent(wanted) - spent(filled), seatedDemands.map((demand) => ({ want: UNBOUNDED, weight: demand.ratio })));
|
|
182
|
+
// What the shares leave goes to the columns that stretch, by the width each
|
|
183
|
+
// already holds: its natural width, since nothing is left over while any
|
|
184
|
+
// column is short, or the cap a fill stopped at. The reference weighs by
|
|
185
|
+
// width too: `ratio_distribute(max_width - table_width, widths)`, though its
|
|
186
|
+
// widths count the padding. A table that does not expand stretches nothing,
|
|
187
|
+
// which is how it stays narrower than the width it was offered. The order is the reference's:
|
|
188
|
+
// Rich pads an expanding table only once `table_width < max_width`, never
|
|
189
|
+
// while it is collapsing a column.
|
|
190
|
+
const held = holding(wanted, filled, shared);
|
|
191
|
+
const stretched = distribute(budget - spent(wanted) - spent(filled) - spent(shared), seatedDemands.map((demand, index) => ({
|
|
192
|
+
want: UNBOUNDED,
|
|
193
|
+
weight: demand.stretch ? held[index] : 0,
|
|
194
|
+
})));
|
|
195
|
+
const columns = held.map((cells, index) => cells + stretched[index]);
|
|
179
196
|
const cellWidths = columns.map((width) => padLeft + width + padRight);
|
|
180
197
|
return {
|
|
181
198
|
edge,
|
|
@@ -189,6 +206,19 @@ function layoutTable(outerWidth, demands, padding, frame) {
|
|
|
189
206
|
cellWidths.reduce((sum, width) => sum + width, 0),
|
|
190
207
|
};
|
|
191
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* The share of the leftover width a column's `ratio` claims: the ratio when it
|
|
211
|
+
* is positive, else 0 — a zero, negative or NaN ratio claims none, and the
|
|
212
|
+
* column sizes to its content. A proportion rather than a count of cells, so it
|
|
213
|
+
* is never floored: `ratio: 0.5` beside `ratio: 1` takes a third, where flooring
|
|
214
|
+
* it to 0 once starved it to a single `…`. Finite, for the reason `demandCells`
|
|
215
|
+
* is.
|
|
216
|
+
*
|
|
217
|
+
* [LAW:one-source-of-truth] `Column.flexible` is this asked as a yes/no, so the
|
|
218
|
+
* width division and the public flag cannot disagree about which columns are
|
|
219
|
+
* elastic.
|
|
220
|
+
*/
|
|
221
|
+
const columnShare = (col) => col.ratio !== undefined && col.ratio > 0 ? Math.min(col.ratio, UNBOUNDED) : 0;
|
|
192
222
|
export class Column {
|
|
193
223
|
_header;
|
|
194
224
|
_footer;
|
|
@@ -224,7 +254,7 @@ export class Column {
|
|
|
224
254
|
* constructor-only stamp held only until the first `columns[0].footer = mine`
|
|
225
255
|
* — which installed content that had parsed no markup, still carried its
|
|
226
256
|
* `end`, and was still owned by the caller, into a slot every reader below
|
|
227
|
-
* assumes `
|
|
257
|
+
* assumes `embeddedText` has been through. [LAW:parse-dont-validate] The setter
|
|
228
258
|
* is the border, so the guarantee holds for the object's whole lifetime and
|
|
229
259
|
* the constructor is one caller of it rather than the one place it is true.
|
|
230
260
|
*
|
|
@@ -233,23 +263,23 @@ export class Column {
|
|
|
233
263
|
* a column always has one and `show_footer` alone decides whether it is
|
|
234
264
|
* drawn. Modelling the absence as `undefined` instead made "no column has a
|
|
235
265
|
* footer" a state the render path could ask about — and it did, skipping the
|
|
236
|
-
* row a caller had asked for. `
|
|
266
|
+
* row a caller had asked for. `embeddedText` already maps nothing onto empty,
|
|
237
267
|
* which is why the setters take `undefined` rather than defaulting around it.
|
|
238
268
|
*/
|
|
239
269
|
get header() {
|
|
240
270
|
return this._header;
|
|
241
271
|
}
|
|
242
272
|
set header(content) {
|
|
243
|
-
this._header =
|
|
273
|
+
this._header = embeddedText(content);
|
|
244
274
|
}
|
|
245
275
|
get footer() {
|
|
246
276
|
return this._footer;
|
|
247
277
|
}
|
|
248
278
|
set footer(content) {
|
|
249
|
-
this._footer =
|
|
279
|
+
this._footer = embeddedText(content);
|
|
250
280
|
}
|
|
251
281
|
get flexible() {
|
|
252
|
-
return this
|
|
282
|
+
return columnShare(this) > 0;
|
|
253
283
|
}
|
|
254
284
|
/** @internal */
|
|
255
285
|
addCell(cell) {
|
|
@@ -308,10 +338,13 @@ export class Table {
|
|
|
308
338
|
this._rows = [];
|
|
309
339
|
this.box = options?.box !== undefined ? options.box : HEAVY_HEAD;
|
|
310
340
|
const titleVal = options?.title;
|
|
311
|
-
this.title = titleVal !== undefined ?
|
|
341
|
+
this.title = titleVal !== undefined ? embeddedText(titleVal) : undefined;
|
|
312
342
|
const captionVal = options?.caption;
|
|
313
|
-
this.caption = captionVal !== undefined ?
|
|
314
|
-
|
|
343
|
+
this.caption = captionVal !== undefined ? embeddedText(captionVal) : undefined;
|
|
344
|
+
// A declared width is the table's size, not a ceiling on it: the
|
|
345
|
+
// reference's `expand` is `self._expand or self.width is not None`, and a
|
|
346
|
+
// port that sized to content inside it drew Rich code narrower than Rich.
|
|
347
|
+
this.expand = (options?.expand ?? false) || options?.width !== undefined;
|
|
315
348
|
this.showHeader = options?.showHeader !== false;
|
|
316
349
|
this.showFooter = options?.showFooter ?? false;
|
|
317
350
|
this.showLines = options?.showLines ?? false;
|
|
@@ -360,7 +393,7 @@ export class Table {
|
|
|
360
393
|
//
|
|
361
394
|
// Ahead of the column loop: a cell that throws must leave no phantom column
|
|
362
395
|
// behind. [LAW:no-ambient-temporal-coupling]
|
|
363
|
-
const resolved = cells.map(
|
|
396
|
+
const resolved = cells.map(embed);
|
|
364
397
|
// Auto-create columns if needed
|
|
365
398
|
while (this._columns.length < resolved.length) {
|
|
366
399
|
this.addColumn();
|
|
@@ -385,9 +418,12 @@ export class Table {
|
|
|
385
418
|
// lacking a header should. [LAW:dataflow-not-control-flow] Both run every
|
|
386
419
|
// render and each returns the receiver when it has nothing to change, so
|
|
387
420
|
// the flags arrive as values rather than as branches around a step.
|
|
388
|
-
const
|
|
389
|
-
const box = (this.showHeader ?
|
|
390
|
-
|
|
421
|
+
const frame = this.box?.substitute(options);
|
|
422
|
+
const box = (this.showHeader ? frame : frame?.plainHeaded()) ?? null;
|
|
423
|
+
// The table's own `style` is the base its border style layers over, and
|
|
424
|
+
// that is all it styles: the reference reads it as `table_style` for the
|
|
425
|
+
// frame and nowhere else, so a cell is untouched by it.
|
|
426
|
+
const borderStyle = getStyle(options, this.style).add(getStyle(options, this.borderStyle));
|
|
391
427
|
const border = borderStyle.isNull ? undefined : borderStyle;
|
|
392
428
|
// The one division of the width every row below is measured against.
|
|
393
429
|
const geometry = this._geometry(this._outerWidth(options));
|
|
@@ -400,10 +436,17 @@ export class Table {
|
|
|
400
436
|
if (box && this.showEdge) {
|
|
401
437
|
yield* box.getTop(geometry.cellWidths, border, edge);
|
|
402
438
|
}
|
|
439
|
+
// Every style a cell is drawn with, resolved once per render rather than
|
|
440
|
+
// once per cell, as the reference's `_get_cells` does per column.
|
|
441
|
+
const headerStyle = getStyle(options, this.headerStyle);
|
|
442
|
+
const footerStyle = getStyle(options, this.footerStyle);
|
|
443
|
+
const columnStyles = this._columns.map((c) => getStyle(options, c.style));
|
|
444
|
+
const stripes = this.rowStyles.map((s) => getStyle(options, s));
|
|
403
445
|
// Header row
|
|
404
446
|
if (this.showHeader) {
|
|
405
447
|
const headerCells = this._columns.map((c) => c.header);
|
|
406
|
-
|
|
448
|
+
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);
|
|
407
450
|
// Header separator
|
|
408
451
|
if (box) {
|
|
409
452
|
yield* box.getRow(geometry.cellWidths, "head", border, edge);
|
|
@@ -412,11 +455,10 @@ export class Table {
|
|
|
412
455
|
// Data rows
|
|
413
456
|
for (let rowIdx = 0; rowIdx < this._rows.length; rowIdx++) {
|
|
414
457
|
const row = this._rows[rowIdx];
|
|
415
|
-
const rowCells = this._columns.map((_, colIdx) => row.cells[colIdx] ??
|
|
416
|
-
const rowStyle =
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
yield* this._renderRow(options, rowCells, geometry, box, "row", border, rowStyle);
|
|
458
|
+
const rowCells = this._columns.map((_, colIdx) => row.cells[colIdx] ?? embeddedText(undefined));
|
|
459
|
+
const rowStyle = stripes[rowIdx % stripes.length] ?? NULL_STYLE;
|
|
460
|
+
const cellStyles = columnStyles.map((s) => s.add(rowStyle));
|
|
461
|
+
yield* this._renderRow(options, rowCells, cellStyles, rowStyle, geometry, box, "row", border);
|
|
420
462
|
// Row separator
|
|
421
463
|
const showSep = this.showLines || row.endSection;
|
|
422
464
|
if (showSep && box && rowIdx < this._rows.length - 1) {
|
|
@@ -429,7 +471,8 @@ export class Table {
|
|
|
429
471
|
yield* box.getRow(geometry.cellWidths, "foot", border, edge);
|
|
430
472
|
}
|
|
431
473
|
const footerCells = this._columns.map((c) => c.footer);
|
|
432
|
-
|
|
474
|
+
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);
|
|
433
476
|
}
|
|
434
477
|
// Bottom border
|
|
435
478
|
if (box && this.showEdge) {
|
|
@@ -444,11 +487,13 @@ export class Table {
|
|
|
444
487
|
* [LAW:one-source-of-truth] Both ends of the range are widths the geometry
|
|
445
488
|
* actually produced — the maximum from the demands as they stand, the
|
|
446
489
|
* minimum from the same layout with every column asking for a single cell.
|
|
447
|
-
* Neither end stretches:
|
|
448
|
-
* leave over, and a renderable reports the width its content
|
|
449
|
-
* than the width it was offered.
|
|
450
|
-
*
|
|
451
|
-
*
|
|
490
|
+
* Neither end stretches or fills into an offer: both only spend cells an
|
|
491
|
+
* offer happens to leave over, and a renderable reports the width its content
|
|
492
|
+
* wants rather than the width it was offered. A declared `width` is not an
|
|
493
|
+
* offer, and the maximum reports it. Letting either in made the table measure
|
|
494
|
+
* `Infinity` against an unbounded offer, where `withBoundedWidth` needs a
|
|
495
|
+
* natural width to fall back on — an expanding table, and an empty
|
|
496
|
+
* `Table.grid()`.
|
|
452
497
|
* Neither can exceed the width offered and the tighter request cannot exceed
|
|
453
498
|
* the looser one, so the range cannot invert. Deriving the minimum from raw
|
|
454
499
|
* column and padding counts instead is what used to return
|
|
@@ -459,7 +504,20 @@ export class Table {
|
|
|
459
504
|
const outerWidth = this._outerWidth(options);
|
|
460
505
|
const frame = this._frame();
|
|
461
506
|
const demands = this._columnDemands();
|
|
462
|
-
|
|
507
|
+
// A declared width is the one stretch a measurement reports, because it is
|
|
508
|
+
// a size the table was given rather than an offer it grew into — the
|
|
509
|
+
// reference measures `self.width` as its maximum, so a `Panel` fitted round
|
|
510
|
+
// a table declared at 40 is 44 wide there, not the table's content width.
|
|
511
|
+
// It is laid out exactly as `render` lays it out, so the two cannot report
|
|
512
|
+
// different widths; a declared width with nothing bounding it
|
|
513
|
+
// (`width: Infinity` at an unbounded offer) has no size to report and falls
|
|
514
|
+
// back to the content, as an unbounded `expand` does.
|
|
515
|
+
const declared = this.tableWidth === undefined
|
|
516
|
+
? undefined
|
|
517
|
+
: layoutTable(outerWidth, demands, this.padding, frame).totalWidth;
|
|
518
|
+
const laidOut = declared !== undefined && declared < UNBOUNDED
|
|
519
|
+
? declared
|
|
520
|
+
: layoutTable(outerWidth, demands.map((demand) => ({ ...demand, fill: 0, stretch: false })), this.padding, frame).totalWidth;
|
|
463
521
|
// `UNBOUNDED` is this table's own infinity, so a layout that reached it has
|
|
464
522
|
// no natural width to report — a column asked for every cell there is. Said
|
|
465
523
|
// as the number, it escapes as a width a caller would try to draw:
|
|
@@ -470,10 +528,12 @@ export class Table {
|
|
|
470
528
|
reserved: 0,
|
|
471
529
|
want: Math.min(1, demand.want),
|
|
472
530
|
weight: 1,
|
|
473
|
-
|
|
531
|
+
fill: 0,
|
|
532
|
+
ratio: 0,
|
|
533
|
+
stretch: false,
|
|
474
534
|
})), this.padding, frame).totalWidth;
|
|
475
535
|
return {
|
|
476
|
-
minimum: Math.min(maximum, Math.max(tightest, this.minWidth ?? 0)),
|
|
536
|
+
minimum: Math.min(maximum, Math.max(tightest, cells(this.minWidth ?? 0))),
|
|
477
537
|
maximum,
|
|
478
538
|
};
|
|
479
539
|
}
|
|
@@ -520,35 +580,56 @@ export class Table {
|
|
|
520
580
|
* mode gets its own path through the width division.
|
|
521
581
|
*/
|
|
522
582
|
_columnDemands() {
|
|
523
|
-
// A ratio on any column makes every non-fixed column elastic: a ratio
|
|
524
|
-
// expresses a split of the whole width, so a column that declares none
|
|
525
|
-
// still holds a share of it (1).
|
|
526
|
-
const elastic = this._columns.some((col) => col.flexible);
|
|
527
583
|
return this._columns.map((col, index) => {
|
|
528
584
|
if (col.width !== undefined) {
|
|
529
585
|
// [LAW:single-enforcer] floored where it is parsed, the same rule
|
|
530
586
|
// `normalizePadding` applies to a negative padding side.
|
|
531
587
|
const declared = demandCells(col.width);
|
|
532
|
-
return { reserved: declared, want: declared, weight: 0,
|
|
588
|
+
return { reserved: declared, want: declared, weight: 0, fill: 0, ratio: 0, stretch: false };
|
|
533
589
|
}
|
|
534
|
-
|
|
535
|
-
|
|
590
|
+
// A flexible column takes its share of whatever the bounded columns
|
|
591
|
+
// leave; every other column asks for its natural width, whether or not a
|
|
592
|
+
// neighbour is flexible. The split is the reference's —
|
|
593
|
+
// `fixed_widths = [0 if column.flexible else _range.maximum ...]` — with
|
|
594
|
+
// one divergence: Rich splits by ratio only when the table expands, and
|
|
595
|
+
// here a ratio is honoured either way. Its bounded part is its floor —
|
|
596
|
+
// the reference's `column.min_width or 1` — bid for in cells like any
|
|
597
|
+
// content column, so a squeezed table still pays a declared `minWidth`.
|
|
598
|
+
const share = columnShare(col);
|
|
599
|
+
if (share > 0) {
|
|
600
|
+
const floor = Math.max(1, demandCells(col.minWidth ?? 0));
|
|
601
|
+
return { reserved: 0, want: floor, weight: floor, fill: 0, ratio: share, stretch: false };
|
|
536
602
|
}
|
|
537
|
-
const
|
|
603
|
+
const widest = this._widestCell(col, index);
|
|
604
|
+
const natural = demandCells(this._bounded(col, widest ?? 1));
|
|
538
605
|
// `expand` is a stretch rather than a larger want: the column still
|
|
539
606
|
// competes for its natural width like any other, and only the cells left
|
|
540
607
|
// once every column has that are shared out. A larger want looks
|
|
541
608
|
// equivalent and is not — it lets a one-cell column claim cells while its
|
|
542
|
-
// neighbour is still truncated. The stretch
|
|
609
|
+
// neighbour is still truncated. The stretch weighs by width, so the
|
|
543
610
|
// widest column grows most, which is what keeps a `Progress` bar from
|
|
544
|
-
// getting no more of the slack than its percentage label.
|
|
545
|
-
// weighs by width too: `ratio_distribute(max_width - table_width, widths)`.
|
|
611
|
+
// getting no more of the slack than its percentage label.
|
|
546
612
|
//
|
|
547
613
|
// One deliberate divergence: Rich stretches declared-width columns as
|
|
548
614
|
// well, so `width: 6` under `expand` renders 21 cells wide — an option
|
|
549
615
|
// quietly meaning something else, the defect rich-justify-0cr exists to
|
|
550
616
|
// remove. A reservation's stretch is zero above.
|
|
551
|
-
|
|
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`.
|
|
625
|
+
return {
|
|
626
|
+
reserved: 0,
|
|
627
|
+
want: natural,
|
|
628
|
+
weight: natural,
|
|
629
|
+
fill: widest === undefined ? demandCells(this._bounded(col, UNBOUNDED)) : 0,
|
|
630
|
+
ratio: 0,
|
|
631
|
+
stretch: this.expand,
|
|
632
|
+
};
|
|
552
633
|
});
|
|
553
634
|
}
|
|
554
635
|
/**
|
|
@@ -566,17 +647,18 @@ export class Table {
|
|
|
566
647
|
if (this.showHeader)
|
|
567
648
|
yield col.header;
|
|
568
649
|
for (const row of this._rows)
|
|
569
|
-
yield row.cells[index] ??
|
|
650
|
+
yield row.cells[index] ?? embeddedText(undefined);
|
|
570
651
|
if (this.showFooter)
|
|
571
652
|
yield col.footer;
|
|
572
653
|
}
|
|
573
654
|
/**
|
|
574
|
-
* The widest cell in a column,
|
|
575
|
-
* Zero
|
|
576
|
-
* nothing else.
|
|
655
|
+
* The widest cell in a column, or `undefined` when the column has no cell to
|
|
656
|
+
* measure. Zero is a column whose cells draw nothing — a gutter asks for its
|
|
657
|
+
* padding and nothing else — and is not the same answer.
|
|
658
|
+
* [LAW:types-are-the-program]
|
|
577
659
|
*/
|
|
578
|
-
|
|
579
|
-
let
|
|
660
|
+
_widestCell(col, index) {
|
|
661
|
+
let widest;
|
|
580
662
|
for (const cell of this._columnCells(col, index)) {
|
|
581
663
|
// The stamped cell, so the width a column asks for is the width its text
|
|
582
664
|
// will occupy — measuring the raw value sized this column to
|
|
@@ -587,26 +669,39 @@ export class Table {
|
|
|
587
669
|
// a pre-existing gap: `_columnDemands` carries no `RenderOptions`, so
|
|
588
670
|
// `Measurement.get` is not reachable from here. It contributes a wrong
|
|
589
671
|
// non-zero width, and narrowing that is its own change.
|
|
590
|
-
|
|
672
|
+
widest = Math.max(widest ?? 0, cell instanceof RichText ? cellLen(cell.plain) : cellLen(String(cell)));
|
|
591
673
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
674
|
+
return widest;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* A natural width held to the column's own `minWidth`/`maxWidth`.
|
|
678
|
+
*
|
|
679
|
+
* A bound is a count of cells, read by the rule every width is read by: NaN
|
|
680
|
+
* and a negative are zero cells, so a NaN floor bounds nothing and a NaN
|
|
681
|
+
* ceiling is a ceiling of zero, as `width: NaN` is. A bare `Math.max` returns
|
|
682
|
+
* NaN instead, and the column vanishes. [LAW:one-source-of-truth]
|
|
683
|
+
*/
|
|
684
|
+
_bounded(col, natural) {
|
|
685
|
+
return Math.min(Math.max(natural, cells(col.minWidth ?? 0)), cells(col.maxWidth ?? Infinity));
|
|
597
686
|
}
|
|
598
|
-
*_renderRow(options, cells, geometry, box, level, border
|
|
687
|
+
*_renderRow(options, cells, styles, rowStyle, geometry, box, level, border) {
|
|
599
688
|
const { padLeft, padRight, columns } = geometry;
|
|
600
|
-
const rowStyle = getStyle(options, ownStyle);
|
|
601
689
|
// [LAW:dataflow-not-control-flow] Header, body and footer share this path;
|
|
602
690
|
// the level crosses as a value the box answers with glyphs, not a branch.
|
|
603
691
|
const frame = box?.getContentChars(level);
|
|
692
|
+
// A divider drawn in blanks is part of the row's stripe, so it carries the
|
|
693
|
+
// row's background under the border's own style; one drawn in ink is the
|
|
694
|
+
// frame's alone. Header and footer rows have no row style, as in the
|
|
695
|
+
// reference, so their dividers are the border's either way.
|
|
696
|
+
const divider = frame && (frame.vertical.trim() === ""
|
|
697
|
+
? new Segment(frame.vertical, rowStyle.backgroundStyle.add(border))
|
|
698
|
+
: new Segment(frame.vertical, border));
|
|
604
699
|
// Render each cell onto the canvas the geometry gave its column. Columns
|
|
605
700
|
// the width could not seat are absent from `columns` and so are never
|
|
606
701
|
// rendered at all.
|
|
607
702
|
const cellLines = columns.map((cellWidth, index) => {
|
|
608
703
|
const col = this._columns[index];
|
|
609
|
-
const cell = cells[index] ??
|
|
704
|
+
const cell = cells[index] ?? embed("");
|
|
610
705
|
// The render's own options with the column's canvas laid over them, so a
|
|
611
706
|
// cell resolves its style names against the same theme as the table. The
|
|
612
707
|
// table owns the row's height: a cell inherits none, as the reference's
|
|
@@ -618,29 +713,32 @@ export class Table {
|
|
|
618
713
|
overflow: col.overflow,
|
|
619
714
|
noWrap: col.noWrap,
|
|
620
715
|
height: undefined,
|
|
621
|
-
maxHeight: undefined,
|
|
622
716
|
})];
|
|
623
|
-
|
|
624
|
-
return lines.length > 0 ? lines : [[new Segment(" ".repeat(cellWidth))]];
|
|
717
|
+
return Segment.splitAndCropLines(segs, cellWidth);
|
|
625
718
|
});
|
|
626
719
|
const maxLines = cellLines.reduce((most, lines) => Math.max(most, lines.length), 1);
|
|
720
|
+
// How many blank lines stand above each cell's content. A header sits on
|
|
721
|
+
// the row's floor, so its column labels line up over the data, and every
|
|
722
|
+
// other row starts at the top — the reference forces both.
|
|
723
|
+
const drop = cellLines.map((lines) => (level === "head" ? maxLines - lines.length : 0));
|
|
627
724
|
for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
|
|
628
725
|
if (frame && geometry.edge === 1) {
|
|
629
726
|
yield new Segment(frame.left, border);
|
|
630
727
|
}
|
|
631
728
|
for (let colIdx = 0; colIdx < columns.length; colIdx++) {
|
|
632
|
-
if (colIdx > 0 &&
|
|
633
|
-
yield
|
|
729
|
+
if (colIdx > 0 && divider) {
|
|
730
|
+
yield divider;
|
|
634
731
|
}
|
|
635
732
|
const cellWidth = columns[colIdx];
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
//
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
733
|
+
const style = styles[colIdx];
|
|
734
|
+
// A cell with fewer lines than the row contributes blanks, so every
|
|
735
|
+
// column spans the same number of rows and the frame stays rectangular.
|
|
736
|
+
const line = cellLines[colIdx][lineIdx - drop[colIdx]] ?? [new Segment(" ".repeat(cellWidth))];
|
|
737
|
+
// The cell's style is the base its content's own spans layer over, and
|
|
738
|
+
// it covers the whole cell — its left and right padding and its blank
|
|
739
|
+
// lines too, as the reference's does — so a background fills the column
|
|
740
|
+
// rather than sitting behind the text alone.
|
|
741
|
+
yield* Segment.applyStyle([...blank(padLeft), ...line, ...blank(padRight)], style.isNull ? undefined : style);
|
|
644
742
|
}
|
|
645
743
|
if (frame && geometry.edge === 1) {
|
|
646
744
|
yield new Segment(frame.right, border);
|
|
@@ -652,15 +750,16 @@ export class Table {
|
|
|
652
750
|
const style = getStyle(options, ownStyle);
|
|
653
751
|
const titleStyle = style.isNull ? undefined : style;
|
|
654
752
|
// The table owns the canvas; the caller's text still says how it meets the
|
|
655
|
-
// edge. A `RichText`'s own `justify` and `
|
|
753
|
+
// edge. A `RichText`'s own `justify` and `overflow` outrank the options
|
|
656
754
|
// `render` is handed, so `titleJustify` would lose to a property the caller
|
|
657
|
-
// may not know it set, and
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
755
|
+
// may not know it set, and an `"ignore"` title would have no edge and run
|
|
756
|
+
// straight through the frame. Every other method, and `noWrap`, cuts within
|
|
757
|
+
// the bound, so those stay theirs. Cleared on a copy rather than in place —
|
|
758
|
+
// the caller's text is theirs. [LAW:one-source-of-truth]
|
|
661
759
|
const source = text.copy();
|
|
662
760
|
source.justify = undefined;
|
|
663
|
-
source.
|
|
761
|
+
if (source.overflow === "ignore")
|
|
762
|
+
source.overflow = undefined;
|
|
664
763
|
// The table's title style is the *base* the content's own spans layer over,
|
|
665
764
|
// which is what the reference emits: a `[red]` title inside an italic table
|
|
666
765
|
// title arrives as italic-red, not one or the other. Rendering `text.plain`
|
|
@@ -684,7 +783,6 @@ export class Table {
|
|
|
684
783
|
overflow: undefined,
|
|
685
784
|
noWrap: false,
|
|
686
785
|
height: undefined,
|
|
687
|
-
maxHeight: undefined,
|
|
688
786
|
})];
|
|
689
787
|
// Every line the text has, because that is what the reference renders — a
|
|
690
788
|
// title of "one\ntwo" occupies two lines there. Taking only the first
|