@promptctl/rich-js 0.24.1 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/core/anchor.d.ts +5 -0
- package/dist/core/anchor.js +11 -4
- package/dist/core/animation.d.ts +175 -10
- package/dist/core/animation.js +283 -15
- package/dist/core/ansi.js +2 -2
- package/dist/core/box.d.ts +7 -7
- package/dist/core/cells.d.ts +23 -13
- package/dist/core/cells.js +58 -48
- package/dist/core/clock.d.ts +2 -2
- package/dist/core/clock.js +6 -4
- package/dist/core/color.d.ts +27 -10
- package/dist/core/color.js +21 -29
- package/dist/core/console.d.ts +28 -17
- package/dist/core/console.js +44 -39
- package/dist/core/easing.d.ts +30 -0
- package/dist/core/easing.js +65 -20
- package/dist/core/emoji.d.ts +1 -1
- package/dist/core/emoji.js +1 -2
- package/dist/core/export-html.js +2 -1
- package/dist/core/export-lines.js +3 -3
- package/dist/core/highlighter.d.ts +9 -8
- package/dist/core/highlighter.js +3 -2
- package/dist/core/json.js +2 -2
- package/dist/core/markup.js +16 -12
- package/dist/core/measure.js +2 -2
- package/dist/core/memo.d.ts +11 -2
- package/dist/core/memo.js +16 -0
- package/dist/core/oklch.js +17 -6
- package/dist/core/osc8.d.ts +1 -1
- package/dist/core/osc8.js +4 -5
- package/dist/core/paint.d.ts +2 -1
- package/dist/core/paint.js +4 -3
- package/dist/core/pattern.d.ts +21 -0
- package/dist/core/pattern.js +21 -0
- package/dist/core/pretty.js +9 -10
- package/dist/core/protocol.d.ts +100 -39
- package/dist/core/protocol.js +167 -21
- package/dist/core/render.d.ts +15 -2
- package/dist/core/render.js +25 -5
- package/dist/core/segment.d.ts +13 -5
- package/dist/core/segment.js +23 -9
- package/dist/core/spinnerData.d.ts +1 -1
- package/dist/core/state.d.ts +53 -0
- package/dist/core/state.js +82 -0
- package/dist/core/strip.d.ts +13 -16
- package/dist/core/strip.js +119 -110
- package/dist/core/style.d.ts +47 -14
- package/dist/core/style.js +57 -28
- package/dist/core/text.d.ts +5 -8
- package/dist/core/text.js +116 -124
- package/dist/core/timeline.d.ts +30 -5
- package/dist/core/timeline.js +43 -7
- package/dist/core/tween.d.ts +1 -1
- package/dist/core/tween.js +5 -3
- package/dist/core/view.d.ts +92 -0
- package/dist/core/view.js +83 -0
- package/dist/host/app.d.ts +49 -21
- package/dist/host/app.js +71 -37
- package/dist/host/input.js +3 -4
- package/dist/index.d.ts +24 -12
- package/dist/index.js +9 -4
- package/dist/node/traceback.js +4 -3
- package/dist/renderables/align.d.ts +2 -3
- package/dist/renderables/align.js +0 -3
- package/dist/renderables/change.d.ts +42 -0
- package/dist/renderables/change.js +112 -0
- package/dist/renderables/columns.d.ts +3 -4
- package/dist/renderables/columns.js +2 -7
- package/dist/renderables/constrain.d.ts +2 -3
- package/dist/renderables/constrain.js +0 -3
- package/dist/renderables/effect.d.ts +132 -54
- package/dist/renderables/effect.js +188 -115
- package/dist/renderables/effects.d.ts +122 -11
- package/dist/renderables/effects.js +238 -94
- package/dist/renderables/embed.d.ts +1 -1
- package/dist/renderables/embed.js +16 -8
- package/dist/renderables/flexStrip.d.ts +2 -3
- package/dist/renderables/flexStrip.js +0 -4
- package/dist/renderables/frames.d.ts +35 -54
- package/dist/renderables/frames.js +43 -58
- package/dist/renderables/glyph-effect.d.ts +158 -0
- package/dist/renderables/glyph-effect.js +237 -0
- package/dist/renderables/glyph-effects.d.ts +147 -0
- package/dist/renderables/glyph-effects.js +283 -0
- package/dist/renderables/group.d.ts +3 -4
- package/dist/renderables/group.js +0 -3
- package/dist/renderables/layout.d.ts +12 -12
- package/dist/renderables/layout.js +40 -46
- package/dist/renderables/lifecycle.d.ts +77 -0
- package/dist/renderables/lifecycle.js +132 -0
- package/dist/renderables/list-view.d.ts +4 -5
- package/dist/renderables/list-view.js +9 -13
- package/dist/renderables/live.d.ts +36 -21
- package/dist/renderables/live.js +88 -41
- package/dist/renderables/markdown-parse.d.ts +4 -4
- package/dist/renderables/markdown-parse.js +52 -46
- package/dist/renderables/markdown-url.js +11 -11
- package/dist/renderables/padding.d.ts +4 -5
- package/dist/renderables/padding.js +0 -3
- package/dist/renderables/panel.d.ts +14 -16
- package/dist/renderables/panel.js +6 -14
- package/dist/renderables/progress.d.ts +83 -34
- package/dist/renderables/progress.js +182 -95
- package/dist/renderables/progressBar.d.ts +22 -12
- package/dist/renderables/progressBar.js +32 -9
- package/dist/renderables/prompt.js +3 -2
- package/dist/renderables/python-space.js +2 -1
- package/dist/renderables/scroll.d.ts +50 -0
- package/dist/renderables/scroll.js +106 -0
- package/dist/renderables/spinner.d.ts +35 -14
- package/dist/renderables/spinner.js +65 -25
- package/dist/renderables/status.js +1 -1
- package/dist/renderables/syntax.d.ts +10 -9
- package/dist/renderables/syntax.js +3 -2
- package/dist/renderables/table.d.ts +34 -40
- package/dist/renderables/table.js +41 -63
- package/dist/renderables/traceback.d.ts +2 -2
- package/dist/renderables/traceback.js +6 -5
- package/dist/renderables/tree.d.ts +27 -25
- package/dist/renderables/tree.js +104 -58
- package/dist/renderables/viewport.d.ts +75 -44
- package/dist/renderables/viewport.js +145 -82
- package/dist/template-bindings/animation-funcs.d.ts +78 -9
- package/dist/template-bindings/animation-funcs.js +132 -14
- package/dist/template-bindings/index.d.ts +5 -5
- package/dist/template-bindings/index.js +5 -5
- package/dist/themes/ramp.d.ts +46 -3
- package/dist/themes/ramp.js +61 -4
- package/dist/themes/registry.js +5 -7
- package/dist/widgets/button.js +3 -2
- package/dist/widgets/checkbox.js +4 -3
- package/dist/widgets/dropdown.d.ts +3 -1
- package/dist/widgets/dropdown.js +20 -15
- package/dist/widgets/event-router.js +4 -2
- package/dist/widgets/focus-manager.d.ts +2 -0
- package/dist/widgets/focus-manager.js +6 -4
- package/dist/widgets/hit.d.ts +6 -0
- package/dist/widgets/hit.js +8 -1
- package/dist/widgets/observable.d.ts +23 -0
- package/dist/widgets/observable.js +43 -0
- package/dist/widgets/slider.d.ts +1 -1
- package/dist/widgets/slider.js +12 -3
- package/dist/widgets/static-item.d.ts +2 -3
- package/dist/widgets/static-item.js +0 -3
- package/dist/widgets/text-input.d.ts +28 -32
- package/dist/widgets/text-input.js +360 -346
- package/dist/widgets/themed-widget.js +2 -1
- package/dist/widgets/toggle.js +4 -3
- package/dist/widgets/types.d.ts +10 -4
- package/dist/widgets/types.js +8 -2
- package/dist/widgets/widget-app.d.ts +14 -4
- package/dist/widgets/widget-app.js +36 -7
- package/dist/widgets/widget-base.d.ts +2 -2
- package/dist/widgets/widget-base.js +29 -18
- package/package.json +8 -1
|
@@ -11,6 +11,7 @@ import { Padding, normalizePadding } from "./padding.js";
|
|
|
11
11
|
import { exactWeights, ratioDistribute, ratioReduce } from "./ratio.js";
|
|
12
12
|
import { getStyle, render, stackedHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
13
13
|
import { Measurement } from "../core/measure.js";
|
|
14
|
+
import { ListState, State } from "../core/state.js";
|
|
14
15
|
/** A `want` no budget can satisfy: the column takes every cell its weight earns. */
|
|
15
16
|
const UNBOUNDED = Number.MAX_SAFE_INTEGER;
|
|
16
17
|
/**
|
|
@@ -256,10 +257,9 @@ export class Column {
|
|
|
256
257
|
noWrap;
|
|
257
258
|
overflow;
|
|
258
259
|
highlight;
|
|
259
|
-
_cells;
|
|
260
260
|
constructor(options) {
|
|
261
|
-
this.
|
|
262
|
-
this.
|
|
261
|
+
this._header = new State(embed(options?.header));
|
|
262
|
+
this._footer = new State(embed(options?.footer));
|
|
263
263
|
this.headerStyle = options?.headerStyle ?? NULL_STYLE;
|
|
264
264
|
this.footerStyle = options?.footerStyle ?? NULL_STYLE;
|
|
265
265
|
this.style = options?.style ?? NULL_STYLE;
|
|
@@ -271,7 +271,6 @@ export class Column {
|
|
|
271
271
|
this.noWrap = options?.noWrap ?? false;
|
|
272
272
|
this.overflow = options?.overflow ?? "ellipsis";
|
|
273
273
|
this.highlight = options?.highlight ?? false;
|
|
274
|
-
this._cells = [];
|
|
275
274
|
}
|
|
276
275
|
/**
|
|
277
276
|
* The two stamped cells, stamped on assignment rather than at the constructor.
|
|
@@ -291,30 +290,30 @@ export class Column {
|
|
|
291
290
|
* which is why the setters take `undefined` rather than defaulting around it.
|
|
292
291
|
*/
|
|
293
292
|
get header() {
|
|
294
|
-
return this._header;
|
|
293
|
+
return this._header.get();
|
|
295
294
|
}
|
|
296
295
|
set header(content) {
|
|
297
|
-
this._header
|
|
296
|
+
this._header.set(embed(content));
|
|
298
297
|
}
|
|
299
298
|
get footer() {
|
|
300
|
-
return this._footer;
|
|
299
|
+
return this._footer.get();
|
|
301
300
|
}
|
|
302
301
|
set footer(content) {
|
|
303
|
-
this._footer
|
|
302
|
+
this._footer.set(embed(content));
|
|
304
303
|
}
|
|
305
304
|
get flexible() {
|
|
306
305
|
return columnShare(this) > 0;
|
|
307
306
|
}
|
|
308
|
-
/** @internal */
|
|
309
|
-
addCell(cell) {
|
|
310
|
-
this._cells.push(cell);
|
|
311
|
-
}
|
|
312
|
-
/** @internal */
|
|
313
|
-
getCells() {
|
|
314
|
-
return this._cells;
|
|
315
|
-
}
|
|
316
307
|
copy() {
|
|
317
|
-
|
|
308
|
+
return new Column({
|
|
309
|
+
// Shared, not copied, as a row's cells are: `embed` already copied a
|
|
310
|
+
// caller's `RichText`, what it hands back is never changed, and `embed`
|
|
311
|
+
// passes a renderable through.
|
|
312
|
+
header: this.header,
|
|
313
|
+
footer: this.footer,
|
|
314
|
+
headerStyle: this.headerStyle,
|
|
315
|
+
footerStyle: this.footerStyle,
|
|
316
|
+
style: this.style,
|
|
318
317
|
justify: this.justify,
|
|
319
318
|
width: this.width,
|
|
320
319
|
minWidth: this.minWidth,
|
|
@@ -324,14 +323,6 @@ export class Column {
|
|
|
324
323
|
overflow: this.overflow,
|
|
325
324
|
highlight: this.highlight,
|
|
326
325
|
});
|
|
327
|
-
col.headerStyle = this.headerStyle;
|
|
328
|
-
col.footerStyle = this.footerStyle;
|
|
329
|
-
col.style = this.style;
|
|
330
|
-
// Shared, not copied, as a row's cells are: `embed` already copied a
|
|
331
|
-
// caller's `RichText`, and what it hands back is never changed.
|
|
332
|
-
col._header = this._header;
|
|
333
|
-
col._footer = this._footer;
|
|
334
|
-
return col;
|
|
335
326
|
}
|
|
336
327
|
}
|
|
337
328
|
// A row's cell in column `index`: a column the row is short of draws a blank one.
|
|
@@ -339,8 +330,9 @@ function cellAt(row, index) {
|
|
|
339
330
|
return row.cells[index] ?? new EmbeddedText(undefined);
|
|
340
331
|
}
|
|
341
332
|
export class Table {
|
|
342
|
-
|
|
343
|
-
|
|
333
|
+
// Rich's Table grows by addColumn and addRow after it is built.
|
|
334
|
+
_columns = new ListState();
|
|
335
|
+
_rows = new ListState();
|
|
344
336
|
box;
|
|
345
337
|
_title;
|
|
346
338
|
_caption;
|
|
@@ -371,8 +363,6 @@ export class Table {
|
|
|
371
363
|
return this._caption;
|
|
372
364
|
}
|
|
373
365
|
constructor(options) {
|
|
374
|
-
this._columns = [];
|
|
375
|
-
this._rows = [];
|
|
376
366
|
this.box = options?.box !== undefined ? options.box : HEAVY_HEAD;
|
|
377
367
|
// An empty title draws no row, as the reference's `if self.title` draws
|
|
378
368
|
// none; markup that styles nothing is a title, and draws a blank one.
|
|
@@ -405,10 +395,10 @@ export class Table {
|
|
|
405
395
|
this.highlight = options?.highlight ?? false;
|
|
406
396
|
}
|
|
407
397
|
get columns() {
|
|
408
|
-
return this._columns;
|
|
398
|
+
return this._columns.get();
|
|
409
399
|
}
|
|
410
400
|
get rowCount() {
|
|
411
|
-
return this._rows.length;
|
|
401
|
+
return this._rows.get().length;
|
|
412
402
|
}
|
|
413
403
|
addColumn(header, options) {
|
|
414
404
|
const col = new Column({ ...options, header: header ?? options?.header, highlight: options?.highlight ?? this.highlight });
|
|
@@ -437,20 +427,18 @@ export class Table {
|
|
|
437
427
|
// behind. [LAW:no-ambient-temporal-coupling]
|
|
438
428
|
const resolved = cells.map(embed);
|
|
439
429
|
// Auto-create columns if needed
|
|
440
|
-
while (this._columns.length < resolved.length) {
|
|
430
|
+
while (this._columns.get().length < resolved.length) {
|
|
441
431
|
this.addColumn();
|
|
442
432
|
}
|
|
443
|
-
this._rows.push({ cells: resolved, endSection });
|
|
433
|
+
this._rows.push({ cells: resolved, endSection: new State(endSection) });
|
|
444
434
|
return this;
|
|
445
435
|
}
|
|
446
436
|
addSection() {
|
|
447
|
-
|
|
448
|
-
this._rows[this._rows.length - 1].endSection = true;
|
|
449
|
-
}
|
|
437
|
+
this._rows.get().at(-1)?.endSection.set(true);
|
|
450
438
|
return this;
|
|
451
439
|
}
|
|
452
440
|
*render(rawOptions) {
|
|
453
|
-
if (this._columns.length === 0) {
|
|
441
|
+
if (this._columns.get().length === 0) {
|
|
454
442
|
yield Segment.line();
|
|
455
443
|
return;
|
|
456
444
|
}
|
|
@@ -483,17 +471,17 @@ export class Table {
|
|
|
483
471
|
// once per cell, as the reference's `_get_cells` does per column.
|
|
484
472
|
const headerStyle = getStyle(options, this.headerStyle);
|
|
485
473
|
const footerStyle = getStyle(options, this.footerStyle);
|
|
486
|
-
const columnStyles = this._columns.map((c) => getStyle(options, c.style));
|
|
474
|
+
const columnStyles = this._columns.get().map((c) => getStyle(options, c.style));
|
|
487
475
|
const stripes = this.rowStyles.map((s) => getStyle(options, s));
|
|
488
476
|
// Each row's padding above and below, by where it stands among every row
|
|
489
477
|
// the table draws, header and footer included, as the reference counts them.
|
|
490
478
|
const firstBody = this.showHeader ? 1 : 0;
|
|
491
|
-
const lastRow = firstBody + this._rows.length + (this.showFooter ? 1 : 0) - 1;
|
|
479
|
+
const lastRow = firstBody + this._rows.get().length + (this.showFooter ? 1 : 0) - 1;
|
|
492
480
|
const rowPadding = (position) => this._rowPadding(position === 0, position === lastRow);
|
|
493
481
|
// Header row
|
|
494
482
|
if (this.showHeader) {
|
|
495
|
-
const headerCells = this._columns.map((c) => c.header);
|
|
496
|
-
const headerStyles = this._columns.map((c) => headerStyle.add(getStyle(options, c.headerStyle)));
|
|
483
|
+
const headerCells = this._columns.get().map((c) => c.header);
|
|
484
|
+
const headerStyles = this._columns.get().map((c) => headerStyle.add(getStyle(options, c.headerStyle)));
|
|
497
485
|
yield* this._renderRow(options, headerCells, headerStyles, NULL_STYLE, geometry, rowPadding(0), box, "head", border);
|
|
498
486
|
// Header separator
|
|
499
487
|
if (box) {
|
|
@@ -501,15 +489,15 @@ export class Table {
|
|
|
501
489
|
}
|
|
502
490
|
}
|
|
503
491
|
// Data rows
|
|
504
|
-
for (let rowIdx = 0; rowIdx < this._rows.length; rowIdx++) {
|
|
505
|
-
const row = this._rows[rowIdx];
|
|
492
|
+
for (let rowIdx = 0; rowIdx < this._rows.get().length; rowIdx++) {
|
|
493
|
+
const row = this._rows.get()[rowIdx];
|
|
506
494
|
const rowCells = this._rowCells(row);
|
|
507
495
|
const rowStyle = stripes[rowIdx % stripes.length] ?? NULL_STYLE;
|
|
508
496
|
const cellStyles = columnStyles.map((s) => s.add(rowStyle));
|
|
509
497
|
yield* this._renderRow(options, rowCells, cellStyles, rowStyle, geometry, rowPadding(firstBody + rowIdx), box, "row", border);
|
|
510
498
|
// Row separator
|
|
511
|
-
const showSep = this.showLines || row.endSection;
|
|
512
|
-
if (showSep && box && rowIdx < this._rows.length - 1) {
|
|
499
|
+
const showSep = this.showLines || row.endSection.get();
|
|
500
|
+
if (showSep && box && rowIdx < this._rows.get().length - 1) {
|
|
513
501
|
yield* box.getRow(geometry.cellWidths, "row", border, edge);
|
|
514
502
|
}
|
|
515
503
|
}
|
|
@@ -518,8 +506,8 @@ export class Table {
|
|
|
518
506
|
if (box) {
|
|
519
507
|
yield* box.getRow(geometry.cellWidths, "foot", border, edge);
|
|
520
508
|
}
|
|
521
|
-
const footerCells = this._columns.map((c) => c.footer);
|
|
522
|
-
const footerStyles = this._columns.map((c) => footerStyle.add(getStyle(options, c.footerStyle)));
|
|
509
|
+
const footerCells = this._columns.get().map((c) => c.footer);
|
|
510
|
+
const footerStyles = this._columns.get().map((c) => footerStyle.add(getStyle(options, c.footerStyle)));
|
|
523
511
|
yield* this._renderRow(options, footerCells, footerStyles, NULL_STYLE, geometry, rowPadding(lastRow), box, "foot", border);
|
|
524
512
|
}
|
|
525
513
|
// Bottom border
|
|
@@ -644,9 +632,9 @@ export class Table {
|
|
|
644
632
|
const frame = this._frame();
|
|
645
633
|
const inner = {
|
|
646
634
|
...options,
|
|
647
|
-
maxWidth: Math.max(0, outerWidth - frame.edge * 2 - frame.divider * Math.max(0, this._columns.length - 1)),
|
|
635
|
+
maxWidth: Math.max(0, outerWidth - frame.edge * 2 - frame.divider * Math.max(0, this._columns.get().length - 1)),
|
|
648
636
|
};
|
|
649
|
-
return this._columns.map((col, index) => {
|
|
637
|
+
return this._columns.get().map((col, index) => {
|
|
650
638
|
const sizing = this._columnSizing(col, index, inner, this._columnPadding(index));
|
|
651
639
|
const floor = sizing.want === 0 ? 1 : 0;
|
|
652
640
|
return { ...sizing, minimum: sizing.minimum + floor, want: sizing.want + floor, floor };
|
|
@@ -752,7 +740,7 @@ export class Table {
|
|
|
752
740
|
_columnPadding(index) {
|
|
753
741
|
const [, right, , left] = this.padding;
|
|
754
742
|
const first = index === 0;
|
|
755
|
-
const last = index === this._columns.length - 1;
|
|
743
|
+
const last = index === this._columns.get().length - 1;
|
|
756
744
|
const collapsed = this.collapsePadding && !first ? Math.max(0, left - right) : left;
|
|
757
745
|
return [!this.padEdge && first ? 0 : collapsed, !this.padEdge && last ? 0 : right];
|
|
758
746
|
}
|
|
@@ -761,18 +749,8 @@ export class Table {
|
|
|
761
749
|
const collapsed = this.collapsePadding && !last ? Math.max(0, top - bottom) : bottom;
|
|
762
750
|
return [!this.padEdge && first ? 0 : top, !this.padEdge && last ? 0 : collapsed];
|
|
763
751
|
}
|
|
764
|
-
// Every cell `render` draws, in the order it draws them: row by row, the
|
|
765
|
-
// header and footer when it shows them.
|
|
766
|
-
*contents() {
|
|
767
|
-
if (this.showHeader)
|
|
768
|
-
yield* this._columns.map((c) => c.header);
|
|
769
|
-
for (const row of this._rows)
|
|
770
|
-
yield* this._rowCells(row);
|
|
771
|
-
if (this.showFooter)
|
|
772
|
-
yield* this._columns.map((c) => c.footer);
|
|
773
|
-
}
|
|
774
752
|
_rowCells(row) {
|
|
775
|
-
return this._columns.map((_, index) => cellAt(row, index));
|
|
753
|
+
return this._columns.get().map((_, index) => cellAt(row, index));
|
|
776
754
|
}
|
|
777
755
|
/**
|
|
778
756
|
* Every cell column `index` draws, in draw order.
|
|
@@ -788,7 +766,7 @@ export class Table {
|
|
|
788
766
|
*_columnCells(col, index) {
|
|
789
767
|
if (this.showHeader)
|
|
790
768
|
yield col.header;
|
|
791
|
-
for (const row of this._rows)
|
|
769
|
+
for (const row of this._rows.get())
|
|
792
770
|
yield cellAt(row, index);
|
|
793
771
|
if (this.showFooter)
|
|
794
772
|
yield col.footer;
|
|
@@ -850,7 +828,7 @@ export class Table {
|
|
|
850
828
|
// width could not seat are absent from `cellWidths` and so are never
|
|
851
829
|
// rendered at all.
|
|
852
830
|
const cellLines = cellWidths.map((cellWidth, index) => {
|
|
853
|
-
const col = this._columns[index];
|
|
831
|
+
const col = this._columns.get()[index];
|
|
854
832
|
const [left, right] = this._columnPadding(index);
|
|
855
833
|
// [LAW:single-enforcer] The padding on every side is part of the cell,
|
|
856
834
|
// drawn by `Padding` as the reference's `_get_cells` wraps each cell in
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
import { Segment } from "../core/segment.js";
|
|
19
19
|
import { type Renderable, type RenderOptions } from "../core/protocol.js";
|
|
20
20
|
export interface TracebackOptions {
|
|
21
|
-
suppress?: string[];
|
|
21
|
+
suppress?: readonly string[];
|
|
22
22
|
maxFrames?: number;
|
|
23
23
|
}
|
|
24
24
|
export declare class Traceback implements Renderable {
|
|
25
25
|
/** What was caught, as it was handed in. */
|
|
26
26
|
readonly error: unknown;
|
|
27
27
|
readonly maxFrames: number;
|
|
28
|
-
readonly suppress: string[];
|
|
28
|
+
readonly suppress: readonly string[];
|
|
29
29
|
private readonly caught;
|
|
30
30
|
constructor(error: unknown, options?: TracebackOptions);
|
|
31
31
|
render(options: RenderOptions): Iterable<Segment>;
|
|
@@ -20,6 +20,7 @@ import { RichText } from "../core/text.js";
|
|
|
20
20
|
import { Pretty, UNSEEN_DATA_BOUNDS } from "../core/pretty.js";
|
|
21
21
|
import { render } from "../core/protocol.js";
|
|
22
22
|
import { paddedContentWidth } from "./padding.js";
|
|
23
|
+
import { pattern } from "../core/pattern.js";
|
|
23
24
|
/**
|
|
24
25
|
* [LAW:parse-dont-validate] The checkpoint between whatever a `catch` caught —
|
|
25
26
|
* JavaScript lets a program throw any value — and the report. Anything with a
|
|
@@ -141,13 +142,13 @@ function parseStack(stack, name, message) {
|
|
|
141
142
|
* any name, so it is read and dropped; the lookahead keeps `at async (…)` — a
|
|
142
143
|
* function named `async` — a name.
|
|
143
144
|
*/
|
|
144
|
-
const V8_FRAME = /^at\s+(?:async\s+(?!\())?(.*)
|
|
145
|
+
const V8_FRAME = pattern(/^at\s+(?:async\s+(?!\())?(.*)$/);
|
|
145
146
|
/**
|
|
146
147
|
* SpiderMonkey and JavaScriptCore write `[fn]@location`, Firefox prefixing an
|
|
147
148
|
* awaited call's name with a cause up to a `*` (`async*`, `promise callback*`),
|
|
148
149
|
* which is dropped as V8's `async` is.
|
|
149
150
|
*/
|
|
150
|
-
const AT_SIGN_FRAME = /^(?:[^@]*\*)?([^@]*)@(.*)
|
|
151
|
+
const AT_SIGN_FRAME = pattern(/^(?:[^@]*\*)?([^@]*)@(.*)$/);
|
|
151
152
|
function parseFrame(text) {
|
|
152
153
|
const v8 = V8_FRAME.exec(text);
|
|
153
154
|
if (v8) {
|
|
@@ -188,9 +189,9 @@ function openingParen(body) {
|
|
|
188
189
|
* the code to it (`file line 8 > eval:1:1`).
|
|
189
190
|
*/
|
|
190
191
|
const LOCATIONS = [
|
|
191
|
-
/^eval at [^]*?\(([^()]+):(\d+):\d+\)
|
|
192
|
-
/^(.*?) line (\d+) > (?:eval|Function)
|
|
193
|
-
/^(.*?):(\d+)(?::\d+)
|
|
192
|
+
pattern(/^eval at [^]*?\(([^()]+):(\d+):\d+\)/),
|
|
193
|
+
pattern(/^(.*?) line (\d+) > (?:eval|Function)/),
|
|
194
|
+
pattern(/^(.*?):(\d+)(?::\d+)?$/),
|
|
194
195
|
];
|
|
195
196
|
function locate(fn, location) {
|
|
196
197
|
for (const shape of LOCATIONS) {
|
|
@@ -4,24 +4,26 @@
|
|
|
4
4
|
import { Segment } from "../core/segment.js";
|
|
5
5
|
import { Style } from "../core/style.js";
|
|
6
6
|
import { RichText } from "../core/text.js";
|
|
7
|
-
import type {
|
|
7
|
+
import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
|
|
8
8
|
export interface TreeOptions {
|
|
9
9
|
expanded?: boolean;
|
|
10
10
|
hideRoot?: boolean;
|
|
11
11
|
guide_style?: string | Style;
|
|
12
12
|
style?: string | Style;
|
|
13
13
|
}
|
|
14
|
-
export declare class Tree implements Renderable, Measurable
|
|
14
|
+
export declare class Tree implements Renderable, Measurable {
|
|
15
15
|
readonly label: Renderable;
|
|
16
|
-
readonly
|
|
17
|
-
|
|
16
|
+
private readonly _children;
|
|
17
|
+
private readonly _expanded;
|
|
18
18
|
readonly hideRoot: boolean;
|
|
19
19
|
readonly guideStyle: string | Style;
|
|
20
20
|
readonly style: string | Style;
|
|
21
21
|
constructor(label: string | RichText | Renderable, options?: TreeOptions);
|
|
22
22
|
add(label: string | RichText | Renderable, options?: TreeOptions): Tree;
|
|
23
|
+
get children(): readonly Tree[];
|
|
24
|
+
get expanded(): boolean;
|
|
25
|
+
set expanded(value: boolean);
|
|
23
26
|
render(rawOptions: RenderOptions): Iterable<Segment>;
|
|
24
|
-
contents(): Iterable<Renderable>;
|
|
25
27
|
/**
|
|
26
28
|
* The rows `render` and `measure` both read, entered in exactly one place so
|
|
27
29
|
* the two cannot start the walk differently. A hidden root takes its own row
|
|
@@ -30,7 +32,8 @@ export declare class Tree implements Renderable, Measurable, Composite {
|
|
|
30
32
|
*/
|
|
31
33
|
private _walk;
|
|
32
34
|
/**
|
|
33
|
-
*
|
|
35
|
+
* This node's subtree walked into the rows it emits, one row per label,
|
|
36
|
+
* guides first, each guide column counted from this node.
|
|
34
37
|
*
|
|
35
38
|
* [LAW:one-source-of-truth] The walk that decides which rows exist and what
|
|
36
39
|
* leads them is here and only here. `render` turns each row into segments and
|
|
@@ -39,35 +42,34 @@ export declare class Tree implements Renderable, Measurable, Composite {
|
|
|
39
42
|
* them run — which they would the moment a second walk existed.
|
|
40
43
|
*
|
|
41
44
|
* [LAW:dataflow-not-control-flow] Every node, at every depth, is walked by
|
|
42
|
-
* this one recursion:
|
|
43
|
-
*
|
|
44
|
-
* style and its guide style are handed down the same way,
|
|
45
|
-
* on the stack its ancestors built.
|
|
45
|
+
* this one recursion: each child's rows get a branch column of their own,
|
|
46
|
+
* settled below the first of them drawn, so no depth is drawn by a rule of
|
|
47
|
+
* its own. A node's style and its guide style are handed down the same way,
|
|
48
|
+
* as one more entry on the stack its ancestors built.
|
|
49
|
+
*
|
|
50
|
+
* The branch is chosen once every child is drawn: a subtree that draws no row
|
|
51
|
+
* takes no place among its siblings, so the last one that draws a row ends
|
|
52
|
+
* the branch, and the branch stands on a subtree's first drawn row — a child
|
|
53
|
+
* that drew nothing above children that did leaves them on its branch, not
|
|
54
|
+
* the sibling's before it. Rich chooses it from the children alone, and
|
|
55
|
+
* draws a fork over a last child that drew nothing — an element not yet
|
|
56
|
+
* arrived, or gone.
|
|
46
57
|
*/
|
|
47
58
|
private _rows;
|
|
59
|
+
/** Each of this node's children walked into its rows, `columns` of guide before each child's own. */
|
|
60
|
+
private _subtrees;
|
|
48
61
|
/**
|
|
49
|
-
* One row of the tree:
|
|
50
|
-
* the label's lines led by the guides.
|
|
51
|
-
*
|
|
52
|
-
* [LAW:single-enforcer] The row's width is divided in exactly one place, here.
|
|
53
|
-
* Before this, each call site emitted its guides and then handed the label
|
|
54
|
-
* `options` unchanged — the label was told it had the whole outer width while
|
|
55
|
-
* the guides had already spent four cells of it, so a tree at maxWidth 5
|
|
56
|
-
* emitted rows of 9. That is the same defect Panel and Table were fixed for,
|
|
57
|
-
* and the same fix: divide the requested width once and let every part read
|
|
58
|
-
* its share off the division. `options` arrives parsed from `render`.
|
|
62
|
+
* One row of the tree: each of the label's lines led by the guides.
|
|
59
63
|
*
|
|
60
|
-
* [LAW:single-enforcer] Tree
|
|
64
|
+
* [LAW:single-enforcer] Tree owns its row boundaries here rather than
|
|
61
65
|
* depending on label renderables to invent trailing newlines: the label's
|
|
62
66
|
* output is cut into lines and every line ends where Tree ends it. A label
|
|
63
67
|
* that emits nothing — one left no column, say — has no row, and one
|
|
64
68
|
* that ends in a newline of its own gets the blank row that line is, as in
|
|
65
69
|
* Rich — padded to the label's width when the label is justified.
|
|
66
70
|
*
|
|
67
|
-
* The node's
|
|
68
|
-
*
|
|
69
|
-
* guides, so a row with a background is filled from its first guide to the
|
|
70
|
-
* end of its label.
|
|
71
|
+
* The node's background alone reaches under the guides, so a row with a
|
|
72
|
+
* background is filled from its first guide to the end of its label.
|
|
71
73
|
*/
|
|
72
74
|
private _renderRow;
|
|
73
75
|
measure(options: RenderOptions): {
|
package/dist/renderables/tree.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Style, NULL_STYLE } from "../core/style.js";
|
|
|
7
7
|
import { embed } from "./embed.js";
|
|
8
8
|
import { drawable, getStyle, render, stackedHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
9
9
|
import { Measurement } from "../core/measure.js";
|
|
10
|
+
import { ListState, State } from "../core/state.js";
|
|
10
11
|
/** Every glyph of every set is this wide, so a row's guides are as wide however they are drawn. */
|
|
11
12
|
const GUIDE_CELLS = 4;
|
|
12
13
|
const LIGHT_GUIDES = { fork: "├── ", end: "└── ", continue: "│ ", space: " " };
|
|
@@ -28,21 +29,38 @@ function glyphsFor(options, style) {
|
|
|
28
29
|
const weight = style.bold === true ? HEAVY_GUIDES : style.underline2 === true ? DOUBLE_GUIDES : LIGHT_GUIDES;
|
|
29
30
|
return drawable(options, weight, ASCII_GUIDES, guideGlyphs);
|
|
30
31
|
}
|
|
32
|
+
/** A row a label is known to draw, unread. */
|
|
33
|
+
const A_ROW = [[]];
|
|
34
|
+
/** The cells a row with `columns` of guide leaves its label. */
|
|
35
|
+
const labelRoom = (options, columns) => Math.max(0, options.maxWidth - columns * GUIDE_CELLS);
|
|
36
|
+
/**
|
|
37
|
+
* `label` drawn in `width` cells, as both `render` and `measure` draw it, so
|
|
38
|
+
* whether it draws a row is answered once. Cropped, so a label that ignores the
|
|
39
|
+
* width it is handed cannot push the row past the offer the guides were fitted
|
|
40
|
+
* into. No highlighter: a label is drawn plain, as Rich's `Tree` hands its
|
|
41
|
+
* labels `highlight=False`.
|
|
42
|
+
*/
|
|
43
|
+
function drawLabel(options, label, width) {
|
|
44
|
+
const labelOptions = { ...options, highlight: false, maxWidth: width, height: stackedHeight(options.height) };
|
|
45
|
+
return Segment.cropLines(render(label, labelOptions), width);
|
|
46
|
+
}
|
|
31
47
|
/** How wide a label wants to be. */
|
|
32
48
|
function labelWidth(options, label) {
|
|
33
49
|
return Measurement.get(options, label).maximum;
|
|
34
50
|
}
|
|
51
|
+
/** Whether a row is drawn: a subtree with one takes a place among its siblings. */
|
|
52
|
+
const isDrawn = (row) => row.lines.length > 0;
|
|
35
53
|
export class Tree {
|
|
36
54
|
label;
|
|
37
|
-
|
|
38
|
-
|
|
55
|
+
// Rich's Tree grows by `add` and is expanded or collapsed after it is built.
|
|
56
|
+
_children = new ListState();
|
|
57
|
+
_expanded;
|
|
39
58
|
hideRoot;
|
|
40
59
|
guideStyle;
|
|
41
60
|
style;
|
|
42
61
|
constructor(label, options) {
|
|
43
62
|
this.label = embed(label);
|
|
44
|
-
this.
|
|
45
|
-
this.expanded = options?.expanded !== false;
|
|
63
|
+
this._expanded = new State(options?.expanded !== false);
|
|
46
64
|
this.hideRoot = options?.hideRoot ?? false;
|
|
47
65
|
this.guideStyle = options?.guide_style ?? NULL_STYLE;
|
|
48
66
|
this.style = options?.style ?? NULL_STYLE;
|
|
@@ -53,33 +71,51 @@ export class Tree {
|
|
|
53
71
|
style: options?.style,
|
|
54
72
|
expanded: options?.expanded,
|
|
55
73
|
});
|
|
56
|
-
this.
|
|
74
|
+
this._children.push(child);
|
|
57
75
|
return child;
|
|
58
76
|
}
|
|
77
|
+
get children() {
|
|
78
|
+
return this._children.get();
|
|
79
|
+
}
|
|
80
|
+
get expanded() {
|
|
81
|
+
return this._expanded.get();
|
|
82
|
+
}
|
|
83
|
+
set expanded(value) {
|
|
84
|
+
this._expanded.set(value);
|
|
85
|
+
}
|
|
59
86
|
*render(rawOptions) {
|
|
60
87
|
const options = withBoundedWidth(rawOptions, this);
|
|
61
|
-
|
|
62
|
-
|
|
88
|
+
const resolve = (styles) => Style.combine(styles.map((style) => getStyle(options, style)));
|
|
89
|
+
// [LAW:single-enforcer] The row's width is divided in exactly one place,
|
|
90
|
+
// here. Before this, each call site emitted its guides and then handed the
|
|
91
|
+
// label `options` unchanged — the label was told it had the whole outer
|
|
92
|
+
// width while the guides had already spent four cells of it, so a tree at
|
|
93
|
+
// maxWidth 5 emitted rows of 9. That is the same defect Panel and Table
|
|
94
|
+
// were fixed for, and the same fix: divide the requested width once and
|
|
95
|
+
// let every part read its share off the division.
|
|
96
|
+
//
|
|
97
|
+
// The guides are cropped by `cellFit`, the label by `drawLabel`. The
|
|
98
|
+
// node's style sits under the label's own, as Rich's `Styled` puts it, so
|
|
99
|
+
// markup in the label still wins.
|
|
100
|
+
const draw = (label, styles, columns) => Segment.splitLines(Segment.applyStyle(drawLabel(options, label, labelRoom(options, columns)), resolve(styles)));
|
|
101
|
+
for (const row of this._walk(draw)) {
|
|
102
|
+
yield* this._renderRow(options, resolve, row);
|
|
63
103
|
}
|
|
64
104
|
}
|
|
65
|
-
// Every label `render` draws, from the same walk. Not `children`, which is
|
|
66
|
-
// the subtrees, as Rich names them.
|
|
67
|
-
*contents() {
|
|
68
|
-
for (const row of this._walk())
|
|
69
|
-
yield row.label;
|
|
70
|
-
}
|
|
71
105
|
/**
|
|
72
106
|
* The rows `render` and `measure` both read, entered in exactly one place so
|
|
73
107
|
* the two cannot start the walk differently. A hidden root takes its own row
|
|
74
108
|
* and the column its children hang from with it, as Rich does — its children
|
|
75
109
|
* stand at the left edge, and its guide style still reaches their guides.
|
|
76
110
|
*/
|
|
77
|
-
_walk() {
|
|
78
|
-
|
|
79
|
-
|
|
111
|
+
_walk(draw) {
|
|
112
|
+
return this.hideRoot
|
|
113
|
+
? this._subtrees(0, draw, [this.guideStyle], [this.style]).flat()
|
|
114
|
+
: this._rows(0, draw, [], []);
|
|
80
115
|
}
|
|
81
116
|
/**
|
|
82
|
-
*
|
|
117
|
+
* This node's subtree walked into the rows it emits, one row per label,
|
|
118
|
+
* guides first, each guide column counted from this node.
|
|
83
119
|
*
|
|
84
120
|
* [LAW:one-source-of-truth] The walk that decides which rows exist and what
|
|
85
121
|
* leads them is here and only here. `render` turns each row into segments and
|
|
@@ -88,50 +124,54 @@ export class Tree {
|
|
|
88
124
|
* them run — which they would the moment a second walk existed.
|
|
89
125
|
*
|
|
90
126
|
* [LAW:dataflow-not-control-flow] Every node, at every depth, is walked by
|
|
91
|
-
* this one recursion:
|
|
92
|
-
*
|
|
93
|
-
* style and its guide style are handed down the same way,
|
|
94
|
-
* on the stack its ancestors built.
|
|
127
|
+
* this one recursion: each child's rows get a branch column of their own,
|
|
128
|
+
* settled below the first of them drawn, so no depth is drawn by a rule of
|
|
129
|
+
* its own. A node's style and its guide style are handed down the same way,
|
|
130
|
+
* as one more entry on the stack its ancestors built.
|
|
131
|
+
*
|
|
132
|
+
* The branch is chosen once every child is drawn: a subtree that draws no row
|
|
133
|
+
* takes no place among its siblings, so the last one that draws a row ends
|
|
134
|
+
* the branch, and the branch stands on a subtree's first drawn row — a child
|
|
135
|
+
* that drew nothing above children that did leaves them on its branch, not
|
|
136
|
+
* the sibling's before it. Rich chooses it from the children alone, and
|
|
137
|
+
* draws a fork over a last child that drew nothing — an element not yet
|
|
138
|
+
* arrived, or gone.
|
|
95
139
|
*/
|
|
96
|
-
|
|
140
|
+
_rows(columns, draw, inheritedGuideStyles, inheritedStyles) {
|
|
97
141
|
const styles = [...inheritedStyles, this.style];
|
|
98
|
-
yield { guides, label: this.label, styles };
|
|
99
|
-
const children = this.expanded ? this.children : [];
|
|
100
142
|
const guideStyles = [...inheritedGuideStyles, this.guideStyle];
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
143
|
+
const own = { guides: [], label: this.label, styles, lines: draw(this.label, styles, columns) };
|
|
144
|
+
const subtrees = this._subtrees(columns + 1, draw, guideStyles, styles);
|
|
145
|
+
const firsts = subtrees.map((rows) => rows.findIndex(isDrawn));
|
|
146
|
+
const last = firsts.reduce((found, first, i) => (first >= 0 ? i : found), -1);
|
|
147
|
+
const branched = subtrees.flatMap((rows, i) => {
|
|
148
|
+
const branch = i < last
|
|
149
|
+
? { first: "fork", rest: "continue", styles: guideStyles }
|
|
150
|
+
: { first: "end", rest: "space", styles: guideStyles };
|
|
151
|
+
const settled = { ...branch, first: branch.rest };
|
|
152
|
+
return rows.map((row, r) => ({ ...row, guides: [r === firsts[i] ? branch : settled, ...row.guides] }));
|
|
153
|
+
});
|
|
154
|
+
return [own, ...branched];
|
|
155
|
+
}
|
|
156
|
+
/** Each of this node's children walked into its rows, `columns` of guide before each child's own. */
|
|
157
|
+
_subtrees(columns, draw, guideStyles, styles) {
|
|
158
|
+
const children = this.expanded ? this.children : [];
|
|
159
|
+
return children.map((child) => child._rows(columns, draw, guideStyles, styles));
|
|
108
160
|
}
|
|
109
161
|
/**
|
|
110
|
-
* One row of the tree:
|
|
111
|
-
* the label's lines led by the guides.
|
|
162
|
+
* One row of the tree: each of the label's lines led by the guides.
|
|
112
163
|
*
|
|
113
|
-
* [LAW:single-enforcer]
|
|
114
|
-
* Before this, each call site emitted its guides and then handed the label
|
|
115
|
-
* `options` unchanged — the label was told it had the whole outer width while
|
|
116
|
-
* the guides had already spent four cells of it, so a tree at maxWidth 5
|
|
117
|
-
* emitted rows of 9. That is the same defect Panel and Table were fixed for,
|
|
118
|
-
* and the same fix: divide the requested width once and let every part read
|
|
119
|
-
* its share off the division. `options` arrives parsed from `render`.
|
|
120
|
-
*
|
|
121
|
-
* [LAW:single-enforcer] Tree also owns its row boundaries here rather than
|
|
164
|
+
* [LAW:single-enforcer] Tree owns its row boundaries here rather than
|
|
122
165
|
* depending on label renderables to invent trailing newlines: the label's
|
|
123
166
|
* output is cut into lines and every line ends where Tree ends it. A label
|
|
124
167
|
* that emits nothing — one left no column, say — has no row, and one
|
|
125
168
|
* that ends in a newline of its own gets the blank row that line is, as in
|
|
126
169
|
* Rich — padded to the label's width when the label is justified.
|
|
127
170
|
*
|
|
128
|
-
* The node's
|
|
129
|
-
*
|
|
130
|
-
* guides, so a row with a background is filled from its first guide to the
|
|
131
|
-
* end of its label.
|
|
171
|
+
* The node's background alone reaches under the guides, so a row with a
|
|
172
|
+
* background is filled from its first guide to the end of its label.
|
|
132
173
|
*/
|
|
133
|
-
*_renderRow(options, row) {
|
|
134
|
-
const resolve = (styles) => Style.combine(styles.map((style) => getStyle(options, style)));
|
|
174
|
+
*_renderRow(options, resolve, row) {
|
|
135
175
|
const style = resolve(row.styles);
|
|
136
176
|
const drawn = row.guides.map((guide) => {
|
|
137
177
|
const guideStyle = resolve(guide.styles);
|
|
@@ -140,13 +180,7 @@ export class Tree {
|
|
|
140
180
|
style: style.backgroundStyle.add(guideStyle).add(REMOVE_GUIDE_STYLES),
|
|
141
181
|
};
|
|
142
182
|
});
|
|
143
|
-
const
|
|
144
|
-
// The guides are cropped by `cellFit`; the label is cropped too, so a label
|
|
145
|
-
// that ignores the width it is handed cannot push the row past the offer the
|
|
146
|
-
// guides were fitted into. No highlighter: a label is drawn plain, as
|
|
147
|
-
// Rich's `Tree` hands its labels `highlight=False`.
|
|
148
|
-
const labelOptions = { ...options, highlight: false, maxWidth: width, height: stackedHeight(options.height) };
|
|
149
|
-
const lines = Segment.splitLines(Segment.applyStyle(Segment.cropLines(render(row.label, labelOptions), width), style));
|
|
183
|
+
const lines = row.lines;
|
|
150
184
|
for (let i = 0; i < lines.length; i++) {
|
|
151
185
|
let left = options.maxWidth;
|
|
152
186
|
for (let g = 0; g < row.guides.length; g++) {
|
|
@@ -168,10 +202,22 @@ export class Tree {
|
|
|
168
202
|
// The widest row the walk emits, not the offer. Reported as the offer, a
|
|
169
203
|
// tree told any parent that asked "I want all of it" — so `Panel` in fit
|
|
170
204
|
// mode drew a 40-cell frame around nine cells of tree, and an unbounded
|
|
171
|
-
// offer came back unbounded.
|
|
205
|
+
// offer came back unbounded. A row the walk does not draw wants nothing.
|
|
206
|
+
// A label that wants cells draws them, so only one that wants none can
|
|
207
|
+
// draw no row, and only that one is drawn to ask — in the one column it
|
|
208
|
+
// wants, as its row does, because under one column nothing draws. A label
|
|
209
|
+
// of no width that does draw is a blank row, as wide as its guides and that
|
|
210
|
+
// column: the width the tree, drawn at what this reports, hands it.
|
|
211
|
+
const widths = new Map();
|
|
212
|
+
const widthOf = (label) => {
|
|
213
|
+
const width = widths.get(label) ?? labelWidth(parsed, label);
|
|
214
|
+
widths.set(label, width);
|
|
215
|
+
return width;
|
|
216
|
+
};
|
|
217
|
+
const draw = (label, _styles, columns) => widthOf(label) > 0 ? A_ROW : Segment.splitLines(drawLabel(parsed, label, Math.min(labelRoom(parsed, columns), 1)));
|
|
172
218
|
let natural = 0;
|
|
173
|
-
for (const row of this._walk()) {
|
|
174
|
-
natural = Math.max(natural, row.guides.length * GUIDE_CELLS +
|
|
219
|
+
for (const row of this._walk(draw).filter(isDrawn)) {
|
|
220
|
+
natural = Math.max(natural, row.guides.length * GUIDE_CELLS + Math.max(1, widthOf(row.label)));
|
|
175
221
|
}
|
|
176
222
|
const maximum = Math.min(natural, ceiling);
|
|
177
223
|
// A row is its guides plus at least one cell of label — but never more than
|