@promptctl/rich-js 0.23.0 → 0.24.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 (117) hide show
  1. package/THIRD-PARTY-NOTICES +29 -1
  2. package/dist/core/animation.d.ts +78 -0
  3. package/dist/core/animation.js +107 -0
  4. package/dist/core/cell-widths.d.ts +9 -0
  5. package/dist/core/cell-widths.js +140 -0
  6. package/dist/core/cells.d.ts +17 -11
  7. package/dist/core/cells.js +100 -25
  8. package/dist/core/clock.d.ts +9 -1
  9. package/dist/core/clock.js +22 -2
  10. package/dist/core/color.d.ts +10 -0
  11. package/dist/core/color.js +36 -1
  12. package/dist/core/console.js +5 -5
  13. package/dist/core/easing.d.ts +138 -7
  14. package/dist/core/easing.js +355 -17
  15. package/dist/core/json.js +2 -1
  16. package/dist/core/measure.js +2 -2
  17. package/dist/core/place.js +2 -1
  18. package/dist/core/pretty.js +2 -2
  19. package/dist/core/protocol.d.ts +73 -19
  20. package/dist/core/protocol.js +56 -15
  21. package/dist/core/render.js +2 -1
  22. package/dist/core/segment.d.ts +28 -1
  23. package/dist/core/segment.js +60 -44
  24. package/dist/core/strip.d.ts +107 -29
  25. package/dist/core/strip.js +242 -131
  26. package/dist/core/style.d.ts +18 -1
  27. package/dist/core/style.js +49 -9
  28. package/dist/core/text.js +26 -15
  29. package/dist/core/timeline.d.ts +84 -0
  30. package/dist/core/timeline.js +144 -0
  31. package/dist/core/tween.d.ts +49 -0
  32. package/dist/core/tween.js +69 -0
  33. package/dist/host/app.js +4 -6
  34. package/dist/host/index.d.ts +1 -1
  35. package/dist/host/input.d.ts +37 -13
  36. package/dist/host/input.js +146 -56
  37. package/dist/host/terminal-host.d.ts +16 -0
  38. package/dist/host/terminal-host.js +1 -0
  39. package/dist/index.d.ts +16 -11
  40. package/dist/index.js +15 -5
  41. package/dist/node/terminal-host.d.ts +8 -1
  42. package/dist/node/terminal-host.js +2 -0
  43. package/dist/renderables/align.d.ts +3 -2
  44. package/dist/renderables/align.js +3 -0
  45. package/dist/renderables/columns.d.ts +3 -2
  46. package/dist/renderables/columns.js +7 -2
  47. package/dist/renderables/constrain.d.ts +3 -2
  48. package/dist/renderables/constrain.js +5 -1
  49. package/dist/renderables/effect.d.ts +83 -30
  50. package/dist/renderables/effect.js +155 -57
  51. package/dist/renderables/effects.d.ts +88 -39
  52. package/dist/renderables/effects.js +444 -105
  53. package/dist/renderables/embed.d.ts +10 -0
  54. package/dist/renderables/embed.js +18 -9
  55. package/dist/renderables/flexStrip.d.ts +3 -2
  56. package/dist/renderables/flexStrip.js +6 -1
  57. package/dist/renderables/frames.d.ts +116 -0
  58. package/dist/renderables/frames.js +133 -0
  59. package/dist/renderables/group.d.ts +3 -2
  60. package/dist/renderables/group.js +5 -2
  61. package/dist/renderables/html-entities.d.ts +8 -0
  62. package/dist/renderables/html-entities.js +386 -0
  63. package/dist/renderables/layout.d.ts +3 -2
  64. package/dist/renderables/layout.js +15 -2
  65. package/dist/renderables/list-view.d.ts +3 -2
  66. package/dist/renderables/list-view.js +12 -5
  67. package/dist/renderables/live.d.ts +23 -7
  68. package/dist/renderables/live.js +69 -35
  69. package/dist/renderables/markdown-parse.d.ts +109 -0
  70. package/dist/renderables/markdown-parse.js +1421 -0
  71. package/dist/renderables/markdown-url.d.ts +19 -0
  72. package/dist/renderables/markdown-url.js +314 -0
  73. package/dist/renderables/markdown.d.ts +7 -6
  74. package/dist/renderables/markdown.js +363 -499
  75. package/dist/renderables/padding.d.ts +8 -20
  76. package/dist/renderables/padding.js +32 -50
  77. package/dist/renderables/panel.d.ts +26 -26
  78. package/dist/renderables/panel.js +67 -103
  79. package/dist/renderables/progress.d.ts +5 -3
  80. package/dist/renderables/progress.js +14 -3
  81. package/dist/renderables/progressBar.js +4 -4
  82. package/dist/renderables/python-space.d.ts +13 -0
  83. package/dist/renderables/python-space.js +24 -0
  84. package/dist/renderables/spinner.d.ts +73 -27
  85. package/dist/renderables/spinner.js +110 -51
  86. package/dist/renderables/status.d.ts +8 -3
  87. package/dist/renderables/status.js +8 -5
  88. package/dist/renderables/syntax.js +2 -1
  89. package/dist/renderables/table.d.ts +19 -11
  90. package/dist/renderables/table.js +140 -156
  91. package/dist/renderables/traceback.d.ts +1 -1
  92. package/dist/renderables/traceback.js +6 -5
  93. package/dist/renderables/tree.d.ts +6 -5
  94. package/dist/renderables/tree.js +11 -7
  95. package/dist/renderables/viewport.d.ts +13 -5
  96. package/dist/renderables/viewport.js +41 -20
  97. package/dist/template-bindings/animation-funcs.d.ts +37 -0
  98. package/dist/template-bindings/animation-funcs.js +83 -0
  99. package/dist/template-bindings/color-funcs.d.ts +13 -0
  100. package/dist/template-bindings/color-funcs.js +1 -1
  101. package/dist/template-bindings/helpers.d.ts +6 -0
  102. package/dist/template-bindings/helpers.js +12 -3
  103. package/dist/template-bindings/index.d.ts +6 -2
  104. package/dist/template-bindings/index.js +8 -3
  105. package/dist/themes/ramp.d.ts +18 -3
  106. package/dist/themes/ramp.js +23 -2
  107. package/dist/widgets/focus-manager.d.ts +21 -14
  108. package/dist/widgets/focus-manager.js +28 -29
  109. package/dist/widgets/index.d.ts +1 -1
  110. package/dist/widgets/static-item.d.ts +4 -3
  111. package/dist/widgets/static-item.js +9 -7
  112. package/dist/widgets/text-input.d.ts +28 -18
  113. package/dist/widgets/text-input.js +144 -101
  114. package/dist/widgets/widget-app.d.ts +19 -3
  115. package/dist/widgets/widget-app.js +39 -21
  116. package/dist/widgets/widget-base.js +0 -3
  117. package/package.json +2 -5
@@ -7,9 +7,9 @@ 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
9
  import { EmbeddedText, embed, present } from "./embed.js";
10
- import { normalizePadding } from "./padding.js";
10
+ import { Padding, normalizePadding } from "./padding.js";
11
11
  import { exactWeights, ratioDistribute, ratioReduce } from "./ratio.js";
12
- import { getStyle, stackedHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
12
+ import { getStyle, render, stackedHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
13
13
  import { Measurement } from "../core/measure.js";
14
14
  /** A `want` no budget can satisfy: the column takes every cell its weight earns. */
15
15
  const UNBOUNDED = Number.MAX_SAFE_INTEGER;
@@ -106,8 +106,7 @@ function distribute(total, demands) {
106
106
  * column evenly (`ratio_reduce(excess, [1] * n, widths, widths)`). Wants that
107
107
  * fit come back as they are.
108
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
109
+ * One divergence: the reference's
111
110
  * even cut runs once, and where a column too narrow to give its share leaves
112
111
  * cells over, the table draws past its width. Here the cut repeats over the
113
112
  * columns with cells left, so the table fits. Each round either fits or
@@ -118,7 +117,7 @@ function reduceToFit(budget, demands) {
118
117
  const excess = () => wants.reduce((sum, want) => sum + want, 0) - budget;
119
118
  for (let over = excess(); over > 0; over = excess()) {
120
119
  // 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));
120
+ const widths = wants.map((want, index) => (demands[index].wrapable && want > 0 ? want : 0));
122
121
  const widest = Math.max(...widths);
123
122
  if (widest === 0)
124
123
  break;
@@ -130,23 +129,6 @@ function reduceToFit(budget, demands) {
130
129
  }
131
130
  return wants;
132
131
  }
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;
149
- }
150
132
  function layoutTable(outerWidth, demands, frame) {
151
133
  // Plain `number`, not the `CellCol` `cells` hands back: this budget is spent
152
134
  // down by arithmetic, and arithmetic on a branded type produces a number.
@@ -160,117 +142,87 @@ function layoutTable(outerWidth, demands, frame) {
160
142
  // flag for the pair, so half a frame is not a shape this renderer can emit.
161
143
  const edge = budget >= frame.edge * 2 ? frame.edge : 0;
162
144
  take(edge * 2);
163
- // A first content cell per column, in column order, each paying for the
164
- // divider that precedes it. The first column the budget cannot seat is where
165
- // the table ends; the rest are dropped. A column that wants no cells is
166
- // seated at zero — the queue exists to stop one column taking a second cell
167
- // before another has its first, and a spacer never joins it.
168
- const seats = [];
169
- while (seats.length < demands.length) {
170
- const seat = Math.min(1, demands[seats.length].want);
171
- const cost = (seats.length > 0 ? frame.divider : 0) + seat;
172
- if (budget < cost)
173
- break;
174
- take(cost);
175
- seats.push(seat);
176
- }
177
- const seated = seats.length;
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.
145
+ // Every column whose divider fits is drawn, in column order, at whatever
146
+ // width the division below leaves it — none at all included, as Rich draws a
147
+ // column it collapsed to nothing. The first divider the budget cannot hold
148
+ // is where the table ends; the rest are dropped, never drawn outside the
149
+ // frame.
150
+ let seated = demands.length > 0 ? 1 : 0;
151
+ while (seated < demands.length && budget >= frame.divider) {
152
+ take(frame.divider);
153
+ seated++;
154
+ }
155
+ // Every column's minimum is paid next, a declared width and a `noWrap`
156
+ // column alike, and a budget too small for them all cuts them in the
157
+ // reference's order, `reduceToFit`'s — never the table's frame. The rest of
158
+ // what each column wanted comes only after. A table whose columns all fit
159
+ // leaves this budget partly unspent.
181
160
  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)));
185
- };
186
- const padLeft = takeLevelled(seatedDemands.map((demand) => demand.pad[0]));
187
- const padRight = takeLevelled(seatedDemands.map((demand) => demand.pad[1]));
188
- // Reservations are paid in column order, each already holding the cell the
189
- // seating pass gave it. A budget too small to cover them all runs out
190
- // partway, which costs the trailing columns their width but never costs the
191
- // table its frame.
192
- const reserved = seatedDemands.map((demand, index) => take(demand.reserved - seats[index]));
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
195
- // whose columns all fit leaves this budget partly unspent.
196
161
  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);
162
+ const holding = (...passes) => seatedDemands.map((_, index) => passes.reduce((sum, granted) => sum + granted[index], 0));
163
+ const needed = reduceToFit(budget, seatedDemands.map((demand) => ({ want: demand.minimum, wrapable: demand.wrapable })));
205
164
  const beyond = distribute(budget - spent(needed), seatedDemands.map((demand, index) => ({
206
- want: Math.max(0, demand.want - afterNeeded[index]),
165
+ want: Math.max(0, demand.want - needed[index]),
207
166
  weight: demand.weight,
208
167
  })));
209
168
  const wanted = needed.map((width, index) => width + beyond[index]);
210
169
  // The unspent part goes first to the columns with nothing to size to, each
211
170
  // alike, up to its `fill` — the reference's `Measurement(1, max_width)`, a
212
171
  // maximum of the whole offer that no content measured.
213
- const afterWant = holding(wanted);
214
172
  const filled = distribute(budget - spent(wanted), seatedDemands.map((demand, index) => ({
215
- want: Math.max(0, demand.fill - afterWant[index]),
173
+ want: Math.max(0, demand.fill - wanted[index]),
216
174
  weight: demand.fill > 0 ? 1 : 0,
217
175
  })));
218
176
  // Then to the ratio columns, by the reference's flexible split:
219
177
  // `ratio_distribute(max_width - fixed_widths, ratios, flex_minimum)`. What
220
178
  // 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.
179
+ // columns already hold included; each is held to at least its want, which is
180
+ // its padding and its floor. The floor is the reference's `column.width or
181
+ // 1`, reading `minWidth` where there is no `width`, where the reference
182
+ // reads none and draws a `min_width=12` ratio column five cells wide.
226
183
  // A ratio column is paid the rest of its part out of the budget, in column
227
184
  // order, so a part the minimums push past what is left is cut there rather
228
185
  // than drawn past the width.
229
186
  const afterFill = holding(wanted, filled);
230
187
  const flexible = seatedDemands.flatMap((demand, index) => (demand.ratio > 0 ? [index] : []));
231
- const padded = (index, content) => padLeft[index] + content + padRight[index];
232
188
  // `UNBOUNDED` holds an unbounded offer, as it does for the stretch below;
233
189
  // it caps the whole split, so the total stays an integer a double holds.
234
190
  const pool = budget - spent(wanted) - spent(filled);
235
191
  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)));
192
+ const parts = ratioDistribute(Math.min(pool + flexible.reduce((sum, index) => sum + afterFill[index], 0), UNBOUNDED), flexible.map((index) => seatedDemands[index].ratio), flexible.map((index) => seatedDemands[index].want));
237
193
  const shared = seatedDemands.map(() => 0);
238
194
  flexible.forEach((index, slot) => {
239
- shared[index] = Math.min(Math.max(0, parts[slot] - padded(index, afterFill[index])), unshared);
195
+ shared[index] = Math.min(Math.max(0, parts[slot] - afterFill[index]), unshared);
240
196
  unshared -= shared[index];
241
197
  });
242
198
  // What the shares leave goes to the columns that stretch, by the width each
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.
199
+ // already holds: its natural width, since nothing is left over while any
200
+ // column is short, or the cap a fill stopped at. The split is the
201
+ // reference's `ratio_distribute(max_width - table_width, widths)`, rounding
202
+ // included; the columns it runs over are this port's. A table that does not
203
+ // expand has no column that stretches, and an empty list takes nothing,
204
+ // which is how it stays narrower than the width it was offered. The order is
205
+ // the reference's: Rich pads an expanding table only once
206
+ // `table_width < max_width`, never while it is collapsing a column.
251
207
  const held = holding(wanted, filled, shared);
252
208
  const stretchers = seatedDemands.flatMap((demand, index) => (demand.stretch ? [index] : []));
253
209
  const stretches = ratioDistribute(
254
210
  // An unbounded offer is held to `UNBOUNDED`, this model's own infinity, as
255
211
  // `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])));
212
+ Math.min(budget - spent(wanted) - spent(filled) - spent(shared), UNBOUNDED), stretchers.map((index) => held[index]));
257
213
  // A table that does not fit gives back each column's `floor`: Rich
258
214
  // re-measures a collapsed table's columns at the widths it gave them, and a
259
215
  // column with nothing to draw measures 0 there however many cells the collapse
260
216
  // 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);
217
+ const natural = demands.reduce((sum, demand) => sum + demand.want, frame.edge * 2 + Math.max(0, demands.length - 1) * frame.divider);
262
218
  const squeezed = natural > outerWidth;
263
- const columns = held.map((width, index) => squeezed ? width - Math.min(width, seatedDemands[index].floor) : width);
219
+ const cellWidths = held.map((width, index) => squeezed ? width - Math.min(width, seatedDemands[index].floor) : width);
264
220
  stretchers.forEach((index, slot) => {
265
- columns[index] += stretches[slot];
221
+ cellWidths[index] += stretches[slot];
266
222
  });
267
- const cellWidths = columns.map((width, index) => padded(index, width));
268
223
  return {
269
224
  edge,
270
225
  divider: frame.divider,
271
- padLeft,
272
- padRight,
273
- columns,
274
226
  cellWidths,
275
227
  totalWidth: edge * 2 +
276
228
  Math.max(0, seated - 1) * frame.divider +
@@ -382,6 +334,10 @@ export class Column {
382
334
  return col;
383
335
  }
384
336
  }
337
+ // A row's cell in column `index`: a column the row is short of draws a blank one.
338
+ function cellAt(row, index) {
339
+ return row.cells[index] ?? new EmbeddedText(undefined);
340
+ }
385
341
  export class Table {
386
342
  _columns;
387
343
  _rows;
@@ -547,7 +503,7 @@ export class Table {
547
503
  // Data rows
548
504
  for (let rowIdx = 0; rowIdx < this._rows.length; rowIdx++) {
549
505
  const row = this._rows[rowIdx];
550
- const rowCells = this._columns.map((_, colIdx) => row.cells[colIdx] ?? new EmbeddedText(undefined));
506
+ const rowCells = this._rowCells(row);
551
507
  const rowStyle = stripes[rowIdx % stripes.length] ?? NULL_STYLE;
552
508
  const cellStyles = columnStyles.map((s) => s.add(rowStyle));
553
509
  yield* this._renderRow(options, rowCells, cellStyles, rowStyle, geometry, rowPadding(firstBody + rowIdx), box, "row", border);
@@ -678,8 +634,11 @@ export class Table {
678
634
  * mode gets its own path through the width division.
679
635
  *
680
636
  * 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.
637
+ * reference's `_measure_column` measures them. Padding is part of every
638
+ * width the demand names, because the reference measures a cell wrapped in
639
+ * its `Padding`, and each sizing adds it where the reference does — a
640
+ * rowless column's minimum is the one width it never reaches. The floor is
641
+ * laid over whichever sizing the column has.
683
642
  */
684
643
  _columnDemands(options, outerWidth) {
685
644
  const frame = this._frame();
@@ -688,13 +647,12 @@ export class Table {
688
647
  maxWidth: Math.max(0, outerWidth - frame.edge * 2 - frame.divider * Math.max(0, this._columns.length - 1)),
689
648
  };
690
649
  return this._columns.map((col, index) => {
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 };
650
+ const sizing = this._columnSizing(col, index, inner, this._columnPadding(index));
651
+ const floor = sizing.want === 0 ? 1 : 0;
652
+ return { ...sizing, minimum: sizing.minimum + floor, want: sizing.want + floor, floor };
695
653
  });
696
654
  }
697
- _columnSizing(col, index, options) {
655
+ _columnSizing(col, index, options, [left, right]) {
698
656
  // A flexible column takes its share of whatever the bounded columns
699
657
  // leave; every other column asks for its natural width, whether or not a
700
658
  // neighbour is flexible. The split is the reference's —
@@ -703,17 +661,27 @@ export class Table {
703
661
  // here a ratio is honoured either way. Its bounded part is its floor —
704
662
  // the reference's `column.width or 1`, reading `minWidth` where there is
705
663
  // 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.
664
+ // table still pays it. A `width` beside a `ratio` is that floor, as the
665
+ // reference's `flex_minimum` reads it, held to the frame as a declared
666
+ // width is.
708
667
  // [LAW:one-source-of-truth] `columnShare` decides first, so a column that
709
668
  // `Column.flexible` calls elastic is one the division shares out.
669
+ const pad = left + right;
670
+ const padded = (content) => demandCells(content + pad);
671
+ // [LAW:single-enforcer] A width a column insists on, held to the width
672
+ // inside the frame — the reference's `with_maximum(max_width)` on a
673
+ // fixed-width column's measurement — so an even cut starts from what the
674
+ // table can hold. It holds a declared width and every ratio floor; a
675
+ // content column's `minWidth` is not held, as the reference clamps to
676
+ // `min_width` after `with_maximum`.
677
+ const held = (width) => Math.min(padded(demandCells(width)), options.maxWidth);
710
678
  const share = columnShare(col);
711
679
  if (share > 0) {
712
680
  const floor = Math.max(1, demandCells(col.width ?? col.minWidth ?? 0));
681
+ const bid = held(floor);
713
682
  return {
714
- reserved: 0,
715
- minimum: floor,
716
- want: floor,
683
+ minimum: bid,
684
+ want: bid,
717
685
  weight: floor,
718
686
  fill: 0,
719
687
  ratio: share,
@@ -724,11 +692,10 @@ export class Table {
724
692
  if (col.width !== undefined) {
725
693
  // [LAW:single-enforcer] floored where it is parsed, the same rule
726
694
  // `normalizePadding` applies to a negative padding side.
727
- const declared = demandCells(col.width);
695
+ const bid = held(col.width);
728
696
  return {
729
- reserved: declared,
730
- minimum: declared,
731
- want: declared,
697
+ minimum: bid,
698
+ want: bid,
732
699
  weight: 0,
733
700
  fill: 0,
734
701
  ratio: 0,
@@ -736,8 +703,8 @@ export class Table {
736
703
  wrapable: false,
737
704
  };
738
705
  }
739
- const range = this._cellRange(col, index, options);
740
- const natural = demandCells(this._bounded(col, range?.maximum ?? 1));
706
+ const range = this._cellRange(col, index, options, [left, right]);
707
+ const natural = demandCells(this._bounded(col, range?.maximum ?? padded(1), pad));
741
708
  // `expand` is a stretch rather than a larger want: the column still
742
709
  // competes for its natural width like any other, and only the cells left
743
710
  // once every column has that are shared out. A larger want looks
@@ -749,25 +716,26 @@ export class Table {
749
716
  // One deliberate divergence: Rich stretches declared-width columns as
750
717
  // well, so `width: 6` under `expand` renders 21 cells wide — an option
751
718
  // quietly meaning something else, the defect rich-justify-0cr exists to
752
- // remove. A reservation's stretch is zero above.
719
+ // remove. A declared width's stretch is zero above.
753
720
  //
754
721
  // A column with no cell at all — no rows, and neither header nor footer
755
722
  // drawn — has nothing to size to, which is not sizing to nothing. The
756
723
  // reference measures it `Measurement(1, max_width)`: one cell of content,
757
724
  // 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`.
725
+ // "no cells" as zero sized it to a two-cell box instead. The one cell and
726
+ // its padding are its want, so a `minWidth` is paid like any column's,
727
+ // and the offer is its `fill`, held to its `maxWidth`. Its minimum is the
728
+ // reference's bare 1, unpadded: `_measure_column` adds the padding only to
729
+ // a declared width and to the `min_width`/`max_width` clamp.
761
730
  //
762
731
  // A column that may not wrap has no narrower width than its natural one
763
732
  // that keeps its content whole — the reference leaves `no_wrap` columns out
764
733
  // of the ones it collapses.
765
734
  return {
766
- reserved: 0,
767
- minimum: col.noWrap ? natural : Math.min(natural, demandCells(this._bounded(col, range?.minimum ?? 1))),
735
+ minimum: col.noWrap ? natural : Math.min(natural, demandCells(this._bounded(col, range?.minimum ?? 1, pad))),
768
736
  want: natural,
769
737
  weight: natural,
770
- fill: range === undefined ? demandCells(this._bounded(col, UNBOUNDED)) : 0,
738
+ fill: range === undefined ? demandCells(this._bounded(col, UNBOUNDED, pad)) : 0,
771
739
  ratio: 0,
772
740
  stretch: this.expand,
773
741
  wrapable: !col.noWrap,
@@ -793,6 +761,19 @@ export class Table {
793
761
  const collapsed = this.collapsePadding && !last ? Math.max(0, top - bottom) : bottom;
794
762
  return [!this.padEdge && first ? 0 : top, !this.padEdge && last ? 0 : collapsed];
795
763
  }
764
+ // Every cell `render` draws, in the order it draws them: row by row, the
765
+ // header and footer when it shows them.
766
+ *contents() {
767
+ if (this.showHeader)
768
+ yield* this._columns.map((c) => c.header);
769
+ for (const row of this._rows)
770
+ yield* this._rowCells(row);
771
+ if (this.showFooter)
772
+ yield* this._columns.map((c) => c.footer);
773
+ }
774
+ _rowCells(row) {
775
+ return this._columns.map((_, index) => cellAt(row, index));
776
+ }
796
777
  /**
797
778
  * Every cell column `index` draws, in draw order.
798
779
  *
@@ -808,18 +789,19 @@ export class Table {
808
789
  if (this.showHeader)
809
790
  yield col.header;
810
791
  for (const row of this._rows)
811
- yield row.cells[index] ?? new EmbeddedText(undefined);
792
+ yield cellAt(row, index);
812
793
  if (this.showFooter)
813
794
  yield col.footer;
814
795
  }
815
796
  /**
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]
797
+ * The widest any cell in a column draws, its padding included, and the
798
+ * narrowest the column can be without cutting one — the reference's
799
+ * `_measure_column` — or `undefined` when the column has no cell to measure.
800
+ * A column whose cells draw nothing measures its padding — a gutter asks for
801
+ * that and nothing else — which is not the same answer.
802
+ * [LAW:types-are-the-program]
821
803
  */
822
- _cellRange(col, index, options) {
804
+ _cellRange(col, index, options, [left, right]) {
823
805
  let range;
824
806
  for (const cell of this._columnCells(col, index)) {
825
807
  // The stamped cell, so the width a column asks for is the width its text
@@ -828,7 +810,10 @@ export class Table {
828
810
  // [LAW:one-source-of-truth] Measured as the cell measures itself, so a
829
811
  // multi-line cell asks for its widest line and a `Panel` for its frame;
830
812
  // one that cannot measure itself asks for every cell there is, as in Rich.
831
- const measured = Measurement.get(options, cell);
813
+ // Wrapped in its padding, as the reference's `_get_cells` hands it to
814
+ // `_measure_column`, so a cell measures what it draws and never past the
815
+ // width it was offered.
816
+ const measured = Measurement.get(options, new Padding(cell, [0, right, 0, left]));
832
817
  range = {
833
818
  minimum: Math.max(range?.minimum ?? 0, measured.minimum),
834
819
  maximum: Math.max(range?.maximum ?? 0, measured.maximum),
@@ -837,18 +822,20 @@ export class Table {
837
822
  return range;
838
823
  }
839
824
  /**
840
- * A natural width held to the column's own `minWidth`/`maxWidth`.
825
+ * A natural width held to the column's own `minWidth`/`maxWidth`, each
826
+ * widened by the column's `pad`, as the reference clamps a padded
827
+ * measurement to `min_width + padding_width`.
841
828
  *
842
829
  * A bound is a count of cells, read by the rule every width is read by: NaN
843
830
  * and a negative are zero cells, so a NaN floor bounds nothing and a NaN
844
- * ceiling is a ceiling of zero, as `width: NaN` is. A bare `Math.max` returns
831
+ * ceiling holds the column to its padding alone, as `width: NaN` does. A bare `Math.max` returns
845
832
  * NaN instead, and the column vanishes. [LAW:one-source-of-truth]
846
833
  */
847
- _bounded(col, natural) {
848
- return Math.min(Math.max(natural, cells(col.minWidth ?? 0)), cells(col.maxWidth ?? Infinity));
834
+ _bounded(col, natural, pad) {
835
+ return Math.min(Math.max(natural, col.minWidth === undefined ? 0 : cells(col.minWidth) + pad), cells(col.maxWidth ?? Infinity) + pad);
849
836
  }
850
837
  *_renderRow(options, cells, styles, rowStyle, geometry, [top, bottom], box, level, border) {
851
- const { padLeft, padRight, columns } = geometry;
838
+ const { cellWidths } = geometry;
852
839
  // [LAW:dataflow-not-control-flow] Header, body and footer share this path;
853
840
  // the level crosses as a value the box answers with glyphs, not a branch.
854
841
  const frame = box?.getContentChars(level);
@@ -859,39 +846,36 @@ export class Table {
859
846
  const divider = frame && (frame.vertical.trim() === ""
860
847
  ? new Segment(frame.vertical, rowStyle.backgroundStyle.add(border))
861
848
  : new Segment(frame.vertical, border));
862
- // Render each cell onto the canvas the geometry gave its column. Columns
863
- // the width could not seat are absent from `columns` and so are never
849
+ // Render each cell at the width the geometry gave its column. Columns the
850
+ // width could not seat are absent from `cellWidths` and so are never
864
851
  // rendered at all.
865
- const paddedWidths = columns.map((cellWidth, index) => padLeft[index] + cellWidth + padRight[index]);
866
- const cellLines = columns.map((cellWidth, index) => {
852
+ const cellLines = cellWidths.map((cellWidth, index) => {
867
853
  const col = this._columns[index];
868
- const cell = cells[index] ?? embed("");
854
+ const [left, right] = this._columnPadding(index);
855
+ // [LAW:single-enforcer] The padding on every side is part of the cell,
856
+ // drawn by `Padding` as the reference's `_get_cells` wraps each cell in
857
+ // one: its sides are paid before its content, so a cell too narrow for
858
+ // them draws as blanks, and a content line sits between its left and
859
+ // right blanks.
860
+ const padded = new Padding(cells[index] ?? embed(""), [top, right, bottom, left]);
869
861
  // The render's own options with the column's canvas laid over them, so a
870
862
  // cell resolves its style names against the same theme as the table. A
871
863
  // table stacks its cells, so each is handed the table's rows as a
872
864
  // ceiling, never a region to fill — the `Height` contract's
873
865
  // `stackedHeight`, where the reference hands a cell `height=None`.
874
866
  // Highlighted as its column says, Rich's `highlight=column.highlight`.
875
- const segs = [...cell.render({
876
- ...options,
877
- highlight: col.highlight,
878
- maxWidth: cellWidth,
879
- justify: col.justify,
880
- overflow: col.overflow,
881
- noWrap: col.noWrap,
882
- height: stackedHeight(options.height),
883
- })];
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
- ];
867
+ // A cell left no column draws nothing at all, its padding included, as
868
+ // the reference's `render_lines` draws nothing under one column.
869
+ const cellOptions = {
870
+ ...options,
871
+ highlight: col.highlight,
872
+ maxWidth: cellWidth,
873
+ justify: col.justify,
874
+ overflow: col.overflow,
875
+ noWrap: col.noWrap,
876
+ height: stackedHeight(options.height),
877
+ };
878
+ return Segment.splitLines(render(padded, cellOptions));
895
879
  });
896
880
  const maxLines = cellLines.reduce((most, lines) => Math.max(most, lines.length), 1);
897
881
  // How many blank lines stand above each cell's content. A header sits on
@@ -902,14 +886,14 @@ export class Table {
902
886
  if (frame && geometry.edge === 1) {
903
887
  yield new Segment(frame.left, border);
904
888
  }
905
- for (let colIdx = 0; colIdx < columns.length; colIdx++) {
889
+ for (let colIdx = 0; colIdx < cellWidths.length; colIdx++) {
906
890
  if (colIdx > 0 && divider) {
907
891
  yield divider;
908
892
  }
909
893
  const style = styles[colIdx];
910
894
  // A cell with fewer lines than the row contributes blanks, so every
911
895
  // column spans the same number of rows and the frame stays rectangular.
912
- const line = cellLines[colIdx][lineIdx - drop[colIdx]] ?? blank(paddedWidths[colIdx]);
896
+ const line = cellLines[colIdx][lineIdx - drop[colIdx]] ?? blank(cellWidths[colIdx]);
913
897
  // The cell's style is the base its content's own spans layer over, and
914
898
  // it covers the whole cell — its padding and its blank lines too, as
915
899
  // the reference's does — so a background fills the column rather than
@@ -959,7 +943,7 @@ export class Table {
959
943
  // lines, and `end="!!"` runs on into the top border there too. Splitting
960
944
  // and re-ending each line moved a non-newline `end` onto the title's own
961
945
  // row, wider than the table.
962
- yield* source.render({
946
+ yield* render(source, {
963
947
  ...options,
964
948
  maxWidth: tableWidth,
965
949
  justify,
@@ -16,7 +16,7 @@
16
16
  * sized and coloured a source-code excerpt this renderer does not produce.
17
17
  */
18
18
  import { Segment } from "../core/segment.js";
19
- import type { Renderable, RenderOptions } from "../core/protocol.js";
19
+ import { type Renderable, type RenderOptions } from "../core/protocol.js";
20
20
  export interface TracebackOptions {
21
21
  suppress?: string[];
22
22
  maxFrames?: number;
@@ -18,7 +18,8 @@
18
18
  import { Segment } from "../core/segment.js";
19
19
  import { RichText } from "../core/text.js";
20
20
  import { Pretty, UNSEEN_DATA_BOUNDS } from "../core/pretty.js";
21
- import { layoutPadding } from "./padding.js";
21
+ import { render } from "../core/protocol.js";
22
+ import { paddedContentWidth } from "./padding.js";
22
23
  /**
23
24
  * [LAW:parse-dont-validate] The checkpoint between whatever a `catch` caught —
24
25
  * JavaScript lets a program throw any value — and the report. Anything with a
@@ -225,7 +226,7 @@ export class Traceback {
225
226
  // width that is left on that line rather than overrunning it.
226
227
  header.append(message({ ...options, maxWidth: Math.max(1, options.maxWidth - header.cellLength) }));
227
228
  header.append("\n\n");
228
- yield* header.render(options);
229
+ yield* render(header, options);
229
230
  // A suppressed frame keeps its place and shows its file and line only: it
230
231
  // is the same frame with no function name to show.
231
232
  const frames = this.caught.frames.map((frame) => this.suppress.some((s) => frame.location.includes(s)) ? { ...frame, function: undefined } : frame);
@@ -253,10 +254,10 @@ export class Traceback {
253
254
  // frame of its own. The width is divided as `Padding` divides it; the rows
254
255
  // are not padded out on the right, as `Padding`'s are, because trailing
255
256
  // blanks on every line of a crash report are bytes nobody asked for.
256
- const { left, contentWidth } = layoutPadding(options.maxWidth, 2, 0);
257
- const indent = new Segment(" ".repeat(left));
257
+ const frameOptions = { ...options, maxWidth: paddedContentWidth(options.maxWidth, 2, 0) };
258
+ const indent = new Segment(" ");
258
259
  for (const entry of entries) {
259
- for (const line of Segment.splitLines(entry.render({ ...options, maxWidth: contentWidth }))) {
260
+ for (const line of Segment.splitLines(render(entry, frameOptions))) {
260
261
  yield indent;
261
262
  yield* line;
262
263
  yield Segment.line();
@@ -4,14 +4,14 @@
4
4
  import { Segment } from "../core/segment.js";
5
5
  import { Style } from "../core/style.js";
6
6
  import { RichText } from "../core/text.js";
7
- import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
7
+ import type { Composite, Renderable, Measurable, RenderOptions } from "../core/protocol.js";
8
8
  export interface TreeOptions {
9
9
  expanded?: boolean;
10
10
  hideRoot?: boolean;
11
11
  guide_style?: string | Style;
12
12
  style?: string | Style;
13
13
  }
14
- export declare class Tree implements Renderable, Measurable {
14
+ export declare class Tree implements Renderable, Measurable, Composite {
15
15
  readonly label: Renderable;
16
16
  readonly children: Tree[];
17
17
  expanded: boolean;
@@ -21,6 +21,7 @@ export declare class Tree implements Renderable, Measurable {
21
21
  constructor(label: string | RichText | Renderable, options?: TreeOptions);
22
22
  add(label: string | RichText | Renderable, options?: TreeOptions): Tree;
23
23
  render(rawOptions: RenderOptions): Iterable<Segment>;
24
+ contents(): Iterable<Renderable>;
24
25
  /**
25
26
  * The rows `render` and `measure` both read, entered in exactly one place so
26
27
  * the two cannot start the walk differently. A hidden root takes its own row
@@ -59,9 +60,9 @@ export declare class Tree implements Renderable, Measurable {
59
60
  * [LAW:single-enforcer] Tree also owns its row boundaries here rather than
60
61
  * depending on label renderables to invent trailing newlines: the label's
61
62
  * output is cut into lines and every line ends where Tree ends it. A label
62
- * that emits nothing still holds its row, and one that ends in a newline of
63
- * its own gets the blank row that line is, as in Rich — padded to the label's
64
- * width when the label is justified.
63
+ * that emits nothing — one left no column, say — has no row, and one
64
+ * that ends in a newline of its own gets the blank row that line is, as in
65
+ * Rich — padded to the label's width when the label is justified.
65
66
  *
66
67
  * The node's style sits under the label's own, as Rich's `Styled` puts it, so
67
68
  * markup in the label still wins; its background alone reaches under the