@promptctl/rich-js 0.24.0 → 0.25.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 (163) hide show
  1. package/README.md +7 -0
  2. package/THIRD-PARTY-NOTICES +29 -1
  3. package/dist/core/anchor.d.ts +5 -0
  4. package/dist/core/anchor.js +11 -4
  5. package/dist/core/animation.d.ts +243 -0
  6. package/dist/core/animation.js +375 -0
  7. package/dist/core/ansi.js +2 -2
  8. package/dist/core/box.d.ts +7 -7
  9. package/dist/core/cell-widths.d.ts +9 -0
  10. package/dist/core/cell-widths.js +140 -0
  11. package/dist/core/cells.d.ts +40 -24
  12. package/dist/core/cells.js +151 -66
  13. package/dist/core/clock.d.ts +2 -2
  14. package/dist/core/clock.js +6 -4
  15. package/dist/core/color.d.ts +27 -10
  16. package/dist/core/color.js +21 -29
  17. package/dist/core/console.d.ts +28 -17
  18. package/dist/core/console.js +48 -43
  19. package/dist/core/easing.d.ts +140 -10
  20. package/dist/core/easing.js +328 -50
  21. package/dist/core/emoji.d.ts +1 -1
  22. package/dist/core/emoji.js +1 -2
  23. package/dist/core/export-html.js +2 -1
  24. package/dist/core/export-lines.js +3 -3
  25. package/dist/core/highlighter.d.ts +9 -8
  26. package/dist/core/highlighter.js +3 -2
  27. package/dist/core/json.js +3 -2
  28. package/dist/core/markup.js +16 -12
  29. package/dist/core/measure.js +3 -3
  30. package/dist/core/memo.d.ts +11 -2
  31. package/dist/core/memo.js +16 -0
  32. package/dist/core/oklch.js +17 -6
  33. package/dist/core/osc8.d.ts +1 -1
  34. package/dist/core/osc8.js +4 -5
  35. package/dist/core/paint.d.ts +2 -1
  36. package/dist/core/paint.js +4 -3
  37. package/dist/core/pattern.d.ts +21 -0
  38. package/dist/core/pattern.js +21 -0
  39. package/dist/core/place.js +2 -1
  40. package/dist/core/pretty.js +11 -12
  41. package/dist/core/protocol.d.ts +136 -21
  42. package/dist/core/protocol.js +203 -16
  43. package/dist/core/render.d.ts +15 -2
  44. package/dist/core/render.js +27 -6
  45. package/dist/core/segment.d.ts +31 -6
  46. package/dist/core/segment.js +68 -53
  47. package/dist/core/spinnerData.d.ts +1 -1
  48. package/dist/core/state.d.ts +53 -0
  49. package/dist/core/state.js +82 -0
  50. package/dist/core/strip.d.ts +11 -13
  51. package/dist/core/strip.js +126 -113
  52. package/dist/core/style.d.ts +47 -14
  53. package/dist/core/style.js +74 -34
  54. package/dist/core/text.d.ts +5 -8
  55. package/dist/core/text.js +133 -133
  56. package/dist/core/timeline.d.ts +109 -0
  57. package/dist/core/timeline.js +180 -0
  58. package/dist/core/tween.d.ts +49 -0
  59. package/dist/core/tween.js +71 -0
  60. package/dist/core/view.d.ts +92 -0
  61. package/dist/core/view.js +83 -0
  62. package/dist/host/app.d.ts +49 -21
  63. package/dist/host/app.js +72 -38
  64. package/dist/host/input.js +3 -4
  65. package/dist/index.d.ts +30 -15
  66. package/dist/index.js +20 -6
  67. package/dist/node/traceback.js +4 -3
  68. package/dist/renderables/change.d.ts +42 -0
  69. package/dist/renderables/change.js +112 -0
  70. package/dist/renderables/columns.d.ts +1 -1
  71. package/dist/renderables/columns.js +3 -3
  72. package/dist/renderables/constrain.js +2 -1
  73. package/dist/renderables/effect.d.ts +155 -92
  74. package/dist/renderables/effect.js +203 -136
  75. package/dist/renderables/effects.d.ts +137 -24
  76. package/dist/renderables/effects.js +276 -113
  77. package/dist/renderables/embed.d.ts +1 -1
  78. package/dist/renderables/embed.js +18 -9
  79. package/dist/renderables/flexStrip.js +2 -1
  80. package/dist/renderables/frames.d.ts +45 -43
  81. package/dist/renderables/frames.js +57 -45
  82. package/dist/renderables/glyph-effect.d.ts +158 -0
  83. package/dist/renderables/glyph-effect.js +237 -0
  84. package/dist/renderables/glyph-effects.d.ts +147 -0
  85. package/dist/renderables/glyph-effects.js +283 -0
  86. package/dist/renderables/group.d.ts +1 -1
  87. package/dist/renderables/group.js +2 -2
  88. package/dist/renderables/html-entities.d.ts +8 -0
  89. package/dist/renderables/html-entities.js +386 -0
  90. package/dist/renderables/layout.d.ts +10 -9
  91. package/dist/renderables/layout.js +42 -35
  92. package/dist/renderables/lifecycle.d.ts +77 -0
  93. package/dist/renderables/lifecycle.js +132 -0
  94. package/dist/renderables/list-view.d.ts +2 -2
  95. package/dist/renderables/list-view.js +11 -8
  96. package/dist/renderables/live.d.ts +36 -21
  97. package/dist/renderables/live.js +89 -42
  98. package/dist/renderables/markdown-parse.d.ts +109 -0
  99. package/dist/renderables/markdown-parse.js +1427 -0
  100. package/dist/renderables/markdown-url.d.ts +19 -0
  101. package/dist/renderables/markdown-url.js +314 -0
  102. package/dist/renderables/markdown.d.ts +5 -7
  103. package/dist/renderables/markdown.js +356 -683
  104. package/dist/renderables/padding.d.ts +7 -20
  105. package/dist/renderables/padding.js +29 -50
  106. package/dist/renderables/panel.d.ts +30 -32
  107. package/dist/renderables/panel.js +65 -109
  108. package/dist/renderables/progress.d.ts +83 -32
  109. package/dist/renderables/progress.js +186 -90
  110. package/dist/renderables/progressBar.d.ts +22 -12
  111. package/dist/renderables/progressBar.js +33 -10
  112. package/dist/renderables/prompt.js +3 -2
  113. package/dist/renderables/python-space.d.ts +13 -0
  114. package/dist/renderables/python-space.js +25 -0
  115. package/dist/renderables/scroll.d.ts +50 -0
  116. package/dist/renderables/scroll.js +106 -0
  117. package/dist/renderables/spinner.d.ts +57 -24
  118. package/dist/renderables/spinner.js +101 -49
  119. package/dist/renderables/status.js +1 -1
  120. package/dist/renderables/syntax.d.ts +10 -9
  121. package/dist/renderables/syntax.js +5 -3
  122. package/dist/renderables/table.d.ts +48 -46
  123. package/dist/renderables/table.js +170 -208
  124. package/dist/renderables/traceback.d.ts +3 -3
  125. package/dist/renderables/traceback.js +12 -10
  126. package/dist/renderables/tree.d.ts +28 -25
  127. package/dist/renderables/tree.js +108 -58
  128. package/dist/renderables/viewport.d.ts +77 -38
  129. package/dist/renderables/viewport.js +156 -72
  130. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  131. package/dist/template-bindings/animation-funcs.js +132 -14
  132. package/dist/template-bindings/index.d.ts +5 -5
  133. package/dist/template-bindings/index.js +7 -6
  134. package/dist/themes/ramp.d.ts +62 -4
  135. package/dist/themes/ramp.js +82 -4
  136. package/dist/themes/registry.js +5 -7
  137. package/dist/widgets/button.js +3 -2
  138. package/dist/widgets/checkbox.js +4 -3
  139. package/dist/widgets/dropdown.d.ts +3 -1
  140. package/dist/widgets/dropdown.js +20 -15
  141. package/dist/widgets/event-router.js +4 -2
  142. package/dist/widgets/focus-manager.d.ts +23 -14
  143. package/dist/widgets/focus-manager.js +34 -33
  144. package/dist/widgets/hit.d.ts +6 -0
  145. package/dist/widgets/hit.js +8 -1
  146. package/dist/widgets/index.d.ts +1 -1
  147. package/dist/widgets/observable.d.ts +23 -0
  148. package/dist/widgets/observable.js +43 -0
  149. package/dist/widgets/slider.d.ts +1 -1
  150. package/dist/widgets/slider.js +12 -3
  151. package/dist/widgets/static-item.d.ts +1 -1
  152. package/dist/widgets/static-item.js +6 -7
  153. package/dist/widgets/text-input.d.ts +53 -47
  154. package/dist/widgets/text-input.js +456 -399
  155. package/dist/widgets/themed-widget.js +2 -1
  156. package/dist/widgets/toggle.js +4 -3
  157. package/dist/widgets/types.d.ts +10 -4
  158. package/dist/widgets/types.js +8 -2
  159. package/dist/widgets/widget-app.d.ts +32 -6
  160. package/dist/widgets/widget-app.js +74 -27
  161. package/dist/widgets/widget-base.d.ts +2 -2
  162. package/dist/widgets/widget-base.js +29 -21
  163. package/package.json +9 -5
@@ -7,10 +7,11 @@ 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
+ import { ListState, State } from "../core/state.js";
14
15
  /** A `want` no budget can satisfy: the column takes every cell its weight earns. */
15
16
  const UNBOUNDED = Number.MAX_SAFE_INTEGER;
16
17
  /**
@@ -106,8 +107,7 @@ function distribute(total, demands) {
106
107
  * column evenly (`ratio_reduce(excess, [1] * n, widths, widths)`). Wants that
107
108
  * fit come back as they are.
108
109
  *
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
110
+ * One divergence: the reference's
111
111
  * even cut runs once, and where a column too narrow to give its share leaves
112
112
  * cells over, the table draws past its width. Here the cut repeats over the
113
113
  * columns with cells left, so the table fits. Each round either fits or
@@ -118,7 +118,7 @@ function reduceToFit(budget, demands) {
118
118
  const excess = () => wants.reduce((sum, want) => sum + want, 0) - budget;
119
119
  for (let over = excess(); over > 0; over = excess()) {
120
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));
121
+ const widths = wants.map((want, index) => (demands[index].wrapable && want > 0 ? want : 0));
122
122
  const widest = Math.max(...widths);
123
123
  if (widest === 0)
124
124
  break;
@@ -130,23 +130,6 @@ function reduceToFit(budget, demands) {
130
130
  }
131
131
  return wants;
132
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;
149
- }
150
133
  function layoutTable(outerWidth, demands, frame) {
151
134
  // Plain `number`, not the `CellCol` `cells` hands back: this budget is spent
152
135
  // down by arithmetic, and arithmetic on a branded type produces a number.
@@ -160,117 +143,87 @@ function layoutTable(outerWidth, demands, frame) {
160
143
  // flag for the pair, so half a frame is not a shape this renderer can emit.
161
144
  const edge = budget >= frame.edge * 2 ? frame.edge : 0;
162
145
  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.
146
+ // Every column whose divider fits is drawn, in column order, at whatever
147
+ // width the division below leaves it — none at all included, as Rich draws a
148
+ // column it collapsed to nothing. The first divider the budget cannot hold
149
+ // is where the table ends; the rest are dropped, never drawn outside the
150
+ // frame.
151
+ let seated = demands.length > 0 ? 1 : 0;
152
+ while (seated < demands.length && budget >= frame.divider) {
153
+ take(frame.divider);
154
+ seated++;
155
+ }
156
+ // Every column's minimum is paid next, a declared width and a `noWrap`
157
+ // column alike, and a budget too small for them all cuts them in the
158
+ // reference's order, `reduceToFit`'s — never the table's frame. The rest of
159
+ // what each column wanted comes only after. A table whose columns all fit
160
+ // leaves this budget partly unspent.
181
161
  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
162
  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);
163
+ const holding = (...passes) => seatedDemands.map((_, index) => passes.reduce((sum, granted) => sum + granted[index], 0));
164
+ const needed = reduceToFit(budget, seatedDemands.map((demand) => ({ want: demand.minimum, wrapable: demand.wrapable })));
205
165
  const beyond = distribute(budget - spent(needed), seatedDemands.map((demand, index) => ({
206
- want: Math.max(0, demand.want - afterNeeded[index]),
166
+ want: Math.max(0, demand.want - needed[index]),
207
167
  weight: demand.weight,
208
168
  })));
209
169
  const wanted = needed.map((width, index) => width + beyond[index]);
210
170
  // The unspent part goes first to the columns with nothing to size to, each
211
171
  // alike, up to its `fill` — the reference's `Measurement(1, max_width)`, a
212
172
  // maximum of the whole offer that no content measured.
213
- const afterWant = holding(wanted);
214
173
  const filled = distribute(budget - spent(wanted), seatedDemands.map((demand, index) => ({
215
- want: Math.max(0, demand.fill - afterWant[index]),
174
+ want: Math.max(0, demand.fill - wanted[index]),
216
175
  weight: demand.fill > 0 ? 1 : 0,
217
176
  })));
218
177
  // Then to the ratio columns, by the reference's flexible split:
219
178
  // `ratio_distribute(max_width - fixed_widths, ratios, flex_minimum)`. What
220
179
  // 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.
180
+ // columns already hold included; each is held to at least its want, which is
181
+ // its padding and its floor. The floor is the reference's `column.width or
182
+ // 1`, reading `minWidth` where there is no `width`, where the reference
183
+ // reads none and draws a `min_width=12` ratio column five cells wide.
226
184
  // A ratio column is paid the rest of its part out of the budget, in column
227
185
  // order, so a part the minimums push past what is left is cut there rather
228
186
  // than drawn past the width.
229
187
  const afterFill = holding(wanted, filled);
230
188
  const flexible = seatedDemands.flatMap((demand, index) => (demand.ratio > 0 ? [index] : []));
231
- const padded = (index, content) => padLeft[index] + content + padRight[index];
232
189
  // `UNBOUNDED` holds an unbounded offer, as it does for the stretch below;
233
190
  // it caps the whole split, so the total stays an integer a double holds.
234
191
  const pool = budget - spent(wanted) - spent(filled);
235
192
  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)));
193
+ 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
194
  const shared = seatedDemands.map(() => 0);
238
195
  flexible.forEach((index, slot) => {
239
- shared[index] = Math.min(Math.max(0, parts[slot] - padded(index, afterFill[index])), unshared);
196
+ shared[index] = Math.min(Math.max(0, parts[slot] - afterFill[index]), unshared);
240
197
  unshared -= shared[index];
241
198
  });
242
199
  // 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.
200
+ // already holds: its natural width, since nothing is left over while any
201
+ // column is short, or the cap a fill stopped at. The split is the
202
+ // reference's `ratio_distribute(max_width - table_width, widths)`, rounding
203
+ // included; the columns it runs over are this port's. A table that does not
204
+ // expand has no column that stretches, and an empty list takes nothing,
205
+ // which is how it stays narrower than the width it was offered. The order is
206
+ // the reference's: Rich pads an expanding table only once
207
+ // `table_width < max_width`, never while it is collapsing a column.
251
208
  const held = holding(wanted, filled, shared);
252
209
  const stretchers = seatedDemands.flatMap((demand, index) => (demand.stretch ? [index] : []));
253
210
  const stretches = ratioDistribute(
254
211
  // An unbounded offer is held to `UNBOUNDED`, this model's own infinity, as
255
212
  // `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])));
213
+ Math.min(budget - spent(wanted) - spent(filled) - spent(shared), UNBOUNDED), stretchers.map((index) => held[index]));
257
214
  // A table that does not fit gives back each column's `floor`: Rich
258
215
  // re-measures a collapsed table's columns at the widths it gave them, and a
259
216
  // column with nothing to draw measures 0 there however many cells the collapse
260
217
  // 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);
218
+ const natural = demands.reduce((sum, demand) => sum + demand.want, frame.edge * 2 + Math.max(0, demands.length - 1) * frame.divider);
262
219
  const squeezed = natural > outerWidth;
263
- const columns = held.map((width, index) => squeezed ? width - Math.min(width, seatedDemands[index].floor) : width);
220
+ const cellWidths = held.map((width, index) => squeezed ? width - Math.min(width, seatedDemands[index].floor) : width);
264
221
  stretchers.forEach((index, slot) => {
265
- columns[index] += stretches[slot];
222
+ cellWidths[index] += stretches[slot];
266
223
  });
267
- const cellWidths = columns.map((width, index) => padded(index, width));
268
224
  return {
269
225
  edge,
270
226
  divider: frame.divider,
271
- padLeft,
272
- padRight,
273
- columns,
274
227
  cellWidths,
275
228
  totalWidth: edge * 2 +
276
229
  Math.max(0, seated - 1) * frame.divider +
@@ -304,10 +257,9 @@ export class Column {
304
257
  noWrap;
305
258
  overflow;
306
259
  highlight;
307
- _cells;
308
260
  constructor(options) {
309
- this.header = options?.header;
310
- this.footer = options?.footer;
261
+ this._header = new State(embed(options?.header));
262
+ this._footer = new State(embed(options?.footer));
311
263
  this.headerStyle = options?.headerStyle ?? NULL_STYLE;
312
264
  this.footerStyle = options?.footerStyle ?? NULL_STYLE;
313
265
  this.style = options?.style ?? NULL_STYLE;
@@ -319,7 +271,6 @@ export class Column {
319
271
  this.noWrap = options?.noWrap ?? false;
320
272
  this.overflow = options?.overflow ?? "ellipsis";
321
273
  this.highlight = options?.highlight ?? false;
322
- this._cells = [];
323
274
  }
324
275
  /**
325
276
  * The two stamped cells, stamped on assignment rather than at the constructor.
@@ -339,30 +290,30 @@ export class Column {
339
290
  * which is why the setters take `undefined` rather than defaulting around it.
340
291
  */
341
292
  get header() {
342
- return this._header;
293
+ return this._header.get();
343
294
  }
344
295
  set header(content) {
345
- this._header = embed(content);
296
+ this._header.set(embed(content));
346
297
  }
347
298
  get footer() {
348
- return this._footer;
299
+ return this._footer.get();
349
300
  }
350
301
  set footer(content) {
351
- this._footer = embed(content);
302
+ this._footer.set(embed(content));
352
303
  }
353
304
  get flexible() {
354
305
  return columnShare(this) > 0;
355
306
  }
356
- /** @internal */
357
- addCell(cell) {
358
- this._cells.push(cell);
359
- }
360
- /** @internal */
361
- getCells() {
362
- return this._cells;
363
- }
364
307
  copy() {
365
- const col = new Column({
308
+ return new Column({
309
+ // Shared, not copied, as a row's cells are: `embed` already copied a
310
+ // caller's `RichText`, what it hands back is never changed, and `embed`
311
+ // passes a renderable through.
312
+ header: this.header,
313
+ footer: this.footer,
314
+ headerStyle: this.headerStyle,
315
+ footerStyle: this.footerStyle,
316
+ style: this.style,
366
317
  justify: this.justify,
367
318
  width: this.width,
368
319
  minWidth: this.minWidth,
@@ -372,19 +323,16 @@ export class Column {
372
323
  overflow: this.overflow,
373
324
  highlight: this.highlight,
374
325
  });
375
- col.headerStyle = this.headerStyle;
376
- col.footerStyle = this.footerStyle;
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;
382
- return col;
383
326
  }
384
327
  }
328
+ // A row's cell in column `index`: a column the row is short of draws a blank one.
329
+ function cellAt(row, index) {
330
+ return row.cells[index] ?? new EmbeddedText(undefined);
331
+ }
385
332
  export class Table {
386
- _columns;
387
- _rows;
333
+ // Rich's Table grows by addColumn and addRow after it is built.
334
+ _columns = new ListState();
335
+ _rows = new ListState();
388
336
  box;
389
337
  _title;
390
338
  _caption;
@@ -415,8 +363,6 @@ export class Table {
415
363
  return this._caption;
416
364
  }
417
365
  constructor(options) {
418
- this._columns = [];
419
- this._rows = [];
420
366
  this.box = options?.box !== undefined ? options.box : HEAVY_HEAD;
421
367
  // An empty title draws no row, as the reference's `if self.title` draws
422
368
  // none; markup that styles nothing is a title, and draws a blank one.
@@ -449,10 +395,10 @@ export class Table {
449
395
  this.highlight = options?.highlight ?? false;
450
396
  }
451
397
  get columns() {
452
- return this._columns;
398
+ return this._columns.get();
453
399
  }
454
400
  get rowCount() {
455
- return this._rows.length;
401
+ return this._rows.get().length;
456
402
  }
457
403
  addColumn(header, options) {
458
404
  const col = new Column({ ...options, header: header ?? options?.header, highlight: options?.highlight ?? this.highlight });
@@ -481,20 +427,18 @@ export class Table {
481
427
  // behind. [LAW:no-ambient-temporal-coupling]
482
428
  const resolved = cells.map(embed);
483
429
  // Auto-create columns if needed
484
- while (this._columns.length < resolved.length) {
430
+ while (this._columns.get().length < resolved.length) {
485
431
  this.addColumn();
486
432
  }
487
- this._rows.push({ cells: resolved, endSection });
433
+ this._rows.push({ cells: resolved, endSection: new State(endSection) });
488
434
  return this;
489
435
  }
490
436
  addSection() {
491
- if (this._rows.length > 0) {
492
- this._rows[this._rows.length - 1].endSection = true;
493
- }
437
+ this._rows.get().at(-1)?.endSection.set(true);
494
438
  return this;
495
439
  }
496
440
  *render(rawOptions) {
497
- if (this._columns.length === 0) {
441
+ if (this._columns.get().length === 0) {
498
442
  yield Segment.line();
499
443
  return;
500
444
  }
@@ -527,17 +471,17 @@ export class Table {
527
471
  // once per cell, as the reference's `_get_cells` does per column.
528
472
  const headerStyle = getStyle(options, this.headerStyle);
529
473
  const footerStyle = getStyle(options, this.footerStyle);
530
- const columnStyles = this._columns.map((c) => getStyle(options, c.style));
474
+ const columnStyles = this._columns.get().map((c) => getStyle(options, c.style));
531
475
  const stripes = this.rowStyles.map((s) => getStyle(options, s));
532
476
  // Each row's padding above and below, by where it stands among every row
533
477
  // the table draws, header and footer included, as the reference counts them.
534
478
  const firstBody = this.showHeader ? 1 : 0;
535
- const lastRow = firstBody + this._rows.length + (this.showFooter ? 1 : 0) - 1;
479
+ const lastRow = firstBody + this._rows.get().length + (this.showFooter ? 1 : 0) - 1;
536
480
  const rowPadding = (position) => this._rowPadding(position === 0, position === lastRow);
537
481
  // Header row
538
482
  if (this.showHeader) {
539
- const headerCells = this._columns.map((c) => c.header);
540
- const headerStyles = this._columns.map((c) => headerStyle.add(getStyle(options, c.headerStyle)));
483
+ const headerCells = this._columns.get().map((c) => c.header);
484
+ const headerStyles = this._columns.get().map((c) => headerStyle.add(getStyle(options, c.headerStyle)));
541
485
  yield* this._renderRow(options, headerCells, headerStyles, NULL_STYLE, geometry, rowPadding(0), box, "head", border);
542
486
  // Header separator
543
487
  if (box) {
@@ -545,15 +489,15 @@ export class Table {
545
489
  }
546
490
  }
547
491
  // Data rows
548
- for (let rowIdx = 0; rowIdx < this._rows.length; rowIdx++) {
549
- const row = this._rows[rowIdx];
550
- const rowCells = this._columns.map((_, colIdx) => row.cells[colIdx] ?? new EmbeddedText(undefined));
492
+ for (let rowIdx = 0; rowIdx < this._rows.get().length; rowIdx++) {
493
+ const row = this._rows.get()[rowIdx];
494
+ const rowCells = this._rowCells(row);
551
495
  const rowStyle = stripes[rowIdx % stripes.length] ?? NULL_STYLE;
552
496
  const cellStyles = columnStyles.map((s) => s.add(rowStyle));
553
497
  yield* this._renderRow(options, rowCells, cellStyles, rowStyle, geometry, rowPadding(firstBody + rowIdx), box, "row", border);
554
498
  // Row separator
555
- const showSep = this.showLines || row.endSection;
556
- if (showSep && box && rowIdx < this._rows.length - 1) {
499
+ const showSep = this.showLines || row.endSection.get();
500
+ if (showSep && box && rowIdx < this._rows.get().length - 1) {
557
501
  yield* box.getRow(geometry.cellWidths, "row", border, edge);
558
502
  }
559
503
  }
@@ -562,8 +506,8 @@ export class Table {
562
506
  if (box) {
563
507
  yield* box.getRow(geometry.cellWidths, "foot", border, edge);
564
508
  }
565
- const footerCells = this._columns.map((c) => c.footer);
566
- const footerStyles = this._columns.map((c) => footerStyle.add(getStyle(options, c.footerStyle)));
509
+ const footerCells = this._columns.get().map((c) => c.footer);
510
+ const footerStyles = this._columns.get().map((c) => footerStyle.add(getStyle(options, c.footerStyle)));
567
511
  yield* this._renderRow(options, footerCells, footerStyles, NULL_STYLE, geometry, rowPadding(lastRow), box, "foot", border);
568
512
  }
569
513
  // Bottom border
@@ -678,23 +622,25 @@ export class Table {
678
622
  * mode gets its own path through the width division.
679
623
  *
680
624
  * 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.
625
+ * reference's `_measure_column` measures them. Padding is part of every
626
+ * width the demand names, because the reference measures a cell wrapped in
627
+ * its `Padding`, and each sizing adds it where the reference does — a
628
+ * rowless column's minimum is the one width it never reaches. The floor is
629
+ * laid over whichever sizing the column has.
683
630
  */
684
631
  _columnDemands(options, outerWidth) {
685
632
  const frame = this._frame();
686
633
  const inner = {
687
634
  ...options,
688
- maxWidth: Math.max(0, outerWidth - frame.edge * 2 - frame.divider * Math.max(0, this._columns.length - 1)),
635
+ maxWidth: Math.max(0, outerWidth - frame.edge * 2 - frame.divider * Math.max(0, this._columns.get().length - 1)),
689
636
  };
690
- 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 };
637
+ return this._columns.get().map((col, index) => {
638
+ const sizing = this._columnSizing(col, index, inner, this._columnPadding(index));
639
+ const floor = sizing.want === 0 ? 1 : 0;
640
+ return { ...sizing, minimum: sizing.minimum + floor, want: sizing.want + floor, floor };
695
641
  });
696
642
  }
697
- _columnSizing(col, index, options) {
643
+ _columnSizing(col, index, options, [left, right]) {
698
644
  // A flexible column takes its share of whatever the bounded columns
699
645
  // leave; every other column asks for its natural width, whether or not a
700
646
  // neighbour is flexible. The split is the reference's —
@@ -703,17 +649,27 @@ export class Table {
703
649
  // here a ratio is honoured either way. Its bounded part is its floor —
704
650
  // the reference's `column.width or 1`, reading `minWidth` where there is
705
651
  // 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.
652
+ // table still pays it. A `width` beside a `ratio` is that floor, as the
653
+ // reference's `flex_minimum` reads it, held to the frame as a declared
654
+ // width is.
708
655
  // [LAW:one-source-of-truth] `columnShare` decides first, so a column that
709
656
  // `Column.flexible` calls elastic is one the division shares out.
657
+ const pad = left + right;
658
+ const padded = (content) => demandCells(content + pad);
659
+ // [LAW:single-enforcer] A width a column insists on, held to the width
660
+ // inside the frame — the reference's `with_maximum(max_width)` on a
661
+ // fixed-width column's measurement — so an even cut starts from what the
662
+ // table can hold. It holds a declared width and every ratio floor; a
663
+ // content column's `minWidth` is not held, as the reference clamps to
664
+ // `min_width` after `with_maximum`.
665
+ const held = (width) => Math.min(padded(demandCells(width)), options.maxWidth);
710
666
  const share = columnShare(col);
711
667
  if (share > 0) {
712
668
  const floor = Math.max(1, demandCells(col.width ?? col.minWidth ?? 0));
669
+ const bid = held(floor);
713
670
  return {
714
- reserved: 0,
715
- minimum: floor,
716
- want: floor,
671
+ minimum: bid,
672
+ want: bid,
717
673
  weight: floor,
718
674
  fill: 0,
719
675
  ratio: share,
@@ -724,11 +680,10 @@ export class Table {
724
680
  if (col.width !== undefined) {
725
681
  // [LAW:single-enforcer] floored where it is parsed, the same rule
726
682
  // `normalizePadding` applies to a negative padding side.
727
- const declared = demandCells(col.width);
683
+ const bid = held(col.width);
728
684
  return {
729
- reserved: declared,
730
- minimum: declared,
731
- want: declared,
685
+ minimum: bid,
686
+ want: bid,
732
687
  weight: 0,
733
688
  fill: 0,
734
689
  ratio: 0,
@@ -736,8 +691,8 @@ export class Table {
736
691
  wrapable: false,
737
692
  };
738
693
  }
739
- const range = this._cellRange(col, index, options);
740
- const natural = demandCells(this._bounded(col, range?.maximum ?? 1));
694
+ const range = this._cellRange(col, index, options, [left, right]);
695
+ const natural = demandCells(this._bounded(col, range?.maximum ?? padded(1), pad));
741
696
  // `expand` is a stretch rather than a larger want: the column still
742
697
  // competes for its natural width like any other, and only the cells left
743
698
  // once every column has that are shared out. A larger want looks
@@ -749,25 +704,26 @@ export class Table {
749
704
  // One deliberate divergence: Rich stretches declared-width columns as
750
705
  // well, so `width: 6` under `expand` renders 21 cells wide — an option
751
706
  // quietly meaning something else, the defect rich-justify-0cr exists to
752
- // remove. A reservation's stretch is zero above.
707
+ // remove. A declared width's stretch is zero above.
753
708
  //
754
709
  // A column with no cell at all — no rows, and neither header nor footer
755
710
  // drawn — has nothing to size to, which is not sizing to nothing. The
756
711
  // reference measures it `Measurement(1, max_width)`: one cell of content,
757
712
  // 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`.
713
+ // "no cells" as zero sized it to a two-cell box instead. The one cell and
714
+ // its padding are its want, so a `minWidth` is paid like any column's,
715
+ // and the offer is its `fill`, held to its `maxWidth`. Its minimum is the
716
+ // reference's bare 1, unpadded: `_measure_column` adds the padding only to
717
+ // a declared width and to the `min_width`/`max_width` clamp.
761
718
  //
762
719
  // A column that may not wrap has no narrower width than its natural one
763
720
  // that keeps its content whole — the reference leaves `no_wrap` columns out
764
721
  // of the ones it collapses.
765
722
  return {
766
- reserved: 0,
767
- minimum: col.noWrap ? natural : Math.min(natural, demandCells(this._bounded(col, range?.minimum ?? 1))),
723
+ minimum: col.noWrap ? natural : Math.min(natural, demandCells(this._bounded(col, range?.minimum ?? 1, pad))),
768
724
  want: natural,
769
725
  weight: natural,
770
- fill: range === undefined ? demandCells(this._bounded(col, UNBOUNDED)) : 0,
726
+ fill: range === undefined ? demandCells(this._bounded(col, UNBOUNDED, pad)) : 0,
771
727
  ratio: 0,
772
728
  stretch: this.expand,
773
729
  wrapable: !col.noWrap,
@@ -784,7 +740,7 @@ export class Table {
784
740
  _columnPadding(index) {
785
741
  const [, right, , left] = this.padding;
786
742
  const first = index === 0;
787
- const last = index === this._columns.length - 1;
743
+ const last = index === this._columns.get().length - 1;
788
744
  const collapsed = this.collapsePadding && !first ? Math.max(0, left - right) : left;
789
745
  return [!this.padEdge && first ? 0 : collapsed, !this.padEdge && last ? 0 : right];
790
746
  }
@@ -793,6 +749,9 @@ export class Table {
793
749
  const collapsed = this.collapsePadding && !last ? Math.max(0, top - bottom) : bottom;
794
750
  return [!this.padEdge && first ? 0 : top, !this.padEdge && last ? 0 : collapsed];
795
751
  }
752
+ _rowCells(row) {
753
+ return this._columns.get().map((_, index) => cellAt(row, index));
754
+ }
796
755
  /**
797
756
  * Every cell column `index` draws, in draw order.
798
757
  *
@@ -807,19 +766,20 @@ export class Table {
807
766
  *_columnCells(col, index) {
808
767
  if (this.showHeader)
809
768
  yield col.header;
810
- for (const row of this._rows)
811
- yield row.cells[index] ?? new EmbeddedText(undefined);
769
+ for (const row of this._rows.get())
770
+ yield cellAt(row, index);
812
771
  if (this.showFooter)
813
772
  yield col.footer;
814
773
  }
815
774
  /**
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]
775
+ * The widest any cell in a column draws, its padding included, and the
776
+ * narrowest the column can be without cutting one — the reference's
777
+ * `_measure_column` — or `undefined` when the column has no cell to measure.
778
+ * A column whose cells draw nothing measures its padding — a gutter asks for
779
+ * that and nothing else — which is not the same answer.
780
+ * [LAW:types-are-the-program]
821
781
  */
822
- _cellRange(col, index, options) {
782
+ _cellRange(col, index, options, [left, right]) {
823
783
  let range;
824
784
  for (const cell of this._columnCells(col, index)) {
825
785
  // The stamped cell, so the width a column asks for is the width its text
@@ -828,7 +788,10 @@ export class Table {
828
788
  // [LAW:one-source-of-truth] Measured as the cell measures itself, so a
829
789
  // multi-line cell asks for its widest line and a `Panel` for its frame;
830
790
  // one that cannot measure itself asks for every cell there is, as in Rich.
831
- const measured = Measurement.get(options, cell);
791
+ // Wrapped in its padding, as the reference's `_get_cells` hands it to
792
+ // `_measure_column`, so a cell measures what it draws and never past the
793
+ // width it was offered.
794
+ const measured = Measurement.get(options, new Padding(cell, [0, right, 0, left]));
832
795
  range = {
833
796
  minimum: Math.max(range?.minimum ?? 0, measured.minimum),
834
797
  maximum: Math.max(range?.maximum ?? 0, measured.maximum),
@@ -837,18 +800,20 @@ export class Table {
837
800
  return range;
838
801
  }
839
802
  /**
840
- * A natural width held to the column's own `minWidth`/`maxWidth`.
803
+ * A natural width held to the column's own `minWidth`/`maxWidth`, each
804
+ * widened by the column's `pad`, as the reference clamps a padded
805
+ * measurement to `min_width + padding_width`.
841
806
  *
842
807
  * A bound is a count of cells, read by the rule every width is read by: NaN
843
808
  * 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
809
+ * ceiling holds the column to its padding alone, as `width: NaN` does. A bare `Math.max` returns
845
810
  * NaN instead, and the column vanishes. [LAW:one-source-of-truth]
846
811
  */
847
- _bounded(col, natural) {
848
- return Math.min(Math.max(natural, cells(col.minWidth ?? 0)), cells(col.maxWidth ?? Infinity));
812
+ _bounded(col, natural, pad) {
813
+ return Math.min(Math.max(natural, col.minWidth === undefined ? 0 : cells(col.minWidth) + pad), cells(col.maxWidth ?? Infinity) + pad);
849
814
  }
850
815
  *_renderRow(options, cells, styles, rowStyle, geometry, [top, bottom], box, level, border) {
851
- const { padLeft, padRight, columns } = geometry;
816
+ const { cellWidths } = geometry;
852
817
  // [LAW:dataflow-not-control-flow] Header, body and footer share this path;
853
818
  // the level crosses as a value the box answers with glyphs, not a branch.
854
819
  const frame = box?.getContentChars(level);
@@ -859,39 +824,36 @@ export class Table {
859
824
  const divider = frame && (frame.vertical.trim() === ""
860
825
  ? new Segment(frame.vertical, rowStyle.backgroundStyle.add(border))
861
826
  : 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
827
+ // Render each cell at the width the geometry gave its column. Columns the
828
+ // width could not seat are absent from `cellWidths` and so are never
864
829
  // rendered at all.
865
- const paddedWidths = columns.map((cellWidth, index) => padLeft[index] + cellWidth + padRight[index]);
866
- const cellLines = columns.map((cellWidth, index) => {
867
- const col = this._columns[index];
868
- const cell = cells[index] ?? embed("");
830
+ const cellLines = cellWidths.map((cellWidth, index) => {
831
+ const col = this._columns.get()[index];
832
+ const [left, right] = this._columnPadding(index);
833
+ // [LAW:single-enforcer] The padding on every side is part of the cell,
834
+ // drawn by `Padding` as the reference's `_get_cells` wraps each cell in
835
+ // one: its sides are paid before its content, so a cell too narrow for
836
+ // them draws as blanks, and a content line sits between its left and
837
+ // right blanks.
838
+ const padded = new Padding(cells[index] ?? embed(""), [top, right, bottom, left]);
869
839
  // The render's own options with the column's canvas laid over them, so a
870
840
  // cell resolves its style names against the same theme as the table. A
871
841
  // table stacks its cells, so each is handed the table's rows as a
872
842
  // ceiling, never a region to fill — the `Height` contract's
873
843
  // `stackedHeight`, where the reference hands a cell `height=None`.
874
844
  // 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
- ];
845
+ // A cell left no column draws nothing at all, its padding included, as
846
+ // the reference's `render_lines` draws nothing under one column.
847
+ const cellOptions = {
848
+ ...options,
849
+ highlight: col.highlight,
850
+ maxWidth: cellWidth,
851
+ justify: col.justify,
852
+ overflow: col.overflow,
853
+ noWrap: col.noWrap,
854
+ height: stackedHeight(options.height),
855
+ };
856
+ return Segment.splitLines(render(padded, cellOptions));
895
857
  });
896
858
  const maxLines = cellLines.reduce((most, lines) => Math.max(most, lines.length), 1);
897
859
  // How many blank lines stand above each cell's content. A header sits on
@@ -902,14 +864,14 @@ export class Table {
902
864
  if (frame && geometry.edge === 1) {
903
865
  yield new Segment(frame.left, border);
904
866
  }
905
- for (let colIdx = 0; colIdx < columns.length; colIdx++) {
867
+ for (let colIdx = 0; colIdx < cellWidths.length; colIdx++) {
906
868
  if (colIdx > 0 && divider) {
907
869
  yield divider;
908
870
  }
909
871
  const style = styles[colIdx];
910
872
  // A cell with fewer lines than the row contributes blanks, so every
911
873
  // column spans the same number of rows and the frame stays rectangular.
912
- const line = cellLines[colIdx][lineIdx - drop[colIdx]] ?? blank(paddedWidths[colIdx]);
874
+ const line = cellLines[colIdx][lineIdx - drop[colIdx]] ?? blank(cellWidths[colIdx]);
913
875
  // The cell's style is the base its content's own spans layer over, and
914
876
  // it covers the whole cell — its padding and its blank lines too, as
915
877
  // the reference's does — so a background fills the column rather than
@@ -959,7 +921,7 @@ export class Table {
959
921
  // lines, and `end="!!"` runs on into the top border there too. Splitting
960
922
  // and re-ending each line moved a non-newline `end` onto the title's own
961
923
  // row, wider than the table.
962
- yield* source.render({
924
+ yield* render(source, {
963
925
  ...options,
964
926
  maxWidth: tableWidth,
965
927
  justify,