@promptctl/rich-js 0.18.0 → 0.19.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 (244) hide show
  1. package/README.md +9 -7
  2. package/dist/core/anchor.d.ts +45 -0
  3. package/dist/core/anchor.js +39 -0
  4. package/dist/core/ansi.d.ts +65 -0
  5. package/dist/core/ansi.js +231 -0
  6. package/dist/core/box.d.ts +145 -0
  7. package/dist/core/box.js +372 -0
  8. package/dist/core/cells.d.ts +192 -0
  9. package/dist/core/cells.js +334 -0
  10. package/dist/core/clock.d.ts +48 -0
  11. package/dist/core/clock.js +42 -0
  12. package/dist/core/color.d.ts +326 -0
  13. package/dist/core/color.js +1225 -0
  14. package/dist/core/console.d.ts +196 -0
  15. package/dist/core/console.js +596 -0
  16. package/dist/core/easing.d.ts +86 -0
  17. package/dist/core/easing.js +171 -0
  18. package/dist/core/emoji.d.ts +24 -0
  19. package/dist/core/emoji.js +391 -0
  20. package/dist/core/env.d.ts +16 -0
  21. package/dist/core/env.js +16 -0
  22. package/dist/core/export-html.d.ts +59 -0
  23. package/dist/core/export-html.js +197 -0
  24. package/dist/core/export-lines.d.ts +130 -0
  25. package/dist/core/export-lines.js +159 -0
  26. package/dist/core/export-svg.d.ts +49 -0
  27. package/dist/core/export-svg.js +213 -0
  28. package/dist/core/fnv1a.d.ts +10 -0
  29. package/dist/core/fnv1a.js +17 -0
  30. package/dist/core/highlighter.d.ts +33 -0
  31. package/dist/core/highlighter.js +190 -0
  32. package/dist/core/json.d.ts +27 -0
  33. package/dist/core/json.js +53 -0
  34. package/dist/core/markup.d.ts +140 -0
  35. package/dist/core/markup.js +654 -0
  36. package/dist/core/measure.d.ts +27 -0
  37. package/dist/core/measure.js +67 -0
  38. package/dist/core/memo.d.ts +19 -0
  39. package/dist/core/memo.js +34 -0
  40. package/dist/core/oklch.d.ts +161 -0
  41. package/dist/core/oklch.js +312 -0
  42. package/dist/core/osc8.d.ts +68 -0
  43. package/dist/core/osc8.js +78 -0
  44. package/dist/core/paint.d.ts +107 -0
  45. package/dist/core/paint.js +150 -0
  46. package/dist/core/place.d.ts +30 -0
  47. package/dist/core/place.js +49 -0
  48. package/dist/core/pretty.d.ts +247 -0
  49. package/dist/core/pretty.js +906 -0
  50. package/dist/core/protocol.d.ts +305 -0
  51. package/dist/core/protocol.js +183 -0
  52. package/dist/core/render.d.ts +87 -0
  53. package/dist/core/render.js +105 -0
  54. package/dist/core/segment.d.ts +162 -0
  55. package/dist/core/segment.js +488 -0
  56. package/dist/core/spinnerData.d.ts +12 -0
  57. package/dist/core/spinnerData.js +117 -0
  58. package/dist/core/strip.d.ts +152 -0
  59. package/dist/core/strip.js +326 -0
  60. package/dist/core/style.d.ts +189 -0
  61. package/dist/core/style.js +758 -0
  62. package/dist/core/subscription.d.ts +12 -0
  63. package/dist/core/subscription.js +12 -0
  64. package/dist/core/text.d.ts +283 -0
  65. package/dist/core/text.js +1097 -0
  66. package/dist/core/wrap.d.ts +46 -0
  67. package/dist/core/wrap.js +140 -0
  68. package/dist/host/app.d.ts +133 -0
  69. package/dist/host/app.js +285 -0
  70. package/dist/host/host-environment.d.ts +26 -0
  71. package/dist/host/host-environment.js +44 -0
  72. package/dist/host/index.d.ts +6 -0
  73. package/dist/host/index.js +3 -0
  74. package/dist/host/terminal-host.d.ts +180 -0
  75. package/dist/host/terminal-host.js +186 -0
  76. package/dist/index.d.ts +98 -0
  77. package/dist/index.js +114 -0
  78. package/dist/node/prompt.d.ts +34 -0
  79. package/dist/node/prompt.js +51 -0
  80. package/dist/node/save.d.ts +18 -0
  81. package/dist/node/save.js +24 -0
  82. package/dist/node/terminal-host.d.ts +114 -0
  83. package/dist/node/terminal-host.js +227 -0
  84. package/dist/node/traceback.d.ts +38 -0
  85. package/dist/node/traceback.js +98 -0
  86. package/dist/renderables/align.d.ts +16 -0
  87. package/dist/renderables/align.js +26 -0
  88. package/dist/renderables/columns.d.ts +77 -0
  89. package/dist/renderables/columns.js +191 -0
  90. package/dist/renderables/constrain.d.ts +15 -0
  91. package/dist/renderables/constrain.js +31 -0
  92. package/dist/renderables/effect.d.ts +98 -0
  93. package/dist/renderables/effect.js +160 -0
  94. package/dist/renderables/embed.d.ts +107 -0
  95. package/dist/renderables/embed.js +160 -0
  96. package/dist/renderables/flexStrip.d.ts +41 -0
  97. package/dist/renderables/flexStrip.js +200 -0
  98. package/dist/renderables/group.d.ts +14 -0
  99. package/dist/renderables/group.js +22 -0
  100. package/dist/renderables/layout.d.ts +75 -0
  101. package/dist/renderables/layout.js +223 -0
  102. package/dist/renderables/live.d.ts +74 -0
  103. package/dist/renderables/live.js +188 -0
  104. package/dist/renderables/markdown.d.ts +38 -0
  105. package/dist/renderables/markdown.js +587 -0
  106. package/dist/renderables/padding.d.ts +59 -0
  107. package/dist/renderables/padding.js +147 -0
  108. package/dist/renderables/panel.d.ts +114 -0
  109. package/dist/renderables/panel.js +303 -0
  110. package/dist/renderables/progress.d.ts +178 -0
  111. package/dist/renderables/progress.js +362 -0
  112. package/dist/renderables/progressBar.d.ts +31 -0
  113. package/dist/renderables/progressBar.js +64 -0
  114. package/dist/renderables/prompt.d.ts +71 -0
  115. package/dist/renderables/prompt.js +165 -0
  116. package/dist/renderables/ratio.d.ts +123 -0
  117. package/dist/renderables/ratio.js +213 -0
  118. package/dist/renderables/rule.d.ts +26 -0
  119. package/dist/renderables/rule.js +86 -0
  120. package/dist/renderables/spinner.d.ts +57 -0
  121. package/dist/renderables/spinner.js +103 -0
  122. package/dist/renderables/status.d.ts +29 -0
  123. package/dist/renderables/status.js +46 -0
  124. package/dist/renderables/syntax.d.ts +65 -0
  125. package/dist/renderables/syntax.js +166 -0
  126. package/dist/renderables/table.d.ts +232 -0
  127. package/dist/renderables/table.js +973 -0
  128. package/dist/renderables/traceback.d.ts +32 -0
  129. package/dist/renderables/traceback.js +279 -0
  130. package/dist/renderables/tree.d.ts +76 -0
  131. package/dist/renderables/tree.js +177 -0
  132. package/dist/renderables/viewport.d.ts +114 -0
  133. package/dist/renderables/viewport.js +196 -0
  134. package/dist/template-bindings/color-funcs.d.ts +109 -0
  135. package/dist/template-bindings/color-funcs.js +243 -0
  136. package/dist/template-bindings/helpers.d.ts +26 -0
  137. package/dist/template-bindings/helpers.js +33 -0
  138. package/dist/template-bindings/index.d.ts +119 -0
  139. package/dist/template-bindings/index.js +162 -0
  140. package/dist/template-bindings/palette-funcs.d.ts +96 -0
  141. package/dist/template-bindings/palette-funcs.js +145 -0
  142. package/dist/template-bindings/style-funcs.d.ts +28 -0
  143. package/dist/template-bindings/style-funcs.js +157 -0
  144. package/dist/themes/buildPalette.d.ts +57 -0
  145. package/dist/themes/buildPalette.js +100 -0
  146. package/dist/themes/colorMath.d.ts +94 -0
  147. package/dist/themes/colorMath.js +280 -0
  148. package/dist/themes/colorRef.d.ts +36 -0
  149. package/dist/themes/colorRef.js +78 -0
  150. package/dist/themes/data/ansi-tables.d.ts +14 -0
  151. package/dist/themes/data/ansi-tables.js +49 -0
  152. package/dist/themes/data/atom-one-dark.d.ts +3 -0
  153. package/dist/themes/data/atom-one-dark.js +177 -0
  154. package/dist/themes/data/atom-one-light.d.ts +3 -0
  155. package/dist/themes/data/atom-one-light.js +177 -0
  156. package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
  157. package/dist/themes/data/catppuccin-frappe.js +177 -0
  158. package/dist/themes/data/catppuccin-latte.d.ts +3 -0
  159. package/dist/themes/data/catppuccin-latte.js +177 -0
  160. package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
  161. package/dist/themes/data/catppuccin-macchiato.js +177 -0
  162. package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
  163. package/dist/themes/data/catppuccin-mocha.js +177 -0
  164. package/dist/themes/data/cyberpunk.d.ts +17 -0
  165. package/dist/themes/data/cyberpunk.js +193 -0
  166. package/dist/themes/data/default.d.ts +3 -0
  167. package/dist/themes/data/default.js +160 -0
  168. package/dist/themes/data/dracula.d.ts +3 -0
  169. package/dist/themes/data/dracula.js +177 -0
  170. package/dist/themes/data/flexoki.d.ts +3 -0
  171. package/dist/themes/data/flexoki.js +177 -0
  172. package/dist/themes/data/gruvbox.d.ts +3 -0
  173. package/dist/themes/data/gruvbox.js +177 -0
  174. package/dist/themes/data/index.d.ts +35 -0
  175. package/dist/themes/data/index.js +64 -0
  176. package/dist/themes/data/monokai.d.ts +3 -0
  177. package/dist/themes/data/monokai.js +177 -0
  178. package/dist/themes/data/nord.d.ts +3 -0
  179. package/dist/themes/data/nord.js +177 -0
  180. package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
  181. package/dist/themes/data/rose-pine-dawn.js +177 -0
  182. package/dist/themes/data/rose-pine-moon.d.ts +3 -0
  183. package/dist/themes/data/rose-pine-moon.js +177 -0
  184. package/dist/themes/data/rose-pine.d.ts +3 -0
  185. package/dist/themes/data/rose-pine.js +177 -0
  186. package/dist/themes/data/solarized-dark.d.ts +3 -0
  187. package/dist/themes/data/solarized-dark.js +177 -0
  188. package/dist/themes/data/solarized-light.d.ts +3 -0
  189. package/dist/themes/data/solarized-light.js +177 -0
  190. package/dist/themes/data/svg-export.d.ts +3 -0
  191. package/dist/themes/data/svg-export.js +177 -0
  192. package/dist/themes/data/textual-ansi.d.ts +3 -0
  193. package/dist/themes/data/textual-ansi.js +160 -0
  194. package/dist/themes/data/textual-dark.d.ts +3 -0
  195. package/dist/themes/data/textual-dark.js +160 -0
  196. package/dist/themes/data/textual-light.d.ts +3 -0
  197. package/dist/themes/data/textual-light.js +160 -0
  198. package/dist/themes/data/tokyo-night.d.ts +3 -0
  199. package/dist/themes/data/tokyo-night.js +177 -0
  200. package/dist/themes/data/types.d.ts +31 -0
  201. package/dist/themes/data/types.js +10 -0
  202. package/dist/themes/palette.d.ts +45 -0
  203. package/dist/themes/palette.js +71 -0
  204. package/dist/themes/ramp.d.ts +70 -0
  205. package/dist/themes/ramp.js +103 -0
  206. package/dist/themes/registry.d.ts +44 -0
  207. package/dist/themes/registry.js +105 -0
  208. package/dist/themes/terminalThemes.d.ts +49 -0
  209. package/dist/themes/terminalThemes.js +63 -0
  210. package/dist/themes/transpose.d.ts +87 -0
  211. package/dist/themes/transpose.js +138 -0
  212. package/dist/widgets/button.d.ts +45 -0
  213. package/dist/widgets/button.js +177 -0
  214. package/dist/widgets/checkbox.d.ts +38 -0
  215. package/dist/widgets/checkbox.js +135 -0
  216. package/dist/widgets/dropdown.d.ts +64 -0
  217. package/dist/widgets/dropdown.js +416 -0
  218. package/dist/widgets/event-router.d.ts +71 -0
  219. package/dist/widgets/event-router.js +629 -0
  220. package/dist/widgets/focus-manager.d.ts +45 -0
  221. package/dist/widgets/focus-manager.js +191 -0
  222. package/dist/widgets/hit.d.ts +64 -0
  223. package/dist/widgets/hit.js +113 -0
  224. package/dist/widgets/index.d.ts +20 -0
  225. package/dist/widgets/index.js +14 -0
  226. package/dist/widgets/ink.d.ts +22 -0
  227. package/dist/widgets/ink.js +31 -0
  228. package/dist/widgets/slider.d.ts +54 -0
  229. package/dist/widgets/slider.js +247 -0
  230. package/dist/widgets/static-item.d.ts +34 -0
  231. package/dist/widgets/static-item.js +54 -0
  232. package/dist/widgets/text-input.d.ts +283 -0
  233. package/dist/widgets/text-input.js +1174 -0
  234. package/dist/widgets/themed-widget.d.ts +20 -0
  235. package/dist/widgets/themed-widget.js +89 -0
  236. package/dist/widgets/toggle.d.ts +41 -0
  237. package/dist/widgets/toggle.js +157 -0
  238. package/dist/widgets/types.d.ts +87 -0
  239. package/dist/widgets/types.js +30 -0
  240. package/dist/widgets/widget-app.d.ts +65 -0
  241. package/dist/widgets/widget-app.js +206 -0
  242. package/dist/widgets/widget-base.d.ts +42 -0
  243. package/dist/widgets/widget-base.js +172 -0
  244. package/package.json +16 -2
@@ -0,0 +1,973 @@
1
+ /**
2
+ * Table — tabular data with headers, borders, auto-sizing, and alignment.
3
+ */
4
+ import { cellCount as cells } from "../core/cells.js";
5
+ import { Segment } from "../core/segment.js";
6
+ import { NULL_STYLE } from "../core/style.js";
7
+ import { HEAVY_HEAD } from "../core/box.js";
8
+ import { RichText } from "../core/text.js";
9
+ import { EmbeddedText, embed, present } from "./embed.js";
10
+ import { normalizePadding } from "./padding.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";
14
+ /** A `want` no budget can satisfy: the column takes every cell its weight earns. */
15
+ const UNBOUNDED = Number.MAX_SAFE_INTEGER;
16
+ /**
17
+ * A column's `want` or `weight`. Finite, because `UNBOUNDED` is this model's
18
+ * own infinity: a literal `Infinity` reaching `distribute` makes a column's
19
+ * weighted share `Infinity / Infinity`, which is NaN, and the NaN then skews
20
+ * every other elastic column in the table.
21
+ */
22
+ const demandCells = (n) => Math.min(cells(n), UNBOUNDED);
23
+ /** `n` cells of padding, as nothing at all when `n` is zero. */
24
+ const blank = (n) => (n > 0 ? [new Segment(" ".repeat(n))] : []);
25
+ /**
26
+ * Hand out `total` cells across `demands`, weighted, and never past a demand's
27
+ * `want`.
28
+ *
29
+ * A column whose proportional share would overshoot its cap is granted its
30
+ * whole want and dropped, and the cells it could not use are reopened to the
31
+ * columns still under their caps. That repeats until everyone still open fits
32
+ * within their share; one largest-remainder pass then places the cells the
33
+ * shares left as fractions, so the granted widths sum to `total` exactly — or
34
+ * to the point where every column is capped, which is how a table stays
35
+ * narrower than a width it was offered.
36
+ *
37
+ * Each round caps at least one column or is the last, so the work is bounded by
38
+ * the number of columns and never by the width. That bound is the point rather
39
+ * than an optimization: handing cells out one at a time made the iteration
40
+ * count the width itself, which stalled on a very wide table and — since
41
+ * `Infinity - 1 === Infinity` — never terminated at all for a `maxWidth` of
42
+ * `Infinity`. Capping by want reaches that case in one round.
43
+ *
44
+ * The arithmetic is exact. A share's fraction is what decides who gets a
45
+ * leftover cell, and in floating point two fractions that are equal — 6/14 and
46
+ * 6/14 for 16 cells over weights 3 and 10 — come out 0.4285714285714284 and
47
+ * 0.4285714285714288, so the tie went wherever the rounding error pointed.
48
+ * The weights are read as whole numbers in one proportion (`exactWeights`) and
49
+ * everything after is integer division.
50
+ */
51
+ function distribute(total, demands) {
52
+ const granted = demands.map(() => 0);
53
+ const bidders = demands.flatMap((demand, index) => demand.weight > 0 && demand.want > 0 ? [{ index, demand }] : []);
54
+ const weights = exactWeights(bidders.map(({ demand }) => demand.weight));
55
+ let open = bidders.map(({ index, demand }, slot) => ({
56
+ index,
57
+ want: BigInt(demand.want),
58
+ weight: weights[slot],
59
+ }));
60
+ const sum = (values) => values.reduce((acc, value) => acc + value, 0n);
61
+ // A budget past every open want grants each its want either way, and that
62
+ // bound is what keeps an unbounded offer — `Infinity` is a legal outer width
63
+ // — out of integer arithmetic that cannot hold it.
64
+ const wantSum = sum(open.map(({ want }) => want));
65
+ let remaining = total >= wantSum ? wantSum : BigInt(Math.max(0, total));
66
+ while (open.length > 0 && remaining > 0n) {
67
+ const weightSum = sum(open.map(({ weight }) => weight));
68
+ const capped = open.filter(({ want, weight }) => remaining * weight >= want * weightSum);
69
+ if (capped.length === 0)
70
+ break;
71
+ for (const { index, want } of capped) {
72
+ granted[index] = Number(want);
73
+ remaining -= want;
74
+ }
75
+ open = open.filter((bid) => !capped.includes(bid));
76
+ }
77
+ if (open.length > 0 && remaining > 0n) {
78
+ const weightSum = sum(open.map(({ weight }) => weight));
79
+ const products = open.map(({ weight }) => remaining * weight);
80
+ const whole = products.map((product) => product / weightSum);
81
+ const fraction = products.map((product) => product % weightSum);
82
+ let residue = remaining - sum(whole);
83
+ // The cells the shares left as fractions go to the largest fraction first,
84
+ // ties to the leftmost column, so the total lands exactly on `remaining`.
85
+ // `Number` of a nonzero difference is never 0, so the sign is exact.
86
+ const byFraction = open
87
+ .map((_, slot) => slot)
88
+ .sort((a, b) => Number(fraction[b] - fraction[a]) || a - b);
89
+ for (const slot of byFraction) {
90
+ if (residue <= 0n)
91
+ break;
92
+ whole[slot]++;
93
+ residue--;
94
+ }
95
+ open.forEach(({ index }, slot) => {
96
+ granted[index] = Number(whole[slot]);
97
+ });
98
+ }
99
+ return granted;
100
+ }
101
+ /**
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.
115
+ */
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;
149
+ }
150
+ function layoutTable(outerWidth, demands, frame) {
151
+ // Plain `number`, not the `CellCol` `cells` hands back: this budget is spent
152
+ // down by arithmetic, and arithmetic on a branded type produces a number.
153
+ let budget = cells(outerWidth);
154
+ const take = (want) => {
155
+ const got = Math.min(Math.max(0, want), budget);
156
+ budget -= got;
157
+ return got;
158
+ };
159
+ // Both edge columns or neither: `Box.getTop` and its siblings take a single
160
+ // flag for the pair, so half a frame is not a shape this renderer can emit.
161
+ const edge = budget >= frame.edge * 2 ? frame.edge : 0;
162
+ 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.
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)));
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
+ 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]),
207
+ weight: demand.weight,
208
+ })));
209
+ const wanted = needed.map((width, index) => width + beyond[index]);
210
+ // The unspent part goes first to the columns with nothing to size to, each
211
+ // alike, up to its `fill` — the reference's `Measurement(1, max_width)`, a
212
+ // maximum of the whole offer that no content measured.
213
+ const afterWant = holding(wanted);
214
+ const filled = distribute(budget - spent(wanted), seatedDemands.map((demand, index) => ({
215
+ want: Math.max(0, demand.fill - afterWant[index]),
216
+ weight: demand.fill > 0 ? 1 : 0,
217
+ })));
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
+ });
242
+ // 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.
251
+ const held = holding(wanted, filled, shared);
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));
268
+ return {
269
+ edge,
270
+ divider: frame.divider,
271
+ padLeft,
272
+ padRight,
273
+ columns,
274
+ cellWidths,
275
+ totalWidth: edge * 2 +
276
+ Math.max(0, seated - 1) * frame.divider +
277
+ cellWidths.reduce((sum, width) => sum + width, 0),
278
+ };
279
+ }
280
+ /**
281
+ * The share of the leftover width a column's `ratio` claims: the ratio when it
282
+ * is positive, else 0 — a zero, negative or NaN ratio claims none, and the
283
+ * column sizes to its content. A proportion rather than a count of cells, so it
284
+ * is never floored: `ratio: 0.5` beside `ratio: 1` takes a third, where flooring
285
+ * it to 0 once starved it to a single `…`. Finite, for the reason `demandCells`
286
+ * is.
287
+ *
288
+ * [LAW:one-source-of-truth] `Column.flexible` is this asked as a yes/no, so the
289
+ * width division and the public flag cannot disagree about which columns are
290
+ * elastic.
291
+ */
292
+ const columnShare = (col) => col.ratio !== undefined && col.ratio > 0 ? Math.min(col.ratio, UNBOUNDED) : 0;
293
+ export class Column {
294
+ _header;
295
+ _footer;
296
+ headerStyle;
297
+ footerStyle;
298
+ style;
299
+ justify;
300
+ width;
301
+ minWidth;
302
+ maxWidth;
303
+ ratio;
304
+ noWrap;
305
+ overflow;
306
+ highlight;
307
+ _cells;
308
+ constructor(options) {
309
+ this.header = options?.header;
310
+ this.footer = options?.footer;
311
+ this.headerStyle = options?.headerStyle ?? NULL_STYLE;
312
+ this.footerStyle = options?.footerStyle ?? NULL_STYLE;
313
+ this.style = options?.style ?? NULL_STYLE;
314
+ this.justify = options?.justify ?? "left";
315
+ this.width = options?.width;
316
+ this.minWidth = options?.minWidth;
317
+ this.maxWidth = options?.maxWidth;
318
+ this.ratio = options?.ratio;
319
+ this.noWrap = options?.noWrap ?? false;
320
+ this.overflow = options?.overflow ?? "ellipsis";
321
+ this.highlight = options?.highlight ?? false;
322
+ this._cells = [];
323
+ }
324
+ /**
325
+ * The two stamped cells, stamped on assignment rather than at the constructor.
326
+ * `Table.columns` hands out the live column and both fields are public, so a
327
+ * constructor-only stamp held only until the first `columns[0].footer = mine`
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
330
+ * is the border, so the guarantee holds for the object's whole lifetime and
331
+ * the constructor is one caller of it rather than the one place it is true.
332
+ *
333
+ * Absent and empty are the same header, and the same footer.
334
+ * [LAW:types-are-the-program] Rich declares `footer: RenderableType = ""`, so
335
+ * a column always has one and `show_footer` alone decides whether it is
336
+ * drawn. Modelling the absence as `undefined` instead made "no column has a
337
+ * footer" a state the render path could ask about — and it did, skipping the
338
+ * row a caller had asked for. `embed` already maps nothing onto empty,
339
+ * which is why the setters take `undefined` rather than defaulting around it.
340
+ */
341
+ get header() {
342
+ return this._header;
343
+ }
344
+ set header(content) {
345
+ this._header = embed(content);
346
+ }
347
+ get footer() {
348
+ return this._footer;
349
+ }
350
+ set footer(content) {
351
+ this._footer = embed(content);
352
+ }
353
+ get flexible() {
354
+ return columnShare(this) > 0;
355
+ }
356
+ /** @internal */
357
+ addCell(cell) {
358
+ this._cells.push(cell);
359
+ }
360
+ /** @internal */
361
+ getCells() {
362
+ return this._cells;
363
+ }
364
+ copy() {
365
+ const col = new Column({
366
+ justify: this.justify,
367
+ width: this.width,
368
+ minWidth: this.minWidth,
369
+ maxWidth: this.maxWidth,
370
+ ratio: this.ratio,
371
+ noWrap: this.noWrap,
372
+ overflow: this.overflow,
373
+ highlight: this.highlight,
374
+ });
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
+ }
384
+ }
385
+ export class Table {
386
+ _columns;
387
+ _rows;
388
+ box;
389
+ _title;
390
+ _caption;
391
+ expand;
392
+ showHeader;
393
+ showFooter;
394
+ showLines;
395
+ showEdge;
396
+ padding;
397
+ collapsePadding;
398
+ padEdge;
399
+ style;
400
+ headerStyle;
401
+ footerStyle;
402
+ borderStyle;
403
+ titleStyle;
404
+ captionStyle;
405
+ titleJustify;
406
+ captionJustify;
407
+ tableWidth;
408
+ minWidth;
409
+ rowStyles;
410
+ highlight;
411
+ get title() {
412
+ return this._title;
413
+ }
414
+ get caption() {
415
+ return this._caption;
416
+ }
417
+ constructor(options) {
418
+ this._columns = [];
419
+ this._rows = [];
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.
423
+ const titleVal = options?.title;
424
+ this._title = present(titleVal) ? new EmbeddedText(titleVal) : undefined;
425
+ const captionVal = options?.caption;
426
+ this._caption = present(captionVal) ? new EmbeddedText(captionVal) : undefined;
427
+ // A declared width is the table's size, not a ceiling on it: the
428
+ // reference's `expand` is `self._expand or self.width is not None`, and a
429
+ // port that sized to content inside it drew Rich code narrower than Rich.
430
+ this.expand = (options?.expand ?? false) || options?.width !== undefined;
431
+ this.showHeader = options?.showHeader !== false;
432
+ this.showFooter = options?.showFooter ?? false;
433
+ this.showLines = options?.showLines ?? false;
434
+ this.showEdge = options?.showEdge !== false;
435
+ this.padding = normalizePadding(options?.padding ?? [0, 1, 0, 1]);
436
+ this.collapsePadding = options?.collapsePadding ?? false;
437
+ this.padEdge = options?.padEdge ?? true;
438
+ this.style = options?.style ?? NULL_STYLE;
439
+ this.headerStyle = options?.headerStyle ?? "table.header";
440
+ this.footerStyle = options?.footerStyle ?? "table.footer";
441
+ this.borderStyle = options?.borderStyle ?? NULL_STYLE;
442
+ this.titleStyle = options?.titleStyle ?? "table.title";
443
+ this.captionStyle = options?.captionStyle ?? "table.caption";
444
+ this.titleJustify = options?.titleJustify ?? "center";
445
+ this.captionJustify = options?.captionJustify ?? "center";
446
+ this.tableWidth = options?.width;
447
+ this.minWidth = options?.minWidth;
448
+ this.rowStyles = options?.rowStyles ?? [];
449
+ this.highlight = options?.highlight ?? false;
450
+ }
451
+ get columns() {
452
+ return this._columns;
453
+ }
454
+ get rowCount() {
455
+ return this._rows.length;
456
+ }
457
+ addColumn(header, options) {
458
+ const col = new Column({ ...options, header: header ?? options?.header, highlight: options?.highlight ?? this.highlight });
459
+ this._columns.push(col);
460
+ return this;
461
+ }
462
+ addRow(...cells) {
463
+ // If last arg is an options object with endSection, extract it
464
+ let endSection = false;
465
+ const lastArg = cells[cells.length - 1];
466
+ if (typeof lastArg === "object" &&
467
+ lastArg !== null &&
468
+ !(lastArg instanceof RichText) &&
469
+ !("render" in lastArg) &&
470
+ "endSection" in lastArg) {
471
+ endSection = lastArg.endSection;
472
+ cells = cells.slice(0, -1);
473
+ }
474
+ // Stamped once, here at the border, so sizing and drawing read one resolved
475
+ // cell instead of each converting the raw value for itself. Two converters
476
+ // is two answers to "what is this cell": they disagreed on a `Panel`, which
477
+ // stringifies to `[object Object]` — a string the tag pattern swallows
478
+ // whole, sizing the column to nothing. [LAW:parse-dont-validate]
479
+ //
480
+ // Ahead of the column loop: a cell that throws must leave no phantom column
481
+ // behind. [LAW:no-ambient-temporal-coupling]
482
+ const resolved = cells.map(embed);
483
+ // Auto-create columns if needed
484
+ while (this._columns.length < resolved.length) {
485
+ this.addColumn();
486
+ }
487
+ this._rows.push({ cells: resolved, endSection });
488
+ return this;
489
+ }
490
+ addSection() {
491
+ if (this._rows.length > 0) {
492
+ this._rows[this._rows.length - 1].endSection = true;
493
+ }
494
+ return this;
495
+ }
496
+ *render(rawOptions) {
497
+ if (this._columns.length === 0) {
498
+ yield Segment.line();
499
+ return;
500
+ }
501
+ const options = withBoundedWidth(rawOptions, this);
502
+ // The two swaps a box goes through before anything is drawn with it, in the
503
+ // reference's order: what the platform can render, then what a table
504
+ // lacking a header should. [LAW:dataflow-not-control-flow] Both run every
505
+ // render and each returns the receiver when it has nothing to change, so
506
+ // the flags arrive as values rather than as branches around a step.
507
+ const frame = this.box?.substitute(options);
508
+ const box = (this.showHeader ? frame : frame?.plainHeaded()) ?? null;
509
+ // The table's own `style` is the base its border style layers over, and
510
+ // that is all it styles: the reference reads it as `table_style` for the
511
+ // frame and nowhere else, so a cell is untouched by it.
512
+ const borderStyle = getStyle(options, this.style).add(getStyle(options, this.borderStyle));
513
+ const border = borderStyle.isNull ? undefined : borderStyle;
514
+ // The one division of the width every row below is measured against.
515
+ const outerWidth = this._outerWidth(options);
516
+ const geometry = this._geometry(options, outerWidth);
517
+ const edge = geometry.edge === 1;
518
+ // Title
519
+ if (this._title) {
520
+ yield* this._renderTitle(options, this._title, geometry.totalWidth, this.titleStyle, this.titleJustify);
521
+ }
522
+ // Top border
523
+ if (box && this.showEdge) {
524
+ yield* box.getTop(geometry.cellWidths, border, edge);
525
+ }
526
+ // Every style a cell is drawn with, resolved once per render rather than
527
+ // once per cell, as the reference's `_get_cells` does per column.
528
+ const headerStyle = getStyle(options, this.headerStyle);
529
+ const footerStyle = getStyle(options, this.footerStyle);
530
+ const columnStyles = this._columns.map((c) => getStyle(options, c.style));
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);
537
+ // Header row
538
+ 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)));
541
+ yield* this._renderRow(options, headerCells, headerStyles, NULL_STYLE, geometry, rowPadding(0), box, "head", border);
542
+ // Header separator
543
+ if (box) {
544
+ yield* box.getRow(geometry.cellWidths, "head", border, edge);
545
+ }
546
+ }
547
+ // 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));
551
+ const rowStyle = stripes[rowIdx % stripes.length] ?? NULL_STYLE;
552
+ const cellStyles = columnStyles.map((s) => s.add(rowStyle));
553
+ yield* this._renderRow(options, rowCells, cellStyles, rowStyle, geometry, rowPadding(firstBody + rowIdx), box, "row", border);
554
+ // Row separator
555
+ const showSep = this.showLines || row.endSection;
556
+ if (showSep && box && rowIdx < this._rows.length - 1) {
557
+ yield* box.getRow(geometry.cellWidths, "row", border, edge);
558
+ }
559
+ }
560
+ // Footer
561
+ if (this.showFooter) {
562
+ if (box) {
563
+ yield* box.getRow(geometry.cellWidths, "foot", border, edge);
564
+ }
565
+ const footerCells = this._columns.map((c) => c.footer);
566
+ const footerStyles = this._columns.map((c) => footerStyle.add(getStyle(options, c.footerStyle)));
567
+ yield* this._renderRow(options, footerCells, footerStyles, NULL_STYLE, geometry, rowPadding(lastRow), box, "foot", border);
568
+ }
569
+ // Bottom border
570
+ if (box && this.showEdge) {
571
+ yield* box.getBottom(geometry.cellWidths, border, edge);
572
+ }
573
+ // Caption
574
+ if (this._caption) {
575
+ yield* this._renderTitle(options, this._caption, geometry.totalWidth, this.captionStyle, this.captionJustify);
576
+ }
577
+ }
578
+ /**
579
+ * [LAW:one-source-of-truth] Both ends of the range are widths the geometry
580
+ * actually produced — the maximum from the demands as they stand, the
581
+ * minimum from the same layout with every column asking for its minimum.
582
+ * Neither end stretches or fills into an offer: both only spend cells an
583
+ * offer happens to leave over, and a renderable reports the width its content
584
+ * wants rather than the width it was offered. A declared `width` is not an
585
+ * offer, and the maximum reports it. Letting either in made the table measure
586
+ * `Infinity` against an unbounded offer, where `withBoundedWidth` needs a
587
+ * natural width to fall back on — an expanding table, and an empty
588
+ * `Table.grid()`.
589
+ * Neither can exceed the width offered and the tighter request cannot exceed
590
+ * the looser one, so the range cannot invert. Deriving the minimum from raw
591
+ * column and padding counts instead is what used to return
592
+ * `{minimum: 6, maximum: 1}` at `maxWidth: 1` — a floor above its own ceiling.
593
+ */
594
+ measure(rawOptions) {
595
+ const options = withCellWidth(rawOptions);
596
+ const outerWidth = this._outerWidth(options);
597
+ const frame = this._frame();
598
+ const demands = this._columnDemands(options, outerWidth);
599
+ // A declared width is the one stretch a measurement reports, because it is
600
+ // a size the table was given rather than an offer it grew into — the
601
+ // reference measures `self.width` as its maximum, so a `Panel` fitted round
602
+ // a table declared at 40 is 44 wide there, not the table's content width.
603
+ // It is laid out exactly as `render` lays it out, so the two cannot report
604
+ // different widths; a declared width with nothing bounding it
605
+ // (`width: Infinity` at an unbounded offer) has no size to report and falls
606
+ // back to the content, as an unbounded `expand` does.
607
+ const declared = this.tableWidth === undefined
608
+ ? undefined
609
+ : layoutTable(outerWidth, demands, frame).totalWidth;
610
+ const laidOut = declared !== undefined && declared < UNBOUNDED
611
+ ? declared
612
+ : layoutTable(outerWidth, demands.map((demand) => ({ ...demand, fill: 0, stretch: false })), frame).totalWidth;
613
+ // `UNBOUNDED` is this table's own infinity, so a layout that reached it has
614
+ // no natural width to report — a column asked for every cell there is. Said
615
+ // as the number, it escapes as a width a caller would try to draw:
616
+ // `minWidth: Infinity` measured 18014398509481988 and rendered
617
+ // `RangeError: Invalid string length` out of the top border.
618
+ const maximum = laidOut >= UNBOUNDED ? Infinity : laidOut;
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;
625
+ return {
626
+ minimum: Math.min(maximum, Math.max(tightest, cells(this.minWidth ?? 0))),
627
+ maximum,
628
+ };
629
+ }
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
+ */
637
+ static grid(options) {
638
+ return new Table({
639
+ ...options,
640
+ box: null,
641
+ showHeader: false,
642
+ showEdge: false,
643
+ padding: options?.padding ?? [0, 1, 0, 0],
644
+ collapsePadding: options?.collapsePadding ?? true,
645
+ padEdge: options?.padEdge ?? false,
646
+ });
647
+ }
648
+ // --- Private ---
649
+ _frame() {
650
+ return {
651
+ divider: this.box ? 1 : 0,
652
+ edge: this.box && this.showEdge ? 1 : 0,
653
+ };
654
+ }
655
+ /**
656
+ * The width this table lays itself out against: what it was told to be, as a
657
+ * count of cells, never wider than what it was offered.
658
+ *
659
+ * [LAW:one-source-of-truth] `render` and `measure` divide the same two fields
660
+ * and once did it in two places. `measure` bounded the declared width by the
661
+ * offer and `render` preferred it outright, walking past the value
662
+ * `withBoundedWidth` had just resolved — so `new Table({width: Infinity})`
663
+ * with a `{ratio: 1}` column granted that column `UNBOUNDED` cells and threw
664
+ * `RangeError: Invalid string length` out of the top border, at a perfectly
665
+ * ordinary 80-cell offer.
666
+ */
667
+ _outerWidth(options) {
668
+ return Math.min(this.tableWidth === undefined ? options.maxWidth : cells(this.tableWidth), options.maxWidth);
669
+ }
670
+ _geometry(options, outerWidth) {
671
+ return layoutTable(outerWidth, this._columnDemands(options, outerWidth), this._frame());
672
+ }
673
+ /**
674
+ * [LAW:dataflow-not-control-flow] The three ways a column can be sized —
675
+ * declared width, ratio, natural content — differ only in the demand they
676
+ * produce, and `expand` only in its `stretch`. They are resolved once, here, into uniform data, so
677
+ * `layoutTable` runs the same apportionment for every table and no sizing
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.
683
+ */
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
+ };
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 };
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));
713
+ return {
714
+ reserved: 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,
734
+ ratio: 0,
735
+ stretch: false,
736
+ wrapable: false,
737
+ };
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];
795
+ }
796
+ /**
797
+ * Every cell column `index` draws, in draw order.
798
+ *
799
+ * [LAW:one-source-of-truth] The reference's `_get_cells` is the one answer to
800
+ * "which cells belong to this column", and both `_measure_column` and
801
+ * `_render` read it. Enumerating that set a second time inside the width path
802
+ * is how the two ends drifted: the header was measured whether or not
803
+ * `showHeader` drew it, and `col.footer` was never measured at all, so a
804
+ * footer wider than its column — a totals row, exactly — was cut to `…`.
805
+ * A flag is the whole membership rule; nothing here asks after content.
806
+ */
807
+ *_columnCells(col, index) {
808
+ if (this.showHeader)
809
+ yield col.header;
810
+ for (const row of this._rows)
811
+ yield row.cells[index] ?? new EmbeddedText(undefined);
812
+ if (this.showFooter)
813
+ yield col.footer;
814
+ }
815
+ /**
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]
821
+ */
822
+ _cellRange(col, index, options) {
823
+ let range;
824
+ for (const cell of this._columnCells(col, index)) {
825
+ // The stamped cell, so the width a column asks for is the width its text
826
+ // will occupy — measuring the raw value sized this column to
827
+ // `[red]Solo[/red]`, fifteen cells for four cells of text.
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
+ };
836
+ }
837
+ return range;
838
+ }
839
+ /**
840
+ * A natural width held to the column's own `minWidth`/`maxWidth`.
841
+ *
842
+ * A bound is a count of cells, read by the rule every width is read by: NaN
843
+ * 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
845
+ * NaN instead, and the column vanishes. [LAW:one-source-of-truth]
846
+ */
847
+ _bounded(col, natural) {
848
+ return Math.min(Math.max(natural, cells(col.minWidth ?? 0)), cells(col.maxWidth ?? Infinity));
849
+ }
850
+ *_renderRow(options, cells, styles, rowStyle, geometry, [top, bottom], box, level, border) {
851
+ const { padLeft, padRight, columns } = geometry;
852
+ // [LAW:dataflow-not-control-flow] Header, body and footer share this path;
853
+ // the level crosses as a value the box answers with glyphs, not a branch.
854
+ const frame = box?.getContentChars(level);
855
+ // A divider drawn in blanks is part of the row's stripe, so it carries the
856
+ // row's background under the border's own style; one drawn in ink is the
857
+ // frame's alone. Header and footer rows have no row style, as in the
858
+ // reference, so their dividers are the border's either way.
859
+ const divider = frame && (frame.vertical.trim() === ""
860
+ ? new Segment(frame.vertical, rowStyle.backgroundStyle.add(border))
861
+ : 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
864
+ // 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("");
869
+ // The render's own options with the column's canvas laid over them, so a
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`.
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
+ ];
895
+ });
896
+ const maxLines = cellLines.reduce((most, lines) => Math.max(most, lines.length), 1);
897
+ // How many blank lines stand above each cell's content. A header sits on
898
+ // the row's floor, so its column labels line up over the data, and every
899
+ // other row starts at the top — the reference forces both.
900
+ const drop = cellLines.map((lines) => (level === "head" ? maxLines - lines.length : 0));
901
+ for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
902
+ if (frame && geometry.edge === 1) {
903
+ yield new Segment(frame.left, border);
904
+ }
905
+ for (let colIdx = 0; colIdx < columns.length; colIdx++) {
906
+ if (colIdx > 0 && divider) {
907
+ yield divider;
908
+ }
909
+ const style = styles[colIdx];
910
+ // A cell with fewer lines than the row contributes blanks, so every
911
+ // column spans the same number of rows and the frame stays rectangular.
912
+ const line = cellLines[colIdx][lineIdx - drop[colIdx]] ?? blank(paddedWidths[colIdx]);
913
+ // The cell's style is the base its content's own spans layer over, and
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);
918
+ }
919
+ if (frame && geometry.edge === 1) {
920
+ yield new Segment(frame.right, border);
921
+ }
922
+ yield Segment.line();
923
+ }
924
+ }
925
+ *_renderTitle(options, text, tableWidth, ownStyle, justify) {
926
+ // The table owns the canvas; the caller's text still says how it meets the
927
+ // edge. A `RichText`'s own `justify` and `overflow` outrank the options
928
+ // `render` is handed, so `titleJustify` would lose to a property the caller
929
+ // may not know it set, and an `"ignore"` title would have no edge and run
930
+ // straight through the frame. Every other method, and `noWrap`, cuts within
931
+ // the bound, so those stay theirs. `text` hands back a copy to clear them
932
+ // on. [LAW:one-source-of-truth]
933
+ //
934
+ // Never highlighted, and markup as the console says: Rich draws a title
935
+ // and caption through `render_str(highlight=False)`.
936
+ //
937
+ // The table's title style is the *base* a string title's own spans layer
938
+ // over, as `render_str(style=)` sets it: a `[red]` title inside an italic
939
+ // table title arrives as italic-red, and the line breaks and the `end` the
940
+ // text emits stay unstyled. A `RichText` title takes none of it, because
941
+ // the reference renders a `Text` title without the title style
942
+ // (rich-table-qj6i).
943
+ const source = text.text({ ...options, highlight: false }, getStyle(options, ownStyle));
944
+ source.justify = undefined;
945
+ if (source.overflow === "ignore")
946
+ source.overflow = undefined;
947
+ //
948
+ // Rendered at the table's own width with nothing suppressed, because that
949
+ // is what the reference hands its title: an annotation too wide for the
950
+ // frame wraps down it rather than being cut off at the corner. `noWrap` and
951
+ // an explicit `crop` stood here and did the cutting (rich-table-6uy.7).
952
+ //
953
+ // [LAW:single-enforcer] The alignment is the renderable's to perform, not
954
+ // just to be told. Padding the lines here as well needed the same
955
+ // rules — that a wrap's trailing whitespace is not content to centre
956
+ // around, that `left` fills the canvas and an unset justify does not — and
957
+ // a second copy of those is a second answer waiting to disagree.
958
+ //
959
+ // Yielded as the text emits it, every line and its own `end` included, as
960
+ // the reference's `console.render` yields it: a title of "one\ntwo" is two
961
+ // lines, and `end="!!"` runs on into the top border there too. Splitting
962
+ // and re-ending each line moved a non-newline `end` onto the title's own
963
+ // row, wider than the table.
964
+ yield* source.render({
965
+ ...options,
966
+ maxWidth: tableWidth,
967
+ justify,
968
+ overflow: undefined,
969
+ noWrap: false,
970
+ height: undefined,
971
+ });
972
+ }
973
+ }