@promptctl/rich-js 0.18.1 → 0.20.0

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