@promptctl/rich-js 0.18.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/dist/core/anchor.d.ts +45 -0
  2. package/dist/core/anchor.js +39 -0
  3. package/dist/core/ansi.d.ts +65 -0
  4. package/dist/core/ansi.js +231 -0
  5. package/dist/core/box.d.ts +143 -0
  6. package/dist/core/box.js +379 -0
  7. package/dist/core/cells.d.ts +159 -0
  8. package/dist/core/cells.js +287 -0
  9. package/dist/core/color.d.ts +246 -0
  10. package/dist/core/color.js +1038 -0
  11. package/dist/core/console.d.ts +165 -0
  12. package/dist/core/console.js +527 -0
  13. package/dist/core/emoji.d.ts +24 -0
  14. package/dist/core/emoji.js +391 -0
  15. package/dist/core/export-html.d.ts +59 -0
  16. package/dist/core/export-html.js +200 -0
  17. package/dist/core/export-lines.d.ts +121 -0
  18. package/dist/core/export-lines.js +141 -0
  19. package/dist/core/highlighter.d.ts +33 -0
  20. package/dist/core/highlighter.js +124 -0
  21. package/dist/core/json.d.ts +27 -0
  22. package/dist/core/json.js +53 -0
  23. package/dist/core/markup.d.ts +119 -0
  24. package/dist/core/markup.js +731 -0
  25. package/dist/core/measure.d.ts +27 -0
  26. package/dist/core/measure.js +62 -0
  27. package/dist/core/oklch.d.ts +161 -0
  28. package/dist/core/oklch.js +312 -0
  29. package/dist/core/osc8.d.ts +68 -0
  30. package/dist/core/osc8.js +90 -0
  31. package/dist/core/place.d.ts +30 -0
  32. package/dist/core/place.js +51 -0
  33. package/dist/core/pretty.d.ts +144 -0
  34. package/dist/core/pretty.js +476 -0
  35. package/dist/core/protocol.d.ts +267 -0
  36. package/dist/core/protocol.js +163 -0
  37. package/dist/core/render.d.ts +84 -0
  38. package/dist/core/render.js +145 -0
  39. package/dist/core/segment.d.ts +161 -0
  40. package/dist/core/segment.js +492 -0
  41. package/dist/core/spinnerData.d.ts +12 -0
  42. package/dist/core/spinnerData.js +117 -0
  43. package/dist/core/strip.d.ts +141 -0
  44. package/dist/core/strip.js +305 -0
  45. package/dist/core/style.d.ts +169 -0
  46. package/dist/core/style.js +695 -0
  47. package/dist/core/subscription.d.ts +12 -0
  48. package/dist/core/subscription.js +12 -0
  49. package/dist/core/text.d.ts +256 -0
  50. package/dist/core/text.js +976 -0
  51. package/dist/core/wrap.d.ts +41 -0
  52. package/dist/core/wrap.js +127 -0
  53. package/dist/host/app.d.ts +121 -0
  54. package/dist/host/app.js +255 -0
  55. package/dist/host/host-environment.d.ts +26 -0
  56. package/dist/host/host-environment.js +44 -0
  57. package/dist/host/index.d.ts +5 -0
  58. package/dist/host/index.js +3 -0
  59. package/dist/host/terminal-host.d.ts +179 -0
  60. package/dist/host/terminal-host.js +186 -0
  61. package/dist/index.d.ts +91 -0
  62. package/dist/index.js +107 -0
  63. package/dist/node/prompt.d.ts +21 -0
  64. package/dist/node/prompt.js +30 -0
  65. package/dist/node/save.d.ts +17 -0
  66. package/dist/node/save.js +21 -0
  67. package/dist/node/terminal-host.d.ts +113 -0
  68. package/dist/node/terminal-host.js +227 -0
  69. package/dist/node/traceback.d.ts +37 -0
  70. package/dist/node/traceback.js +134 -0
  71. package/dist/renderables/align.d.ts +16 -0
  72. package/dist/renderables/align.js +33 -0
  73. package/dist/renderables/columns.d.ts +61 -0
  74. package/dist/renderables/columns.js +152 -0
  75. package/dist/renderables/constrain.d.ts +15 -0
  76. package/dist/renderables/constrain.js +37 -0
  77. package/dist/renderables/embed.d.ts +50 -0
  78. package/dist/renderables/embed.js +71 -0
  79. package/dist/renderables/flexStrip.d.ts +41 -0
  80. package/dist/renderables/flexStrip.js +200 -0
  81. package/dist/renderables/group.d.ts +11 -0
  82. package/dist/renderables/group.js +17 -0
  83. package/dist/renderables/layout.d.ts +92 -0
  84. package/dist/renderables/layout.js +312 -0
  85. package/dist/renderables/live.d.ts +50 -0
  86. package/dist/renderables/live.js +175 -0
  87. package/dist/renderables/markdown.d.ts +24 -0
  88. package/dist/renderables/markdown.js +184 -0
  89. package/dist/renderables/padding.d.ts +38 -0
  90. package/dist/renderables/padding.js +141 -0
  91. package/dist/renderables/panel.d.ts +116 -0
  92. package/dist/renderables/panel.js +307 -0
  93. package/dist/renderables/progress.d.ts +90 -0
  94. package/dist/renderables/progress.js +238 -0
  95. package/dist/renderables/progressBar.d.ts +31 -0
  96. package/dist/renderables/progressBar.js +53 -0
  97. package/dist/renderables/prompt.d.ts +49 -0
  98. package/dist/renderables/prompt.js +125 -0
  99. package/dist/renderables/rule.d.ts +25 -0
  100. package/dist/renderables/rule.js +90 -0
  101. package/dist/renderables/spinner.d.ts +31 -0
  102. package/dist/renderables/spinner.js +70 -0
  103. package/dist/renderables/status.d.ts +23 -0
  104. package/dist/renderables/status.js +60 -0
  105. package/dist/renderables/syntax.d.ts +35 -0
  106. package/dist/renderables/syntax.js +115 -0
  107. package/dist/renderables/table.d.ts +194 -0
  108. package/dist/renderables/table.js +795 -0
  109. package/dist/renderables/traceback.d.ts +31 -0
  110. package/dist/renderables/traceback.js +108 -0
  111. package/dist/renderables/tree.d.ts +68 -0
  112. package/dist/renderables/tree.js +150 -0
  113. package/dist/renderables/viewport.d.ts +114 -0
  114. package/dist/renderables/viewport.js +198 -0
  115. package/dist/template-bindings/color-funcs.d.ts +87 -0
  116. package/dist/template-bindings/color-funcs.js +217 -0
  117. package/dist/template-bindings/helpers.d.ts +24 -0
  118. package/dist/template-bindings/helpers.js +48 -0
  119. package/dist/template-bindings/index.d.ts +107 -0
  120. package/dist/template-bindings/index.js +146 -0
  121. package/dist/template-bindings/palette-funcs.d.ts +94 -0
  122. package/dist/template-bindings/palette-funcs.js +141 -0
  123. package/dist/template-bindings/style-funcs.d.ts +28 -0
  124. package/dist/template-bindings/style-funcs.js +155 -0
  125. package/dist/themes/buildPalette.d.ts +36 -0
  126. package/dist/themes/buildPalette.js +56 -0
  127. package/dist/themes/colorMath.d.ts +82 -0
  128. package/dist/themes/colorMath.js +254 -0
  129. package/dist/themes/colorRef.d.ts +51 -0
  130. package/dist/themes/colorRef.js +96 -0
  131. package/dist/themes/data/ansi-tables.d.ts +14 -0
  132. package/dist/themes/data/ansi-tables.js +49 -0
  133. package/dist/themes/data/atom-one-dark.d.ts +3 -0
  134. package/dist/themes/data/atom-one-dark.js +177 -0
  135. package/dist/themes/data/atom-one-light.d.ts +3 -0
  136. package/dist/themes/data/atom-one-light.js +177 -0
  137. package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
  138. package/dist/themes/data/catppuccin-frappe.js +177 -0
  139. package/dist/themes/data/catppuccin-latte.d.ts +3 -0
  140. package/dist/themes/data/catppuccin-latte.js +177 -0
  141. package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
  142. package/dist/themes/data/catppuccin-macchiato.js +177 -0
  143. package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
  144. package/dist/themes/data/catppuccin-mocha.js +177 -0
  145. package/dist/themes/data/cyberpunk.d.ts +17 -0
  146. package/dist/themes/data/cyberpunk.js +193 -0
  147. package/dist/themes/data/default.d.ts +3 -0
  148. package/dist/themes/data/default.js +160 -0
  149. package/dist/themes/data/dracula.d.ts +3 -0
  150. package/dist/themes/data/dracula.js +177 -0
  151. package/dist/themes/data/flexoki.d.ts +3 -0
  152. package/dist/themes/data/flexoki.js +177 -0
  153. package/dist/themes/data/gruvbox.d.ts +3 -0
  154. package/dist/themes/data/gruvbox.js +177 -0
  155. package/dist/themes/data/index.d.ts +35 -0
  156. package/dist/themes/data/index.js +64 -0
  157. package/dist/themes/data/monokai.d.ts +3 -0
  158. package/dist/themes/data/monokai.js +177 -0
  159. package/dist/themes/data/nord.d.ts +3 -0
  160. package/dist/themes/data/nord.js +177 -0
  161. package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
  162. package/dist/themes/data/rose-pine-dawn.js +177 -0
  163. package/dist/themes/data/rose-pine-moon.d.ts +3 -0
  164. package/dist/themes/data/rose-pine-moon.js +177 -0
  165. package/dist/themes/data/rose-pine.d.ts +3 -0
  166. package/dist/themes/data/rose-pine.js +177 -0
  167. package/dist/themes/data/solarized-dark.d.ts +3 -0
  168. package/dist/themes/data/solarized-dark.js +177 -0
  169. package/dist/themes/data/solarized-light.d.ts +3 -0
  170. package/dist/themes/data/solarized-light.js +177 -0
  171. package/dist/themes/data/svg-export.d.ts +3 -0
  172. package/dist/themes/data/svg-export.js +177 -0
  173. package/dist/themes/data/textual-ansi.d.ts +3 -0
  174. package/dist/themes/data/textual-ansi.js +160 -0
  175. package/dist/themes/data/textual-dark.d.ts +3 -0
  176. package/dist/themes/data/textual-dark.js +160 -0
  177. package/dist/themes/data/textual-light.d.ts +3 -0
  178. package/dist/themes/data/textual-light.js +160 -0
  179. package/dist/themes/data/tokyo-night.d.ts +3 -0
  180. package/dist/themes/data/tokyo-night.js +177 -0
  181. package/dist/themes/data/types.d.ts +31 -0
  182. package/dist/themes/data/types.js +10 -0
  183. package/dist/themes/palette.d.ts +21 -0
  184. package/dist/themes/palette.js +28 -0
  185. package/dist/themes/ramp.d.ts +83 -0
  186. package/dist/themes/ramp.js +122 -0
  187. package/dist/themes/registry.d.ts +44 -0
  188. package/dist/themes/registry.js +94 -0
  189. package/dist/themes/terminalThemes.d.ts +49 -0
  190. package/dist/themes/terminalThemes.js +63 -0
  191. package/dist/themes/transpose.d.ts +87 -0
  192. package/dist/themes/transpose.js +138 -0
  193. package/dist/widgets/button.d.ts +49 -0
  194. package/dist/widgets/button.js +208 -0
  195. package/dist/widgets/checkbox.d.ts +41 -0
  196. package/dist/widgets/checkbox.js +157 -0
  197. package/dist/widgets/dropdown.d.ts +67 -0
  198. package/dist/widgets/dropdown.js +450 -0
  199. package/dist/widgets/event-router.d.ts +71 -0
  200. package/dist/widgets/event-router.js +629 -0
  201. package/dist/widgets/focus-manager.d.ts +45 -0
  202. package/dist/widgets/focus-manager.js +191 -0
  203. package/dist/widgets/hit.d.ts +64 -0
  204. package/dist/widgets/hit.js +113 -0
  205. package/dist/widgets/index.d.ts +20 -0
  206. package/dist/widgets/index.js +14 -0
  207. package/dist/widgets/slider.d.ts +57 -0
  208. package/dist/widgets/slider.js +270 -0
  209. package/dist/widgets/static-item.d.ts +34 -0
  210. package/dist/widgets/static-item.js +54 -0
  211. package/dist/widgets/text-input.d.ts +274 -0
  212. package/dist/widgets/text-input.js +1165 -0
  213. package/dist/widgets/toggle.d.ts +44 -0
  214. package/dist/widgets/toggle.js +180 -0
  215. package/dist/widgets/types.d.ts +87 -0
  216. package/dist/widgets/types.js +30 -0
  217. package/dist/widgets/widget-app.d.ts +63 -0
  218. package/dist/widgets/widget-app.js +199 -0
  219. package/dist/widgets/widget-base.d.ts +40 -0
  220. package/dist/widgets/widget-base.js +165 -0
  221. package/package.json +6 -2
@@ -0,0 +1,795 @@
1
+ /**
2
+ * Table — tabular data with headers, borders, auto-sizing, and alignment.
3
+ */
4
+ import { cellLen, 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 { embed, embeddedText } from "./embed.js";
10
+ import { normalizePadding } from "./padding.js";
11
+ import { getStyle, withBoundedWidth, withCellWidth } from "../core/protocol.js";
12
+ /** A `want` no budget can satisfy: the column takes every cell its weight earns. */
13
+ const UNBOUNDED = Number.MAX_SAFE_INTEGER;
14
+ /**
15
+ * A column's `want` or `weight`. Finite, because `UNBOUNDED` is this model's
16
+ * own infinity: a literal `Infinity` reaching `distribute` makes a column's
17
+ * weighted share `Infinity / Infinity`, which is NaN, and the NaN then skews
18
+ * every other elastic column in the table.
19
+ */
20
+ const demandCells = (n) => Math.min(cells(n), UNBOUNDED);
21
+ /** `n` cells of padding, as nothing at all when `n` is zero. */
22
+ const blank = (n) => (n > 0 ? [new Segment(" ".repeat(n))] : []);
23
+ /**
24
+ * Hand out `total` cells across `demands`, weighted, and never past a demand's
25
+ * `want`.
26
+ *
27
+ * A column whose proportional share would overshoot its cap is granted its
28
+ * whole want and dropped, and the cells it could not use are reopened to the
29
+ * columns still under their caps. That repeats until everyone still open fits
30
+ * within their share; one largest-remainder pass then places the cells the
31
+ * shares left as fractions, so the granted widths sum to `total` exactly — or
32
+ * to the point where every column is capped, which is how a table stays
33
+ * narrower than a width it was offered.
34
+ *
35
+ * Each round caps at least one column or is the last, so the work is bounded by
36
+ * the number of columns and never by the width. That bound is the point rather
37
+ * than an optimization: handing cells out one at a time made the iteration
38
+ * count the width itself, which stalled on a very wide table and — since
39
+ * `Infinity - 1 === Infinity` — never terminated at all for a `maxWidth` of
40
+ * `Infinity` held open by a ratio column. Capping by want reaches that case in
41
+ * one round.
42
+ *
43
+ * The arithmetic is exact. A share's fraction is what decides who gets a
44
+ * leftover cell, and in floating point two fractions that are equal — 6/14 and
45
+ * 6/14 for 16 cells over weights 3 and 10 — come out 0.4285714285714284 and
46
+ * 0.4285714285714288, so the tie went wherever the rounding error pointed.
47
+ * The weights are read as whole numbers in one proportion (`exactWeights`) and
48
+ * everything after is integer division.
49
+ */
50
+ function distribute(total, demands) {
51
+ const granted = demands.map(() => 0);
52
+ const bidders = demands.flatMap((demand, index) => demand.weight > 0 && demand.want > 0 ? [{ index, demand }] : []);
53
+ const weights = exactWeights(bidders.map(({ demand }) => demand.weight));
54
+ let open = bidders.map(({ index, demand }, slot) => ({
55
+ index,
56
+ want: BigInt(demand.want),
57
+ weight: weights[slot],
58
+ }));
59
+ const sum = (values) => values.reduce((acc, value) => acc + value, 0n);
60
+ // A budget past every open want grants each its want either way, and that
61
+ // bound is what keeps an unbounded offer — `Infinity` is a legal outer width
62
+ // — out of integer arithmetic that cannot hold it.
63
+ const wantSum = sum(open.map(({ want }) => want));
64
+ let remaining = total >= wantSum ? wantSum : BigInt(Math.max(0, total));
65
+ while (open.length > 0 && remaining > 0n) {
66
+ const weightSum = sum(open.map(({ weight }) => weight));
67
+ const capped = open.filter(({ want, weight }) => remaining * weight >= want * weightSum);
68
+ if (capped.length === 0)
69
+ break;
70
+ for (const { index, want } of capped) {
71
+ granted[index] = Number(want);
72
+ remaining -= want;
73
+ }
74
+ open = open.filter((bid) => !capped.includes(bid));
75
+ }
76
+ if (open.length > 0 && remaining > 0n) {
77
+ const weightSum = sum(open.map(({ weight }) => weight));
78
+ const products = open.map(({ weight }) => remaining * weight);
79
+ const whole = products.map((product) => product / weightSum);
80
+ const fraction = products.map((product) => product % weightSum);
81
+ let residue = remaining - sum(whole);
82
+ // The cells the shares left as fractions go to the largest fraction first,
83
+ // ties to the leftmost column, so the total lands exactly on `remaining`.
84
+ // `Number` of a nonzero difference is never 0, so the sign is exact.
85
+ const byFraction = open
86
+ .map((_, slot) => slot)
87
+ .sort((a, b) => Number(fraction[b] - fraction[a]) || a - b);
88
+ for (const slot of byFraction) {
89
+ if (residue <= 0n)
90
+ break;
91
+ whole[slot]++;
92
+ residue--;
93
+ }
94
+ open.forEach(({ index }, slot) => {
95
+ granted[index] = Number(whole[slot]);
96
+ });
97
+ }
98
+ return granted;
99
+ }
100
+ /**
101
+ * Weights as integers in one exact proportion. Each is read as the decimal it
102
+ * prints as — the number its caller wrote, `0.1` rather than the binary double
103
+ * nearest it — and all are scaled by the one power of ten that makes them
104
+ * whole, so `0.1 : 0.3 : 1` is exactly `1 : 3 : 10`. A weight that does not
105
+ * print as a decimal, `Infinity`, is refused by `BigInt` with a SyntaxError —
106
+ * the loud end of what `demandCells` exists to prevent.
107
+ */
108
+ function exactWeights(weights) {
109
+ const decimals = weights.map((weight) => {
110
+ const [digits = "", power = "0"] = String(weight).split("e");
111
+ const [whole = "", fraction = ""] = digits.split(".");
112
+ return { mantissa: BigInt(whole + fraction), exponent: fraction.length - Number(power) };
113
+ });
114
+ const common = Math.max(...decimals.map(({ exponent }) => exponent));
115
+ return decimals.map(({ mantissa, exponent }) => mantissa * 10n ** BigInt(common - exponent));
116
+ }
117
+ function layoutTable(outerWidth, demands, padding, frame) {
118
+ const [, padRightWanted, , padLeftWanted] = padding;
119
+ // Plain `number`, not the `CellCol` `cells` hands back: this budget is spent
120
+ // down by arithmetic, and arithmetic on a branded type produces a number.
121
+ let budget = cells(outerWidth);
122
+ const take = (want) => {
123
+ const got = Math.min(Math.max(0, want), budget);
124
+ budget -= got;
125
+ return got;
126
+ };
127
+ // Both edge columns or neither: `Box.getTop` and its siblings take a single
128
+ // flag for the pair, so half a frame is not a shape this renderer can emit.
129
+ const edge = budget >= frame.edge * 2 ? frame.edge : 0;
130
+ take(edge * 2);
131
+ // A first content cell per column, in column order, each paying for the
132
+ // divider that precedes it. The first column the budget cannot seat is where
133
+ // the table ends; the rest are dropped. A column that wants no cells is
134
+ // seated at zero — the queue exists to stop one column taking a second cell
135
+ // before another has its first, and a spacer never joins it.
136
+ const seats = [];
137
+ while (seats.length < demands.length) {
138
+ const seat = Math.min(1, demands[seats.length].want);
139
+ const cost = (seats.length > 0 ? frame.divider : 0) + seat;
140
+ if (budget < cost)
141
+ break;
142
+ take(cost);
143
+ seats.push(seat);
144
+ }
145
+ const seated = seats.length;
146
+ // Padding is uniform across columns or it is not padding, so it is bought
147
+ // for every seated column at once and skipped entirely when only some could
148
+ // afford it.
149
+ const takePerColumn = (want) => {
150
+ const per = seated === 0
151
+ ? 0
152
+ : Math.min(Math.max(0, want), Math.floor(budget / seated));
153
+ budget -= per * seated;
154
+ return per;
155
+ };
156
+ const padLeft = takePerColumn(padLeftWanted);
157
+ const padRight = takePerColumn(padRightWanted);
158
+ // Reservations are paid in column order, each already holding the cell the
159
+ // seating pass gave it. A budget too small to cover them all runs out
160
+ // partway, which costs the trailing columns their width but never costs the
161
+ // table its frame.
162
+ const seatedDemands = demands.slice(0, seated);
163
+ const reserved = seatedDemands.map((demand, index) => take(demand.reserved - seats[index]));
164
+ // What is left to apportion is the rest of what each column wanted. A table
165
+ // whose columns all fit leaves this budget partly unspent.
166
+ const wanted = distribute(budget, seatedDemands.map((demand, index) => ({
167
+ want: Math.max(0, demand.want - seats[index] - reserved[index]),
168
+ weight: demand.weight,
169
+ })));
170
+ const spent = (granted) => granted.reduce((sum, cells) => sum + cells, 0);
171
+ const holding = (...passes) => seatedDemands.map((_, index) => passes.reduce((sum, granted) => sum + granted[index], seats[index] + reserved[index]));
172
+ // The unspent part goes first to the columns with nothing to size to, each
173
+ // alike, up to its `fill` — the reference's `Measurement(1, max_width)`, a
174
+ // maximum of the whole offer that no content measured.
175
+ const afterWant = holding(wanted);
176
+ const filled = distribute(budget - spent(wanted), seatedDemands.map((demand, index) => ({
177
+ want: Math.max(0, demand.fill - afterWant[index]),
178
+ weight: demand.fill > 0 ? 1 : 0,
179
+ })));
180
+ // Then to the ratio columns, in proportion.
181
+ const shared = distribute(budget - spent(wanted) - spent(filled), seatedDemands.map((demand) => ({ want: UNBOUNDED, weight: demand.ratio })));
182
+ // What the shares leave goes to the columns that stretch, by the width each
183
+ // already holds: its natural width, since nothing is left over while any
184
+ // column is short, or the cap a fill stopped at. The reference weighs by
185
+ // width too: `ratio_distribute(max_width - table_width, widths)`, though its
186
+ // widths count the padding. A table that does not expand stretches nothing,
187
+ // which is how it stays narrower than the width it was offered. The order is the reference's:
188
+ // Rich pads an expanding table only once `table_width < max_width`, never
189
+ // while it is collapsing a column.
190
+ const held = holding(wanted, filled, shared);
191
+ const stretched = distribute(budget - spent(wanted) - spent(filled) - spent(shared), seatedDemands.map((demand, index) => ({
192
+ want: UNBOUNDED,
193
+ weight: demand.stretch ? held[index] : 0,
194
+ })));
195
+ const columns = held.map((cells, index) => cells + stretched[index]);
196
+ const cellWidths = columns.map((width) => padLeft + width + padRight);
197
+ return {
198
+ edge,
199
+ divider: frame.divider,
200
+ padLeft,
201
+ padRight,
202
+ columns,
203
+ cellWidths,
204
+ totalWidth: edge * 2 +
205
+ Math.max(0, seated - 1) * frame.divider +
206
+ cellWidths.reduce((sum, width) => sum + width, 0),
207
+ };
208
+ }
209
+ /**
210
+ * The share of the leftover width a column's `ratio` claims: the ratio when it
211
+ * is positive, else 0 — a zero, negative or NaN ratio claims none, and the
212
+ * column sizes to its content. A proportion rather than a count of cells, so it
213
+ * is never floored: `ratio: 0.5` beside `ratio: 1` takes a third, where flooring
214
+ * it to 0 once starved it to a single `…`. Finite, for the reason `demandCells`
215
+ * is.
216
+ *
217
+ * [LAW:one-source-of-truth] `Column.flexible` is this asked as a yes/no, so the
218
+ * width division and the public flag cannot disagree about which columns are
219
+ * elastic.
220
+ */
221
+ const columnShare = (col) => col.ratio !== undefined && col.ratio > 0 ? Math.min(col.ratio, UNBOUNDED) : 0;
222
+ export class Column {
223
+ _header;
224
+ _footer;
225
+ headerStyle;
226
+ footerStyle;
227
+ style;
228
+ justify;
229
+ width;
230
+ minWidth;
231
+ maxWidth;
232
+ ratio;
233
+ noWrap;
234
+ overflow;
235
+ _cells;
236
+ constructor(options) {
237
+ this.header = options?.header;
238
+ this.footer = options?.footer;
239
+ this.headerStyle = options?.headerStyle ?? NULL_STYLE;
240
+ this.footerStyle = options?.footerStyle ?? NULL_STYLE;
241
+ this.style = options?.style ?? NULL_STYLE;
242
+ this.justify = options?.justify ?? "left";
243
+ this.width = options?.width;
244
+ this.minWidth = options?.minWidth;
245
+ this.maxWidth = options?.maxWidth;
246
+ this.ratio = options?.ratio;
247
+ this.noWrap = options?.noWrap ?? false;
248
+ this.overflow = options?.overflow ?? "ellipsis";
249
+ this._cells = [];
250
+ }
251
+ /**
252
+ * The two stamped cells, parsed on assignment rather than at the constructor.
253
+ * `Table.columns` hands out the live column and both fields are public, so a
254
+ * constructor-only stamp held only until the first `columns[0].footer = mine`
255
+ * — which installed content that had parsed no markup, still carried its
256
+ * `end`, and was still owned by the caller, into a slot every reader below
257
+ * assumes `embeddedText` has been through. [LAW:parse-dont-validate] The setter
258
+ * is the border, so the guarantee holds for the object's whole lifetime and
259
+ * the constructor is one caller of it rather than the one place it is true.
260
+ *
261
+ * Absent and empty are the same header, and the same footer.
262
+ * [LAW:types-are-the-program] Rich declares `footer: RenderableType = ""`, so
263
+ * a column always has one and `show_footer` alone decides whether it is
264
+ * drawn. Modelling the absence as `undefined` instead made "no column has a
265
+ * footer" a state the render path could ask about — and it did, skipping the
266
+ * row a caller had asked for. `embeddedText` already maps nothing onto empty,
267
+ * which is why the setters take `undefined` rather than defaulting around it.
268
+ */
269
+ get header() {
270
+ return this._header;
271
+ }
272
+ set header(content) {
273
+ this._header = embeddedText(content);
274
+ }
275
+ get footer() {
276
+ return this._footer;
277
+ }
278
+ set footer(content) {
279
+ this._footer = embeddedText(content);
280
+ }
281
+ get flexible() {
282
+ return columnShare(this) > 0;
283
+ }
284
+ /** @internal */
285
+ addCell(cell) {
286
+ this._cells.push(cell);
287
+ }
288
+ /** @internal */
289
+ getCells() {
290
+ return this._cells;
291
+ }
292
+ copy() {
293
+ const col = new Column({
294
+ // No `.copy()` here: the crossing the constructor routes through copies
295
+ // a `RichText` already, and a second copy is a second home for that rule.
296
+ // [LAW:single-enforcer]
297
+ header: this.header,
298
+ footer: this.footer,
299
+ justify: this.justify,
300
+ width: this.width,
301
+ minWidth: this.minWidth,
302
+ maxWidth: this.maxWidth,
303
+ ratio: this.ratio,
304
+ noWrap: this.noWrap,
305
+ overflow: this.overflow,
306
+ });
307
+ col.headerStyle = this.headerStyle;
308
+ col.footerStyle = this.footerStyle;
309
+ col.style = this.style;
310
+ return col;
311
+ }
312
+ }
313
+ export class Table {
314
+ _columns;
315
+ _rows;
316
+ box;
317
+ title;
318
+ caption;
319
+ expand;
320
+ showHeader;
321
+ showFooter;
322
+ showLines;
323
+ showEdge;
324
+ padding;
325
+ style;
326
+ headerStyle;
327
+ footerStyle;
328
+ borderStyle;
329
+ titleStyle;
330
+ captionStyle;
331
+ titleJustify;
332
+ captionJustify;
333
+ tableWidth;
334
+ minWidth;
335
+ rowStyles;
336
+ constructor(options) {
337
+ this._columns = [];
338
+ this._rows = [];
339
+ this.box = options?.box !== undefined ? options.box : HEAVY_HEAD;
340
+ const titleVal = options?.title;
341
+ this.title = titleVal !== undefined ? embeddedText(titleVal) : undefined;
342
+ const captionVal = options?.caption;
343
+ this.caption = captionVal !== undefined ? embeddedText(captionVal) : undefined;
344
+ // A declared width is the table's size, not a ceiling on it: the
345
+ // reference's `expand` is `self._expand or self.width is not None`, and a
346
+ // port that sized to content inside it drew Rich code narrower than Rich.
347
+ this.expand = (options?.expand ?? false) || options?.width !== undefined;
348
+ this.showHeader = options?.showHeader !== false;
349
+ this.showFooter = options?.showFooter ?? false;
350
+ this.showLines = options?.showLines ?? false;
351
+ this.showEdge = options?.showEdge !== false;
352
+ this.padding = normalizePadding(options?.padding ?? [0, 1, 0, 1]);
353
+ this.style = options?.style ?? NULL_STYLE;
354
+ this.headerStyle = options?.headerStyle ?? "table.header";
355
+ this.footerStyle = options?.footerStyle ?? "table.footer";
356
+ this.borderStyle = options?.borderStyle ?? NULL_STYLE;
357
+ this.titleStyle = options?.titleStyle ?? "table.title";
358
+ this.captionStyle = options?.captionStyle ?? "table.caption";
359
+ this.titleJustify = options?.titleJustify ?? "center";
360
+ this.captionJustify = options?.captionJustify ?? "center";
361
+ this.tableWidth = options?.width;
362
+ this.minWidth = options?.minWidth;
363
+ this.rowStyles = options?.rowStyles ?? [];
364
+ }
365
+ get columns() {
366
+ return this._columns;
367
+ }
368
+ get rowCount() {
369
+ return this._rows.length;
370
+ }
371
+ addColumn(header, options) {
372
+ const col = new Column({ ...options, header: header ?? options?.header });
373
+ this._columns.push(col);
374
+ return this;
375
+ }
376
+ addRow(...cells) {
377
+ // If last arg is an options object with endSection, extract it
378
+ let endSection = false;
379
+ const lastArg = cells[cells.length - 1];
380
+ if (typeof lastArg === "object" &&
381
+ lastArg !== null &&
382
+ !(lastArg instanceof RichText) &&
383
+ !("render" in lastArg) &&
384
+ "endSection" in lastArg) {
385
+ endSection = lastArg.endSection;
386
+ cells = cells.slice(0, -1);
387
+ }
388
+ // Stamped once, here at the border, so sizing and drawing read one resolved
389
+ // cell instead of each converting the raw value for itself. Two converters
390
+ // is two answers to "what is this cell": they disagreed on a `Panel`, which
391
+ // stringifies to `[object Object]` — a string the tag pattern swallows
392
+ // whole, sizing the column to nothing. [LAW:parse-dont-validate]
393
+ //
394
+ // Ahead of the column loop: a cell that throws must leave no phantom column
395
+ // behind. [LAW:no-ambient-temporal-coupling]
396
+ const resolved = cells.map(embed);
397
+ // Auto-create columns if needed
398
+ while (this._columns.length < resolved.length) {
399
+ this.addColumn();
400
+ }
401
+ this._rows.push({ cells: resolved, endSection });
402
+ return this;
403
+ }
404
+ addSection() {
405
+ if (this._rows.length > 0) {
406
+ this._rows[this._rows.length - 1].endSection = true;
407
+ }
408
+ return this;
409
+ }
410
+ *render(rawOptions) {
411
+ if (this._columns.length === 0) {
412
+ yield Segment.line();
413
+ return;
414
+ }
415
+ const options = withBoundedWidth(rawOptions, this);
416
+ // The two swaps a box goes through before anything is drawn with it, in the
417
+ // reference's order: what the platform can render, then what a table
418
+ // lacking a header should. [LAW:dataflow-not-control-flow] Both run every
419
+ // render and each returns the receiver when it has nothing to change, so
420
+ // the flags arrive as values rather than as branches around a step.
421
+ const frame = this.box?.substitute(options);
422
+ const box = (this.showHeader ? frame : frame?.plainHeaded()) ?? null;
423
+ // The table's own `style` is the base its border style layers over, and
424
+ // that is all it styles: the reference reads it as `table_style` for the
425
+ // frame and nowhere else, so a cell is untouched by it.
426
+ const borderStyle = getStyle(options, this.style).add(getStyle(options, this.borderStyle));
427
+ const border = borderStyle.isNull ? undefined : borderStyle;
428
+ // The one division of the width every row below is measured against.
429
+ const geometry = this._geometry(this._outerWidth(options));
430
+ const edge = geometry.edge === 1;
431
+ // Title
432
+ if (this.title) {
433
+ yield* this._renderTitle(options, this.title, geometry.totalWidth, this.titleStyle, this.titleJustify);
434
+ }
435
+ // Top border
436
+ if (box && this.showEdge) {
437
+ yield* box.getTop(geometry.cellWidths, border, edge);
438
+ }
439
+ // Every style a cell is drawn with, resolved once per render rather than
440
+ // once per cell, as the reference's `_get_cells` does per column.
441
+ const headerStyle = getStyle(options, this.headerStyle);
442
+ const footerStyle = getStyle(options, this.footerStyle);
443
+ const columnStyles = this._columns.map((c) => getStyle(options, c.style));
444
+ const stripes = this.rowStyles.map((s) => getStyle(options, s));
445
+ // Header row
446
+ if (this.showHeader) {
447
+ const headerCells = this._columns.map((c) => c.header);
448
+ const headerStyles = this._columns.map((c) => headerStyle.add(getStyle(options, c.headerStyle)));
449
+ yield* this._renderRow(options, headerCells, headerStyles, NULL_STYLE, geometry, box, "head", border);
450
+ // Header separator
451
+ if (box) {
452
+ yield* box.getRow(geometry.cellWidths, "head", border, edge);
453
+ }
454
+ }
455
+ // Data rows
456
+ for (let rowIdx = 0; rowIdx < this._rows.length; rowIdx++) {
457
+ const row = this._rows[rowIdx];
458
+ const rowCells = this._columns.map((_, colIdx) => row.cells[colIdx] ?? embeddedText(undefined));
459
+ const rowStyle = stripes[rowIdx % stripes.length] ?? NULL_STYLE;
460
+ const cellStyles = columnStyles.map((s) => s.add(rowStyle));
461
+ yield* this._renderRow(options, rowCells, cellStyles, rowStyle, geometry, box, "row", border);
462
+ // Row separator
463
+ const showSep = this.showLines || row.endSection;
464
+ if (showSep && box && rowIdx < this._rows.length - 1) {
465
+ yield* box.getRow(geometry.cellWidths, "row", border, edge);
466
+ }
467
+ }
468
+ // Footer
469
+ if (this.showFooter) {
470
+ if (box) {
471
+ yield* box.getRow(geometry.cellWidths, "foot", border, edge);
472
+ }
473
+ const footerCells = this._columns.map((c) => c.footer);
474
+ const footerStyles = this._columns.map((c) => footerStyle.add(getStyle(options, c.footerStyle)));
475
+ yield* this._renderRow(options, footerCells, footerStyles, NULL_STYLE, geometry, box, "foot", border);
476
+ }
477
+ // Bottom border
478
+ if (box && this.showEdge) {
479
+ yield* box.getBottom(geometry.cellWidths, border, edge);
480
+ }
481
+ // Caption
482
+ if (this.caption) {
483
+ yield* this._renderTitle(options, this.caption, geometry.totalWidth, this.captionStyle, this.captionJustify);
484
+ }
485
+ }
486
+ /**
487
+ * [LAW:one-source-of-truth] Both ends of the range are widths the geometry
488
+ * actually produced — the maximum from the demands as they stand, the
489
+ * minimum from the same layout with every column asking for a single cell.
490
+ * Neither end stretches or fills into an offer: both only spend cells an
491
+ * offer happens to leave over, and a renderable reports the width its content
492
+ * wants rather than the width it was offered. A declared `width` is not an
493
+ * offer, and the maximum reports it. Letting either in made the table measure
494
+ * `Infinity` against an unbounded offer, where `withBoundedWidth` needs a
495
+ * natural width to fall back on — an expanding table, and an empty
496
+ * `Table.grid()`.
497
+ * Neither can exceed the width offered and the tighter request cannot exceed
498
+ * the looser one, so the range cannot invert. Deriving the minimum from raw
499
+ * column and padding counts instead is what used to return
500
+ * `{minimum: 6, maximum: 1}` at `maxWidth: 1` — a floor above its own ceiling.
501
+ */
502
+ measure(rawOptions) {
503
+ const options = withCellWidth(rawOptions);
504
+ const outerWidth = this._outerWidth(options);
505
+ const frame = this._frame();
506
+ const demands = this._columnDemands();
507
+ // A declared width is the one stretch a measurement reports, because it is
508
+ // a size the table was given rather than an offer it grew into — the
509
+ // reference measures `self.width` as its maximum, so a `Panel` fitted round
510
+ // a table declared at 40 is 44 wide there, not the table's content width.
511
+ // It is laid out exactly as `render` lays it out, so the two cannot report
512
+ // different widths; a declared width with nothing bounding it
513
+ // (`width: Infinity` at an unbounded offer) has no size to report and falls
514
+ // back to the content, as an unbounded `expand` does.
515
+ const declared = this.tableWidth === undefined
516
+ ? undefined
517
+ : layoutTable(outerWidth, demands, this.padding, frame).totalWidth;
518
+ const laidOut = declared !== undefined && declared < UNBOUNDED
519
+ ? declared
520
+ : layoutTable(outerWidth, demands.map((demand) => ({ ...demand, fill: 0, stretch: false })), this.padding, frame).totalWidth;
521
+ // `UNBOUNDED` is this table's own infinity, so a layout that reached it has
522
+ // no natural width to report — a column asked for every cell there is. Said
523
+ // as the number, it escapes as a width a caller would try to draw:
524
+ // `minWidth: Infinity` measured 18014398509481988 and rendered
525
+ // `RangeError: Invalid string length` out of the top border.
526
+ const maximum = laidOut >= UNBOUNDED ? Infinity : laidOut;
527
+ const tightest = layoutTable(outerWidth, demands.map((demand) => ({
528
+ reserved: 0,
529
+ want: Math.min(1, demand.want),
530
+ weight: 1,
531
+ fill: 0,
532
+ ratio: 0,
533
+ stretch: false,
534
+ })), this.padding, frame).totalWidth;
535
+ return {
536
+ minimum: Math.min(maximum, Math.max(tightest, cells(this.minWidth ?? 0))),
537
+ maximum,
538
+ };
539
+ }
540
+ // --- Static ---
541
+ static grid(options) {
542
+ return new Table({
543
+ ...options,
544
+ box: null,
545
+ showHeader: false,
546
+ showEdge: false,
547
+ padding: options?.padding ?? [0, 1, 0, 0],
548
+ });
549
+ }
550
+ // --- Private ---
551
+ _frame() {
552
+ return {
553
+ divider: this.box ? 1 : 0,
554
+ edge: this.box && this.showEdge ? 1 : 0,
555
+ };
556
+ }
557
+ /**
558
+ * The width this table lays itself out against: what it was told to be, as a
559
+ * count of cells, never wider than what it was offered.
560
+ *
561
+ * [LAW:one-source-of-truth] `render` and `measure` divide the same two fields
562
+ * and once did it in two places. `measure` bounded the declared width by the
563
+ * offer and `render` preferred it outright, walking past the value
564
+ * `withBoundedWidth` had just resolved — so `new Table({width: Infinity})`
565
+ * with a `{ratio: 1}` column granted that column `UNBOUNDED` cells and threw
566
+ * `RangeError: Invalid string length` out of the top border, at a perfectly
567
+ * ordinary 80-cell offer.
568
+ */
569
+ _outerWidth(options) {
570
+ return Math.min(this.tableWidth === undefined ? options.maxWidth : cells(this.tableWidth), options.maxWidth);
571
+ }
572
+ _geometry(outerWidth) {
573
+ return layoutTable(outerWidth, this._columnDemands(), this.padding, this._frame());
574
+ }
575
+ /**
576
+ * [LAW:dataflow-not-control-flow] The three ways a column can be sized —
577
+ * declared width, ratio, natural content — differ only in the demand they
578
+ * produce, and `expand` only in its `stretch`. They are resolved once, here, into uniform data, so
579
+ * `layoutTable` runs the same apportionment for every table and no sizing
580
+ * mode gets its own path through the width division.
581
+ */
582
+ _columnDemands() {
583
+ return this._columns.map((col, index) => {
584
+ if (col.width !== undefined) {
585
+ // [LAW:single-enforcer] floored where it is parsed, the same rule
586
+ // `normalizePadding` applies to a negative padding side.
587
+ const declared = demandCells(col.width);
588
+ return { reserved: declared, want: declared, weight: 0, fill: 0, ratio: 0, stretch: false };
589
+ }
590
+ // A flexible column takes its share of whatever the bounded columns
591
+ // leave; every other column asks for its natural width, whether or not a
592
+ // neighbour is flexible. The split is the reference's —
593
+ // `fixed_widths = [0 if column.flexible else _range.maximum ...]` — with
594
+ // one divergence: Rich splits by ratio only when the table expands, and
595
+ // here a ratio is honoured either way. Its bounded part is its floor —
596
+ // the reference's `column.min_width or 1` — bid for in cells like any
597
+ // content column, so a squeezed table still pays a declared `minWidth`.
598
+ const share = columnShare(col);
599
+ if (share > 0) {
600
+ const floor = Math.max(1, demandCells(col.minWidth ?? 0));
601
+ return { reserved: 0, want: floor, weight: floor, fill: 0, ratio: share, stretch: false };
602
+ }
603
+ const widest = this._widestCell(col, index);
604
+ const natural = demandCells(this._bounded(col, widest ?? 1));
605
+ // `expand` is a stretch rather than a larger want: the column still
606
+ // competes for its natural width like any other, and only the cells left
607
+ // once every column has that are shared out. A larger want looks
608
+ // equivalent and is not — it lets a one-cell column claim cells while its
609
+ // neighbour is still truncated. The stretch weighs by width, so the
610
+ // widest column grows most, which is what keeps a `Progress` bar from
611
+ // getting no more of the slack than its percentage label.
612
+ //
613
+ // One deliberate divergence: Rich stretches declared-width columns as
614
+ // well, so `width: 6` under `expand` renders 21 cells wide — an option
615
+ // quietly meaning something else, the defect rich-justify-0cr exists to
616
+ // remove. A reservation's stretch is zero above.
617
+ //
618
+ // A column with no cell at all — no rows, and neither header nor footer
619
+ // drawn — has nothing to size to, which is not sizing to nothing. The
620
+ // reference measures it `Measurement(1, max_width)`: one cell of content,
621
+ // and a maximum of the whole offer, so a rowless table fills it. Reading
622
+ // "no cells" as zero sized it to a two-cell box instead. The one cell is
623
+ // its want, so a `minWidth` is paid like any column's, and the offer is
624
+ // its `fill`, held to its `maxWidth`.
625
+ return {
626
+ reserved: 0,
627
+ want: natural,
628
+ weight: natural,
629
+ fill: widest === undefined ? demandCells(this._bounded(col, UNBOUNDED)) : 0,
630
+ ratio: 0,
631
+ stretch: this.expand,
632
+ };
633
+ });
634
+ }
635
+ /**
636
+ * Every cell column `index` draws, in draw order.
637
+ *
638
+ * [LAW:one-source-of-truth] The reference's `_get_cells` is the one answer to
639
+ * "which cells belong to this column", and both `_measure_column` and
640
+ * `_render` read it. Enumerating that set a second time inside the width path
641
+ * is how the two ends drifted: the header was measured whether or not
642
+ * `showHeader` drew it, and `col.footer` was never measured at all, so a
643
+ * footer wider than its column — a totals row, exactly — was cut to `…`.
644
+ * A flag is the whole membership rule; nothing here asks after content.
645
+ */
646
+ *_columnCells(col, index) {
647
+ if (this.showHeader)
648
+ yield col.header;
649
+ for (const row of this._rows)
650
+ yield row.cells[index] ?? embeddedText(undefined);
651
+ if (this.showFooter)
652
+ yield col.footer;
653
+ }
654
+ /**
655
+ * The widest cell in a column, or `undefined` when the column has no cell to
656
+ * measure. Zero is a column whose cells draw nothing — a gutter asks for its
657
+ * padding and nothing else — and is not the same answer.
658
+ * [LAW:types-are-the-program]
659
+ */
660
+ _widestCell(col, index) {
661
+ let widest;
662
+ for (const cell of this._columnCells(col, index)) {
663
+ // The stamped cell, so the width a column asks for is the width its text
664
+ // will occupy — measuring the raw value sized this column to
665
+ // `[red]Solo[/red]`, fifteen cells for four cells of text.
666
+ // [LAW:one-source-of-truth]
667
+ //
668
+ // A cell that draws itself is stringified rather than measured, which is
669
+ // a pre-existing gap: `_columnDemands` carries no `RenderOptions`, so
670
+ // `Measurement.get` is not reachable from here. It contributes a wrong
671
+ // non-zero width, and narrowing that is its own change.
672
+ widest = Math.max(widest ?? 0, cell instanceof RichText ? cellLen(cell.plain) : cellLen(String(cell)));
673
+ }
674
+ return widest;
675
+ }
676
+ /**
677
+ * A natural width held to the column's own `minWidth`/`maxWidth`.
678
+ *
679
+ * A bound is a count of cells, read by the rule every width is read by: NaN
680
+ * and a negative are zero cells, so a NaN floor bounds nothing and a NaN
681
+ * ceiling is a ceiling of zero, as `width: NaN` is. A bare `Math.max` returns
682
+ * NaN instead, and the column vanishes. [LAW:one-source-of-truth]
683
+ */
684
+ _bounded(col, natural) {
685
+ return Math.min(Math.max(natural, cells(col.minWidth ?? 0)), cells(col.maxWidth ?? Infinity));
686
+ }
687
+ *_renderRow(options, cells, styles, rowStyle, geometry, box, level, border) {
688
+ const { padLeft, padRight, columns } = geometry;
689
+ // [LAW:dataflow-not-control-flow] Header, body and footer share this path;
690
+ // the level crosses as a value the box answers with glyphs, not a branch.
691
+ const frame = box?.getContentChars(level);
692
+ // A divider drawn in blanks is part of the row's stripe, so it carries the
693
+ // row's background under the border's own style; one drawn in ink is the
694
+ // frame's alone. Header and footer rows have no row style, as in the
695
+ // reference, so their dividers are the border's either way.
696
+ const divider = frame && (frame.vertical.trim() === ""
697
+ ? new Segment(frame.vertical, rowStyle.backgroundStyle.add(border))
698
+ : new Segment(frame.vertical, border));
699
+ // Render each cell onto the canvas the geometry gave its column. Columns
700
+ // the width could not seat are absent from `columns` and so are never
701
+ // rendered at all.
702
+ const cellLines = columns.map((cellWidth, index) => {
703
+ const col = this._columns[index];
704
+ const cell = cells[index] ?? embed("");
705
+ // The render's own options with the column's canvas laid over them, so a
706
+ // cell resolves its style names against the same theme as the table. The
707
+ // table owns the row's height: a cell inherits none, as the reference's
708
+ // `height=None` has it.
709
+ const segs = [...cell.render({
710
+ ...options,
711
+ maxWidth: cellWidth,
712
+ justify: col.justify,
713
+ overflow: col.overflow,
714
+ noWrap: col.noWrap,
715
+ height: undefined,
716
+ })];
717
+ return Segment.splitAndCropLines(segs, cellWidth);
718
+ });
719
+ const maxLines = cellLines.reduce((most, lines) => Math.max(most, lines.length), 1);
720
+ // How many blank lines stand above each cell's content. A header sits on
721
+ // the row's floor, so its column labels line up over the data, and every
722
+ // other row starts at the top — the reference forces both.
723
+ const drop = cellLines.map((lines) => (level === "head" ? maxLines - lines.length : 0));
724
+ for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
725
+ if (frame && geometry.edge === 1) {
726
+ yield new Segment(frame.left, border);
727
+ }
728
+ for (let colIdx = 0; colIdx < columns.length; colIdx++) {
729
+ if (colIdx > 0 && divider) {
730
+ yield divider;
731
+ }
732
+ const cellWidth = columns[colIdx];
733
+ const style = styles[colIdx];
734
+ // A cell with fewer lines than the row contributes blanks, so every
735
+ // column spans the same number of rows and the frame stays rectangular.
736
+ const line = cellLines[colIdx][lineIdx - drop[colIdx]] ?? [new Segment(" ".repeat(cellWidth))];
737
+ // The cell's style is the base its content's own spans layer over, and
738
+ // it covers the whole cell — its left and right padding and its blank
739
+ // lines too, as the reference's does — so a background fills the column
740
+ // rather than sitting behind the text alone.
741
+ yield* Segment.applyStyle([...blank(padLeft), ...line, ...blank(padRight)], style.isNull ? undefined : style);
742
+ }
743
+ if (frame && geometry.edge === 1) {
744
+ yield new Segment(frame.right, border);
745
+ }
746
+ yield Segment.line();
747
+ }
748
+ }
749
+ *_renderTitle(options, text, tableWidth, ownStyle, justify) {
750
+ const style = getStyle(options, ownStyle);
751
+ const titleStyle = style.isNull ? undefined : style;
752
+ // The table owns the canvas; the caller's text still says how it meets the
753
+ // edge. A `RichText`'s own `justify` and `overflow` outrank the options
754
+ // `render` is handed, so `titleJustify` would lose to a property the caller
755
+ // may not know it set, and an `"ignore"` title would have no edge and run
756
+ // straight through the frame. Every other method, and `noWrap`, cuts within
757
+ // the bound, so those stay theirs. Cleared on a copy rather than in place —
758
+ // the caller's text is theirs. [LAW:one-source-of-truth]
759
+ const source = text.copy();
760
+ source.justify = undefined;
761
+ if (source.overflow === "ignore")
762
+ source.overflow = undefined;
763
+ // The table's title style is the *base* the content's own spans layer over,
764
+ // which is what the reference emits: a `[red]` title inside an italic table
765
+ // title arrives as italic-red, not one or the other. Rendering `text.plain`
766
+ // here read the characters and dropped every span attached to them, so a
767
+ // styled title lost its styling and parsed markup silently did nothing.
768
+ //
769
+ // Rendered at the table's own width with nothing suppressed, because that
770
+ // is what the reference hands its title: an annotation too wide for the
771
+ // frame wraps down it rather than being cut off at the corner. `noWrap` and
772
+ // an explicit `crop` stood here and did the cutting (rich-table-6uy.7).
773
+ //
774
+ // [LAW:single-enforcer] The alignment is the renderable's to perform, not
775
+ // just to be told. Padding the lines here as well needed the same
776
+ // rules — that a wrap's trailing whitespace is not content to centre
777
+ // around, that `left` fills the canvas and an unset justify does not — and
778
+ // a second copy of those is a second answer waiting to disagree.
779
+ const rendered = [...source.render({
780
+ ...options,
781
+ maxWidth: tableWidth,
782
+ justify,
783
+ overflow: undefined,
784
+ noWrap: false,
785
+ height: undefined,
786
+ })];
787
+ // Every line the text has, because that is what the reference renders — a
788
+ // title of "one\ntwo" occupies two lines there. Taking only the first
789
+ // dropped the rest with no truncation mark.
790
+ for (const line of Segment.splitLines(rendered)) {
791
+ yield* Segment.applyStyle(line, titleStyle);
792
+ yield Segment.line();
793
+ }
794
+ }
795
+ }