@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.
Files changed (147) hide show
  1. package/README.md +13 -26
  2. package/dist/core/anchor.d.ts +45 -0
  3. package/dist/core/anchor.js +39 -0
  4. package/dist/core/ansi.d.ts +65 -0
  5. package/dist/core/ansi.js +231 -0
  6. package/dist/core/box.d.ts +0 -1
  7. package/dist/core/box.js +6 -18
  8. package/dist/core/cells.d.ts +16 -5
  9. package/dist/core/cells.js +31 -10
  10. package/dist/core/color.d.ts +4 -8
  11. package/dist/core/color.js +28 -19
  12. package/dist/core/console.d.ts +13 -4
  13. package/dist/core/console.js +91 -36
  14. package/dist/core/export-html.d.ts +40 -5
  15. package/dist/core/export-html.js +111 -17
  16. package/dist/core/markup.d.ts +27 -5
  17. package/dist/core/markup.js +282 -170
  18. package/dist/core/measure.d.ts +1 -1
  19. package/dist/core/measure.js +2 -1
  20. package/dist/core/place.d.ts +30 -0
  21. package/dist/core/place.js +51 -0
  22. package/dist/core/pretty.d.ts +3 -0
  23. package/dist/core/pretty.js +5 -1
  24. package/dist/core/protocol.d.ts +136 -6
  25. package/dist/core/protocol.js +64 -2
  26. package/dist/core/render.d.ts +2 -2
  27. package/dist/core/render.js +2 -2
  28. package/dist/core/segment.d.ts +23 -4
  29. package/dist/core/segment.js +75 -16
  30. package/dist/core/spinnerData.d.ts +2 -0
  31. package/dist/core/spinnerData.js +3 -1
  32. package/dist/core/strip.d.ts +3 -7
  33. package/dist/core/strip.js +32 -35
  34. package/dist/core/style.d.ts +25 -0
  35. package/dist/core/style.js +61 -89
  36. package/dist/core/text.d.ts +16 -15
  37. package/dist/core/text.js +62 -76
  38. package/dist/host/app.d.ts +121 -0
  39. package/dist/host/app.js +255 -0
  40. package/dist/host/host-environment.d.ts +26 -0
  41. package/dist/host/host-environment.js +44 -0
  42. package/dist/host/index.d.ts +4 -2
  43. package/dist/host/index.js +2 -1
  44. package/dist/host/terminal-host.d.ts +43 -7
  45. package/dist/host/terminal-host.js +57 -3
  46. package/dist/index.d.ts +7 -3
  47. package/dist/index.js +4 -1
  48. package/dist/node/terminal-host.d.ts +31 -2
  49. package/dist/node/terminal-host.js +69 -2
  50. package/dist/node/traceback.d.ts +16 -6
  51. package/dist/node/traceback.js +30 -8
  52. package/dist/renderables/align.d.ts +2 -2
  53. package/dist/renderables/align.js +6 -19
  54. package/dist/renderables/columns.js +4 -20
  55. package/dist/renderables/embed.d.ts +50 -0
  56. package/dist/renderables/embed.js +71 -0
  57. package/dist/renderables/group.js +3 -1
  58. package/dist/renderables/layout.js +31 -34
  59. package/dist/renderables/live.d.ts +6 -3
  60. package/dist/renderables/live.js +89 -45
  61. package/dist/renderables/markdown.d.ts +1 -1
  62. package/dist/renderables/markdown.js +6 -4
  63. package/dist/renderables/padding.js +3 -2
  64. package/dist/renderables/panel.js +26 -41
  65. package/dist/renderables/progress.js +5 -8
  66. package/dist/renderables/progressBar.js +6 -5
  67. package/dist/renderables/rule.d.ts +3 -2
  68. package/dist/renderables/rule.js +9 -9
  69. package/dist/renderables/spinner.d.ts +5 -3
  70. package/dist/renderables/spinner.js +14 -8
  71. package/dist/renderables/syntax.d.ts +1 -1
  72. package/dist/renderables/syntax.js +2 -1
  73. package/dist/renderables/table.d.ts +26 -14
  74. package/dist/renderables/table.js +239 -141
  75. package/dist/renderables/traceback.js +8 -9
  76. package/dist/renderables/tree.d.ts +17 -3
  77. package/dist/renderables/tree.js +62 -71
  78. package/dist/renderables/viewport.d.ts +114 -0
  79. package/dist/renderables/viewport.js +198 -0
  80. package/dist/template-bindings/color-funcs.js +11 -13
  81. package/dist/template-bindings/index.js +4 -2
  82. package/dist/template-bindings/palette-funcs.js +6 -16
  83. package/dist/template-bindings/style-funcs.js +4 -0
  84. package/dist/themes/data/ansi-tables.d.ts +14 -0
  85. package/dist/themes/data/ansi-tables.js +49 -0
  86. package/dist/themes/data/atom-one-dark.js +19 -0
  87. package/dist/themes/data/atom-one-light.js +19 -0
  88. package/dist/themes/data/catppuccin-frappe.js +19 -0
  89. package/dist/themes/data/catppuccin-latte.js +19 -0
  90. package/dist/themes/data/catppuccin-macchiato.js +19 -0
  91. package/dist/themes/data/catppuccin-mocha.js +19 -0
  92. package/dist/themes/data/cyberpunk.js +2 -0
  93. package/dist/themes/data/default.js +2 -0
  94. package/dist/themes/data/dracula.js +19 -0
  95. package/dist/themes/data/flexoki.js +19 -0
  96. package/dist/themes/data/gruvbox.js +19 -0
  97. package/dist/themes/data/monokai.js +19 -0
  98. package/dist/themes/data/nord.js +19 -0
  99. package/dist/themes/data/rose-pine-dawn.js +19 -0
  100. package/dist/themes/data/rose-pine-moon.js +19 -0
  101. package/dist/themes/data/rose-pine.js +19 -0
  102. package/dist/themes/data/solarized-dark.js +19 -0
  103. package/dist/themes/data/solarized-light.js +19 -0
  104. package/dist/themes/data/svg-export.js +19 -0
  105. package/dist/themes/data/textual-ansi.js +2 -0
  106. package/dist/themes/data/textual-dark.js +2 -0
  107. package/dist/themes/data/textual-light.js +2 -0
  108. package/dist/themes/data/tokyo-night.js +19 -0
  109. package/dist/themes/data/types.d.ts +16 -0
  110. package/dist/themes/data/types.js +10 -1
  111. package/dist/themes/registry.d.ts +3 -2
  112. package/dist/themes/registry.js +13 -1
  113. package/dist/themes/terminalThemes.d.ts +2 -7
  114. package/dist/themes/terminalThemes.js +4 -9
  115. package/dist/widgets/button.d.ts +1 -1
  116. package/dist/widgets/button.js +4 -2
  117. package/dist/widgets/checkbox.d.ts +2 -2
  118. package/dist/widgets/checkbox.js +5 -3
  119. package/dist/widgets/dropdown.d.ts +2 -2
  120. package/dist/widgets/dropdown.js +13 -16
  121. package/dist/widgets/event-router.d.ts +21 -28
  122. package/dist/widgets/event-router.js +80 -73
  123. package/dist/widgets/focus-manager.d.ts +25 -6
  124. package/dist/widgets/focus-manager.js +63 -65
  125. package/dist/widgets/hit.d.ts +64 -0
  126. package/dist/widgets/hit.js +113 -0
  127. package/dist/widgets/index.d.ts +5 -7
  128. package/dist/widgets/index.js +3 -4
  129. package/dist/widgets/slider.d.ts +2 -2
  130. package/dist/widgets/slider.js +7 -7
  131. package/dist/widgets/static-item.d.ts +9 -15
  132. package/dist/widgets/static-item.js +9 -15
  133. package/dist/widgets/text-input.d.ts +2 -2
  134. package/dist/widgets/text-input.js +17 -14
  135. package/dist/widgets/toggle.d.ts +1 -1
  136. package/dist/widgets/toggle.js +3 -2
  137. package/dist/widgets/types.d.ts +14 -35
  138. package/dist/widgets/types.js +0 -1
  139. package/dist/widgets/widget-app.d.ts +63 -0
  140. package/dist/widgets/widget-app.js +199 -0
  141. package/dist/widgets/widget-base.d.ts +4 -6
  142. package/dist/widgets/widget-base.js +22 -30
  143. package/package.json +12 -4
  144. package/dist/host/host-stream.d.ts +0 -30
  145. package/dist/host/host-stream.js +0 -35
  146. package/dist/widgets/screen.d.ts +0 -98
  147. 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 { renderMarkup } from "../core/markup.js";
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
- let open = demands
83
- .map((_, index) => index)
84
- .filter((index) => demands[index].weight > 0 && demands[index].want > 0);
85
- let remaining = Math.max(0, total);
86
- const weightOf = (indices) => indices.reduce((sum, index) => sum + demands[index].weight, 0);
87
- while (open.length > 0 && remaining > 0) {
88
- const weightSum = weightOf(open);
89
- const capped = open.filter((index) => (remaining * demands[index].weight) / weightSum >= demands[index].want);
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] = demands[index].want;
94
- remaining -= demands[index].want;
70
+ for (const { index, want } of capped) {
71
+ granted[index] = Number(want);
72
+ remaining -= want;
95
73
  }
96
- open = open.filter((index) => !capped.includes(index));
74
+ open = open.filter((bid) => !capped.includes(bid));
97
75
  }
98
- if (open.length > 0 && remaining > 0) {
99
- const weightSum = weightOf(open);
100
- const shares = open.map((index) => (remaining * demands[index].weight) / weightSum);
101
- const whole = shares.map(Math.floor);
102
- let residue = remaining - whole.reduce((sum, cells) => sum + cells, 0);
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) => shares[b] - whole[b] - (shares[a] - whole[a]) || a - b);
87
+ .sort((a, b) => Number(fraction[b] - fraction[a]) || a - b);
108
88
  for (const slot of byFraction) {
109
- if (residue <= 0)
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
- // The unspent part goes to the columns by their stretch. A table that does
174
- // not expand stretches nothing, which is how it stays narrower than the width
175
- // it was offered. The order is the reference's: Rich pads an expanding table
176
- // only once `table_width < max_width`, never while it is collapsing a column.
177
- const stretched = distribute(budget - wanted.reduce((sum, cells) => sum + cells, 0), seatedDemands.map((demand) => ({ want: UNBOUNDED, weight: demand.stretch })));
178
- const columns = wanted.map((cells, index) => seats[index] + reserved[index] + cells + stretched[index]);
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 `toCellText` has been through. [LAW:parse-dont-validate] The setter
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. `toCellText` already maps nothing onto empty,
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 = toCellText(content);
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 = toCellText(content);
279
+ this._footer = embeddedText(content);
250
280
  }
251
281
  get flexible() {
252
- return this.ratio !== undefined && this.ratio > 0;
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 ? toCellText(titleVal) : undefined;
341
+ this.title = titleVal !== undefined ? embeddedText(titleVal) : undefined;
312
342
  const captionVal = options?.caption;
313
- this.caption = captionVal !== undefined ? toCellText(captionVal) : undefined;
314
- this.expand = options?.expand ?? false;
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(toRenderable);
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 drawable = this.box?.substitute({ asciiOnly: options.asciiOnly });
389
- const box = (this.showHeader ? drawable : drawable?.plainHeaded()) ?? null;
390
- const borderStyle = getStyle(options, this.borderStyle);
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
- yield* this._renderRow(options, headerCells, geometry, box, "head", border, this.headerStyle);
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] ?? toCellText(undefined));
416
- const rowStyle = this.rowStyles.length > 0
417
- ? this.rowStyles[rowIdx % this.rowStyles.length]
418
- : NULL_STYLE;
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
- yield* this._renderRow(options, footerCells, geometry, box, "foot", border, this.footerStyle);
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: a stretch only spends cells an offer happens to
448
- * leave over, and a renderable reports the width its content wants rather
449
- * than the width it was offered. Letting it in made an expanding table
450
- * measure `Infinity` against an unbounded offer, where `withBoundedWidth`
451
- * needs a natural width to fall back on.
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
- const laidOut = layoutTable(outerWidth, demands.map((demand) => ({ ...demand, stretch: 0 })), this.padding, frame).totalWidth;
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
- stretch: 0,
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, stretch: 0 };
588
+ return { reserved: declared, want: declared, weight: 0, fill: 0, ratio: 0, stretch: false };
533
589
  }
534
- if (elastic) {
535
- return { reserved: 0, want: UNBOUNDED, weight: demandCells(col.ratio ?? 1), stretch: 0 };
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 natural = demandCells(this._naturalWidth(col, index));
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 is the natural width, so the
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. The reference
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
- return { reserved: 0, want: natural, weight: natural, stretch: this.expand ? natural : 0 };
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] ?? toCellText(undefined);
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, bounded by its own `minWidth`/`maxWidth`.
575
- * Zero for a column that draws nothing — a gutter asks for its padding and
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
- _naturalWidth(col, index) {
579
- let natural = 0;
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
- natural = Math.max(natural, cell instanceof RichText ? cellLen(cell.plain) : cellLen(String(cell)));
672
+ widest = Math.max(widest ?? 0, cell instanceof RichText ? cellLen(cell.plain) : cellLen(String(cell)));
591
673
  }
592
- if (col.minWidth !== undefined)
593
- natural = Math.max(natural, col.minWidth);
594
- if (col.maxWidth !== undefined)
595
- natural = Math.min(natural, col.maxWidth);
596
- return natural;
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, ownStyle) {
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] ?? toRenderable("");
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
- const lines = Segment.splitLines(segs).map((line) => Segment.adjustLineLength(line, cellWidth));
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 && frame) {
633
- yield new Segment(frame.vertical, border);
729
+ if (colIdx > 0 && divider) {
730
+ yield divider;
634
731
  }
635
732
  const cellWidth = columns[colIdx];
636
- if (padLeft > 0)
637
- yield new Segment(" ".repeat(padLeft));
638
- // A cell that ran out of lines contributes blanks, so every column
639
- // spans the same number of rows and the frame stays rectangular.
640
- const line = cellLines[colIdx][lineIdx] ?? [new Segment(" ".repeat(cellWidth))];
641
- yield* rowStyle.isNull ? line : Segment.applyStyle(line, rowStyle);
642
- if (padRight > 0)
643
- yield new Segment(" ".repeat(padRight));
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 `noWrap` outrank the options
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 a `noWrap` title would leave at its natural width
658
- // and run straight through the frame. `overflow` stays theirs: every method
659
- // cuts within a bound it cannot lift, so none can escape. Cleared on a copy
660
- // rather than in place — the caller's text is theirs. [LAW:one-source-of-truth]
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.noWrap = false;
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