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