@promptctl/rich-js 0.7.0 → 0.9.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 (201) hide show
  1. package/README.md +99 -31
  2. package/dist/core/box.d.ts +96 -42
  3. package/dist/core/box.d.ts.map +1 -1
  4. package/dist/core/box.js +348 -490
  5. package/dist/core/box.js.map +1 -1
  6. package/dist/core/cells.d.ts +46 -1
  7. package/dist/core/cells.d.ts.map +1 -1
  8. package/dist/core/cells.js +55 -14
  9. package/dist/core/cells.js.map +1 -1
  10. package/dist/core/color.d.ts.map +1 -1
  11. package/dist/core/color.js +22 -8
  12. package/dist/core/color.js.map +1 -1
  13. package/dist/core/console.d.ts +30 -14
  14. package/dist/core/console.d.ts.map +1 -1
  15. package/dist/core/console.js +176 -104
  16. package/dist/core/console.js.map +1 -1
  17. package/dist/core/highlighter.d.ts +2 -1
  18. package/dist/core/highlighter.d.ts.map +1 -1
  19. package/dist/core/highlighter.js +28 -6
  20. package/dist/core/highlighter.js.map +1 -1
  21. package/dist/{renderables → core}/json.d.ts +8 -3
  22. package/dist/core/json.d.ts.map +1 -0
  23. package/dist/{renderables → core}/json.js +7 -2
  24. package/dist/core/json.js.map +1 -0
  25. package/dist/core/markup.d.ts +6 -9
  26. package/dist/core/markup.d.ts.map +1 -1
  27. package/dist/core/markup.js +153 -47
  28. package/dist/core/markup.js.map +1 -1
  29. package/dist/core/measure.d.ts +11 -0
  30. package/dist/core/measure.d.ts.map +1 -1
  31. package/dist/core/measure.js +15 -3
  32. package/dist/core/measure.js.map +1 -1
  33. package/dist/core/oklch.d.ts +22 -0
  34. package/dist/core/oklch.d.ts.map +1 -1
  35. package/dist/core/oklch.js +62 -18
  36. package/dist/core/oklch.js.map +1 -1
  37. package/dist/core/pretty.d.ts +122 -0
  38. package/dist/core/pretty.d.ts.map +1 -0
  39. package/dist/core/pretty.js +398 -0
  40. package/dist/core/pretty.js.map +1 -0
  41. package/dist/core/protocol.d.ts +80 -0
  42. package/dist/core/protocol.d.ts.map +1 -1
  43. package/dist/core/protocol.js +85 -0
  44. package/dist/core/protocol.js.map +1 -1
  45. package/dist/core/segment.d.ts +26 -0
  46. package/dist/core/segment.d.ts.map +1 -1
  47. package/dist/core/segment.js +45 -0
  48. package/dist/core/segment.js.map +1 -1
  49. package/dist/core/strip.d.ts +9 -5
  50. package/dist/core/strip.d.ts.map +1 -1
  51. package/dist/core/strip.js +39 -41
  52. package/dist/core/strip.js.map +1 -1
  53. package/dist/core/style.d.ts +19 -0
  54. package/dist/core/style.d.ts.map +1 -1
  55. package/dist/core/style.js +46 -38
  56. package/dist/core/style.js.map +1 -1
  57. package/dist/core/subscription.d.ts +13 -0
  58. package/dist/core/subscription.d.ts.map +1 -0
  59. package/dist/core/subscription.js +13 -0
  60. package/dist/core/subscription.js.map +1 -0
  61. package/dist/core/text.d.ts +102 -4
  62. package/dist/core/text.d.ts.map +1 -1
  63. package/dist/core/text.js +332 -162
  64. package/dist/core/text.js.map +1 -1
  65. package/dist/core/wrap.d.ts +42 -0
  66. package/dist/core/wrap.d.ts.map +1 -0
  67. package/dist/core/wrap.js +128 -0
  68. package/dist/core/wrap.js.map +1 -0
  69. package/dist/host/host-stream.d.ts.map +1 -0
  70. package/dist/host/host-stream.js.map +1 -0
  71. package/dist/host/index.d.ts +4 -0
  72. package/dist/host/index.d.ts.map +1 -0
  73. package/dist/host/index.js +3 -0
  74. package/dist/host/index.js.map +1 -0
  75. package/dist/{widgets → host}/terminal-host.d.ts +20 -64
  76. package/dist/host/terminal-host.d.ts.map +1 -0
  77. package/dist/host/terminal-host.js +133 -0
  78. package/dist/host/terminal-host.js.map +1 -0
  79. package/dist/index.d.ts +13 -36
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +35 -22
  82. package/dist/index.js.map +1 -1
  83. package/dist/node/terminal-host.d.ts +85 -0
  84. package/dist/node/terminal-host.d.ts.map +1 -0
  85. package/dist/node/terminal-host.js +161 -0
  86. package/dist/node/terminal-host.js.map +1 -0
  87. package/dist/node/traceback.d.ts +28 -0
  88. package/dist/node/traceback.d.ts.map +1 -0
  89. package/dist/node/traceback.js +113 -0
  90. package/dist/node/traceback.js.map +1 -0
  91. package/dist/renderables/columns.d.ts +24 -2
  92. package/dist/renderables/columns.d.ts.map +1 -1
  93. package/dist/renderables/columns.js +91 -32
  94. package/dist/renderables/columns.js.map +1 -1
  95. package/dist/renderables/layout.d.ts +61 -5
  96. package/dist/renderables/layout.d.ts.map +1 -1
  97. package/dist/renderables/layout.js +191 -19
  98. package/dist/renderables/layout.js.map +1 -1
  99. package/dist/renderables/markdown.d.ts +1 -1
  100. package/dist/renderables/markdown.d.ts.map +1 -1
  101. package/dist/renderables/markdown.js +7 -14
  102. package/dist/renderables/markdown.js.map +1 -1
  103. package/dist/renderables/padding.d.ts +16 -3
  104. package/dist/renderables/padding.d.ts.map +1 -1
  105. package/dist/renderables/padding.js +96 -47
  106. package/dist/renderables/padding.js.map +1 -1
  107. package/dist/renderables/panel.d.ts +48 -7
  108. package/dist/renderables/panel.d.ts.map +1 -1
  109. package/dist/renderables/panel.js +175 -106
  110. package/dist/renderables/panel.js.map +1 -1
  111. package/dist/renderables/progress.d.ts +3 -3
  112. package/dist/renderables/progress.d.ts.map +1 -1
  113. package/dist/renderables/progress.js +11 -9
  114. package/dist/renderables/progress.js.map +1 -1
  115. package/dist/renderables/progressBar.d.ts +3 -3
  116. package/dist/renderables/progressBar.d.ts.map +1 -1
  117. package/dist/renderables/progressBar.js +9 -15
  118. package/dist/renderables/progressBar.js.map +1 -1
  119. package/dist/renderables/prompt.js +1 -1
  120. package/dist/renderables/prompt.js.map +1 -1
  121. package/dist/renderables/rule.d.ts +1 -1
  122. package/dist/renderables/rule.d.ts.map +1 -1
  123. package/dist/renderables/rule.js +5 -10
  124. package/dist/renderables/rule.js.map +1 -1
  125. package/dist/renderables/spinner.d.ts +2 -2
  126. package/dist/renderables/spinner.d.ts.map +1 -1
  127. package/dist/renderables/spinner.js +6 -11
  128. package/dist/renderables/spinner.js.map +1 -1
  129. package/dist/renderables/status.d.ts.map +1 -1
  130. package/dist/renderables/status.js +5 -11
  131. package/dist/renderables/status.js.map +1 -1
  132. package/dist/renderables/table.d.ts +90 -16
  133. package/dist/renderables/table.d.ts.map +1 -1
  134. package/dist/renderables/table.js +481 -223
  135. package/dist/renderables/table.js.map +1 -1
  136. package/dist/renderables/traceback.d.ts +7 -3
  137. package/dist/renderables/traceback.d.ts.map +1 -1
  138. package/dist/renderables/traceback.js +15 -18
  139. package/dist/renderables/traceback.js.map +1 -1
  140. package/dist/renderables/tree.d.ts +29 -4
  141. package/dist/renderables/tree.d.ts.map +1 -1
  142. package/dist/renderables/tree.js +86 -33
  143. package/dist/renderables/tree.js.map +1 -1
  144. package/dist/template-bindings/color-funcs.d.ts +3 -3
  145. package/dist/template-bindings/color-funcs.js +3 -3
  146. package/dist/template-bindings/helpers.d.ts.map +1 -1
  147. package/dist/template-bindings/helpers.js +19 -1
  148. package/dist/template-bindings/helpers.js.map +1 -1
  149. package/dist/template-bindings/index.d.ts +6 -7
  150. package/dist/template-bindings/index.d.ts.map +1 -1
  151. package/dist/template-bindings/index.js +6 -7
  152. package/dist/template-bindings/index.js.map +1 -1
  153. package/dist/template-bindings/palette-funcs.d.ts +17 -5
  154. package/dist/template-bindings/palette-funcs.d.ts.map +1 -1
  155. package/dist/template-bindings/palette-funcs.js +69 -6
  156. package/dist/template-bindings/palette-funcs.js.map +1 -1
  157. package/dist/template-bindings/style-funcs.d.ts +1 -1
  158. package/dist/template-bindings/style-funcs.js +1 -1
  159. package/dist/themes/ramp.d.ts +84 -0
  160. package/dist/themes/ramp.d.ts.map +1 -0
  161. package/dist/themes/ramp.js +123 -0
  162. package/dist/themes/ramp.js.map +1 -0
  163. package/dist/widgets/dropdown.d.ts +1 -1
  164. package/dist/widgets/dropdown.js +1 -1
  165. package/dist/widgets/event-router.d.ts +3 -2
  166. package/dist/widgets/event-router.d.ts.map +1 -1
  167. package/dist/widgets/focus-manager.d.ts +2 -1
  168. package/dist/widgets/focus-manager.d.ts.map +1 -1
  169. package/dist/widgets/index.d.ts +3 -6
  170. package/dist/widgets/index.d.ts.map +1 -1
  171. package/dist/widgets/index.js +4 -3
  172. package/dist/widgets/index.js.map +1 -1
  173. package/dist/widgets/screen.d.ts +1 -1
  174. package/dist/widgets/screen.d.ts.map +1 -1
  175. package/dist/widgets/text-input.d.ts +2 -3
  176. package/dist/widgets/text-input.d.ts.map +1 -1
  177. package/dist/widgets/text-input.js +4 -12
  178. package/dist/widgets/text-input.js.map +1 -1
  179. package/dist/widgets/types.d.ts +1 -1
  180. package/dist/widgets/types.d.ts.map +1 -1
  181. package/dist/widgets/types.js.map +1 -1
  182. package/dist/widgets/widget-base.d.ts +2 -1
  183. package/dist/widgets/widget-base.d.ts.map +1 -1
  184. package/package.json +38 -6
  185. package/dist/renderables/json.d.ts.map +0 -1
  186. package/dist/renderables/json.js.map +0 -1
  187. package/dist/renderables/pretty.d.ts +0 -29
  188. package/dist/renderables/pretty.d.ts.map +0 -1
  189. package/dist/renderables/pretty.js +0 -141
  190. package/dist/renderables/pretty.js.map +0 -1
  191. package/dist/themes/paletteResolver.d.ts +0 -35
  192. package/dist/themes/paletteResolver.d.ts.map +0 -1
  193. package/dist/themes/paletteResolver.js +0 -88
  194. package/dist/themes/paletteResolver.js.map +0 -1
  195. package/dist/widgets/host-stream.d.ts.map +0 -1
  196. package/dist/widgets/host-stream.js.map +0 -1
  197. package/dist/widgets/terminal-host.d.ts.map +0 -1
  198. package/dist/widgets/terminal-host.js +0 -258
  199. package/dist/widgets/terminal-host.js.map +0 -1
  200. /package/dist/{widgets → host}/host-stream.d.ts +0 -0
  201. /package/dist/{widgets → host}/host-stream.js +0 -0
@@ -1,38 +1,191 @@
1
1
  /**
2
2
  * Table — tabular data with headers, borders, auto-sizing, and alignment.
3
3
  */
4
- import { cellLen } from "../core/cells.js";
4
+ import { cellLen, cellCount as cells } from "../core/cells.js";
5
5
  import { Segment } from "../core/segment.js";
6
- import { Style, NULL_STYLE } from "../core/style.js";
6
+ import { NULL_STYLE } from "../core/style.js";
7
7
  import { HEAVY_HEAD } from "../core/box.js";
8
8
  import { RichText } from "../core/text.js";
9
- function resolveStyle(style) {
10
- if (style === undefined)
11
- return NULL_STYLE;
12
- if (typeof style === "string")
13
- return Style.parse(style);
14
- return style;
9
+ import { renderMarkup } from "../core/markup.js";
10
+ import { normalizePadding } from "./padding.js";
11
+ import { getStyle, withBoundedWidth, withCellWidth } from "../core/protocol.js";
12
+ /**
13
+ * The one crossing where caller text becomes styled table content.
14
+ *
15
+ * Cells, headers, footers, the title and the caption each used to build their
16
+ * own `RichText` straight from the constructor — which does not parse markup —
17
+ * so the markup rule had five homes and was absent from all five, and
18
+ * `[red]Solo[/red]` reached the terminal with its tags intact.
19
+ * [LAW:single-enforcer]
20
+ *
21
+ * Parsing is unconditional because that is what the reference does rather than
22
+ * because it is the simpler branch: Rich's `Console.__init__` declares
23
+ * `markup: bool = True`, and all five positions reach the wire through Rich's
24
+ * own `render_str`. A table-level opt-out would be a mode with no reference
25
+ * behaviour to define. [LAW:no-mode-explosion]
26
+ *
27
+ * `end` is cleared on both arms, not passed in, because a table cell is a
28
+ * fragment rather than a line — the same two steps Rich takes for a printed
29
+ * string. The `RichText` arm copies first: clearing in place would reach back
30
+ * into the caller's object.
31
+ */
32
+ function toCellText(content) {
33
+ const text = content instanceof RichText ? content.copy() : renderMarkup(String(content ?? ""));
34
+ text.end = "";
35
+ return text;
15
36
  }
16
37
  function toRenderable(content) {
17
- if (content instanceof RichText)
18
- return content;
38
+ // A `RichText` leaves through this arm too — it implements `Renderable`, so
39
+ // the prototype carries `render` and the instance is returned untouched.
19
40
  if (typeof content === "object" && content !== null && "render" in content) {
20
41
  return content;
21
42
  }
22
- return new RichText(String(content ?? ""), { end: "" });
43
+ return toCellText(content);
44
+ }
45
+ /** A `want` no budget can satisfy: the column takes every cell its weight earns. */
46
+ const UNBOUNDED = Number.MAX_SAFE_INTEGER;
47
+ /**
48
+ * A column's `want` or `weight`. Finite, because `UNBOUNDED` is this model's
49
+ * own infinity: a literal `Infinity` reaching `distribute` makes a column's
50
+ * weighted share `Infinity / Infinity`, which is NaN, and the NaN then skews
51
+ * every other elastic column in the table.
52
+ */
53
+ const demandCells = (n) => Math.min(cells(n), UNBOUNDED);
54
+ /**
55
+ * Hand out `total` cells across `demands`, weighted, and never past a demand's
56
+ * `want`.
57
+ *
58
+ * A column whose proportional share would overshoot its cap is granted its
59
+ * whole want and dropped, and the cells it could not use are reopened to the
60
+ * columns still under their caps. That repeats until everyone still open fits
61
+ * within their share; one largest-remainder pass then places the cells the
62
+ * shares left as fractions, so the granted widths sum to `total` exactly — or
63
+ * to the point where every column is capped, which is how a table stays
64
+ * narrower than a width it was offered.
65
+ *
66
+ * Each round caps at least one column or is the last, so the work is bounded by
67
+ * the number of columns and never by the width. That bound is the point rather
68
+ * than an optimization: handing cells out one at a time made the iteration
69
+ * count the width itself, which stalled on a very wide table and — since
70
+ * `Infinity - 1 === Infinity` — never terminated at all for a `maxWidth` of
71
+ * `Infinity` held open by a ratio column. Capping by want reaches that case in
72
+ * one round.
73
+ */
74
+ function distribute(total, demands) {
75
+ const granted = demands.map(() => 0);
76
+ let open = demands
77
+ .map((_, index) => index)
78
+ .filter((index) => demands[index].weight > 0 && demands[index].want > 0);
79
+ let remaining = Math.max(0, total);
80
+ const weightOf = (indices) => indices.reduce((sum, index) => sum + demands[index].weight, 0);
81
+ while (open.length > 0 && remaining > 0) {
82
+ const weightSum = weightOf(open);
83
+ const capped = open.filter((index) => (remaining * demands[index].weight) / weightSum >= demands[index].want);
84
+ if (capped.length === 0)
85
+ break;
86
+ for (const index of capped) {
87
+ granted[index] = demands[index].want;
88
+ remaining -= demands[index].want;
89
+ }
90
+ open = open.filter((index) => !capped.includes(index));
91
+ }
92
+ if (open.length > 0 && remaining > 0) {
93
+ const weightSum = weightOf(open);
94
+ const shares = open.map((index) => (remaining * demands[index].weight) / weightSum);
95
+ const whole = shares.map(Math.floor);
96
+ let residue = remaining - whole.reduce((sum, cells) => sum + cells, 0);
97
+ // The cells the shares left as fractions go to the largest fraction first,
98
+ // ties to the leftmost column, so the total lands exactly on `remaining`.
99
+ const byFraction = open
100
+ .map((_, slot) => slot)
101
+ .sort((a, b) => shares[b] - whole[b] - (shares[a] - whole[a]) || a - b);
102
+ for (const slot of byFraction) {
103
+ if (residue <= 0)
104
+ break;
105
+ whole[slot]++;
106
+ residue--;
107
+ }
108
+ open.forEach((index, slot) => {
109
+ granted[index] = whole[slot];
110
+ });
111
+ }
112
+ return granted;
23
113
  }
24
- function normalizePadding(padding) {
25
- if (padding === undefined)
26
- return [0, 1, 0, 1];
27
- if (typeof padding === "number")
28
- return [padding, padding, padding, padding];
29
- if (padding.length === 2)
30
- return [padding[0], padding[1], padding[0], padding[1]];
31
- return padding;
114
+ function layoutTable(outerWidth, demands, padding, frame) {
115
+ const [, padRightWanted, , padLeftWanted] = padding;
116
+ // Plain `number`, not the `CellCol` `cells` hands back: this budget is spent
117
+ // down by arithmetic, and arithmetic on a branded type produces a number.
118
+ let budget = cells(outerWidth);
119
+ const take = (want) => {
120
+ const got = Math.min(Math.max(0, want), budget);
121
+ budget -= got;
122
+ return got;
123
+ };
124
+ // Both edge columns or neither: `Box.getTop` and its siblings take a single
125
+ // flag for the pair, so half a frame is not a shape this renderer can emit.
126
+ const edge = budget >= frame.edge * 2 ? frame.edge : 0;
127
+ take(edge * 2);
128
+ // A first content cell per column, in column order, each paying for the
129
+ // divider that precedes it. The first column the budget cannot seat is where
130
+ // the table ends; the rest are dropped. A column that wants no cells is
131
+ // seated at zero — the queue exists to stop one column taking a second cell
132
+ // before another has its first, and a spacer never joins it.
133
+ const seats = [];
134
+ while (seats.length < demands.length) {
135
+ const seat = Math.min(1, demands[seats.length].want);
136
+ const cost = (seats.length > 0 ? frame.divider : 0) + seat;
137
+ if (budget < cost)
138
+ break;
139
+ take(cost);
140
+ seats.push(seat);
141
+ }
142
+ const seated = seats.length;
143
+ // Padding is uniform across columns or it is not padding, so it is bought
144
+ // for every seated column at once and skipped entirely when only some could
145
+ // afford it.
146
+ const takePerColumn = (want) => {
147
+ const per = seated === 0
148
+ ? 0
149
+ : Math.min(Math.max(0, want), Math.floor(budget / seated));
150
+ budget -= per * seated;
151
+ return per;
152
+ };
153
+ const padLeft = takePerColumn(padLeftWanted);
154
+ const padRight = takePerColumn(padRightWanted);
155
+ // Reservations are paid in column order, each already holding the cell the
156
+ // seating pass gave it. A budget too small to cover them all runs out
157
+ // partway, which costs the trailing columns their width but never costs the
158
+ // table its frame.
159
+ const seatedDemands = demands.slice(0, seated);
160
+ const reserved = seatedDemands.map((demand, index) => take(demand.reserved - seats[index]));
161
+ // What is left to apportion is the rest of what each column wanted. A table
162
+ // whose columns all fit leaves this budget partly unspent.
163
+ const wanted = distribute(budget, seatedDemands.map((demand, index) => ({
164
+ want: Math.max(0, demand.want - seats[index] - reserved[index]),
165
+ weight: demand.weight,
166
+ })));
167
+ // The unspent part goes to the columns by their stretch. A table that does
168
+ // not expand stretches nothing, which is how it stays narrower than the width
169
+ // it was offered. The order is the reference's: Rich pads an expanding table
170
+ // only once `table_width < max_width`, never while it is collapsing a column.
171
+ const stretched = distribute(budget - wanted.reduce((sum, cells) => sum + cells, 0), seatedDemands.map((demand) => ({ want: UNBOUNDED, weight: demand.stretch })));
172
+ const columns = wanted.map((cells, index) => seats[index] + reserved[index] + cells + stretched[index]);
173
+ const cellWidths = columns.map((width) => padLeft + width + padRight);
174
+ return {
175
+ edge,
176
+ divider: frame.divider,
177
+ padLeft,
178
+ padRight,
179
+ columns,
180
+ cellWidths,
181
+ totalWidth: edge * 2 +
182
+ Math.max(0, seated - 1) * frame.divider +
183
+ cellWidths.reduce((sum, width) => sum + width, 0),
184
+ };
32
185
  }
33
186
  export class Column {
34
- header;
35
- footer;
187
+ _header;
188
+ _footer;
36
189
  headerStyle;
37
190
  footerStyle;
38
191
  style;
@@ -45,17 +198,11 @@ export class Column {
45
198
  overflow;
46
199
  _cells;
47
200
  constructor(options) {
48
- const headerVal = options?.header;
49
- this.header = headerVal instanceof RichText
50
- ? headerVal
51
- : new RichText(headerVal ?? "", { end: "" });
52
- const footerVal = options?.footer;
53
- this.footer = footerVal !== undefined
54
- ? (footerVal instanceof RichText ? footerVal : new RichText(footerVal, { end: "" }))
55
- : undefined;
56
- this.headerStyle = resolveStyle(options?.headerStyle);
57
- this.footerStyle = resolveStyle(options?.footerStyle);
58
- this.style = resolveStyle(options?.style);
201
+ this.header = options?.header;
202
+ this.footer = options?.footer;
203
+ this.headerStyle = options?.headerStyle ?? NULL_STYLE;
204
+ this.footerStyle = options?.footerStyle ?? NULL_STYLE;
205
+ this.style = options?.style ?? NULL_STYLE;
59
206
  this.justify = options?.justify ?? "left";
60
207
  this.width = options?.width;
61
208
  this.minWidth = options?.minWidth;
@@ -65,6 +212,36 @@ export class Column {
65
212
  this.overflow = options?.overflow ?? "ellipsis";
66
213
  this._cells = [];
67
214
  }
215
+ /**
216
+ * The two stamped cells, parsed on assignment rather than at the constructor.
217
+ * `Table.columns` hands out the live column and both fields are public, so a
218
+ * constructor-only stamp held only until the first `columns[0].footer = mine`
219
+ * — which installed content that had parsed no markup, still carried its
220
+ * `end`, and was still owned by the caller, into a slot every reader below
221
+ * assumes `toCellText` has been through. [LAW:parse-dont-validate] The setter
222
+ * is the border, so the guarantee holds for the object's whole lifetime and
223
+ * the constructor is one caller of it rather than the one place it is true.
224
+ *
225
+ * Absent and empty are the same header, and the same footer.
226
+ * [LAW:types-are-the-program] Rich declares `footer: RenderableType = ""`, so
227
+ * a column always has one and `show_footer` alone decides whether it is
228
+ * drawn. Modelling the absence as `undefined` instead made "no column has a
229
+ * footer" a state the render path could ask about — and it did, skipping the
230
+ * row a caller had asked for. `toCellText` already maps nothing onto empty,
231
+ * which is why the setters take `undefined` rather than defaulting around it.
232
+ */
233
+ get header() {
234
+ return this._header;
235
+ }
236
+ set header(content) {
237
+ this._header = toCellText(content);
238
+ }
239
+ get footer() {
240
+ return this._footer;
241
+ }
242
+ set footer(content) {
243
+ this._footer = toCellText(content);
244
+ }
68
245
  get flexible() {
69
246
  return this.ratio !== undefined && this.ratio > 0;
70
247
  }
@@ -78,8 +255,11 @@ export class Column {
78
255
  }
79
256
  copy() {
80
257
  const col = new Column({
81
- header: this.header.copy(),
82
- footer: this.footer?.copy(),
258
+ // No `.copy()` here: the crossing the constructor routes through copies
259
+ // a `RichText` already, and a second copy is a second home for that rule.
260
+ // [LAW:single-enforcer]
261
+ header: this.header,
262
+ footer: this.footer,
83
263
  justify: this.justify,
84
264
  width: this.width,
85
265
  minWidth: this.minWidth,
@@ -122,25 +302,21 @@ export class Table {
122
302
  this._rows = [];
123
303
  this.box = options?.box !== undefined ? options.box : HEAVY_HEAD;
124
304
  const titleVal = options?.title;
125
- this.title = titleVal !== undefined
126
- ? (titleVal instanceof RichText ? titleVal : new RichText(titleVal, { end: "" }))
127
- : undefined;
305
+ this.title = titleVal !== undefined ? toCellText(titleVal) : undefined;
128
306
  const captionVal = options?.caption;
129
- this.caption = captionVal !== undefined
130
- ? (captionVal instanceof RichText ? captionVal : new RichText(captionVal, { end: "" }))
131
- : undefined;
307
+ this.caption = captionVal !== undefined ? toCellText(captionVal) : undefined;
132
308
  this.expand = options?.expand ?? false;
133
309
  this.showHeader = options?.showHeader !== false;
134
310
  this.showFooter = options?.showFooter ?? false;
135
311
  this.showLines = options?.showLines ?? false;
136
312
  this.showEdge = options?.showEdge !== false;
137
- this.padding = normalizePadding(options?.padding);
138
- this.style = resolveStyle(options?.style);
139
- this.headerStyle = resolveStyle(options?.headerStyle ?? "table.header");
140
- this.footerStyle = resolveStyle(options?.footerStyle ?? "table.footer");
141
- this.borderStyle = resolveStyle(options?.borderStyle);
142
- this.titleStyle = resolveStyle(options?.titleStyle ?? "table.title");
143
- this.captionStyle = resolveStyle(options?.captionStyle ?? "table.caption");
313
+ this.padding = normalizePadding(options?.padding ?? [0, 1, 0, 1]);
314
+ this.style = options?.style ?? NULL_STYLE;
315
+ this.headerStyle = options?.headerStyle ?? "table.header";
316
+ this.footerStyle = options?.footerStyle ?? "table.footer";
317
+ this.borderStyle = options?.borderStyle ?? NULL_STYLE;
318
+ this.titleStyle = options?.titleStyle ?? "table.title";
319
+ this.captionStyle = options?.captionStyle ?? "table.caption";
144
320
  this.titleJustify = options?.titleJustify ?? "center";
145
321
  this.captionJustify = options?.captionJustify ?? "center";
146
322
  this.tableWidth = options?.width;
@@ -170,11 +346,20 @@ export class Table {
170
346
  endSection = lastArg.endSection;
171
347
  cells = cells.slice(0, -1);
172
348
  }
349
+ // Stamped once, here at the border, so sizing and drawing read one resolved
350
+ // cell instead of each converting the raw value for itself. Two converters
351
+ // is two answers to "what is this cell": they disagreed on a `Panel`, which
352
+ // stringifies to `[object Object]` — a string the tag pattern swallows
353
+ // whole, sizing the column to nothing. [LAW:parse-dont-validate]
354
+ //
355
+ // Ahead of the column loop: a cell that throws must leave no phantom column
356
+ // behind. [LAW:no-ambient-temporal-coupling]
357
+ const resolved = cells.map(toRenderable);
173
358
  // Auto-create columns if needed
174
- while (this._columns.length < cells.length) {
359
+ while (this._columns.length < resolved.length) {
175
360
  this.addColumn();
176
361
  }
177
- this._rows.push({ cells, endSection });
362
+ this._rows.push({ cells: resolved, endSection });
178
363
  return this;
179
364
  }
180
365
  addSection() {
@@ -183,78 +368,107 @@ export class Table {
183
368
  }
184
369
  return this;
185
370
  }
186
- *render(options) {
371
+ *render(rawOptions) {
187
372
  if (this._columns.length === 0) {
188
373
  yield Segment.line();
189
374
  return;
190
375
  }
191
- const box = this.box
192
- ? (options.asciiOnly ? this.box.substitute({ asciiOnly: true }) : this.box)
193
- : null;
194
- const border = this.borderStyle.isNull ? undefined : this.borderStyle;
195
- // Calculate column widths
196
- const totalWidth = this.tableWidth ?? (this.expand ? options.maxWidth : undefined);
197
- const colWidths = this._calculateWidths(options, totalWidth);
198
- const tableActualWidth = this._totalTableWidth(colWidths, box);
199
- const [_padTop, padRight, _padBottom, padLeft] = this.padding;
376
+ const options = withBoundedWidth(rawOptions, this);
377
+ // The two swaps a box goes through before anything is drawn with it, in the
378
+ // reference's order: what the platform can render, then what a table
379
+ // lacking a header should. [LAW:dataflow-not-control-flow] Both run every
380
+ // render and each returns the receiver when it has nothing to change, so
381
+ // the flags arrive as values rather than as branches around a step.
382
+ const drawable = this.box?.substitute({ asciiOnly: options.asciiOnly });
383
+ const box = (this.showHeader ? drawable : drawable?.plainHeaded()) ?? null;
384
+ const borderStyle = getStyle(options, this.borderStyle);
385
+ const border = borderStyle.isNull ? undefined : borderStyle;
386
+ // The one division of the width every row below is measured against.
387
+ const geometry = this._geometry(this._outerWidth(options));
388
+ const edge = geometry.edge === 1;
200
389
  // Title
201
390
  if (this.title) {
202
- yield* this._renderTitle(this.title, tableActualWidth, this.titleStyle, this.titleJustify);
391
+ yield* this._renderTitle(options, this.title, geometry.totalWidth, this.titleStyle, this.titleJustify);
203
392
  }
204
393
  // Top border
205
394
  if (box && this.showEdge) {
206
- yield* box.getTop(colWidths.map((w) => w + padLeft + padRight), border, true);
395
+ yield* box.getTop(geometry.cellWidths, border, edge);
207
396
  }
208
397
  // Header row
209
- if (this.showHeader && this._columns.some((c) => c.header.hasContent)) {
398
+ if (this.showHeader) {
210
399
  const headerCells = this._columns.map((c) => c.header);
211
- yield* this._renderRow(headerCells, colWidths, box, border, this.headerStyle);
400
+ yield* this._renderRow(options, headerCells, geometry, box, "head", border, this.headerStyle);
212
401
  // Header separator
213
402
  if (box) {
214
- yield* box.getRow(colWidths.map((w) => w + padLeft + padRight), "head", border, this.showEdge);
403
+ yield* box.getRow(geometry.cellWidths, "head", border, edge);
215
404
  }
216
405
  }
217
406
  // Data rows
218
407
  for (let rowIdx = 0; rowIdx < this._rows.length; rowIdx++) {
219
408
  const row = this._rows[rowIdx];
220
- const rowCells = [];
221
- for (let colIdx = 0; colIdx < this._columns.length; colIdx++) {
222
- const cell = row.cells[colIdx];
223
- rowCells.push(toRenderable(cell));
224
- }
409
+ const rowCells = this._columns.map((_, colIdx) => row.cells[colIdx] ?? toCellText(undefined));
225
410
  const rowStyle = this.rowStyles.length > 0
226
- ? resolveStyle(this.rowStyles[rowIdx % this.rowStyles.length])
411
+ ? this.rowStyles[rowIdx % this.rowStyles.length]
227
412
  : NULL_STYLE;
228
- yield* this._renderRow(rowCells, colWidths, box, border, rowStyle);
413
+ yield* this._renderRow(options, rowCells, geometry, box, "row", border, rowStyle);
229
414
  // Row separator
230
415
  const showSep = this.showLines || row.endSection;
231
416
  if (showSep && box && rowIdx < this._rows.length - 1) {
232
- yield* box.getRow(colWidths.map((w) => w + padLeft + padRight), "row", border, this.showEdge);
417
+ yield* box.getRow(geometry.cellWidths, "row", border, edge);
233
418
  }
234
419
  }
235
420
  // Footer
236
- if (this.showFooter && this._columns.some((c) => c.footer)) {
421
+ if (this.showFooter) {
237
422
  if (box) {
238
- yield* box.getRow(colWidths.map((w) => w + padLeft + padRight), "foot", border, this.showEdge);
423
+ yield* box.getRow(geometry.cellWidths, "foot", border, edge);
239
424
  }
240
- const footerCells = this._columns.map((c) => (c.footer ?? new RichText("", { end: "" })));
241
- yield* this._renderRow(footerCells, colWidths, box, border, this.footerStyle);
425
+ const footerCells = this._columns.map((c) => c.footer);
426
+ yield* this._renderRow(options, footerCells, geometry, box, "foot", border, this.footerStyle);
242
427
  }
243
428
  // Bottom border
244
429
  if (box && this.showEdge) {
245
- yield* box.getBottom(colWidths.map((w) => w + padLeft + padRight), border, true);
430
+ yield* box.getBottom(geometry.cellWidths, border, edge);
246
431
  }
247
432
  // Caption
248
433
  if (this.caption) {
249
- yield* this._renderTitle(this.caption, tableActualWidth, this.captionStyle, this.captionJustify);
434
+ yield* this._renderTitle(options, this.caption, geometry.totalWidth, this.captionStyle, this.captionJustify);
250
435
  }
251
436
  }
252
- measure(options) {
253
- const colWidths = this._calculateWidths(options);
254
- const total = this._totalTableWidth(colWidths, this.box);
437
+ /**
438
+ * [LAW:one-source-of-truth] Both ends of the range are widths the geometry
439
+ * actually produced — the maximum from the demands as they stand, the
440
+ * minimum from the same layout with every column asking for a single cell.
441
+ * Neither end stretches: a stretch only spends cells an offer happens to
442
+ * leave over, and a renderable reports the width its content wants rather
443
+ * than the width it was offered. Letting it in made an expanding table
444
+ * measure `Infinity` against an unbounded offer, where `withBoundedWidth`
445
+ * needs a natural width to fall back on.
446
+ * Neither can exceed the width offered and the tighter request cannot exceed
447
+ * the looser one, so the range cannot invert. Deriving the minimum from raw
448
+ * column and padding counts instead is what used to return
449
+ * `{minimum: 6, maximum: 1}` at `maxWidth: 1` — a floor above its own ceiling.
450
+ */
451
+ measure(rawOptions) {
452
+ const options = withCellWidth(rawOptions);
453
+ const outerWidth = this._outerWidth(options);
454
+ const frame = this._frame();
455
+ const demands = this._columnDemands();
456
+ const laidOut = layoutTable(outerWidth, demands.map((demand) => ({ ...demand, stretch: 0 })), this.padding, frame).totalWidth;
457
+ // `UNBOUNDED` is this table's own infinity, so a layout that reached it has
458
+ // no natural width to report — a column asked for every cell there is. Said
459
+ // as the number, it escapes as a width a caller would try to draw:
460
+ // `minWidth: Infinity` measured 18014398509481988 and rendered
461
+ // `RangeError: Invalid string length` out of the top border.
462
+ const maximum = laidOut >= UNBOUNDED ? Infinity : laidOut;
463
+ const tightest = layoutTable(outerWidth, demands.map((demand) => ({
464
+ reserved: 0,
465
+ want: Math.min(1, demand.want),
466
+ weight: 1,
467
+ stretch: 0,
468
+ })), this.padding, frame).totalWidth;
255
469
  return {
256
- minimum: Math.max(this._columns.length * 2, this.minWidth ?? 0),
257
- maximum: Math.min(total, options.maxWidth),
470
+ minimum: Math.min(maximum, Math.max(tightest, this.minWidth ?? 0)),
471
+ maximum,
258
472
  };
259
473
  }
260
474
  // --- Static ---
@@ -268,167 +482,211 @@ export class Table {
268
482
  });
269
483
  }
270
484
  // --- Private ---
271
- _calculateWidths(options, totalWidth) {
272
- const numCols = this._columns.length;
273
- if (numCols === 0)
274
- return [];
275
- const [_padTop, padRight, _padBottom, padLeft] = this.padding;
276
- const cellPad = padLeft + padRight;
277
- const borders = this.box
278
- ? (this.showEdge ? 2 : 0) + (numCols - 1)
279
- : 0;
280
- const availableWidth = (totalWidth ?? options.maxWidth) - borders - cellPad * numCols;
281
- const widths = new Array(numCols);
282
- // Fixed width columns first
283
- let remainingWidth = availableWidth;
284
- let flexCols = 0;
285
- for (let i = 0; i < numCols; i++) {
286
- const col = this._columns[i];
485
+ _frame() {
486
+ return {
487
+ divider: this.box ? 1 : 0,
488
+ edge: this.box && this.showEdge ? 1 : 0,
489
+ };
490
+ }
491
+ /**
492
+ * The width this table lays itself out against: what it was told to be, as a
493
+ * count of cells, never wider than what it was offered.
494
+ *
495
+ * [LAW:one-source-of-truth] `render` and `measure` divide the same two fields
496
+ * and once did it in two places. `measure` bounded the declared width by the
497
+ * offer and `render` preferred it outright, walking past the value
498
+ * `withBoundedWidth` had just resolved — so `new Table({width: Infinity})`
499
+ * with a `{ratio: 1}` column granted that column `UNBOUNDED` cells and threw
500
+ * `RangeError: Invalid string length` out of the top border, at a perfectly
501
+ * ordinary 80-cell offer.
502
+ */
503
+ _outerWidth(options) {
504
+ return Math.min(this.tableWidth === undefined ? options.maxWidth : cells(this.tableWidth), options.maxWidth);
505
+ }
506
+ _geometry(outerWidth) {
507
+ return layoutTable(outerWidth, this._columnDemands(), this.padding, this._frame());
508
+ }
509
+ /**
510
+ * [LAW:dataflow-not-control-flow] The three ways a column can be sized —
511
+ * declared width, ratio, natural content — differ only in the demand they
512
+ * produce, and `expand` only in its `stretch`. They are resolved once, here, into uniform data, so
513
+ * `layoutTable` runs the same apportionment for every table and no sizing
514
+ * mode gets its own path through the width division.
515
+ */
516
+ _columnDemands() {
517
+ // A ratio on any column makes every non-fixed column elastic: a ratio
518
+ // expresses a split of the whole width, so a column that declares none
519
+ // still holds a share of it (1).
520
+ const elastic = this._columns.some((col) => col.flexible);
521
+ return this._columns.map((col, index) => {
287
522
  if (col.width !== undefined) {
288
- widths[i] = col.width;
289
- remainingWidth -= col.width;
290
- }
291
- else {
292
- widths[i] = 0;
293
- flexCols++;
523
+ // [LAW:single-enforcer] floored where it is parsed, the same rule
524
+ // `normalizePadding` applies to a negative padding side.
525
+ const declared = demandCells(col.width);
526
+ return { reserved: declared, want: declared, weight: 0, stretch: 0 };
294
527
  }
295
- }
296
- // Distribute remaining width among flex columns
297
- if (flexCols > 0) {
298
- // Measure content to determine natural widths
299
- const naturalWidths = [];
300
- for (let i = 0; i < numCols; i++) {
301
- const col = this._columns[i];
302
- if (col.width !== undefined) {
303
- naturalWidths.push(col.width);
304
- continue;
305
- }
306
- let maxContent = cellLen(col.header.plain);
307
- for (const row of this._rows) {
308
- const cell = row.cells[i];
309
- const cellText = String(cell ?? "");
310
- maxContent = Math.max(maxContent, cellLen(cellText));
311
- }
312
- if (col.minWidth !== undefined)
313
- maxContent = Math.max(maxContent, col.minWidth);
314
- if (col.maxWidth !== undefined)
315
- maxContent = Math.min(maxContent, col.maxWidth);
316
- naturalWidths.push(maxContent);
317
- }
318
- // Check if ratio-based distribution applies
319
- const hasRatios = this._columns.some((c) => c.flexible);
320
- if (hasRatios) {
321
- const totalRatio = this._columns.reduce((s, c) => s + (c.ratio ?? 1), 0);
322
- for (let i = 0; i < numCols; i++) {
323
- if (this._columns[i].width !== undefined)
324
- continue;
325
- const ratio = this._columns[i].ratio ?? 1;
326
- widths[i] = Math.max(1, Math.floor(remainingWidth * ratio / totalRatio));
327
- }
528
+ if (elastic) {
529
+ return { reserved: 0, want: UNBOUNDED, weight: demandCells(col.ratio ?? 1), stretch: 0 };
328
530
  }
329
- else {
330
- // Distribute equally or by natural width
331
- const totalNatural = naturalWidths.reduce((s, w, i) => this._columns[i].width !== undefined ? s : s + w, 0);
332
- for (let i = 0; i < numCols; i++) {
333
- if (this._columns[i].width !== undefined)
334
- continue;
335
- const natural = naturalWidths[i];
336
- if (totalNatural <= remainingWidth) {
337
- widths[i] = natural;
338
- }
339
- else {
340
- widths[i] = Math.max(1, Math.floor(remainingWidth * natural / totalNatural));
341
- }
342
- }
343
- }
344
- }
345
- return widths;
531
+ const natural = demandCells(this._naturalWidth(col, index));
532
+ // `expand` is a stretch rather than a larger want: the column still
533
+ // competes for its natural width like any other, and only the cells left
534
+ // once every column has that are shared out. A larger want looks
535
+ // equivalent and is not — it lets a one-cell column claim cells while its
536
+ // neighbour is still truncated. The stretch is the natural width, so the
537
+ // widest column grows most, which is what keeps a `Progress` bar from
538
+ // getting no more of the slack than its percentage label. The reference
539
+ // weighs by width too: `ratio_distribute(max_width - table_width, widths)`.
540
+ //
541
+ // One deliberate divergence: Rich stretches declared-width columns as
542
+ // well, so `width: 6` under `expand` renders 21 cells wide — an option
543
+ // quietly meaning something else, the defect rich-justify-0cr exists to
544
+ // remove. A reservation's stretch is zero above.
545
+ return { reserved: 0, want: natural, weight: natural, stretch: this.expand ? natural : 0 };
546
+ });
346
547
  }
347
- _totalTableWidth(colWidths, box) {
348
- const [_padTop, padRight, _padBottom, padLeft] = this.padding;
349
- const cellPad = padLeft + padRight;
350
- const contentWidth = colWidths.reduce((s, w) => s + w + cellPad, 0);
351
- const borders = box
352
- ? (this.showEdge ? 2 : 0) + (colWidths.length - 1)
353
- : 0;
354
- return contentWidth + borders;
548
+ /**
549
+ * Every cell column `index` draws, in draw order.
550
+ *
551
+ * [LAW:one-source-of-truth] The reference's `_get_cells` is the one answer to
552
+ * "which cells belong to this column", and both `_measure_column` and
553
+ * `_render` read it. Enumerating that set a second time inside the width path
554
+ * is how the two ends drifted: the header was measured whether or not
555
+ * `showHeader` drew it, and `col.footer` was never measured at all, so a
556
+ * footer wider than its column — a totals row, exactly — was cut to `…`.
557
+ * A flag is the whole membership rule; nothing here asks after content.
558
+ */
559
+ *_columnCells(col, index) {
560
+ if (this.showHeader)
561
+ yield col.header;
562
+ for (const row of this._rows)
563
+ yield row.cells[index] ?? toCellText(undefined);
564
+ if (this.showFooter)
565
+ yield col.footer;
355
566
  }
356
- *_renderRow(cells, colWidths, box, border, rowStyle) {
357
- const [_padTop, padRight, _padBottom, padLeft] = this.padding;
358
- // Render each cell and split into lines
359
- const cellLines = [];
360
- let maxLines = 1;
361
- for (let i = 0; i < this._columns.length; i++) {
362
- const col = this._columns[i];
363
- const cell = cells[i] ?? toRenderable("");
364
- const cellWidth = colWidths[i] ?? 1;
365
- const cellOpts = {
366
- ...({ maxWidth: cellWidth }),
367
- justify: col.justify,
368
- overflow: col.overflow,
369
- noWrap: col.noWrap,
370
- };
371
- const segs = [...cell.render(cellOpts)];
372
- const lines = Segment.splitLines(segs);
373
- const adjusted = lines.map((line) => Segment.adjustLineLength(line, cellWidth));
374
- // Ensure at least one line
375
- const finalLines = adjusted.length > 0 ? adjusted : [[new Segment(" ".repeat(cellWidth))]];
376
- cellLines.push(finalLines);
377
- maxLines = Math.max(maxLines, finalLines.length);
567
+ /**
568
+ * The widest cell in a column, bounded by its own `minWidth`/`maxWidth`.
569
+ * Zero for a column that draws nothing — a gutter asks for its padding and
570
+ * nothing else.
571
+ */
572
+ _naturalWidth(col, index) {
573
+ let natural = 0;
574
+ for (const cell of this._columnCells(col, index)) {
575
+ // The stamped cell, so the width a column asks for is the width its text
576
+ // will occupy — measuring the raw value sized this column to
577
+ // `[red]Solo[/red]`, fifteen cells for four cells of text.
578
+ // [LAW:one-source-of-truth]
579
+ //
580
+ // A cell that draws itself is stringified rather than measured, which is
581
+ // a pre-existing gap: `_columnDemands` carries no `RenderOptions`, so
582
+ // `Measurement.get` is not reachable from here. It contributes a wrong
583
+ // non-zero width, and narrowing that is its own change.
584
+ natural = Math.max(natural, cell instanceof RichText ? cellLen(cell.plain) : cellLen(String(cell)));
378
585
  }
379
- // Render line by line
586
+ if (col.minWidth !== undefined)
587
+ natural = Math.max(natural, col.minWidth);
588
+ if (col.maxWidth !== undefined)
589
+ natural = Math.min(natural, col.maxWidth);
590
+ return natural;
591
+ }
592
+ *_renderRow(options, cells, geometry, box, level, border, ownStyle) {
593
+ const { padLeft, padRight, columns } = geometry;
594
+ const rowStyle = getStyle(options, ownStyle);
595
+ // [LAW:dataflow-not-control-flow] Header, body and footer share this path;
596
+ // the level crosses as a value the box answers with glyphs, not a branch.
597
+ const frame = box?.getContentChars(level);
598
+ // Render each cell onto the canvas the geometry gave its column. Columns
599
+ // the width could not seat are absent from `columns` and so are never
600
+ // rendered at all.
601
+ const cellLines = columns.map((cellWidth, index) => {
602
+ const col = this._columns[index];
603
+ const cell = cells[index] ?? toRenderable("");
604
+ // The render's own options with the column's canvas laid over them, so a
605
+ // cell resolves its style names against the same theme as the table. The
606
+ // table owns the row's height: a cell inherits none, as the reference's
607
+ // `height=None` has it.
608
+ const segs = [...cell.render({
609
+ ...options,
610
+ maxWidth: cellWidth,
611
+ justify: col.justify,
612
+ overflow: col.overflow,
613
+ noWrap: col.noWrap,
614
+ height: undefined,
615
+ maxHeight: undefined,
616
+ })];
617
+ const lines = Segment.splitLines(segs).map((line) => Segment.adjustLineLength(line, cellWidth));
618
+ return lines.length > 0 ? lines : [[new Segment(" ".repeat(cellWidth))]];
619
+ });
620
+ const maxLines = cellLines.reduce((most, lines) => Math.max(most, lines.length), 1);
380
621
  for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
381
- if (box && this.showEdge) {
382
- yield new Segment(box.left, border);
622
+ if (frame && geometry.edge === 1) {
623
+ yield new Segment(frame.left, border);
383
624
  }
384
- for (let colIdx = 0; colIdx < this._columns.length; colIdx++) {
385
- if (colIdx > 0 && box) {
386
- yield new Segment(box.vertical, border);
625
+ for (let colIdx = 0; colIdx < columns.length; colIdx++) {
626
+ if (colIdx > 0 && frame) {
627
+ yield new Segment(frame.vertical, border);
387
628
  }
388
- const colLines = cellLines[colIdx];
389
- const cellWidth = colWidths[colIdx] ?? 1;
390
- // Left padding
629
+ const cellWidth = columns[colIdx];
391
630
  if (padLeft > 0)
392
631
  yield new Segment(" ".repeat(padLeft));
393
- const line = colLines[lineIdx];
394
- if (line) {
395
- const adjusted = Segment.adjustLineLength(line, cellWidth);
396
- const styled = rowStyle.isNull ? adjusted : [...Segment.applyStyle(adjusted, rowStyle)];
397
- yield* styled;
398
- }
399
- else {
400
- yield new Segment(" ".repeat(cellWidth));
401
- }
402
- // Right padding
632
+ // A cell that ran out of lines contributes blanks, so every column
633
+ // spans the same number of rows and the frame stays rectangular.
634
+ const line = cellLines[colIdx][lineIdx] ?? [new Segment(" ".repeat(cellWidth))];
635
+ yield* rowStyle.isNull ? line : Segment.applyStyle(line, rowStyle);
403
636
  if (padRight > 0)
404
637
  yield new Segment(" ".repeat(padRight));
405
638
  }
406
- if (box && this.showEdge) {
407
- yield new Segment(box.right, border);
639
+ if (frame && geometry.edge === 1) {
640
+ yield new Segment(frame.right, border);
408
641
  }
409
642
  yield Segment.line();
410
643
  }
411
644
  }
412
- *_renderTitle(text, tableWidth, style, justify) {
645
+ *_renderTitle(options, text, tableWidth, ownStyle, justify) {
646
+ const style = getStyle(options, ownStyle);
413
647
  const titleStyle = style.isNull ? undefined : style;
414
- const plain = text.plain;
415
- const textWidth = cellLen(plain);
416
- if (textWidth >= tableWidth) {
417
- yield new Segment(plain.slice(0, tableWidth), titleStyle);
648
+ // The table owns the canvas; the caller's text still says how it meets the
649
+ // edge. A `RichText`'s own `justify` and `noWrap` outrank the options
650
+ // `render` is handed, so `titleJustify` would lose to a property the caller
651
+ // may not know it set, and a `noWrap` title would leave at its natural width
652
+ // and run straight through the frame. `overflow` stays theirs: every method
653
+ // cuts within a bound it cannot lift, so none can escape. Cleared on a copy
654
+ // rather than in place — the caller's text is theirs. [LAW:one-source-of-truth]
655
+ const source = text.copy();
656
+ source.justify = undefined;
657
+ source.noWrap = false;
658
+ // The table's title style is the *base* the content's own spans layer over,
659
+ // which is what the reference emits: a `[red]` title inside an italic table
660
+ // title arrives as italic-red, not one or the other. Rendering `text.plain`
661
+ // here read the characters and dropped every span attached to them, so a
662
+ // styled title lost its styling and parsed markup silently did nothing.
663
+ //
664
+ // Rendered at the table's own width with nothing suppressed, because that
665
+ // is what the reference hands its title: an annotation too wide for the
666
+ // frame wraps down it rather than being cut off at the corner. `noWrap` and
667
+ // an explicit `crop` stood here and did the cutting (rich-table-6uy.7).
668
+ //
669
+ // [LAW:single-enforcer] The alignment is the renderable's to perform, not
670
+ // just to be told. Padding the lines here as well needed the same
671
+ // rules — that a wrap's trailing whitespace is not content to centre
672
+ // around, that `left` fills the canvas and an unset justify does not — and
673
+ // a second copy of those is a second answer waiting to disagree.
674
+ const rendered = [...source.render({
675
+ ...options,
676
+ maxWidth: tableWidth,
677
+ justify,
678
+ overflow: undefined,
679
+ noWrap: false,
680
+ height: undefined,
681
+ maxHeight: undefined,
682
+ })];
683
+ // Every line the text has, because that is what the reference renders — a
684
+ // title of "one\ntwo" occupies two lines there. Taking only the first
685
+ // dropped the rest with no truncation mark.
686
+ for (const line of Segment.splitLines(rendered)) {
687
+ yield* Segment.applyStyle(line, titleStyle);
418
688
  yield Segment.line();
419
- return;
420
689
  }
421
- const gap = tableWidth - textWidth;
422
- const leftPad = justify === "right" ? gap
423
- : justify === "center" ? Math.floor(gap / 2)
424
- : 0;
425
- const rightPad = gap - leftPad;
426
- if (leftPad > 0)
427
- yield new Segment(" ".repeat(leftPad));
428
- yield new Segment(plain, titleStyle);
429
- if (rightPad > 0)
430
- yield new Segment(" ".repeat(rightPad));
431
- yield Segment.line();
432
690
  }
433
691
  }
434
692
  //# sourceMappingURL=table.js.map