@promptctl/rich-js 0.7.0 → 0.9.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 +99 -31
- package/dist/core/box.d.ts +96 -42
- package/dist/core/box.d.ts.map +1 -1
- package/dist/core/box.js +348 -490
- package/dist/core/box.js.map +1 -1
- package/dist/core/cells.d.ts +46 -1
- package/dist/core/cells.d.ts.map +1 -1
- package/dist/core/cells.js +55 -14
- package/dist/core/cells.js.map +1 -1
- package/dist/core/color.d.ts.map +1 -1
- package/dist/core/color.js +22 -8
- package/dist/core/color.js.map +1 -1
- package/dist/core/console.d.ts +30 -14
- package/dist/core/console.d.ts.map +1 -1
- package/dist/core/console.js +176 -104
- package/dist/core/console.js.map +1 -1
- package/dist/core/highlighter.d.ts +2 -1
- package/dist/core/highlighter.d.ts.map +1 -1
- package/dist/core/highlighter.js +28 -6
- package/dist/core/highlighter.js.map +1 -1
- package/dist/{renderables → core}/json.d.ts +8 -3
- package/dist/core/json.d.ts.map +1 -0
- package/dist/{renderables → core}/json.js +7 -2
- package/dist/core/json.js.map +1 -0
- package/dist/core/markup.d.ts +6 -9
- package/dist/core/markup.d.ts.map +1 -1
- package/dist/core/markup.js +153 -47
- package/dist/core/markup.js.map +1 -1
- package/dist/core/measure.d.ts +11 -0
- package/dist/core/measure.d.ts.map +1 -1
- package/dist/core/measure.js +15 -3
- package/dist/core/measure.js.map +1 -1
- package/dist/core/oklch.d.ts +22 -0
- package/dist/core/oklch.d.ts.map +1 -1
- package/dist/core/oklch.js +62 -18
- package/dist/core/oklch.js.map +1 -1
- package/dist/core/pretty.d.ts +122 -0
- package/dist/core/pretty.d.ts.map +1 -0
- package/dist/core/pretty.js +398 -0
- package/dist/core/pretty.js.map +1 -0
- package/dist/core/protocol.d.ts +80 -0
- package/dist/core/protocol.d.ts.map +1 -1
- package/dist/core/protocol.js +85 -0
- package/dist/core/protocol.js.map +1 -1
- package/dist/core/segment.d.ts +26 -0
- package/dist/core/segment.d.ts.map +1 -1
- package/dist/core/segment.js +45 -0
- package/dist/core/segment.js.map +1 -1
- package/dist/core/strip.d.ts +9 -5
- package/dist/core/strip.d.ts.map +1 -1
- package/dist/core/strip.js +39 -41
- package/dist/core/strip.js.map +1 -1
- package/dist/core/style.d.ts +19 -0
- package/dist/core/style.d.ts.map +1 -1
- package/dist/core/style.js +46 -38
- package/dist/core/style.js.map +1 -1
- package/dist/core/subscription.d.ts +13 -0
- package/dist/core/subscription.d.ts.map +1 -0
- package/dist/core/subscription.js +13 -0
- package/dist/core/subscription.js.map +1 -0
- package/dist/core/text.d.ts +102 -4
- package/dist/core/text.d.ts.map +1 -1
- package/dist/core/text.js +332 -162
- package/dist/core/text.js.map +1 -1
- package/dist/core/wrap.d.ts +42 -0
- package/dist/core/wrap.d.ts.map +1 -0
- package/dist/core/wrap.js +128 -0
- package/dist/core/wrap.js.map +1 -0
- package/dist/host/host-stream.d.ts.map +1 -0
- package/dist/host/host-stream.js.map +1 -0
- package/dist/host/index.d.ts +4 -0
- package/dist/host/index.d.ts.map +1 -0
- package/dist/host/index.js +3 -0
- package/dist/host/index.js.map +1 -0
- package/dist/{widgets → host}/terminal-host.d.ts +20 -64
- package/dist/host/terminal-host.d.ts.map +1 -0
- package/dist/host/terminal-host.js +133 -0
- package/dist/host/terminal-host.js.map +1 -0
- package/dist/index.d.ts +13 -36
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -22
- package/dist/index.js.map +1 -1
- package/dist/node/terminal-host.d.ts +85 -0
- package/dist/node/terminal-host.d.ts.map +1 -0
- package/dist/node/terminal-host.js +161 -0
- package/dist/node/terminal-host.js.map +1 -0
- package/dist/node/traceback.d.ts +28 -0
- package/dist/node/traceback.d.ts.map +1 -0
- package/dist/node/traceback.js +113 -0
- package/dist/node/traceback.js.map +1 -0
- package/dist/renderables/columns.d.ts +24 -2
- package/dist/renderables/columns.d.ts.map +1 -1
- package/dist/renderables/columns.js +91 -32
- package/dist/renderables/columns.js.map +1 -1
- package/dist/renderables/layout.d.ts +61 -5
- package/dist/renderables/layout.d.ts.map +1 -1
- package/dist/renderables/layout.js +191 -19
- package/dist/renderables/layout.js.map +1 -1
- package/dist/renderables/markdown.d.ts +1 -1
- package/dist/renderables/markdown.d.ts.map +1 -1
- package/dist/renderables/markdown.js +7 -14
- package/dist/renderables/markdown.js.map +1 -1
- package/dist/renderables/padding.d.ts +16 -3
- package/dist/renderables/padding.d.ts.map +1 -1
- package/dist/renderables/padding.js +96 -47
- package/dist/renderables/padding.js.map +1 -1
- package/dist/renderables/panel.d.ts +48 -7
- package/dist/renderables/panel.d.ts.map +1 -1
- package/dist/renderables/panel.js +175 -106
- package/dist/renderables/panel.js.map +1 -1
- package/dist/renderables/progress.d.ts +3 -3
- package/dist/renderables/progress.d.ts.map +1 -1
- package/dist/renderables/progress.js +11 -9
- package/dist/renderables/progress.js.map +1 -1
- package/dist/renderables/progressBar.d.ts +3 -3
- package/dist/renderables/progressBar.d.ts.map +1 -1
- package/dist/renderables/progressBar.js +9 -15
- package/dist/renderables/progressBar.js.map +1 -1
- package/dist/renderables/prompt.js +1 -1
- package/dist/renderables/prompt.js.map +1 -1
- package/dist/renderables/rule.d.ts +1 -1
- package/dist/renderables/rule.d.ts.map +1 -1
- package/dist/renderables/rule.js +5 -10
- package/dist/renderables/rule.js.map +1 -1
- package/dist/renderables/spinner.d.ts +2 -2
- package/dist/renderables/spinner.d.ts.map +1 -1
- package/dist/renderables/spinner.js +6 -11
- package/dist/renderables/spinner.js.map +1 -1
- package/dist/renderables/status.d.ts.map +1 -1
- package/dist/renderables/status.js +5 -11
- package/dist/renderables/status.js.map +1 -1
- package/dist/renderables/table.d.ts +90 -16
- package/dist/renderables/table.d.ts.map +1 -1
- package/dist/renderables/table.js +481 -223
- package/dist/renderables/table.js.map +1 -1
- package/dist/renderables/traceback.d.ts +7 -3
- package/dist/renderables/traceback.d.ts.map +1 -1
- package/dist/renderables/traceback.js +15 -18
- package/dist/renderables/traceback.js.map +1 -1
- package/dist/renderables/tree.d.ts +29 -4
- package/dist/renderables/tree.d.ts.map +1 -1
- package/dist/renderables/tree.js +86 -33
- package/dist/renderables/tree.js.map +1 -1
- package/dist/template-bindings/color-funcs.d.ts +3 -3
- package/dist/template-bindings/color-funcs.js +3 -3
- package/dist/template-bindings/helpers.d.ts.map +1 -1
- package/dist/template-bindings/helpers.js +19 -1
- package/dist/template-bindings/helpers.js.map +1 -1
- package/dist/template-bindings/index.d.ts +6 -7
- package/dist/template-bindings/index.d.ts.map +1 -1
- package/dist/template-bindings/index.js +6 -7
- package/dist/template-bindings/index.js.map +1 -1
- package/dist/template-bindings/palette-funcs.d.ts +17 -5
- package/dist/template-bindings/palette-funcs.d.ts.map +1 -1
- package/dist/template-bindings/palette-funcs.js +69 -6
- package/dist/template-bindings/palette-funcs.js.map +1 -1
- package/dist/template-bindings/style-funcs.d.ts +1 -1
- package/dist/template-bindings/style-funcs.js +1 -1
- package/dist/themes/ramp.d.ts +84 -0
- package/dist/themes/ramp.d.ts.map +1 -0
- package/dist/themes/ramp.js +123 -0
- package/dist/themes/ramp.js.map +1 -0
- package/dist/widgets/dropdown.d.ts +1 -1
- package/dist/widgets/dropdown.js +1 -1
- package/dist/widgets/event-router.d.ts +3 -2
- package/dist/widgets/event-router.d.ts.map +1 -1
- package/dist/widgets/focus-manager.d.ts +2 -1
- package/dist/widgets/focus-manager.d.ts.map +1 -1
- package/dist/widgets/index.d.ts +3 -6
- package/dist/widgets/index.d.ts.map +1 -1
- package/dist/widgets/index.js +4 -3
- package/dist/widgets/index.js.map +1 -1
- package/dist/widgets/screen.d.ts +1 -1
- package/dist/widgets/screen.d.ts.map +1 -1
- package/dist/widgets/text-input.d.ts +2 -3
- package/dist/widgets/text-input.d.ts.map +1 -1
- package/dist/widgets/text-input.js +4 -12
- package/dist/widgets/text-input.js.map +1 -1
- package/dist/widgets/types.d.ts +1 -1
- package/dist/widgets/types.d.ts.map +1 -1
- package/dist/widgets/types.js.map +1 -1
- package/dist/widgets/widget-base.d.ts +2 -1
- package/dist/widgets/widget-base.d.ts.map +1 -1
- package/package.json +38 -6
- package/dist/renderables/json.d.ts.map +0 -1
- package/dist/renderables/json.js.map +0 -1
- package/dist/renderables/pretty.d.ts +0 -29
- package/dist/renderables/pretty.d.ts.map +0 -1
- package/dist/renderables/pretty.js +0 -141
- package/dist/renderables/pretty.js.map +0 -1
- package/dist/themes/paletteResolver.d.ts +0 -35
- package/dist/themes/paletteResolver.d.ts.map +0 -1
- package/dist/themes/paletteResolver.js +0 -88
- package/dist/themes/paletteResolver.js.map +0 -1
- package/dist/widgets/host-stream.d.ts.map +0 -1
- package/dist/widgets/host-stream.js.map +0 -1
- package/dist/widgets/terminal-host.d.ts.map +0 -1
- package/dist/widgets/terminal-host.js +0 -258
- package/dist/widgets/terminal-host.js.map +0 -1
- /package/dist/{widgets → host}/host-stream.d.ts +0 -0
- /package/dist/{widgets → host}/host-stream.js +0 -0
|
@@ -1,38 +1,191 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Table — tabular data with headers, borders, auto-sizing, and alignment.
|
|
3
3
|
*/
|
|
4
|
-
import { cellLen } from "../core/cells.js";
|
|
4
|
+
import { cellLen, cellCount as cells } from "../core/cells.js";
|
|
5
5
|
import { Segment } from "../core/segment.js";
|
|
6
|
-
import {
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
import { renderMarkup } from "../core/markup.js";
|
|
10
|
+
import { normalizePadding } from "./padding.js";
|
|
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;
|
|
15
36
|
}
|
|
16
37
|
function toRenderable(content) {
|
|
17
|
-
|
|
18
|
-
|
|
38
|
+
// A `RichText` leaves through this arm too — it implements `Renderable`, so
|
|
39
|
+
// the prototype carries `render` and the instance is returned untouched.
|
|
19
40
|
if (typeof content === "object" && content !== null && "render" in content) {
|
|
20
41
|
return content;
|
|
21
42
|
}
|
|
22
|
-
return
|
|
43
|
+
return toCellText(content);
|
|
44
|
+
}
|
|
45
|
+
/** A `want` no budget can satisfy: the column takes every cell its weight earns. */
|
|
46
|
+
const UNBOUNDED = Number.MAX_SAFE_INTEGER;
|
|
47
|
+
/**
|
|
48
|
+
* A column's `want` or `weight`. Finite, because `UNBOUNDED` is this model's
|
|
49
|
+
* own infinity: a literal `Infinity` reaching `distribute` makes a column's
|
|
50
|
+
* weighted share `Infinity / Infinity`, which is NaN, and the NaN then skews
|
|
51
|
+
* every other elastic column in the table.
|
|
52
|
+
*/
|
|
53
|
+
const demandCells = (n) => Math.min(cells(n), UNBOUNDED);
|
|
54
|
+
/**
|
|
55
|
+
* Hand out `total` cells across `demands`, weighted, and never past a demand's
|
|
56
|
+
* `want`.
|
|
57
|
+
*
|
|
58
|
+
* A column whose proportional share would overshoot its cap is granted its
|
|
59
|
+
* whole want and dropped, and the cells it could not use are reopened to the
|
|
60
|
+
* columns still under their caps. That repeats until everyone still open fits
|
|
61
|
+
* within their share; one largest-remainder pass then places the cells the
|
|
62
|
+
* shares left as fractions, so the granted widths sum to `total` exactly — or
|
|
63
|
+
* to the point where every column is capped, which is how a table stays
|
|
64
|
+
* narrower than a width it was offered.
|
|
65
|
+
*
|
|
66
|
+
* Each round caps at least one column or is the last, so the work is bounded by
|
|
67
|
+
* the number of columns and never by the width. That bound is the point rather
|
|
68
|
+
* than an optimization: handing cells out one at a time made the iteration
|
|
69
|
+
* count the width itself, which stalled on a very wide table and — since
|
|
70
|
+
* `Infinity - 1 === Infinity` — never terminated at all for a `maxWidth` of
|
|
71
|
+
* `Infinity` held open by a ratio column. Capping by want reaches that case in
|
|
72
|
+
* one round.
|
|
73
|
+
*/
|
|
74
|
+
function distribute(total, demands) {
|
|
75
|
+
const granted = demands.map(() => 0);
|
|
76
|
+
let open = demands
|
|
77
|
+
.map((_, index) => index)
|
|
78
|
+
.filter((index) => demands[index].weight > 0 && demands[index].want > 0);
|
|
79
|
+
let remaining = Math.max(0, total);
|
|
80
|
+
const weightOf = (indices) => indices.reduce((sum, index) => sum + demands[index].weight, 0);
|
|
81
|
+
while (open.length > 0 && remaining > 0) {
|
|
82
|
+
const weightSum = weightOf(open);
|
|
83
|
+
const capped = open.filter((index) => (remaining * demands[index].weight) / weightSum >= demands[index].want);
|
|
84
|
+
if (capped.length === 0)
|
|
85
|
+
break;
|
|
86
|
+
for (const index of capped) {
|
|
87
|
+
granted[index] = demands[index].want;
|
|
88
|
+
remaining -= demands[index].want;
|
|
89
|
+
}
|
|
90
|
+
open = open.filter((index) => !capped.includes(index));
|
|
91
|
+
}
|
|
92
|
+
if (open.length > 0 && remaining > 0) {
|
|
93
|
+
const weightSum = weightOf(open);
|
|
94
|
+
const shares = open.map((index) => (remaining * demands[index].weight) / weightSum);
|
|
95
|
+
const whole = shares.map(Math.floor);
|
|
96
|
+
let residue = remaining - whole.reduce((sum, cells) => sum + cells, 0);
|
|
97
|
+
// The cells the shares left as fractions go to the largest fraction first,
|
|
98
|
+
// ties to the leftmost column, so the total lands exactly on `remaining`.
|
|
99
|
+
const byFraction = open
|
|
100
|
+
.map((_, slot) => slot)
|
|
101
|
+
.sort((a, b) => shares[b] - whole[b] - (shares[a] - whole[a]) || a - b);
|
|
102
|
+
for (const slot of byFraction) {
|
|
103
|
+
if (residue <= 0)
|
|
104
|
+
break;
|
|
105
|
+
whole[slot]++;
|
|
106
|
+
residue--;
|
|
107
|
+
}
|
|
108
|
+
open.forEach((index, slot) => {
|
|
109
|
+
granted[index] = whole[slot];
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return granted;
|
|
23
113
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
114
|
+
function layoutTable(outerWidth, demands, padding, frame) {
|
|
115
|
+
const [, padRightWanted, , padLeftWanted] = padding;
|
|
116
|
+
// Plain `number`, not the `CellCol` `cells` hands back: this budget is spent
|
|
117
|
+
// down by arithmetic, and arithmetic on a branded type produces a number.
|
|
118
|
+
let budget = cells(outerWidth);
|
|
119
|
+
const take = (want) => {
|
|
120
|
+
const got = Math.min(Math.max(0, want), budget);
|
|
121
|
+
budget -= got;
|
|
122
|
+
return got;
|
|
123
|
+
};
|
|
124
|
+
// Both edge columns or neither: `Box.getTop` and its siblings take a single
|
|
125
|
+
// flag for the pair, so half a frame is not a shape this renderer can emit.
|
|
126
|
+
const edge = budget >= frame.edge * 2 ? frame.edge : 0;
|
|
127
|
+
take(edge * 2);
|
|
128
|
+
// A first content cell per column, in column order, each paying for the
|
|
129
|
+
// divider that precedes it. The first column the budget cannot seat is where
|
|
130
|
+
// the table ends; the rest are dropped. A column that wants no cells is
|
|
131
|
+
// seated at zero — the queue exists to stop one column taking a second cell
|
|
132
|
+
// before another has its first, and a spacer never joins it.
|
|
133
|
+
const seats = [];
|
|
134
|
+
while (seats.length < demands.length) {
|
|
135
|
+
const seat = Math.min(1, demands[seats.length].want);
|
|
136
|
+
const cost = (seats.length > 0 ? frame.divider : 0) + seat;
|
|
137
|
+
if (budget < cost)
|
|
138
|
+
break;
|
|
139
|
+
take(cost);
|
|
140
|
+
seats.push(seat);
|
|
141
|
+
}
|
|
142
|
+
const seated = seats.length;
|
|
143
|
+
// Padding is uniform across columns or it is not padding, so it is bought
|
|
144
|
+
// for every seated column at once and skipped entirely when only some could
|
|
145
|
+
// afford it.
|
|
146
|
+
const takePerColumn = (want) => {
|
|
147
|
+
const per = seated === 0
|
|
148
|
+
? 0
|
|
149
|
+
: Math.min(Math.max(0, want), Math.floor(budget / seated));
|
|
150
|
+
budget -= per * seated;
|
|
151
|
+
return per;
|
|
152
|
+
};
|
|
153
|
+
const padLeft = takePerColumn(padLeftWanted);
|
|
154
|
+
const padRight = takePerColumn(padRightWanted);
|
|
155
|
+
// Reservations are paid in column order, each already holding the cell the
|
|
156
|
+
// seating pass gave it. A budget too small to cover them all runs out
|
|
157
|
+
// partway, which costs the trailing columns their width but never costs the
|
|
158
|
+
// table its frame.
|
|
159
|
+
const seatedDemands = demands.slice(0, seated);
|
|
160
|
+
const reserved = seatedDemands.map((demand, index) => take(demand.reserved - seats[index]));
|
|
161
|
+
// What is left to apportion is the rest of what each column wanted. A table
|
|
162
|
+
// whose columns all fit leaves this budget partly unspent.
|
|
163
|
+
const wanted = distribute(budget, seatedDemands.map((demand, index) => ({
|
|
164
|
+
want: Math.max(0, demand.want - seats[index] - reserved[index]),
|
|
165
|
+
weight: demand.weight,
|
|
166
|
+
})));
|
|
167
|
+
// The unspent part goes to the columns by their stretch. A table that does
|
|
168
|
+
// not expand stretches nothing, which is how it stays narrower than the width
|
|
169
|
+
// it was offered. The order is the reference's: Rich pads an expanding table
|
|
170
|
+
// only once `table_width < max_width`, never while it is collapsing a column.
|
|
171
|
+
const stretched = distribute(budget - wanted.reduce((sum, cells) => sum + cells, 0), seatedDemands.map((demand) => ({ want: UNBOUNDED, weight: demand.stretch })));
|
|
172
|
+
const columns = wanted.map((cells, index) => seats[index] + reserved[index] + cells + stretched[index]);
|
|
173
|
+
const cellWidths = columns.map((width) => padLeft + width + padRight);
|
|
174
|
+
return {
|
|
175
|
+
edge,
|
|
176
|
+
divider: frame.divider,
|
|
177
|
+
padLeft,
|
|
178
|
+
padRight,
|
|
179
|
+
columns,
|
|
180
|
+
cellWidths,
|
|
181
|
+
totalWidth: edge * 2 +
|
|
182
|
+
Math.max(0, seated - 1) * frame.divider +
|
|
183
|
+
cellWidths.reduce((sum, width) => sum + width, 0),
|
|
184
|
+
};
|
|
32
185
|
}
|
|
33
186
|
export class Column {
|
|
34
|
-
|
|
35
|
-
|
|
187
|
+
_header;
|
|
188
|
+
_footer;
|
|
36
189
|
headerStyle;
|
|
37
190
|
footerStyle;
|
|
38
191
|
style;
|
|
@@ -45,17 +198,11 @@ export class Column {
|
|
|
45
198
|
overflow;
|
|
46
199
|
_cells;
|
|
47
200
|
constructor(options) {
|
|
48
|
-
|
|
49
|
-
this.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
this.footer = footerVal !== undefined
|
|
54
|
-
? (footerVal instanceof RichText ? footerVal : new RichText(footerVal, { end: "" }))
|
|
55
|
-
: undefined;
|
|
56
|
-
this.headerStyle = resolveStyle(options?.headerStyle);
|
|
57
|
-
this.footerStyle = resolveStyle(options?.footerStyle);
|
|
58
|
-
this.style = resolveStyle(options?.style);
|
|
201
|
+
this.header = options?.header;
|
|
202
|
+
this.footer = options?.footer;
|
|
203
|
+
this.headerStyle = options?.headerStyle ?? NULL_STYLE;
|
|
204
|
+
this.footerStyle = options?.footerStyle ?? NULL_STYLE;
|
|
205
|
+
this.style = options?.style ?? NULL_STYLE;
|
|
59
206
|
this.justify = options?.justify ?? "left";
|
|
60
207
|
this.width = options?.width;
|
|
61
208
|
this.minWidth = options?.minWidth;
|
|
@@ -65,6 +212,36 @@ export class Column {
|
|
|
65
212
|
this.overflow = options?.overflow ?? "ellipsis";
|
|
66
213
|
this._cells = [];
|
|
67
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* The two stamped cells, parsed on assignment rather than at the constructor.
|
|
217
|
+
* `Table.columns` hands out the live column and both fields are public, so a
|
|
218
|
+
* constructor-only stamp held only until the first `columns[0].footer = mine`
|
|
219
|
+
* — which installed content that had parsed no markup, still carried its
|
|
220
|
+
* `end`, and was still owned by the caller, into a slot every reader below
|
|
221
|
+
* assumes `toCellText` has been through. [LAW:parse-dont-validate] The setter
|
|
222
|
+
* is the border, so the guarantee holds for the object's whole lifetime and
|
|
223
|
+
* the constructor is one caller of it rather than the one place it is true.
|
|
224
|
+
*
|
|
225
|
+
* Absent and empty are the same header, and the same footer.
|
|
226
|
+
* [LAW:types-are-the-program] Rich declares `footer: RenderableType = ""`, so
|
|
227
|
+
* a column always has one and `show_footer` alone decides whether it is
|
|
228
|
+
* drawn. Modelling the absence as `undefined` instead made "no column has a
|
|
229
|
+
* footer" a state the render path could ask about — and it did, skipping the
|
|
230
|
+
* row a caller had asked for. `toCellText` already maps nothing onto empty,
|
|
231
|
+
* which is why the setters take `undefined` rather than defaulting around it.
|
|
232
|
+
*/
|
|
233
|
+
get header() {
|
|
234
|
+
return this._header;
|
|
235
|
+
}
|
|
236
|
+
set header(content) {
|
|
237
|
+
this._header = toCellText(content);
|
|
238
|
+
}
|
|
239
|
+
get footer() {
|
|
240
|
+
return this._footer;
|
|
241
|
+
}
|
|
242
|
+
set footer(content) {
|
|
243
|
+
this._footer = toCellText(content);
|
|
244
|
+
}
|
|
68
245
|
get flexible() {
|
|
69
246
|
return this.ratio !== undefined && this.ratio > 0;
|
|
70
247
|
}
|
|
@@ -78,8 +255,11 @@ export class Column {
|
|
|
78
255
|
}
|
|
79
256
|
copy() {
|
|
80
257
|
const col = new Column({
|
|
81
|
-
|
|
82
|
-
|
|
258
|
+
// No `.copy()` here: the crossing the constructor routes through copies
|
|
259
|
+
// a `RichText` already, and a second copy is a second home for that rule.
|
|
260
|
+
// [LAW:single-enforcer]
|
|
261
|
+
header: this.header,
|
|
262
|
+
footer: this.footer,
|
|
83
263
|
justify: this.justify,
|
|
84
264
|
width: this.width,
|
|
85
265
|
minWidth: this.minWidth,
|
|
@@ -122,25 +302,21 @@ export class Table {
|
|
|
122
302
|
this._rows = [];
|
|
123
303
|
this.box = options?.box !== undefined ? options.box : HEAVY_HEAD;
|
|
124
304
|
const titleVal = options?.title;
|
|
125
|
-
this.title = titleVal !== undefined
|
|
126
|
-
? (titleVal instanceof RichText ? titleVal : new RichText(titleVal, { end: "" }))
|
|
127
|
-
: undefined;
|
|
305
|
+
this.title = titleVal !== undefined ? toCellText(titleVal) : undefined;
|
|
128
306
|
const captionVal = options?.caption;
|
|
129
|
-
this.caption = captionVal !== undefined
|
|
130
|
-
? (captionVal instanceof RichText ? captionVal : new RichText(captionVal, { end: "" }))
|
|
131
|
-
: undefined;
|
|
307
|
+
this.caption = captionVal !== undefined ? toCellText(captionVal) : undefined;
|
|
132
308
|
this.expand = options?.expand ?? false;
|
|
133
309
|
this.showHeader = options?.showHeader !== false;
|
|
134
310
|
this.showFooter = options?.showFooter ?? false;
|
|
135
311
|
this.showLines = options?.showLines ?? false;
|
|
136
312
|
this.showEdge = options?.showEdge !== false;
|
|
137
|
-
this.padding = normalizePadding(options?.padding);
|
|
138
|
-
this.style =
|
|
139
|
-
this.headerStyle =
|
|
140
|
-
this.footerStyle =
|
|
141
|
-
this.borderStyle =
|
|
142
|
-
this.titleStyle =
|
|
143
|
-
this.captionStyle =
|
|
313
|
+
this.padding = normalizePadding(options?.padding ?? [0, 1, 0, 1]);
|
|
314
|
+
this.style = options?.style ?? NULL_STYLE;
|
|
315
|
+
this.headerStyle = options?.headerStyle ?? "table.header";
|
|
316
|
+
this.footerStyle = options?.footerStyle ?? "table.footer";
|
|
317
|
+
this.borderStyle = options?.borderStyle ?? NULL_STYLE;
|
|
318
|
+
this.titleStyle = options?.titleStyle ?? "table.title";
|
|
319
|
+
this.captionStyle = options?.captionStyle ?? "table.caption";
|
|
144
320
|
this.titleJustify = options?.titleJustify ?? "center";
|
|
145
321
|
this.captionJustify = options?.captionJustify ?? "center";
|
|
146
322
|
this.tableWidth = options?.width;
|
|
@@ -170,11 +346,20 @@ export class Table {
|
|
|
170
346
|
endSection = lastArg.endSection;
|
|
171
347
|
cells = cells.slice(0, -1);
|
|
172
348
|
}
|
|
349
|
+
// Stamped once, here at the border, so sizing and drawing read one resolved
|
|
350
|
+
// cell instead of each converting the raw value for itself. Two converters
|
|
351
|
+
// is two answers to "what is this cell": they disagreed on a `Panel`, which
|
|
352
|
+
// stringifies to `[object Object]` — a string the tag pattern swallows
|
|
353
|
+
// whole, sizing the column to nothing. [LAW:parse-dont-validate]
|
|
354
|
+
//
|
|
355
|
+
// Ahead of the column loop: a cell that throws must leave no phantom column
|
|
356
|
+
// behind. [LAW:no-ambient-temporal-coupling]
|
|
357
|
+
const resolved = cells.map(toRenderable);
|
|
173
358
|
// Auto-create columns if needed
|
|
174
|
-
while (this._columns.length <
|
|
359
|
+
while (this._columns.length < resolved.length) {
|
|
175
360
|
this.addColumn();
|
|
176
361
|
}
|
|
177
|
-
this._rows.push({ cells, endSection });
|
|
362
|
+
this._rows.push({ cells: resolved, endSection });
|
|
178
363
|
return this;
|
|
179
364
|
}
|
|
180
365
|
addSection() {
|
|
@@ -183,78 +368,107 @@ export class Table {
|
|
|
183
368
|
}
|
|
184
369
|
return this;
|
|
185
370
|
}
|
|
186
|
-
*render(
|
|
371
|
+
*render(rawOptions) {
|
|
187
372
|
if (this._columns.length === 0) {
|
|
188
373
|
yield Segment.line();
|
|
189
374
|
return;
|
|
190
375
|
}
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const
|
|
376
|
+
const options = withBoundedWidth(rawOptions, this);
|
|
377
|
+
// The two swaps a box goes through before anything is drawn with it, in the
|
|
378
|
+
// reference's order: what the platform can render, then what a table
|
|
379
|
+
// lacking a header should. [LAW:dataflow-not-control-flow] Both run every
|
|
380
|
+
// render and each returns the receiver when it has nothing to change, so
|
|
381
|
+
// the flags arrive as values rather than as branches around a step.
|
|
382
|
+
const drawable = this.box?.substitute({ asciiOnly: options.asciiOnly });
|
|
383
|
+
const box = (this.showHeader ? drawable : drawable?.plainHeaded()) ?? null;
|
|
384
|
+
const borderStyle = getStyle(options, this.borderStyle);
|
|
385
|
+
const border = borderStyle.isNull ? undefined : borderStyle;
|
|
386
|
+
// The one division of the width every row below is measured against.
|
|
387
|
+
const geometry = this._geometry(this._outerWidth(options));
|
|
388
|
+
const edge = geometry.edge === 1;
|
|
200
389
|
// Title
|
|
201
390
|
if (this.title) {
|
|
202
|
-
yield* this._renderTitle(this.title,
|
|
391
|
+
yield* this._renderTitle(options, this.title, geometry.totalWidth, this.titleStyle, this.titleJustify);
|
|
203
392
|
}
|
|
204
393
|
// Top border
|
|
205
394
|
if (box && this.showEdge) {
|
|
206
|
-
yield* box.getTop(
|
|
395
|
+
yield* box.getTop(geometry.cellWidths, border, edge);
|
|
207
396
|
}
|
|
208
397
|
// Header row
|
|
209
|
-
if (this.showHeader
|
|
398
|
+
if (this.showHeader) {
|
|
210
399
|
const headerCells = this._columns.map((c) => c.header);
|
|
211
|
-
yield* this._renderRow(headerCells,
|
|
400
|
+
yield* this._renderRow(options, headerCells, geometry, box, "head", border, this.headerStyle);
|
|
212
401
|
// Header separator
|
|
213
402
|
if (box) {
|
|
214
|
-
yield* box.getRow(
|
|
403
|
+
yield* box.getRow(geometry.cellWidths, "head", border, edge);
|
|
215
404
|
}
|
|
216
405
|
}
|
|
217
406
|
// Data rows
|
|
218
407
|
for (let rowIdx = 0; rowIdx < this._rows.length; rowIdx++) {
|
|
219
408
|
const row = this._rows[rowIdx];
|
|
220
|
-
const rowCells = [];
|
|
221
|
-
for (let colIdx = 0; colIdx < this._columns.length; colIdx++) {
|
|
222
|
-
const cell = row.cells[colIdx];
|
|
223
|
-
rowCells.push(toRenderable(cell));
|
|
224
|
-
}
|
|
409
|
+
const rowCells = this._columns.map((_, colIdx) => row.cells[colIdx] ?? toCellText(undefined));
|
|
225
410
|
const rowStyle = this.rowStyles.length > 0
|
|
226
|
-
?
|
|
411
|
+
? this.rowStyles[rowIdx % this.rowStyles.length]
|
|
227
412
|
: NULL_STYLE;
|
|
228
|
-
yield* this._renderRow(rowCells,
|
|
413
|
+
yield* this._renderRow(options, rowCells, geometry, box, "row", border, rowStyle);
|
|
229
414
|
// Row separator
|
|
230
415
|
const showSep = this.showLines || row.endSection;
|
|
231
416
|
if (showSep && box && rowIdx < this._rows.length - 1) {
|
|
232
|
-
yield* box.getRow(
|
|
417
|
+
yield* box.getRow(geometry.cellWidths, "row", border, edge);
|
|
233
418
|
}
|
|
234
419
|
}
|
|
235
420
|
// Footer
|
|
236
|
-
if (this.showFooter
|
|
421
|
+
if (this.showFooter) {
|
|
237
422
|
if (box) {
|
|
238
|
-
yield* box.getRow(
|
|
423
|
+
yield* box.getRow(geometry.cellWidths, "foot", border, edge);
|
|
239
424
|
}
|
|
240
|
-
const footerCells = this._columns.map((c) =>
|
|
241
|
-
yield* this._renderRow(footerCells,
|
|
425
|
+
const footerCells = this._columns.map((c) => c.footer);
|
|
426
|
+
yield* this._renderRow(options, footerCells, geometry, box, "foot", border, this.footerStyle);
|
|
242
427
|
}
|
|
243
428
|
// Bottom border
|
|
244
429
|
if (box && this.showEdge) {
|
|
245
|
-
yield* box.getBottom(
|
|
430
|
+
yield* box.getBottom(geometry.cellWidths, border, edge);
|
|
246
431
|
}
|
|
247
432
|
// Caption
|
|
248
433
|
if (this.caption) {
|
|
249
|
-
yield* this._renderTitle(this.caption,
|
|
434
|
+
yield* this._renderTitle(options, this.caption, geometry.totalWidth, this.captionStyle, this.captionJustify);
|
|
250
435
|
}
|
|
251
436
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
437
|
+
/**
|
|
438
|
+
* [LAW:one-source-of-truth] Both ends of the range are widths the geometry
|
|
439
|
+
* actually produced — the maximum from the demands as they stand, the
|
|
440
|
+
* minimum from the same layout with every column asking for a single cell.
|
|
441
|
+
* Neither end stretches: a stretch only spends cells an offer happens to
|
|
442
|
+
* leave over, and a renderable reports the width its content wants rather
|
|
443
|
+
* than the width it was offered. Letting it in made an expanding table
|
|
444
|
+
* measure `Infinity` against an unbounded offer, where `withBoundedWidth`
|
|
445
|
+
* needs a natural width to fall back on.
|
|
446
|
+
* Neither can exceed the width offered and the tighter request cannot exceed
|
|
447
|
+
* the looser one, so the range cannot invert. Deriving the minimum from raw
|
|
448
|
+
* column and padding counts instead is what used to return
|
|
449
|
+
* `{minimum: 6, maximum: 1}` at `maxWidth: 1` — a floor above its own ceiling.
|
|
450
|
+
*/
|
|
451
|
+
measure(rawOptions) {
|
|
452
|
+
const options = withCellWidth(rawOptions);
|
|
453
|
+
const outerWidth = this._outerWidth(options);
|
|
454
|
+
const frame = this._frame();
|
|
455
|
+
const demands = this._columnDemands();
|
|
456
|
+
const laidOut = layoutTable(outerWidth, demands.map((demand) => ({ ...demand, stretch: 0 })), this.padding, frame).totalWidth;
|
|
457
|
+
// `UNBOUNDED` is this table's own infinity, so a layout that reached it has
|
|
458
|
+
// no natural width to report — a column asked for every cell there is. Said
|
|
459
|
+
// as the number, it escapes as a width a caller would try to draw:
|
|
460
|
+
// `minWidth: Infinity` measured 18014398509481988 and rendered
|
|
461
|
+
// `RangeError: Invalid string length` out of the top border.
|
|
462
|
+
const maximum = laidOut >= UNBOUNDED ? Infinity : laidOut;
|
|
463
|
+
const tightest = layoutTable(outerWidth, demands.map((demand) => ({
|
|
464
|
+
reserved: 0,
|
|
465
|
+
want: Math.min(1, demand.want),
|
|
466
|
+
weight: 1,
|
|
467
|
+
stretch: 0,
|
|
468
|
+
})), this.padding, frame).totalWidth;
|
|
255
469
|
return {
|
|
256
|
-
minimum: Math.max(
|
|
257
|
-
maximum
|
|
470
|
+
minimum: Math.min(maximum, Math.max(tightest, this.minWidth ?? 0)),
|
|
471
|
+
maximum,
|
|
258
472
|
};
|
|
259
473
|
}
|
|
260
474
|
// --- Static ---
|
|
@@ -268,167 +482,211 @@ export class Table {
|
|
|
268
482
|
});
|
|
269
483
|
}
|
|
270
484
|
// --- Private ---
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
485
|
+
_frame() {
|
|
486
|
+
return {
|
|
487
|
+
divider: this.box ? 1 : 0,
|
|
488
|
+
edge: this.box && this.showEdge ? 1 : 0,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* The width this table lays itself out against: what it was told to be, as a
|
|
493
|
+
* count of cells, never wider than what it was offered.
|
|
494
|
+
*
|
|
495
|
+
* [LAW:one-source-of-truth] `render` and `measure` divide the same two fields
|
|
496
|
+
* and once did it in two places. `measure` bounded the declared width by the
|
|
497
|
+
* offer and `render` preferred it outright, walking past the value
|
|
498
|
+
* `withBoundedWidth` had just resolved — so `new Table({width: Infinity})`
|
|
499
|
+
* with a `{ratio: 1}` column granted that column `UNBOUNDED` cells and threw
|
|
500
|
+
* `RangeError: Invalid string length` out of the top border, at a perfectly
|
|
501
|
+
* ordinary 80-cell offer.
|
|
502
|
+
*/
|
|
503
|
+
_outerWidth(options) {
|
|
504
|
+
return Math.min(this.tableWidth === undefined ? options.maxWidth : cells(this.tableWidth), options.maxWidth);
|
|
505
|
+
}
|
|
506
|
+
_geometry(outerWidth) {
|
|
507
|
+
return layoutTable(outerWidth, this._columnDemands(), this.padding, this._frame());
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* [LAW:dataflow-not-control-flow] The three ways a column can be sized —
|
|
511
|
+
* declared width, ratio, natural content — differ only in the demand they
|
|
512
|
+
* produce, and `expand` only in its `stretch`. They are resolved once, here, into uniform data, so
|
|
513
|
+
* `layoutTable` runs the same apportionment for every table and no sizing
|
|
514
|
+
* mode gets its own path through the width division.
|
|
515
|
+
*/
|
|
516
|
+
_columnDemands() {
|
|
517
|
+
// A ratio on any column makes every non-fixed column elastic: a ratio
|
|
518
|
+
// expresses a split of the whole width, so a column that declares none
|
|
519
|
+
// still holds a share of it (1).
|
|
520
|
+
const elastic = this._columns.some((col) => col.flexible);
|
|
521
|
+
return this._columns.map((col, index) => {
|
|
287
522
|
if (col.width !== undefined) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
widths[i] = 0;
|
|
293
|
-
flexCols++;
|
|
523
|
+
// [LAW:single-enforcer] floored where it is parsed, the same rule
|
|
524
|
+
// `normalizePadding` applies to a negative padding side.
|
|
525
|
+
const declared = demandCells(col.width);
|
|
526
|
+
return { reserved: declared, want: declared, weight: 0, stretch: 0 };
|
|
294
527
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (flexCols > 0) {
|
|
298
|
-
// Measure content to determine natural widths
|
|
299
|
-
const naturalWidths = [];
|
|
300
|
-
for (let i = 0; i < numCols; i++) {
|
|
301
|
-
const col = this._columns[i];
|
|
302
|
-
if (col.width !== undefined) {
|
|
303
|
-
naturalWidths.push(col.width);
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
let maxContent = cellLen(col.header.plain);
|
|
307
|
-
for (const row of this._rows) {
|
|
308
|
-
const cell = row.cells[i];
|
|
309
|
-
const cellText = String(cell ?? "");
|
|
310
|
-
maxContent = Math.max(maxContent, cellLen(cellText));
|
|
311
|
-
}
|
|
312
|
-
if (col.minWidth !== undefined)
|
|
313
|
-
maxContent = Math.max(maxContent, col.minWidth);
|
|
314
|
-
if (col.maxWidth !== undefined)
|
|
315
|
-
maxContent = Math.min(maxContent, col.maxWidth);
|
|
316
|
-
naturalWidths.push(maxContent);
|
|
317
|
-
}
|
|
318
|
-
// Check if ratio-based distribution applies
|
|
319
|
-
const hasRatios = this._columns.some((c) => c.flexible);
|
|
320
|
-
if (hasRatios) {
|
|
321
|
-
const totalRatio = this._columns.reduce((s, c) => s + (c.ratio ?? 1), 0);
|
|
322
|
-
for (let i = 0; i < numCols; i++) {
|
|
323
|
-
if (this._columns[i].width !== undefined)
|
|
324
|
-
continue;
|
|
325
|
-
const ratio = this._columns[i].ratio ?? 1;
|
|
326
|
-
widths[i] = Math.max(1, Math.floor(remainingWidth * ratio / totalRatio));
|
|
327
|
-
}
|
|
528
|
+
if (elastic) {
|
|
529
|
+
return { reserved: 0, want: UNBOUNDED, weight: demandCells(col.ratio ?? 1), stretch: 0 };
|
|
328
530
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
return widths;
|
|
531
|
+
const natural = demandCells(this._naturalWidth(col, index));
|
|
532
|
+
// `expand` is a stretch rather than a larger want: the column still
|
|
533
|
+
// competes for its natural width like any other, and only the cells left
|
|
534
|
+
// once every column has that are shared out. A larger want looks
|
|
535
|
+
// equivalent and is not — it lets a one-cell column claim cells while its
|
|
536
|
+
// neighbour is still truncated. The stretch is the natural width, so the
|
|
537
|
+
// widest column grows most, which is what keeps a `Progress` bar from
|
|
538
|
+
// getting no more of the slack than its percentage label. The reference
|
|
539
|
+
// weighs by width too: `ratio_distribute(max_width - table_width, widths)`.
|
|
540
|
+
//
|
|
541
|
+
// One deliberate divergence: Rich stretches declared-width columns as
|
|
542
|
+
// well, so `width: 6` under `expand` renders 21 cells wide — an option
|
|
543
|
+
// quietly meaning something else, the defect rich-justify-0cr exists to
|
|
544
|
+
// remove. A reservation's stretch is zero above.
|
|
545
|
+
return { reserved: 0, want: natural, weight: natural, stretch: this.expand ? natural : 0 };
|
|
546
|
+
});
|
|
346
547
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
548
|
+
/**
|
|
549
|
+
* Every cell column `index` draws, in draw order.
|
|
550
|
+
*
|
|
551
|
+
* [LAW:one-source-of-truth] The reference's `_get_cells` is the one answer to
|
|
552
|
+
* "which cells belong to this column", and both `_measure_column` and
|
|
553
|
+
* `_render` read it. Enumerating that set a second time inside the width path
|
|
554
|
+
* is how the two ends drifted: the header was measured whether or not
|
|
555
|
+
* `showHeader` drew it, and `col.footer` was never measured at all, so a
|
|
556
|
+
* footer wider than its column — a totals row, exactly — was cut to `…`.
|
|
557
|
+
* A flag is the whole membership rule; nothing here asks after content.
|
|
558
|
+
*/
|
|
559
|
+
*_columnCells(col, index) {
|
|
560
|
+
if (this.showHeader)
|
|
561
|
+
yield col.header;
|
|
562
|
+
for (const row of this._rows)
|
|
563
|
+
yield row.cells[index] ?? toCellText(undefined);
|
|
564
|
+
if (this.showFooter)
|
|
565
|
+
yield col.footer;
|
|
355
566
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
// Ensure at least one line
|
|
375
|
-
const finalLines = adjusted.length > 0 ? adjusted : [[new Segment(" ".repeat(cellWidth))]];
|
|
376
|
-
cellLines.push(finalLines);
|
|
377
|
-
maxLines = Math.max(maxLines, finalLines.length);
|
|
567
|
+
/**
|
|
568
|
+
* The widest cell in a column, bounded by its own `minWidth`/`maxWidth`.
|
|
569
|
+
* Zero for a column that draws nothing — a gutter asks for its padding and
|
|
570
|
+
* nothing else.
|
|
571
|
+
*/
|
|
572
|
+
_naturalWidth(col, index) {
|
|
573
|
+
let natural = 0;
|
|
574
|
+
for (const cell of this._columnCells(col, index)) {
|
|
575
|
+
// The stamped cell, so the width a column asks for is the width its text
|
|
576
|
+
// will occupy — measuring the raw value sized this column to
|
|
577
|
+
// `[red]Solo[/red]`, fifteen cells for four cells of text.
|
|
578
|
+
// [LAW:one-source-of-truth]
|
|
579
|
+
//
|
|
580
|
+
// A cell that draws itself is stringified rather than measured, which is
|
|
581
|
+
// a pre-existing gap: `_columnDemands` carries no `RenderOptions`, so
|
|
582
|
+
// `Measurement.get` is not reachable from here. It contributes a wrong
|
|
583
|
+
// non-zero width, and narrowing that is its own change.
|
|
584
|
+
natural = Math.max(natural, cell instanceof RichText ? cellLen(cell.plain) : cellLen(String(cell)));
|
|
378
585
|
}
|
|
379
|
-
|
|
586
|
+
if (col.minWidth !== undefined)
|
|
587
|
+
natural = Math.max(natural, col.minWidth);
|
|
588
|
+
if (col.maxWidth !== undefined)
|
|
589
|
+
natural = Math.min(natural, col.maxWidth);
|
|
590
|
+
return natural;
|
|
591
|
+
}
|
|
592
|
+
*_renderRow(options, cells, geometry, box, level, border, ownStyle) {
|
|
593
|
+
const { padLeft, padRight, columns } = geometry;
|
|
594
|
+
const rowStyle = getStyle(options, ownStyle);
|
|
595
|
+
// [LAW:dataflow-not-control-flow] Header, body and footer share this path;
|
|
596
|
+
// the level crosses as a value the box answers with glyphs, not a branch.
|
|
597
|
+
const frame = box?.getContentChars(level);
|
|
598
|
+
// Render each cell onto the canvas the geometry gave its column. Columns
|
|
599
|
+
// the width could not seat are absent from `columns` and so are never
|
|
600
|
+
// rendered at all.
|
|
601
|
+
const cellLines = columns.map((cellWidth, index) => {
|
|
602
|
+
const col = this._columns[index];
|
|
603
|
+
const cell = cells[index] ?? toRenderable("");
|
|
604
|
+
// The render's own options with the column's canvas laid over them, so a
|
|
605
|
+
// cell resolves its style names against the same theme as the table. The
|
|
606
|
+
// table owns the row's height: a cell inherits none, as the reference's
|
|
607
|
+
// `height=None` has it.
|
|
608
|
+
const segs = [...cell.render({
|
|
609
|
+
...options,
|
|
610
|
+
maxWidth: cellWidth,
|
|
611
|
+
justify: col.justify,
|
|
612
|
+
overflow: col.overflow,
|
|
613
|
+
noWrap: col.noWrap,
|
|
614
|
+
height: undefined,
|
|
615
|
+
maxHeight: undefined,
|
|
616
|
+
})];
|
|
617
|
+
const lines = Segment.splitLines(segs).map((line) => Segment.adjustLineLength(line, cellWidth));
|
|
618
|
+
return lines.length > 0 ? lines : [[new Segment(" ".repeat(cellWidth))]];
|
|
619
|
+
});
|
|
620
|
+
const maxLines = cellLines.reduce((most, lines) => Math.max(most, lines.length), 1);
|
|
380
621
|
for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
|
|
381
|
-
if (
|
|
382
|
-
yield new Segment(
|
|
622
|
+
if (frame && geometry.edge === 1) {
|
|
623
|
+
yield new Segment(frame.left, border);
|
|
383
624
|
}
|
|
384
|
-
for (let colIdx = 0; colIdx <
|
|
385
|
-
if (colIdx > 0 &&
|
|
386
|
-
yield new Segment(
|
|
625
|
+
for (let colIdx = 0; colIdx < columns.length; colIdx++) {
|
|
626
|
+
if (colIdx > 0 && frame) {
|
|
627
|
+
yield new Segment(frame.vertical, border);
|
|
387
628
|
}
|
|
388
|
-
const
|
|
389
|
-
const cellWidth = colWidths[colIdx] ?? 1;
|
|
390
|
-
// Left padding
|
|
629
|
+
const cellWidth = columns[colIdx];
|
|
391
630
|
if (padLeft > 0)
|
|
392
631
|
yield new Segment(" ".repeat(padLeft));
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
yield* styled;
|
|
398
|
-
}
|
|
399
|
-
else {
|
|
400
|
-
yield new Segment(" ".repeat(cellWidth));
|
|
401
|
-
}
|
|
402
|
-
// Right padding
|
|
632
|
+
// A cell that ran out of lines contributes blanks, so every column
|
|
633
|
+
// spans the same number of rows and the frame stays rectangular.
|
|
634
|
+
const line = cellLines[colIdx][lineIdx] ?? [new Segment(" ".repeat(cellWidth))];
|
|
635
|
+
yield* rowStyle.isNull ? line : Segment.applyStyle(line, rowStyle);
|
|
403
636
|
if (padRight > 0)
|
|
404
637
|
yield new Segment(" ".repeat(padRight));
|
|
405
638
|
}
|
|
406
|
-
if (
|
|
407
|
-
yield new Segment(
|
|
639
|
+
if (frame && geometry.edge === 1) {
|
|
640
|
+
yield new Segment(frame.right, border);
|
|
408
641
|
}
|
|
409
642
|
yield Segment.line();
|
|
410
643
|
}
|
|
411
644
|
}
|
|
412
|
-
*_renderTitle(text, tableWidth,
|
|
645
|
+
*_renderTitle(options, text, tableWidth, ownStyle, justify) {
|
|
646
|
+
const style = getStyle(options, ownStyle);
|
|
413
647
|
const titleStyle = style.isNull ? undefined : style;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
648
|
+
// The table owns the canvas; the caller's text still says how it meets the
|
|
649
|
+
// edge. A `RichText`'s own `justify` and `noWrap` outrank the options
|
|
650
|
+
// `render` is handed, so `titleJustify` would lose to a property the caller
|
|
651
|
+
// may not know it set, and a `noWrap` title would leave at its natural width
|
|
652
|
+
// and run straight through the frame. `overflow` stays theirs: every method
|
|
653
|
+
// cuts within a bound it cannot lift, so none can escape. Cleared on a copy
|
|
654
|
+
// rather than in place — the caller's text is theirs. [LAW:one-source-of-truth]
|
|
655
|
+
const source = text.copy();
|
|
656
|
+
source.justify = undefined;
|
|
657
|
+
source.noWrap = false;
|
|
658
|
+
// The table's title style is the *base* the content's own spans layer over,
|
|
659
|
+
// which is what the reference emits: a `[red]` title inside an italic table
|
|
660
|
+
// title arrives as italic-red, not one or the other. Rendering `text.plain`
|
|
661
|
+
// here read the characters and dropped every span attached to them, so a
|
|
662
|
+
// styled title lost its styling and parsed markup silently did nothing.
|
|
663
|
+
//
|
|
664
|
+
// Rendered at the table's own width with nothing suppressed, because that
|
|
665
|
+
// is what the reference hands its title: an annotation too wide for the
|
|
666
|
+
// frame wraps down it rather than being cut off at the corner. `noWrap` and
|
|
667
|
+
// an explicit `crop` stood here and did the cutting (rich-table-6uy.7).
|
|
668
|
+
//
|
|
669
|
+
// [LAW:single-enforcer] The alignment is the renderable's to perform, not
|
|
670
|
+
// just to be told. Padding the lines here as well needed the same
|
|
671
|
+
// rules — that a wrap's trailing whitespace is not content to centre
|
|
672
|
+
// around, that `left` fills the canvas and an unset justify does not — and
|
|
673
|
+
// a second copy of those is a second answer waiting to disagree.
|
|
674
|
+
const rendered = [...source.render({
|
|
675
|
+
...options,
|
|
676
|
+
maxWidth: tableWidth,
|
|
677
|
+
justify,
|
|
678
|
+
overflow: undefined,
|
|
679
|
+
noWrap: false,
|
|
680
|
+
height: undefined,
|
|
681
|
+
maxHeight: undefined,
|
|
682
|
+
})];
|
|
683
|
+
// Every line the text has, because that is what the reference renders — a
|
|
684
|
+
// title of "one\ntwo" occupies two lines there. Taking only the first
|
|
685
|
+
// dropped the rest with no truncation mark.
|
|
686
|
+
for (const line of Segment.splitLines(rendered)) {
|
|
687
|
+
yield* Segment.applyStyle(line, titleStyle);
|
|
418
688
|
yield Segment.line();
|
|
419
|
-
return;
|
|
420
689
|
}
|
|
421
|
-
const gap = tableWidth - textWidth;
|
|
422
|
-
const leftPad = justify === "right" ? gap
|
|
423
|
-
: justify === "center" ? Math.floor(gap / 2)
|
|
424
|
-
: 0;
|
|
425
|
-
const rightPad = gap - leftPad;
|
|
426
|
-
if (leftPad > 0)
|
|
427
|
-
yield new Segment(" ".repeat(leftPad));
|
|
428
|
-
yield new Segment(plain, titleStyle);
|
|
429
|
-
if (rightPad > 0)
|
|
430
|
-
yield new Segment(" ".repeat(rightPad));
|
|
431
|
-
yield Segment.line();
|
|
432
690
|
}
|
|
433
691
|
}
|
|
434
692
|
//# sourceMappingURL=table.js.map
|