@promptctl/rich-js 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +9 -7
  2. package/dist/core/box.d.ts +3 -1
  3. package/dist/core/box.js +6 -13
  4. package/dist/core/cells.d.ts +61 -28
  5. package/dist/core/cells.js +171 -124
  6. package/dist/core/clock.d.ts +48 -0
  7. package/dist/core/clock.js +42 -0
  8. package/dist/core/color.d.ts +98 -18
  9. package/dist/core/color.js +331 -144
  10. package/dist/core/console.d.ts +38 -7
  11. package/dist/core/console.js +159 -90
  12. package/dist/core/easing.d.ts +86 -0
  13. package/dist/core/easing.js +171 -0
  14. package/dist/core/env.d.ts +16 -0
  15. package/dist/core/env.js +16 -0
  16. package/dist/core/export-html.js +1 -4
  17. package/dist/core/export-lines.d.ts +22 -13
  18. package/dist/core/export-lines.js +37 -19
  19. package/dist/core/export-svg.d.ts +49 -0
  20. package/dist/core/export-svg.js +213 -0
  21. package/dist/core/fnv1a.d.ts +10 -0
  22. package/dist/core/fnv1a.js +17 -0
  23. package/dist/core/highlighter.d.ts +2 -2
  24. package/dist/core/highlighter.js +113 -47
  25. package/dist/core/json.js +1 -1
  26. package/dist/core/markup.d.ts +28 -7
  27. package/dist/core/markup.js +235 -312
  28. package/dist/core/measure.d.ts +3 -3
  29. package/dist/core/measure.js +12 -7
  30. package/dist/core/memo.d.ts +19 -0
  31. package/dist/core/memo.js +34 -0
  32. package/dist/core/osc8.d.ts +6 -6
  33. package/dist/core/osc8.js +9 -21
  34. package/dist/core/paint.d.ts +107 -0
  35. package/dist/core/paint.js +150 -0
  36. package/dist/core/place.d.ts +1 -1
  37. package/dist/core/place.js +4 -6
  38. package/dist/core/pretty.d.ts +105 -2
  39. package/dist/core/pretty.js +527 -97
  40. package/dist/core/protocol.d.ts +41 -3
  41. package/dist/core/protocol.js +20 -0
  42. package/dist/core/render.d.ts +33 -30
  43. package/dist/core/render.js +43 -83
  44. package/dist/core/segment.d.ts +6 -5
  45. package/dist/core/segment.js +11 -15
  46. package/dist/core/strip.d.ts +13 -2
  47. package/dist/core/strip.js +44 -23
  48. package/dist/core/style.d.ts +27 -7
  49. package/dist/core/style.js +97 -34
  50. package/dist/core/text.d.ts +43 -16
  51. package/dist/core/text.js +180 -59
  52. package/dist/core/wrap.d.ts +5 -0
  53. package/dist/core/wrap.js +16 -3
  54. package/dist/host/app.d.ts +31 -19
  55. package/dist/host/app.js +90 -60
  56. package/dist/host/index.d.ts +2 -1
  57. package/dist/host/terminal-host.d.ts +3 -2
  58. package/dist/index.d.ts +17 -10
  59. package/dist/index.js +11 -4
  60. package/dist/node/prompt.d.ts +13 -0
  61. package/dist/node/prompt.js +30 -9
  62. package/dist/node/save.d.ts +4 -3
  63. package/dist/node/save.js +6 -3
  64. package/dist/node/terminal-host.d.ts +3 -2
  65. package/dist/node/traceback.d.ts +2 -1
  66. package/dist/node/traceback.js +3 -39
  67. package/dist/renderables/align.js +2 -9
  68. package/dist/renderables/columns.d.ts +45 -29
  69. package/dist/renderables/columns.js +142 -103
  70. package/dist/renderables/constrain.js +6 -12
  71. package/dist/renderables/effect.d.ts +98 -0
  72. package/dist/renderables/effect.js +160 -0
  73. package/dist/renderables/embed.d.ts +87 -30
  74. package/dist/renderables/embed.js +134 -45
  75. package/dist/renderables/group.d.ts +6 -3
  76. package/dist/renderables/group.js +5 -0
  77. package/dist/renderables/layout.d.ts +6 -23
  78. package/dist/renderables/layout.js +24 -113
  79. package/dist/renderables/live.d.ts +40 -16
  80. package/dist/renderables/live.js +96 -83
  81. package/dist/renderables/markdown.d.ts +16 -2
  82. package/dist/renderables/markdown.js +521 -118
  83. package/dist/renderables/padding.d.ts +22 -1
  84. package/dist/renderables/padding.js +30 -24
  85. package/dist/renderables/panel.d.ts +31 -33
  86. package/dist/renderables/panel.js +137 -141
  87. package/dist/renderables/progress.d.ts +104 -16
  88. package/dist/renderables/progress.js +205 -81
  89. package/dist/renderables/progressBar.d.ts +2 -2
  90. package/dist/renderables/progressBar.js +30 -19
  91. package/dist/renderables/prompt.d.ts +37 -15
  92. package/dist/renderables/prompt.js +125 -85
  93. package/dist/renderables/ratio.d.ts +123 -0
  94. package/dist/renderables/ratio.js +213 -0
  95. package/dist/renderables/rule.d.ts +1 -0
  96. package/dist/renderables/rule.js +44 -48
  97. package/dist/renderables/spinner.d.ts +28 -2
  98. package/dist/renderables/spinner.js +52 -19
  99. package/dist/renderables/status.d.ts +13 -7
  100. package/dist/renderables/status.js +15 -29
  101. package/dist/renderables/syntax.d.ts +37 -7
  102. package/dist/renderables/syntax.js +134 -83
  103. package/dist/renderables/table.d.ts +58 -20
  104. package/dist/renderables/table.js +370 -192
  105. package/dist/renderables/traceback.d.ts +5 -4
  106. package/dist/renderables/traceback.js +239 -68
  107. package/dist/renderables/tree.d.ts +10 -2
  108. package/dist/renderables/tree.js +61 -34
  109. package/dist/renderables/viewport.js +2 -4
  110. package/dist/template-bindings/color-funcs.d.ts +31 -9
  111. package/dist/template-bindings/color-funcs.js +40 -14
  112. package/dist/template-bindings/helpers.d.ts +4 -2
  113. package/dist/template-bindings/helpers.js +5 -20
  114. package/dist/template-bindings/index.d.ts +29 -17
  115. package/dist/template-bindings/index.js +36 -20
  116. package/dist/template-bindings/palette-funcs.d.ts +3 -1
  117. package/dist/template-bindings/palette-funcs.js +11 -7
  118. package/dist/template-bindings/style-funcs.js +4 -2
  119. package/dist/themes/buildPalette.d.ts +24 -3
  120. package/dist/themes/buildPalette.js +52 -8
  121. package/dist/themes/colorMath.d.ts +29 -17
  122. package/dist/themes/colorMath.js +74 -48
  123. package/dist/themes/colorRef.d.ts +2 -17
  124. package/dist/themes/colorRef.js +13 -31
  125. package/dist/themes/palette.d.ts +24 -0
  126. package/dist/themes/palette.js +46 -3
  127. package/dist/themes/ramp.d.ts +21 -34
  128. package/dist/themes/ramp.js +23 -42
  129. package/dist/themes/registry.js +39 -28
  130. package/dist/widgets/button.d.ts +2 -6
  131. package/dist/widgets/button.js +9 -40
  132. package/dist/widgets/checkbox.d.ts +2 -5
  133. package/dist/widgets/checkbox.js +9 -31
  134. package/dist/widgets/dropdown.d.ts +2 -5
  135. package/dist/widgets/dropdown.js +14 -48
  136. package/dist/widgets/ink.d.ts +22 -0
  137. package/dist/widgets/ink.js +31 -0
  138. package/dist/widgets/slider.d.ts +2 -5
  139. package/dist/widgets/slider.js +11 -34
  140. package/dist/widgets/text-input.d.ts +19 -10
  141. package/dist/widgets/text-input.js +156 -147
  142. package/dist/widgets/themed-widget.d.ts +20 -0
  143. package/dist/widgets/themed-widget.js +89 -0
  144. package/dist/widgets/toggle.d.ts +2 -5
  145. package/dist/widgets/toggle.js +8 -31
  146. package/dist/widgets/widget-app.d.ts +2 -0
  147. package/dist/widgets/widget-app.js +13 -6
  148. package/dist/widgets/widget-base.d.ts +2 -0
  149. package/dist/widgets/widget-base.js +9 -2
  150. package/package.json +11 -1
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Effected — a renderable whose finished cells' colours pass through an
3
+ * effect at a moment `t`.
4
+ *
5
+ * An effect is one pure function: a cell's colours, where the cell is, and a
6
+ * time in, the cell's new colours out. Shimmer, pulse, fade and dissolve are
7
+ * all this one shape, differing by what the function does with its inputs
8
+ * [LAW:one-type-per-behavior]; two effects stack by nesting one wrapper in
9
+ * another, which is the same as calling one after the other.
10
+ *
11
+ * [LAW:no-ambient-temporal-coupling] `t` is a constructor argument, not a
12
+ * clock read. Whoever draws frames builds the view for each frame's `t`, so
13
+ * the same `t` always draws the same bytes, at 30 frames a second or one
14
+ * every two seconds.
15
+ *
16
+ * What an effect may not do is the contract this module holds:
17
+ *
18
+ * - It changes how cells look, never which cells exist or who drew them. The
19
+ * child's text is walked by grapheme cluster, so a wide glyph is one cell
20
+ * to the effect and is never cut, and a run cut out of a segment keeps that
21
+ * segment's link and meta and carries its anchor shifted to the run's first
22
+ * cell (`Style.shiftedBy`), so hit testing still finds the owner.
23
+ * - A cell whose colours come back unchanged keeps its original `ColorSpec`,
24
+ * and a segment whose every cell is unchanged is yielded as it was, so the
25
+ * identity effect is byte-identical. Neighbouring cells given equal colours
26
+ * stay one segment: a segment is cut only where the colours change.
27
+ * - New colours are built with `ColorSpec.fromRgba`, never parsed from a
28
+ * string, so no frame adds an entry to a parse cache.
29
+ * - With no colour output (`colorSystem: null`) there is nothing for an
30
+ * effect to change, and the child's segments pass through untouched.
31
+ *
32
+ * Positions are the child's own: row and column count from the top-left of
33
+ * what the child drew, so a sweep crosses the element, not the screen.
34
+ */
35
+ import { cellLen, graphemes } from "../core/cells.js";
36
+ import { ColorDepth, ColorSpec, SURFACE_BLACK } from "../core/color.js";
37
+ import { fnv1a } from "../core/fnv1a.js";
38
+ import { Measurement } from "../core/measure.js";
39
+ import { Segment } from "../core/segment.js";
40
+ import { Style } from "../core/style.js";
41
+ export class Effected {
42
+ renderable;
43
+ effect;
44
+ t;
45
+ key;
46
+ theme;
47
+ constructor(renderable, effect, options) {
48
+ this.renderable = renderable;
49
+ this.effect = effect;
50
+ if (!Number.isFinite(options.t)) {
51
+ throw new RangeError(`Effected needs a finite t, got ${options.t}`);
52
+ }
53
+ this.t = options.t;
54
+ this.key = options.key;
55
+ this.theme = options.theme;
56
+ }
57
+ *render(options) {
58
+ const segments = this.renderable.render(options);
59
+ if (options.colorSystem === null) {
60
+ yield* segments;
61
+ return;
62
+ }
63
+ // No depth named is no downgrade, as `drawnColors` reads it.
64
+ const depth = options.colorSystem ?? ColorDepth.TRUECOLOR;
65
+ let row = 0;
66
+ let col = 0;
67
+ for (const segment of segments) {
68
+ if (segment.isControl) {
69
+ yield segment;
70
+ continue;
71
+ }
72
+ const base = segment.style ?? Style.null();
73
+ // [LAW:single-enforcer] `drawnColors` is what the wire writes at this
74
+ // depth — alpha flattened, downgraded — and the theme resolves what it
75
+ // leaves to the terminal. A slot the effect leaves alone keeps the
76
+ // written spec, so it stays what the screen showed whatever moves
77
+ // beside it.
78
+ const wire = base.drawnColors(options.colorSystem ?? undefined);
79
+ const ink = (wire.color ?? DEFAULT).getTruecolor(this.theme, true);
80
+ const paper = (wire.bgcolor ?? DEFAULT).getTruecolor(this.theme, false);
81
+ const from = onScreen({ fg: ink, bg: paper }, base);
82
+ // Each grapheme's colours, merged into runs of equal colours as they
83
+ // come; `undefined` is a run left as the segment drew it.
84
+ const runs = [];
85
+ for (const glyph of graphemes(segment.text)) {
86
+ let colors;
87
+ if (glyph === "\n") {
88
+ row++;
89
+ col = 0;
90
+ }
91
+ else {
92
+ const to = this.effect(from, { row, col, seed: this.seed(row, col) }, this.t);
93
+ colors = sameColors(to, from) ? undefined : to;
94
+ col += cellLen(glyph);
95
+ }
96
+ const last = runs[runs.length - 1];
97
+ if (last !== undefined && sameRun(last.colors, colors)) {
98
+ last.text += glyph;
99
+ last.cells += cellLen(glyph);
100
+ }
101
+ else {
102
+ runs.push({ text: glyph, cells: cellLen(glyph), colors });
103
+ }
104
+ }
105
+ if (runs.every((run) => run.colors === undefined)) {
106
+ yield segment;
107
+ continue;
108
+ }
109
+ let offset = 0;
110
+ for (const run of runs) {
111
+ const shifted = base.shiftedBy(offset);
112
+ // Back from the screen to the style's slots: the same swap undoes itself.
113
+ const slots = run.colors === undefined ? undefined : onScreen(run.colors, base);
114
+ const style = slots === undefined ? shifted : shifted.add(this.respec(wire, { fg: ink, bg: paper }, slots, depth));
115
+ yield new Segment(run.text, style);
116
+ offset += run.cells;
117
+ }
118
+ }
119
+ }
120
+ measure(options) {
121
+ return Measurement.get(options, this.renderable);
122
+ }
123
+ /**
124
+ * The style that draws a run's slot colours `to` where the segment drew
125
+ * `from` with `wire`: a slot the effect left alone keeps its written spec,
126
+ * and a moved one is laid on what is beneath it — the ground on
127
+ * `SURFACE_BLACK`, the glyph on that ground, as the writer lays any
128
+ * translucent colour (`Style.drawnColors`) — then drawn in the colour nearest it at `depth`
129
+ * that the terminal shows (`ColorSpec.matchOn`), so a colour moved a little
130
+ * off a theme slot or the default colour is still drawn as that one.
131
+ */
132
+ respec(wire, from, to, depth) {
133
+ // [LAW:one-source-of-truth] The writer's surface, so one translucent colour draws one way in a frame.
134
+ const ground = to.bg.compositeOver(SURFACE_BLACK);
135
+ const glyph = to.fg.compositeOver(ground);
136
+ const drawn = (spec, was, now, foreground) => sameColor(was, now) ? spec : ColorSpec.matchOn(now, depth, this.theme, foreground);
137
+ return Style.fromColor(drawn(wire.color, from.fg, glyph, true), drawn(wire.bgcolor, from.bg, ground, false));
138
+ }
139
+ /** A cell's seed: FNV-1a of the key and the position, scaled into [0, 1). */
140
+ seed(row, col) {
141
+ return Number.parseInt(fnv1a(`${this.key}:${row}:${col}`), 16) / 0x1_0000_0000;
142
+ }
143
+ }
144
+ const DEFAULT = ColorSpec.default();
145
+ /**
146
+ * A style's colour slots as the screen shows them, or the reverse: `reverse`
147
+ * swaps glyph and ground, and swapping twice is where you started.
148
+ */
149
+ function onScreen(colors, style) {
150
+ return style.reverse === true ? { fg: colors.bg, bg: colors.fg } : colors;
151
+ }
152
+ function sameColor(a, b) {
153
+ return a.red === b.red && a.green === b.green && a.blue === b.blue && a.alpha === b.alpha;
154
+ }
155
+ function sameColors(a, b) {
156
+ return sameColor(a.fg, b.fg) && sameColor(a.bg, b.bg);
157
+ }
158
+ function sameRun(a, b) {
159
+ return a === undefined || b === undefined ? a === b : sameColors(a, b);
160
+ }
@@ -1,50 +1,107 @@
1
1
  /**
2
2
  * The one crossing where content a caller hands a renderable — a table cell, a
3
- * panel's body, title or subtitle, a tree label, a column item, a layout pane,
4
- * a rule's title — becomes something that renderable can lay out.
3
+ * panel's body, title or subtitle, a padding's body, a tree label, a column
4
+ * item, a layout pane, a rule's title — becomes something that renderable can
5
+ * lay out.
5
6
  * [LAW:single-enforcer]
6
7
  *
7
- * Each of those sites used to build its own `RichText` straight from the
8
- * constructor, which does not parse markup, so `[red]Solo[/red]` reached the
9
- * terminal with its tags intact. Table cells were fixed on their own first; the
10
- * rest kept the defect until the rule moved here.
8
+ * A string is kept as written and read when it is drawn, through `readStr`,
9
+ * under the options it is drawn with. That is where a console's settings
10
+ * arrive: its `markup` decides whether the string is markup, and its
11
+ * highlighter highlights it, exactly as they do for a string handed to `print`.
12
+ * Parsed when the renderable was built, a string was markup under a console
13
+ * that had turned markup off, never met the console's highlighter, and an
14
+ * unmatched closing tag threw from a constructor rather than from the render
15
+ * that would draw it, where Rich's does (rich-markup-3sw).
11
16
  *
12
- * Markup is parsed because Rich parses it: every one of these positions
13
- * reaches the wire through Rich's `render_str`, and a console's markup is on by
14
- * default. Rich's `render_str` also honours `Console(markup=False)` and runs the
15
- * console's highlighter; this crossing does neither, because the console's
16
- * settings do not reach it (rich-markup-3sw).
17
- *
18
- * `end` is cleared because embedded text is a fragment rather than a printed
19
- * line; left at the default `"\n"` it draws a trailing blank row. A `RichText`
20
- * is copied first, since clearing in place would reach back into the caller's
21
- * object.
17
+ * Text keeps its own `end`, as a body does in Rich: `RichText("foo\n")` draws
18
+ * the blank row its default `"\n"` end makes, and a string reads with that
19
+ * default end as `render_str` gives it. Only a label, set into a line it
20
+ * shares, clears it. A `RichText` is copied when it is handed over, since the
21
+ * caller still holds it, and every text drawn from it is a copy again, since
22
+ * the drawing site may change it.
22
23
  */
23
24
  import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
24
25
  import { Segment } from "../core/segment.js";
25
26
  import type { Style } from "../core/style.js";
26
27
  import { RichText } from "../core/text.js";
27
28
  /**
28
- * Caller content as the text an embedding site lays out, `end` cleared. A
29
- * string is the only kind of content that can contain markup, so it is the only
30
- * kind parsed; any other value is its `String` form as written, so an object's
31
- * `[object Object]` is not eaten as a tag.
29
+ * Caller content as text an embedding site lays out. A string is the only kind
30
+ * of content that can contain markup, so it is the only kind whose reading
31
+ * waits for the options; any other value is its `String` form as written, so an
32
+ * object's `[object Object]` is not eaten as a tag.
32
33
  */
33
- export declare function embeddedText(content: unknown): RichText;
34
+ export declare class EmbeddedText implements Renderable, Measurable {
35
+ private readonly _source;
36
+ private readonly _readings;
37
+ constructor(content: unknown);
38
+ /**
39
+ * The text this content draws under `options`. A string reads with `base` as
40
+ * its style, as Rich's `render_str(style=)` gives one; text handed over
41
+ * keeps its own, since the caller already said how it looks — so a table's
42
+ * `Text` title draws without the title style, as Rich's does.
43
+ */
44
+ text(options: RenderOptions, base?: Style): RichText;
45
+ render(options: RenderOptions): Iterable<Segment>;
46
+ /**
47
+ * Measured unhighlighted, as Rich's `Measurement.get` reads a string with
48
+ * `highlight=False`: so a container that clears the highlighter for what it
49
+ * draws measures what it draws, whether or not it clears it for what it
50
+ * measures, and no highlighter runs just to be thrown away.
51
+ */
52
+ measure(options: RenderOptions): {
53
+ minimum: number;
54
+ maximum: number;
55
+ };
56
+ }
34
57
  /**
35
58
  * Caller content as something an embedding site can render. A non-text
36
- * `Renderable` (a nested `Panel` or `Table`) passes through: it carries no
37
- * `end` to clear and no markup to parse. Everything else is `embeddedText`.
59
+ * `Renderable` (a nested `Panel` or `Table`) passes through: it has no markup
60
+ * to parse. Everything else is `EmbeddedText`.
38
61
  */
39
62
  export declare function embed(content: unknown): Renderable & Partial<Measurable>;
40
63
  /**
41
64
  * Caller content set into a line it shares with other drawing — a panel's
42
- * title or subtitle in its border, a rule's title — as one line of segments:
43
- * a space either side, its own styles over `base`. The caller cuts it to the
44
- * room it has with `Segment.adjustLineLength`, which is cell-aware.
65
+ * title or subtitle in its border, a rule's title — read as Rich's `_title`
66
+ * reads it: newlines become spaces, so the label stays on its line, and tabs
67
+ * are widened to their stops. Whatever sets it off from the line around it —
68
+ * a panel's space either side, a rule's gaps — is the embedding site's.
45
69
  *
46
- * Content with no text is no label at all, not two spaces: an absent title, an
47
- * empty string, an empty `RichText` and markup that styles nothing all draw
48
- * the plain line, as Rich's do (an empty `Text` is falsy there).
70
+ * Held once by the renderable that owns it, so a string's markup is read once
71
+ * and every measure and render after reuses that reading.
72
+ */
73
+ export declare class InlineLabel {
74
+ private readonly _content;
75
+ constructor(content: string | RichText);
76
+ /** The label drawn under `options`: one line, its own overflow kept for the caller's cut. */
77
+ text(options: RenderOptions): RichText;
78
+ }
79
+ /**
80
+ * A label cut to `width` cells as Rich's `Text.truncate` cuts one: its plain
81
+ * text set to the width less `marker` — a wide character the cut splits leaves
82
+ * a space in its cell rather than a cell of the line around it — then the
83
+ * marker, with every span that ran past the cut trimmed to the new end, so the
84
+ * pad and the marker take the styling of what they replace. A label that fits
85
+ * is left as it is.
86
+ */
87
+ export declare function cutLabel(text: RichText, width: number, marker: string): void;
88
+ /**
89
+ * Whether caller content is there at all, decided on the content as given, as
90
+ * Rich's `if self.title` decides it: absent, an empty string and an empty
91
+ * `Text` are not, and markup that styles nothing is — it reads as empty text,
92
+ * but the string that holds it is not empty.
93
+ */
94
+ export declare function present(content: string | RichText | undefined): content is string | RichText;
95
+ /**
96
+ * A label's text as one line of segments at its natural width, its own styles
97
+ * laid over `base`. The caller has already cut it to its room, so it leaves as
98
+ * it stands: `"ignore"` also leaves it unjustified, its spaces where `pad` put
99
+ * them.
100
+ */
101
+ export declare function drawLabel(text: RichText, options: RenderOptions, base: Style | undefined): Segment[];
102
+ /**
103
+ * Caller content as a label, or no label at all: content that is not
104
+ * `present` draws the plain rule, and markup that styles nothing is an empty
105
+ * label, which still opens a gap in the rule, as Rich's does.
49
106
  */
50
- export declare function inlineLabel(content: unknown, options: RenderOptions, base: Style | undefined): Segment[];
107
+ export declare function inlineLabel(content: string | RichText | undefined): InlineLabel | undefined;
@@ -1,71 +1,160 @@
1
1
  /**
2
2
  * The one crossing where content a caller hands a renderable — a table cell, a
3
- * panel's body, title or subtitle, a tree label, a column item, a layout pane,
4
- * a rule's title — becomes something that renderable can lay out.
3
+ * panel's body, title or subtitle, a padding's body, a tree label, a column
4
+ * item, a layout pane, a rule's title — becomes something that renderable can
5
+ * lay out.
5
6
  * [LAW:single-enforcer]
6
7
  *
7
- * Each of those sites used to build its own `RichText` straight from the
8
- * constructor, which does not parse markup, so `[red]Solo[/red]` reached the
9
- * terminal with its tags intact. Table cells were fixed on their own first; the
10
- * rest kept the defect until the rule moved here.
8
+ * A string is kept as written and read when it is drawn, through `readStr`,
9
+ * under the options it is drawn with. That is where a console's settings
10
+ * arrive: its `markup` decides whether the string is markup, and its
11
+ * highlighter highlights it, exactly as they do for a string handed to `print`.
12
+ * Parsed when the renderable was built, a string was markup under a console
13
+ * that had turned markup off, never met the console's highlighter, and an
14
+ * unmatched closing tag threw from a constructor rather than from the render
15
+ * that would draw it, where Rich's does (rich-markup-3sw).
11
16
  *
12
- * Markup is parsed because Rich parses it: every one of these positions
13
- * reaches the wire through Rich's `render_str`, and a console's markup is on by
14
- * default. Rich's `render_str` also honours `Console(markup=False)` and runs the
15
- * console's highlighter; this crossing does neither, because the console's
16
- * settings do not reach it (rich-markup-3sw).
17
- *
18
- * `end` is cleared because embedded text is a fragment rather than a printed
19
- * line; left at the default `"\n"` it draws a trailing blank row. A `RichText`
20
- * is copied first, since clearing in place would reach back into the caller's
21
- * object.
17
+ * Text keeps its own `end`, as a body does in Rich: `RichText("foo\n")` draws
18
+ * the blank row its default `"\n"` end makes, and a string reads with that
19
+ * default end as `render_str` gives it. Only a label, set into a line it
20
+ * shares, clears it. A `RichText` is copied when it is handed over, since the
21
+ * caller still holds it, and every text drawn from it is a copy again, since
22
+ * the drawing site may change it.
22
23
  */
23
- import { renderMarkup } from "../core/markup.js";
24
+ import { cellCount, cellLen, setCellSize } from "../core/cells.js";
25
+ import { activeHighlighter, readStr } from "../core/markup.js";
24
26
  import { Segment } from "../core/segment.js";
25
27
  import { RichText } from "../core/text.js";
26
28
  /**
27
- * Caller content as the text an embedding site lays out, `end` cleared. A
28
- * string is the only kind of content that can contain markup, so it is the only
29
- * kind parsed; any other value is its `String` form as written, so an object's
30
- * `[object Object]` is not eaten as a tag.
29
+ * Caller content as text an embedding site lays out. A string is the only kind
30
+ * of content that can contain markup, so it is the only kind whose reading
31
+ * waits for the options; any other value is its `String` form as written, so an
32
+ * object's `[object Object]` is not eaten as a tag.
31
33
  */
32
- export function embeddedText(content) {
33
- const text = content instanceof RichText
34
- ? content.copy()
35
- : typeof content === "string"
36
- ? renderMarkup(content)
37
- : new RichText(String(content ?? ""));
38
- text.end = "";
39
- return text;
34
+ export class EmbeddedText {
35
+ // [LAW:types-are-the-program] Two kinds, and the kind is the discriminator: a
36
+ // string still to be read, or text whose reading is already fixed.
37
+ _source;
38
+ // A string's reading under each markup setting, made the first time one is
39
+ // asked for. Parsing is the costly half of drawing a string, and a table
40
+ // measures every cell more than once per print; read on every call, a
41
+ // 500-row table of markup printed 75% slower.
42
+ _readings = new Map();
43
+ constructor(content) {
44
+ if (typeof content === "string") {
45
+ this._source = content;
46
+ }
47
+ else {
48
+ this._source = content instanceof RichText ? content.copy() : new RichText(String(content ?? ""));
49
+ }
50
+ }
51
+ /**
52
+ * The text this content draws under `options`. A string reads with `base` as
53
+ * its style, as Rich's `render_str(style=)` gives one; text handed over
54
+ * keeps its own, since the caller already said how it looks — so a table's
55
+ * `Text` title draws without the title style, as Rich's does.
56
+ */
57
+ text(options, base) {
58
+ if (typeof this._source !== "string")
59
+ return this._source.copy();
60
+ const markup = options.markup !== false;
61
+ const reading = this._readings.get(markup) ?? readStr(this._source, markup);
62
+ this._readings.set(markup, reading);
63
+ const text = reading.copy();
64
+ if (base)
65
+ text.style = base;
66
+ activeHighlighter(options)?.highlight(text);
67
+ return text;
68
+ }
69
+ render(options) {
70
+ return this.text(options).render(options);
71
+ }
72
+ /**
73
+ * Measured unhighlighted, as Rich's `Measurement.get` reads a string with
74
+ * `highlight=False`: so a container that clears the highlighter for what it
75
+ * draws measures what it draws, whether or not it clears it for what it
76
+ * measures, and no highlighter runs just to be thrown away.
77
+ */
78
+ measure(options) {
79
+ return this.text({ ...options, highlight: false }).measure(options);
80
+ }
40
81
  }
41
82
  /**
42
83
  * Caller content as something an embedding site can render. A non-text
43
- * `Renderable` (a nested `Panel` or `Table`) passes through: it carries no
44
- * `end` to clear and no markup to parse. Everything else is `embeddedText`.
84
+ * `Renderable` (a nested `Panel` or `Table`) passes through: it has no markup
85
+ * to parse. Everything else is `EmbeddedText`.
45
86
  */
46
87
  export function embed(content) {
47
88
  if (!(content instanceof RichText) && typeof content === "object" && content !== null && "render" in content) {
48
89
  return content;
49
90
  }
50
- return embeddedText(content);
91
+ return new EmbeddedText(content);
51
92
  }
52
93
  /**
53
94
  * Caller content set into a line it shares with other drawing — a panel's
54
- * title or subtitle in its border, a rule's title — as one line of segments:
55
- * a space either side, its own styles over `base`. The caller cuts it to the
56
- * room it has with `Segment.adjustLineLength`, which is cell-aware.
95
+ * title or subtitle in its border, a rule's title — read as Rich's `_title`
96
+ * reads it: newlines become spaces, so the label stays on its line, and tabs
97
+ * are widened to their stops. Whatever sets it off from the line around it —
98
+ * a panel's space either side, a rule's gaps — is the embedding site's.
57
99
  *
58
- * Content with no text is no label at all, not two spaces: an absent title, an
59
- * empty string, an empty `RichText` and markup that styles nothing all draw
60
- * the plain line, as Rich's do (an empty `Text` is falsy there).
100
+ * Held once by the renderable that owns it, so a string's markup is read once
101
+ * and every measure and render after reuses that reading.
102
+ */
103
+ export class InlineLabel {
104
+ _content;
105
+ constructor(content) {
106
+ this._content = new EmbeddedText(content);
107
+ }
108
+ /** The label drawn under `options`: one line, its own overflow kept for the caller's cut. */
109
+ text(options) {
110
+ const text = this._content.text(options);
111
+ // A fragment of the line, so its own end is not drawn, as Rich's `_title` clears it.
112
+ text.end = "";
113
+ text.plain = text.plain.replaceAll("\n", " ");
114
+ // Tabs widened before anything measures it, so a cut to the border and
115
+ // the width a title asks for count the cells the label will draw.
116
+ return text.expandTabs();
117
+ }
118
+ }
119
+ /**
120
+ * A label cut to `width` cells as Rich's `Text.truncate` cuts one: its plain
121
+ * text set to the width less `marker` — a wide character the cut splits leaves
122
+ * a space in its cell rather than a cell of the line around it — then the
123
+ * marker, with every span that ran past the cut trimmed to the new end, so the
124
+ * pad and the marker take the styling of what they replace. A label that fits
125
+ * is left as it is.
61
126
  */
62
- export function inlineLabel(content, options, base) {
63
- // The label leaves at its natural width, `"ignore"`, because the caller does
64
- // the cutting; the label's own overflow method would cut it first, and to a
65
- // width no one measured. `"ignore"` also leaves it unjustified, so its spaces
66
- // stay where `pad` put them.
67
- const bare = embeddedText(content);
68
- const text = bare.plain === "" ? bare : bare.pad(1);
127
+ export function cutLabel(text, width, marker) {
128
+ if (text.cellLength <= width)
129
+ return;
130
+ text.plain = setCellSize(text.plain, cellCount(width - cellLen(marker))) + marker;
131
+ }
132
+ /**
133
+ * Whether caller content is there at all, decided on the content as given, as
134
+ * Rich's `if self.title` decides it: absent, an empty string and an empty
135
+ * `Text` are not, and markup that styles nothing is — it reads as empty text,
136
+ * but the string that holds it is not empty.
137
+ */
138
+ export function present(content) {
139
+ // Asked of what the content is rather than of what it is not, so a `null`
140
+ // from untyped JS is no content, as Rich's `None` is.
141
+ return typeof content === "string" ? content !== "" : content instanceof RichText && content.plain !== "";
142
+ }
143
+ /**
144
+ * A label's text as one line of segments at its natural width, its own styles
145
+ * laid over `base`. The caller has already cut it to its room, so it leaves as
146
+ * it stands: `"ignore"` also leaves it unjustified, its spaces where `pad` put
147
+ * them.
148
+ */
149
+ export function drawLabel(text, options, base) {
69
150
  text.overflow = "ignore";
70
151
  return [...Segment.applyStyle(text.render(options), base)];
71
152
  }
153
+ /**
154
+ * Caller content as a label, or no label at all: content that is not
155
+ * `present` draws the plain rule, and markup that styles nothing is an empty
156
+ * label, which still opens a gap in the rule, as Rich's does.
157
+ */
158
+ export function inlineLabel(content) {
159
+ return present(content) ? new InlineLabel(content) : undefined;
160
+ }
@@ -2,10 +2,13 @@
2
2
  * Group — renders multiple renderables in sequence.
3
3
  * No visual chrome — purely a container.
4
4
  */
5
- import { Segment } from "../core/segment.js";
6
- import type { Renderable, RenderOptions } from "../core/protocol.js";
7
- export declare class Group implements Renderable {
5
+ import type { Segment } from "../core/segment.js";
6
+ import { type Measurement } from "../core/measure.js";
7
+ import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
8
+ export declare class Group implements Renderable, Measurable {
8
9
  readonly renderables: Renderable[];
9
10
  constructor(...renderables: Renderable[]);
10
11
  render(options: RenderOptions): Iterable<Segment>;
12
+ /** As wide as its widest member: the reference's `measure_renderables`. */
13
+ measure(options: RenderOptions): Measurement;
11
14
  }
@@ -2,6 +2,7 @@
2
2
  * Group — renders multiple renderables in sequence.
3
3
  * No visual chrome — purely a container.
4
4
  */
5
+ import { measureRenderables } from "../core/measure.js";
5
6
  import { stackedHeight } from "../core/protocol.js";
6
7
  export class Group {
7
8
  renderables;
@@ -14,4 +15,8 @@ export class Group {
14
15
  yield* renderable.render(memberOptions);
15
16
  }
16
17
  }
18
+ /** As wide as its widest member: the reference's `measure_renderables`. */
19
+ measure(options) {
20
+ return measureRenderables(options, this.renderables);
21
+ }
17
22
  }
@@ -3,6 +3,8 @@
3
3
  */
4
4
  import { Segment } from "../core/segment.js";
5
5
  import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
6
+ import type { CellCol } from "../core/cells.js";
7
+ import type { Weight } from "./ratio.js";
6
8
  export interface LayoutOptions {
7
9
  name?: string;
8
10
  ratio?: number;
@@ -25,7 +27,7 @@ export declare class Layout implements Renderable, Measurable {
25
27
  * constructor. All three are public and a caller reaches them long after
26
28
  * construction — `layout.getByName("pane")!.ratio = -1` walked straight past a
27
29
  * constructor-only parse and put a negative weight back into the division that
28
- * `_rowBudgetFor` and `_distributeSpace` are written to trust.
30
+ * `ratioResolve` and `ratioBudget` are written to trust.
29
31
  *
30
32
  * [LAW:parse-dont-validate] The setter is the border, so the guarantee holds
31
33
  * for the object's whole lifetime and nothing downstream re-checks. `size` and
@@ -33,11 +35,11 @@ export declare class Layout implements Renderable, Measurable {
33
35
  * fractions. Absence is preserved rather than parsed: an undefined `size`
34
36
  * selects a flex pane, and `cellCount` would read it as a declared zero.
35
37
  */
36
- get ratio(): number;
38
+ get ratio(): Weight;
37
39
  set ratio(value: number);
38
- get size(): number | undefined;
40
+ get size(): CellCol | undefined;
39
41
  set size(value: number | undefined);
40
- get minimumSize(): number;
42
+ get minimumSize(): CellCol;
41
43
  set minimumSize(value: number);
42
44
  get children(): Layout[];
43
45
  /**
@@ -57,7 +59,6 @@ export declare class Layout implements Renderable, Measurable {
57
59
  render(rawOptions: RenderOptions): Iterable<Segment>;
58
60
  private _renderColumn;
59
61
  private _renderRow;
60
- private _distributeSpace;
61
62
  /**
62
63
  * The width this layout would take if nothing constrained it: its content's
63
64
  * for a leaf, its children's laid out the way the split lays them out.
@@ -67,24 +68,6 @@ export declare class Layout implements Renderable, Measurable {
67
68
  * width there would report a two-line pane as two cells wide.
68
69
  */
69
70
  private _naturalWidth;
70
- /**
71
- * The budget at which `_distributeSpace` gives every child of a row at least
72
- * the width it wants — the inverse of the ratio rule above, and the reason
73
- * this is not the sum of those widths.
74
- *
75
- * [LAW:one-source-of-truth] A growing child receives `floor(budget * ratio /
76
- * totalRatio)`, so to receive `want` cells it needs the budget to reach
77
- * `want * totalRatio / ratio`, and one budget serves them all: the row needs
78
- * the largest demand, never their total. Summed instead, a 1:1 split of
79
- * "left" and "right" reported 9 and then rendered "right" into the 4 cells
80
- * that `floor(9/2)` actually hands it.
81
- *
82
- * The panes that do not grow are paid first here for the same reason they
83
- * are paid first there — and it is the reason this divides by no zero: a
84
- * `ratio` of 0 never reaches the division, because a pane that does not grow
85
- * demands nothing of a budget for growing.
86
- */
87
- private _rowBudgetFor;
88
71
  measure(rawOptions: RenderOptions): {
89
72
  minimum: number;
90
73
  maximum: number;